1//
2// debugview.cpp
3//
4// Description: Manages the interaction with GDB
5//
6//
7// Copyright (c) 2008-2010 Ian Wakeling <ian.wakeling@ntlworld.com>
8// Copyright (c) 2011 Kåre Särs <kare.sars@iki.fi>
9//
10// This library is free software; you can redistribute it and/or
11// modify it under the terms of the GNU Library General Public
12// License version 2 as published by the Free Software Foundation.
13//
14// This library is distributed in the hope that it will be useful,
15// but WITHOUT ANY WARRANTY; without even the implied warranty of
16// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17// Library General Public License for more details.
18//
19// You should have received a copy of the GNU Library General Public License
20// along with this library; see the file COPYING.LIB. If not, write to
21// the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
22// Boston, MA 02110-1301, USA.
23
24#include "debugview.h"
25#include "debugview.moc"
26
27#include <QtCore/QRegExp>
28#include <QtCore/QFile>
29#include <QtCore/QTimer>
30
31#include <kmessagebox.h>
32#include <kurlrequester.h>
33#include <kurl.h>
34#include <kdebug.h>
35#include <klocale.h>
36
37#include <signal.h>
38#include <stdlib.h>
39
40static const QString PromptStr = "(prompt)";
41
42DebugView::DebugView( QObject* parent )
43: QObject( parent ),
44 m_debugProcess(0),
45 m_state( none ),
46 m_subState( normal ),
47 m_debugLocationChanged( true ),
48 m_queryLocals( false )
49{
50}
51
52
53DebugView::~DebugView()
54{
55 if ( m_debugProcess.state() != QProcess::NotRunning )
56 {
57 m_debugProcess.kill();
58 m_debugProcess.blockSignals( true );
59 m_debugProcess.waitForFinished();
60 }
61}
62
63void DebugView::runDebugger(const GDBTargetConf &conf, const QStringList &ioFifos)
64{
65 if (conf.executable.isEmpty()) {
66 return;
67 }
68 m_targetConf = conf;
69 if (ioFifos.size() == 3) {
70 m_ioPipeString = QString("< %1 1> %2 2> %3")
71 .arg(ioFifos[0])
72 .arg(ioFifos[1])
73 .arg(ioFifos[2]);
74 }
75
76 if (m_state == none) {
77 m_outBuffer.clear();
78 m_errBuffer.clear();
79 m_errorList.clear();
80
81 //create a process to control GDB
82 m_debugProcess.setWorkingDirectory(m_targetConf.workDir);
83
84 connect( &m_debugProcess, SIGNAL(error(QProcess::ProcessError)),
85 this, SLOT(slotError()) );
86
87 connect( &m_debugProcess, SIGNAL(readyReadStandardError()),
88 this, SLOT(slotReadDebugStdErr()) );
89
90 connect( &m_debugProcess, SIGNAL(readyReadStandardOutput()),
91 this, SLOT(slotReadDebugStdOut()) );
92
93 connect( &m_debugProcess, SIGNAL(finished(int,QProcess::ExitStatus)),
94 this, SLOT(slotDebugFinished(int,QProcess::ExitStatus)) );
95
96 m_debugProcess.setShellCommand(m_targetConf.gdbCmd);
97 m_debugProcess.setOutputChannelMode(KProcess::SeparateChannels);
98 m_debugProcess.start();
99
100 m_nextCommands << "set pagination off";
101 m_state = ready;
102 }
103 else
104 {
105 // On startup the gdb prompt will trigger the "nextCommands",
106 // here we have to trigger it manually.
107 QTimer::singleShot(0, this, SLOT(issueNextCommand()));
108 }
109 m_nextCommands << QString("file %1").arg(m_targetConf.executable);
110 m_nextCommands << QString("set args %1 %2").arg(m_targetConf.arguments).arg(m_ioPipeString);
111 m_nextCommands << QString("set inferior-tty /dev/null");
112 m_nextCommands << m_targetConf.customInit;
113 m_nextCommands << QString("(Q) info breakpoints");
114}
115
116bool DebugView::debuggerRunning() const
117{
118 return( m_state != none );
119}
120
121bool DebugView::debuggerBusy() const
122{
123 return( m_state == executingCmd );
124}
125
126bool DebugView::hasBreakpoint( const KUrl& url, int line )
127{
128 for (int i = 0; i<m_breakPointList.size(); i++) {
129 if ( (url == m_breakPointList[i].file) && (line == m_breakPointList[i].line) ) {
130 return true;
131 }
132 }
133 return false;
134}
135
136void DebugView::toggleBreakpoint( KUrl const& url, int line )
137{
138 if( m_state == ready )
139 {
140 QString cmd;
141 if (hasBreakpoint( url, line ))
142 {
143 cmd = QString("clear %1:%2").arg(url.path()).arg(line);
144 }
145 else {
146 cmd = QString("break %1:%2").arg(url.path()).arg(line);
147 }
148 issueCommand( cmd );
149 }
150}
151
152void DebugView::slotError()
153{
154 KMessageBox::sorry( NULL, i18n("Could not start debugger process") );
155}
156
157void DebugView::slotReadDebugStdOut()
158{
159 m_outBuffer += QString::fromLocal8Bit( m_debugProcess.readAllStandardOutput().data() );
160 int end=0;
161 // handle one line at a time
162 do {
163 end = m_outBuffer.indexOf('\n');
164 if (end < 0) break;
165 processLine( m_outBuffer.mid(0, end) );
166 m_outBuffer.remove(0,end+1);
167 } while (1);
168
169 if (m_outBuffer == "(gdb) " || m_outBuffer == ">")
170 {
171 m_outBuffer.clear();
172 processLine( PromptStr );
173 }
174}
175
176void DebugView::slotReadDebugStdErr()
177{
178 m_errBuffer += QString::fromLocal8Bit( m_debugProcess.readAllStandardError().data() );
179 int end=0;
180 // add whole lines at a time to the error list
181 do {
182 end = m_errBuffer.indexOf('\n');
183 if (end < 0) break;
184 m_errorList << m_errBuffer.mid(0, end);
185 m_errBuffer.remove(0,end+1);
186 } while (1);
187
188 processErrors();
189}
190
191void DebugView::slotDebugFinished( int /*exitCode*/, QProcess::ExitStatus status )
192{
193 if( status != QProcess::NormalExit )
194 {
195 emit outputText( i18n("*** gdb exited normally ***") + '\n' );
196 }
197
198 m_state = none;
199 emit readyForInput( false );
200
201 // remove all old breakpoints
202 BreakPoint bPoint;
203 while ( m_breakPointList.size() > 0 )
204 {
205 bPoint = m_breakPointList.takeFirst();
206 emit breakPointCleared( bPoint.file, bPoint.line -1 );
207 }
208
209 emit gdbEnded();
210}
211
212void DebugView::movePC( KUrl const& url, int line )
213{
214 if( m_state == ready )
215 {
216 QString cmd = QString("tbreak %1:%2").arg(url.path()).arg(line);
217 m_nextCommands << QString("jump %1:%2").arg(url.path()).arg(line);
218 issueCommand( cmd );
219 }
220}
221
222void DebugView::runToCursor( KUrl const& url, int line )
223{
224 if( m_state == ready )
225 {
226 QString cmd = QString("tbreak %1:%2").arg(url.path()).arg(line);
227 m_nextCommands << "continue";
228 issueCommand( cmd );
229 }
230}
231
232void DebugView::slotInterrupt()
233{
234 if (m_state == executingCmd) {
235 m_debugLocationChanged = true;
236 }
237 int pid = m_debugProcess.pid();
238 if (pid != 0) {
239 ::kill(pid, SIGINT);
240 }
241}
242
243void DebugView::slotKill()
244{
245 if( m_state != ready )
246 {
247 slotInterrupt();
248 m_state = ready;
249 }
250 issueCommand( "kill" );
251}
252
253void DebugView::slotReRun()
254{
255 slotKill();
256 m_nextCommands << QString("file %1").arg(m_targetConf.executable);
257 m_nextCommands << QString("set args %1 %2").arg(m_targetConf.arguments).arg(m_ioPipeString);
258 m_nextCommands << QString("set inferior-tty /dev/null");
259 m_nextCommands << m_targetConf.customInit;
260 m_nextCommands << QString("(Q) info breakpoints");
261
262 m_nextCommands << QString("tbreak main");
263 m_nextCommands << QString("run");
264 m_nextCommands << QString("p setvbuf(stdout, 0, %1, 1024)").arg(_IOLBF);
265 m_nextCommands << QString("continue");
266}
267
268void DebugView::slotStepInto()
269{
270 issueCommand( "step" );
271}
272
273void DebugView::slotStepOver()
274{
275 issueCommand( "next" );
276}
277
278void DebugView::slotStepOut()
279{
280 issueCommand( "finish" );
281}
282
283void DebugView::slotContinue()
284{
285 issueCommand( "continue" );
286}
287
288static QRegExp breakpointList( "Num\\s+Type\\s+Disp\\s+Enb\\s+Address\\s+What.*" );
289static QRegExp breakpointListed( "(\\d)\\s+breakpoint\\s+keep\\sy\\s+0x[\\da-f]+\\sin\\s.+\\sat\\s([^:]+):(\\d+).*" );
290static QRegExp stackFrameAny( "#(\\d+)\\s(.*)" );
291static QRegExp stackFrameFile( "#(\\d+)\\s+(?:0x[\\da-f]+\\s*in\\s)*(\\S+)(\\s\\(.*\\)) at ([^:]+):(\\d+).*" );
292static QRegExp changeFile( "(?:(?:Temporary\\sbreakpoint|Breakpoint)\\s*\\d+,\\s*|0x[\\da-f]+\\s*in\\s*)?[^\\s]+\\s*\\([^)]*\\)\\s*at\\s*([^:]+):(\\d+).*" );
293static QRegExp changeLine( "(\\d+)\\s+.*" );
294static QRegExp breakPointReg( "Breakpoint\\s+(\\d+)\\s+at\\s+0x[\\da-f]+:\\s+file\\s+([^\\,]+)\\,\\s+line\\s+(\\d+).*" );
295static QRegExp breakPointMultiReg( "Breakpoint\\s+(\\d+)\\s+at\\s+0x[\\da-f]+:\\s+([^\\,]+):(\\d+).*" );
296static QRegExp breakPointDel( "Deleted\\s+breakpoint.*" );
297static QRegExp exitProgram( "(?:Program|.*Inferior.*)\\s+exited.*" );
298static QRegExp threadLine( "\\**\\s+(\\d+)\\s+Thread.*" );
299
300void DebugView::processLine( QString line )
301{
302 if (line.isEmpty()) return;
303
304 switch( m_state )
305 {
306 case none:
307 case ready:
308 if( PromptStr == line )
309 {
310 // we get here after initialization
311 QTimer::singleShot(0, this, SLOT(issueNextCommand()));
312 }
313 break;
314
315 case executingCmd:
316 if( breakpointList.exactMatch( line ) )
317 {
318 m_state = listingBreakpoints;
319 emit clearBreakpointMarks();
320 m_breakPointList.clear();
321 }
322 else if ( line.contains( "No breakpoints or watchpoints.") )
323 {
324 emit clearBreakpointMarks();
325 m_breakPointList.clear();
326 }
327 else if ( stackFrameAny.exactMatch( line ) )
328 {
329 if ( m_lastCommand.contains( "info stack" ) )
330 {
331 emit stackFrameInfo( stackFrameAny.cap(1), stackFrameAny.cap(2));
332 }
333 else
334 {
335 m_subState = ( m_subState == normal ) ? stackFrameSeen : stackTraceSeen;
336
337 m_newFrameLevel = stackFrameAny.cap( 1 ).toInt();
338
339 if ( stackFrameFile.exactMatch( line ) )
340 {
341 m_newFrameFile = stackFrameFile.cap( 4 );
342 }
343 }
344 }
345 else if( changeFile.exactMatch( line ) )
346 {
347 m_currentFile = changeFile.cap( 1 ).trimmed();
348 int lineNum = changeFile.cap( 2 ).toInt();
349
350 if ( !m_nextCommands.contains("continue") ) {
351 // GDB uses 1 based line numbers, kate uses 0 based...
352 emit debugLocationChanged( resolveFileName(m_currentFile), lineNum - 1 );
353 }
354 m_debugLocationChanged = true;
355 }
356 else if( changeLine.exactMatch( line ) )
357 {
358 int lineNum = changeLine.cap( 1 ).toInt();
359
360 if( m_subState == stackFrameSeen )
361 {
362 m_currentFile = m_newFrameFile;
363 }
364 if ( !m_nextCommands.contains("continue") ) {
365 // GDB uses 1 based line numbers, kate uses 0 based...
366 emit debugLocationChanged( resolveFileName(m_currentFile), lineNum - 1 );
367 }
368 m_debugLocationChanged = true;
369 }
370 else if (breakPointReg.exactMatch(line))
371 {
372 BreakPoint breakPoint;
373 breakPoint.number = breakPointReg.cap( 1 ).toInt();
374 breakPoint.file = resolveFileName( breakPointReg.cap( 2 ) );
375 breakPoint.line = breakPointReg.cap( 3 ).toInt();
376 m_breakPointList << breakPoint;
377 emit breakPointSet( breakPoint.file, breakPoint.line -1 );
378 }
379 else if (breakPointMultiReg.exactMatch(line))
380 {
381 BreakPoint breakPoint;
382 breakPoint.number = breakPointMultiReg.cap( 1 ).toInt();
383 breakPoint.file = resolveFileName( breakPointMultiReg.cap( 2 ) );
384 breakPoint.line = breakPointMultiReg.cap( 3 ).toInt();
385 m_breakPointList << breakPoint;
386 emit breakPointSet( breakPoint.file, breakPoint.line -1 );
387 }
388 else if (breakPointDel.exactMatch(line))
389 {
390 line.remove("Deleted breakpoint");
391 line.remove('s'); // in case of multiple breakpoints
392 QStringList numbers = line.split(' ', QString::SkipEmptyParts);
393 for (int i=0; i<numbers.size(); i++)
394 {
395 for (int j = 0; j<m_breakPointList.size(); j++)
396 {
397 if ( numbers[i].toInt() == m_breakPointList[j].number )
398 {
399 emit breakPointCleared( m_breakPointList[j].file, m_breakPointList[j].line -1 );
400 m_breakPointList.removeAt(j);
401 break;
402 }
403 }
404 }
405 }
406 else if ( exitProgram.exactMatch( line ) ||
407 line.contains( "The program no longer exists" ) ||
408 line.contains( "Kill the program being debugged" ) )
409 {
410 // if there are still commands to execute remove them to remove unneeded output
411 // except if the "kill was for "re-run"
412 if ( ( m_nextCommands.size() > 0 ) && !m_nextCommands[0].contains("file") )
413 {
414 m_nextCommands.clear();
415 }
416 m_debugLocationChanged = false; // do not insert (Q) commands
417 emit programEnded();
418 }
419 else if( PromptStr == line )
420 {
421 if( m_subState == stackFrameSeen )
422 {
423 emit stackFrameChanged( m_newFrameLevel );
424 }
425 m_state = ready;
426
427 // Give the error a possibility get noticed since stderr and stdout are not in sync
428 QTimer::singleShot(0, this, SLOT(issueNextCommand()));
429 }
430 break;
431
432 case listingBreakpoints:
433 if (breakpointListed.exactMatch( line ) )
434 {
435 BreakPoint breakPoint;
436 breakPoint.number = breakpointListed.cap( 1 ).toInt();
437 breakPoint.file = resolveFileName( breakpointListed.cap( 2 ) );
438 breakPoint.line = breakpointListed.cap( 3 ).toInt();
439 m_breakPointList << breakPoint;
440 emit breakPointSet( breakPoint.file, breakPoint.line -1 );
441 }
442 else if( PromptStr == line )
443 {
444 m_state = ready;
445 QTimer::singleShot(0, this, SLOT(issueNextCommand()));
446 }
447 break;
448 case infoArgs:
449 if( PromptStr == line )
450 {
451 m_state = ready;
452 QTimer::singleShot(0, this, SLOT(issueNextCommand()));
453 }
454 else {
455 emit infoLocal( line );
456 }
457 break;
458 case printThis:
459 if( PromptStr == line )
460 {
461 m_state = ready;
462 QTimer::singleShot(0, this, SLOT(issueNextCommand()));
463 }
464 else {
465 emit infoLocal( line );
466 }
467 break;
468 case infoLocals:
469 if( PromptStr == line )
470 {
471 m_state = ready;
472 emit infoLocal( QString() );
473 QTimer::singleShot(0, this, SLOT(issueNextCommand()));
474 }
475 else {
476 emit infoLocal( line );
477 }
478 break;
479 case infoStack:
480 if( PromptStr == line )
481 {
482 m_state = ready;
483 emit stackFrameInfo( QString(), QString() );
484 QTimer::singleShot(0, this, SLOT(issueNextCommand()));
485 }
486 else if ( stackFrameAny.exactMatch( line ) )
487 {
488 emit stackFrameInfo( stackFrameAny.cap(1), stackFrameAny.cap(2));
489 }
490 break;
491 case infoThreads:
492 if( PromptStr == line )
493 {
494 m_state = ready;
495 QTimer::singleShot(0, this, SLOT(issueNextCommand()));
496 }
497 else if ( threadLine.exactMatch( line ) )
498 {
499 emit threadInfo( threadLine.cap(1).toInt(), (line[0] == '*'));
500 }
501 break;
502 }
503 outputTextMaybe( line );
504}
505
506void DebugView::processErrors()
507{
508 QString error;
509 while (m_errorList.size() > 0) {
510 error = m_errorList.takeFirst();
511 //kDebug() << error;
512 if( error == "The program is not being run." )
513 {
514 if ( m_lastCommand == "continue" )
515 {
516 m_nextCommands.clear();
517 m_nextCommands << QString("tbreak main");
518 m_nextCommands << QString("run");
519 m_nextCommands << QString("p setvbuf(stdout, 0, %1, 1024)").arg(_IOLBF);
520 m_nextCommands << QString("continue");
521 QTimer::singleShot(0, this, SLOT(issueNextCommand()));
522 }
523 else if ( ( m_lastCommand == "step" ) ||
524 ( m_lastCommand == "next" ) ||
525 ( m_lastCommand == "finish" ) )
526 {
527 m_nextCommands.clear();
528 m_nextCommands << "tbreak main";
529 m_nextCommands << "run";
530 m_nextCommands << QString("p setvbuf(stdout, 0, %1, 1024)").arg(_IOLBF);
531 QTimer::singleShot(0, this, SLOT(issueNextCommand()));
532 }
533 else if ((m_lastCommand == "kill"))
534 {
535 if ( m_nextCommands.size() > 0 )
536 {
537 if ( !m_nextCommands[0].contains("file") )
538 {
539 m_nextCommands.clear();
540 m_nextCommands << "quit";
541 }
542 // else continue with "ReRun"
543 }
544 else
545 {
546 m_nextCommands << "quit";
547 }
548 m_state = ready;
549 QTimer::singleShot(0, this, SLOT(issueNextCommand()));
550 }
551 // else do nothing
552 }
553 else if ( error.contains( "No line " ) ||
554 error.contains( "No source file named" ) )
555 {
556 // setting a breakpoint failed. Do not continue.
557 m_nextCommands.clear();
558 emit readyForInput( true );
559 }
560 else if ( error.contains( "No stack" ) )
561 {
562 m_nextCommands.clear();
563 emit programEnded();
564 }
565
566 if ((m_lastCommand == "(Q)print *this") && error.contains("No symbol \"this\" in current context.")) {
567 continue;
568 }
569 emit outputError( error + '\n' );
570 }
571 }
572
573void DebugView::issueCommand( QString const& cmd )
574{
575 if( m_state == ready )
576 {
577 emit readyForInput( false );
578 m_state = executingCmd;
579 if (cmd == "(Q)info locals") {
580 m_state = infoLocals;
581 }
582 else if (cmd == "(Q)info args") {
583 m_state = infoArgs;
584 }
585 else if (cmd == "(Q)print *this") {
586 m_state = printThis;
587 }
588 else if (cmd == "(Q)info stack") {
589 m_state = infoStack;
590 }
591 else if (cmd == "(Q)info thread") {
592 emit threadInfo( -1 , false );
593 m_state = infoThreads;
594 }
595 m_subState = normal;
596 m_lastCommand = cmd;
597
598 if ( cmd.startsWith("(Q)") )
599 {
600 m_debugProcess.write( cmd.mid(3).toLocal8Bit() + '\n' );
601 }
602 else {
603 emit outputText( "(gdb) " + cmd + '\n' );
604 m_debugProcess.write( cmd.toLocal8Bit() + '\n' );
605 }
606 }
607}
608
609void DebugView::issueNextCommand()
610{
611 if( m_state == ready )
612 {
613 if( m_nextCommands.size() > 0)
614 {
615 QString cmd = m_nextCommands.takeFirst();
616 //kDebug() << "Next command" << cmd;
617 issueCommand( cmd );
618 }
619 else
620 {
621 // FIXME "thread" needs a better generic solution
622 if (m_debugLocationChanged || m_lastCommand.startsWith("thread")) {
623 m_debugLocationChanged = false;
624 if (m_queryLocals && !m_lastCommand.startsWith("(Q)")) {
625 m_nextCommands << "(Q)info stack";
626 m_nextCommands << "(Q)frame";
627 m_nextCommands << "(Q)info args";
628 m_nextCommands << "(Q)print *this";
629 m_nextCommands << "(Q)info locals";
630 m_nextCommands << "(Q)info thread";
631 issueNextCommand();
632 return;
633 }
634 }
635 emit readyForInput( true );
636 }
637 }
638}
639
640KUrl DebugView::resolveFileName( const QString &fileName )
641{
642 KUrl url;
643
644 //did we end up with an absolute path or a relative one?
645 if ( QFileInfo(fileName).isAbsolute() ) {
646 url.setPath( fileName );
647 url.cleanPath();
648 }
649 else {
650 url.setPath(m_targetConf.workDir);
651 url.addPath(fileName);
652 url.cleanPath();
653
654 if (!QFileInfo(url.path()).exists()) {
655 url.setPath(m_targetConf.executable);
656 url.upUrl(); // get path
657 url.addPath(fileName);
658 url.cleanPath();
659 }
660 // Now, if not found just give up ;)
661 }
662
663 return url;
664}
665
666void DebugView::outputTextMaybe( const QString &text )
667{
668 if ( !m_lastCommand.startsWith( "(Q)" ) && !text.contains( PromptStr ) )
669 {
670 emit outputText( text + '\n' );
671 }
672}
673
674
675void DebugView::slotQueryLocals(bool query)
676{
677 m_queryLocals = query;
678 if ( query && ( m_state == ready ) && ( m_nextCommands.size() == 0 ) )
679 {
680 m_nextCommands << "(Q)info stack";
681 m_nextCommands << "(Q)frame";
682 m_nextCommands << "(Q)info args";
683 m_nextCommands << "(Q)print *this";
684 m_nextCommands << "(Q)info locals";
685 m_nextCommands << "(Q)info thread";
686 issueNextCommand();
687 }
688}
689