Warning, file /kdevelop/kdev-python/debugger/debugsession.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* 0002 SPDX-FileCopyrightText: 2012 Sven Brauch <svenbrauch@googlemail.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include <QTimer> 0008 #include <QApplication> 0009 0010 #include <KLocalizedString> 0011 #include <signal.h> 0012 0013 #include <debugger/framestack/framestackmodel.h> 0014 #include <interfaces/icore.h> 0015 #include <interfaces/idocumentcontroller.h> 0016 #include <util/environmentprofilelist.h> 0017 0018 #include "debugsession.h" 0019 #include "pdbframestackmodel.h" 0020 #include "variablecontroller.h" 0021 #include "variable.h" 0022 #include "breakpointcontroller.h" 0023 0024 #include <QDebug> 0025 #include <QStandardPaths> 0026 #include "debuggerdebug.h" 0027 0028 #ifdef Q_OS_WIN 0029 #include <windows.h> 0030 #define INTERRUPT_DEBUGGER GenerateConsoleCtrlEvent(CTRL_C_EVENT, m_debuggerProcess->processId()) 0031 #else 0032 #define INTERRUPT_DEBUGGER kill(m_debuggerProcess->pid(), SIGINT) 0033 #endif 0034 0035 using namespace KDevelop; 0036 0037 static QByteArray debuggerPrompt = "__KDEVPYTHON_DEBUGGER_PROMPT"; 0038 static QByteArray debuggerOutputBegin = "__KDEVPYTHON_BEGIN_DEBUGGER_OUTPUT>>>"; 0039 static QByteArray debuggerOutputEnd = "<<<__KDEVPYTHON_END___DEBUGGER_OUTPUT"; 0040 0041 namespace Python { 0042 0043 DebugSession::DebugSession(QStringList program, const QUrl &workingDirectory, 0044 const QString& envProfileName) : 0045 IDebugSession() 0046 , m_breakpointController(nullptr) 0047 , m_variableController(nullptr) 0048 , m_frameStackModel(nullptr) 0049 , m_workingDirectory(workingDirectory) 0050 , m_envProfileName(envProfileName) 0051 , m_nextNotifyMethod(nullptr) 0052 , m_inDebuggerData(0) 0053 { 0054 qCDebug(KDEV_PYTHON_DEBUGGER) << "creating debug session"; 0055 m_program = program; 0056 m_breakpointController = new Python::BreakpointController(this); 0057 m_variableController = new VariableController(this); 0058 m_frameStackModel = new PdbFrameStackModel(this); 0059 } 0060 0061 IBreakpointController* DebugSession::breakpointController() const 0062 { 0063 return m_breakpointController; 0064 } 0065 0066 IVariableController* DebugSession::variableController() const 0067 { 0068 return m_variableController; 0069 } 0070 0071 IFrameStackModel* DebugSession::frameStackModel() const 0072 { 0073 return m_frameStackModel; 0074 } 0075 0076 void DebugSession::start() 0077 { 0078 setState(StartingState); 0079 m_debuggerProcess = new KProcess(this); 0080 m_debuggerProcess->setProgram(m_program); 0081 m_debuggerProcess->setOutputChannelMode(KProcess::SeparateChannels); 0082 m_debuggerProcess->blockSignals(true); 0083 m_debuggerProcess->setWorkingDirectory(m_workingDirectory.path()); 0084 0085 const KDevelop::EnvironmentProfileList environmentProfiles(KSharedConfig::openConfig()); 0086 const auto environment = environmentProfiles.variables(m_envProfileName); 0087 0088 QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); 0089 for(auto i = environment.cbegin(); i != environment.cend(); i++ ) 0090 { 0091 env.insert(i.key(), i.value()); 0092 } 0093 m_debuggerProcess->setProcessEnvironment(env); 0094 0095 connect(m_debuggerProcess, &QProcess::readyReadStandardOutput, this, &DebugSession::dataAvailable); 0096 connect(m_debuggerProcess, SIGNAL(finished(int)), this, SLOT(debuggerQuit(int))); 0097 connect(this, &DebugSession::debuggerReady, this, &DebugSession::checkCommandQueue); 0098 connect(this, &DebugSession::commandAdded, this, &DebugSession::checkCommandQueue); 0099 m_debuggerProcess->start(); 0100 m_debuggerProcess->waitForStarted(); 0101 auto dir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, 0102 "kdevpythonsupport/debugger/", QStandardPaths::LocateDirectory); 0103 InternalPdbCommand* path = new InternalPdbCommand(nullptr, nullptr, 0104 "import sys; sys.path.append('"+dir+"')\n"); 0105 InternalPdbCommand* cmd = new InternalPdbCommand(nullptr, nullptr, "import __kdevpython_debugger_utils\n"); 0106 addCommand(path); 0107 addCommand(cmd); 0108 updateLocation(); 0109 m_debuggerProcess->blockSignals(false); 0110 } 0111 0112 void DebugSession::debuggerQuit(int ) 0113 { 0114 setState(EndedState); 0115 } 0116 0117 QStringList byteArrayToStringList(const QByteArray& r) { 0118 QStringList items; 0119 foreach ( const QByteArray& item, r.split('\n') ) { 0120 items << item.data(); 0121 } 0122 if ( r.endsWith('\n') ) { 0123 items.pop_back(); 0124 } 0125 return items; 0126 } 0127 0128 void DebugSession::dataAvailable() 0129 { 0130 QByteArray data = m_debuggerProcess->readAllStandardOutput(); 0131 qCDebug(KDEV_PYTHON_DEBUGGER) << data.length() << "bytes of data available"; 0132 0133 // remove pointless state changes 0134 data.replace(debuggerOutputBegin+debuggerOutputEnd, ""); 0135 data.replace(debuggerOutputEnd+debuggerOutputBegin, ""); 0136 0137 bool endsWithPrompt = false; 0138 if ( data.endsWith(debuggerPrompt) ) { 0139 endsWithPrompt = true; 0140 // remove the prompt 0141 data = data.mid(0, data.length() - debuggerPrompt.length()); 0142 } 0143 0144 // scan the data, and separate program output from debugger output 0145 int len = data.length(); 0146 int delimiterSkip = debuggerOutputEnd.length(); 0147 int i = 0; 0148 QByteArray realData; 0149 while ( i < len ) { 0150 int nextChangeAt = data.indexOf(m_inDebuggerData ? debuggerOutputEnd : debuggerOutputBegin, i); 0151 bool atLastChange = nextChangeAt == -1; 0152 nextChangeAt = atLastChange ? len : qMin(nextChangeAt, len); 0153 0154 0155 qCDebug(KDEV_PYTHON_DEBUGGER) << data; 0156 Q_ASSERT(m_inDebuggerData == 0 || m_inDebuggerData == 1); 0157 0158 if ( m_inDebuggerData == 1 ) { 0159 QString newDebuggerData = data.mid(i, nextChangeAt - i); 0160 m_buffer.append(newDebuggerData); 0161 if ( data.indexOf("Uncaught exception. Entering post mortem debugging") != -1 ) { 0162 emit realDataReceived(QStringList() << "*****" 0163 << " " + i18n("The program being debugged raised an uncaught exception.") 0164 << " " + i18n("You can now inspect the status of the program after it exited.") 0165 << " " + i18n("The debugger will silently stop when the next command is triggered.") 0166 << "*****"); 0167 InternalPdbCommand* cmd = new InternalPdbCommand(nullptr, nullptr, "import __kdevpython_debugger_utils\n"); 0168 addCommand(cmd); 0169 } 0170 } 0171 else if ( m_inDebuggerData == 0 ) { 0172 QByteArray d = data.mid(i, nextChangeAt - i); 0173 if ( d.length() > 0 ) { 0174 realData.append(d); 0175 } 0176 } 0177 0178 i = nextChangeAt + delimiterSkip; 0179 if ( m_inDebuggerData != 1 ) m_inDebuggerData = 1; 0180 else m_inDebuggerData = 0; 0181 0182 if ( atLastChange ) { 0183 break; 0184 } 0185 } 0186 0187 while (int index = realData.indexOf(debuggerPrompt) != -1 ) { 0188 realData.remove(index-1, debuggerPrompt.length()); 0189 } 0190 if ( ! realData.isEmpty() ) { 0191 // FIXME this is not very elegant. 0192 QStringList items = byteArrayToStringList(realData); 0193 emit realDataReceived(items); 0194 } 0195 0196 // Although unbuffered, it seems guaranteed that the debugger prompt is written at once. 0197 // I don't think a python statement like print "FooBar" will ever break the output into two parts. 0198 // TODO find explicit documentation for this somewhere. 0199 if ( endsWithPrompt ) { 0200 if ( state() == StartingState ) { 0201 setState(PausedState); 0202 raiseEvent(connected_to_program); 0203 } 0204 else { 0205 notifyNext(); 0206 if ( m_commandQueue.isEmpty() ) { 0207 qCDebug(KDEV_PYTHON_DEBUGGER) << "Changing state to PausedState"; 0208 setState(PausedState); 0209 } 0210 } 0211 m_processBusy = false; 0212 emit debuggerReady(); 0213 } 0214 0215 data = m_debuggerProcess->readAllStandardError(); 0216 if ( ! data.isEmpty() ) { 0217 emit stderrReceived(byteArrayToStringList(data)); 0218 } 0219 } 0220 0221 void DebugSession::setNotifyNext(QPointer<QObject> object, const char* method) 0222 { 0223 qCDebug(KDEV_PYTHON_DEBUGGER) << "set notify next:" << object << method; 0224 m_nextNotifyObject = object; 0225 m_nextNotifyMethod = method; 0226 } 0227 0228 void DebugSession::notifyNext() 0229 { 0230 qCDebug(KDEV_PYTHON_DEBUGGER) << "notify next:" << m_nextNotifyObject << this; 0231 if ( m_nextNotifyMethod && m_nextNotifyObject ) { 0232 QMetaObject::invokeMethod(m_nextNotifyObject.data(), m_nextNotifyMethod, 0233 Qt::DirectConnection, Q_ARG(QByteArray, m_buffer)); 0234 } 0235 else { 0236 qCDebug(KDEV_PYTHON_DEBUGGER) << "notify called, but nothing to notify!"; 0237 } 0238 m_buffer.clear(); 0239 m_nextNotifyMethod = nullptr; 0240 m_nextNotifyObject.clear(); 0241 } 0242 0243 void DebugSession::processNextCommand() 0244 { 0245 qCDebug(KDEV_PYTHON_DEBUGGER) << "processing next debugger command in queue"; 0246 if ( m_processBusy || m_state == EndedState ) { 0247 qCDebug(KDEV_PYTHON_DEBUGGER) << "process is busy or ended, aborting"; 0248 return; 0249 } 0250 m_processBusy = true; 0251 PdbCommand* cmd = m_commandQueue.first(); 0252 Q_ASSERT(cmd->type() != PdbCommand::InvalidType); 0253 if ( cmd->type() == PdbCommand::UserType ) { 0254 setState(ActiveState); 0255 } 0256 m_commandQueue.removeFirst(); 0257 setNotifyNext(cmd->notifyObject(), cmd->notifyMethod()); 0258 cmd->run(this); 0259 qCDebug(KDEV_PYTHON_DEBUGGER) << "command executed, deleting it."; 0260 delete cmd; 0261 if ( ! m_commandQueue.isEmpty() ) { 0262 processNextCommand(); 0263 } 0264 } 0265 0266 void DebugSession::setState(DebuggerState state) 0267 { 0268 qCDebug(KDEV_PYTHON_DEBUGGER) << "Setting state to" << state; 0269 0270 if ( state == m_state ) { 0271 return; 0272 } 0273 m_state = state; 0274 if ( m_state == EndedState ) { 0275 raiseEvent(debugger_exited); 0276 emit finished(); 0277 } 0278 else if ( m_state == ActiveState || m_state == StartingState || m_state == StoppingState ) { 0279 raiseEvent(debugger_busy); 0280 } 0281 else if ( m_state == PausedState ) { 0282 raiseEvent(debugger_ready); 0283 if ( currentUrl().isValid() ) { 0284 emit showStepInSource(currentUrl(), currentLine(), currentAddr()); 0285 } 0286 } 0287 0288 qCDebug(KDEV_PYTHON_DEBUGGER) << "debugger state changed to" << m_state; 0289 raiseEvent(program_state_changed); 0290 emit stateChanged(m_state); 0291 } 0292 0293 void DebugSession::write(const QByteArray& cmd) 0294 { 0295 qCDebug(KDEV_PYTHON_DEBUGGER) << " >>> WRITE:" << cmd; 0296 m_debuggerProcess->write(cmd); 0297 } 0298 0299 void DebugSession::stepOut() 0300 { 0301 // TODO this only steps out of functions; use temporary breakpoints for loops maybe? 0302 addSimpleUserCommand("return"); 0303 } 0304 0305 void DebugSession::stepOverInstruction() 0306 { 0307 addSimpleUserCommand("next"); 0308 } 0309 0310 void DebugSession::stepInto() 0311 { 0312 addSimpleUserCommand("step"); 0313 } 0314 0315 void DebugSession::stepIntoInstruction() 0316 { 0317 addSimpleUserCommand("step"); 0318 } 0319 0320 void DebugSession::stepOver() 0321 { 0322 addSimpleUserCommand("next"); 0323 } 0324 0325 void DebugSession::jumpToCursor() 0326 { 0327 if (KDevelop::IDocument* doc = KDevelop::ICore::self()->documentController()->activeDocument()) { 0328 KTextEditor::Cursor cursor = doc->cursorPosition(); 0329 if ( cursor.isValid() ) { 0330 // TODO disable all other breakpoints 0331 addSimpleUserCommand(QString("jump " + QString::number(cursor.line() + 1)).toUtf8()); 0332 } 0333 } 0334 } 0335 0336 void DebugSession::runToCursor() 0337 { 0338 if (KDevelop::IDocument* doc = KDevelop::ICore::self()->documentController()->activeDocument()) { 0339 KTextEditor::Cursor cursor = doc->cursorPosition(); 0340 if ( cursor.isValid() ) { 0341 // TODO disable all other breakpoints 0342 QString temporaryBreakpointLocation = doc->url().path() + ':' + QString::number(cursor.line() + 1); 0343 InternalPdbCommand* temporaryBreakpointCmd = new InternalPdbCommand(nullptr, nullptr, "tbreak " + temporaryBreakpointLocation + '\n'); 0344 addCommand(temporaryBreakpointCmd); 0345 addSimpleInternalCommand("continue"); 0346 updateLocation(); 0347 } 0348 } 0349 } 0350 0351 void DebugSession::run() 0352 { 0353 addSimpleUserCommand("continue"); 0354 } 0355 0356 void DebugSession::interruptDebugger() 0357 { 0358 INTERRUPT_DEBUGGER; 0359 updateLocation(); 0360 setState(PausedState); 0361 } 0362 0363 void DebugSession::addCommand(PdbCommand* cmd) 0364 { 0365 if ( m_state == EndedState || m_state == StoppingState ) { 0366 return; 0367 } 0368 qCDebug(KDEV_PYTHON_DEBUGGER) << " +++ adding command to queue:" << cmd; 0369 m_commandQueue.append(cmd); 0370 if ( cmd->type() == PdbCommand::UserType ) { 0371 // this is queued and will run after the command is executed. 0372 updateLocation(); 0373 } 0374 emit commandAdded(); 0375 } 0376 0377 void DebugSession::checkCommandQueue() 0378 { 0379 qCDebug(KDEV_PYTHON_DEBUGGER) << "items in queue:" << m_commandQueue.length(); 0380 if ( m_commandQueue.isEmpty() ) { 0381 return; 0382 } 0383 processNextCommand(); 0384 } 0385 0386 void DebugSession::clearObjectTable() 0387 { 0388 addSimpleInternalCommand("__kdevpython_debugger_utils.cleanup()"); 0389 } 0390 0391 void DebugSession::addSimpleUserCommand(const QString& cmd) 0392 { 0393 clearObjectTable(); 0394 UserPdbCommand* cmdObject = new UserPdbCommand(nullptr, nullptr, cmd + '\n'); 0395 Q_ASSERT(cmdObject->type() == PdbCommand::UserType); 0396 addCommand(cmdObject); 0397 } 0398 0399 void DebugSession::addSimpleInternalCommand(const QString& cmd) 0400 { 0401 Q_ASSERT( ! cmd.endsWith('\n') ); 0402 InternalPdbCommand* cmdObject = new InternalPdbCommand(nullptr, nullptr, cmd + '\n'); 0403 addCommand(cmdObject); 0404 } 0405 0406 void DebugSession::runImmediately(const QString& cmd) 0407 { 0408 Q_ASSERT(cmd.endsWith('\n')); 0409 if ( state() == ActiveState ) { 0410 m_nextNotifyMethod = nullptr; 0411 m_nextNotifyObject.clear(); // TODO is this correct? 0412 qCDebug(KDEV_PYTHON_DEBUGGER) << "interrupting debugger"; 0413 INTERRUPT_DEBUGGER; 0414 write(cmd.toUtf8()); 0415 write("continue\n"); 0416 updateLocation(); 0417 } 0418 else { 0419 addCommand(new InternalPdbCommand(nullptr, nullptr, cmd)); 0420 } 0421 } 0422 0423 void DebugSession::addBreakpoint(Breakpoint* bp) 0424 { 0425 QString location = bp->url().path() + ":" + QString::number(bp->line() + 1); 0426 qCDebug(KDEV_PYTHON_DEBUGGER) << "adding breakpoint" << location; 0427 runImmediately("break " + location + '\n'); 0428 } 0429 0430 void DebugSession::removeBreakpoint(Breakpoint* bp) 0431 { 0432 QString location = bp->url().path() + ":" + QString::number(bp->line() + 1); 0433 qCDebug(KDEV_PYTHON_DEBUGGER) << "deleting breakpoint" << location; 0434 runImmediately("clear " + location + '\n'); 0435 } 0436 0437 void DebugSession::createVariable(Python::Variable* variable, QObject* callback, const char* callbackMethod) 0438 { 0439 qCDebug(KDEV_PYTHON_DEBUGGER) << "asked to create variable"; 0440 auto text = ("print(__kdevpython_debugger_utils.obj_to_string(" + variable->expression() + "))\n").toUtf8(); 0441 auto cmd = new InternalPdbCommand(variable, "dataFetched", text); 0442 variable->m_notifyCreated = callback; 0443 variable->m_notifyCreatedMethod = callbackMethod; 0444 addCommand(cmd); 0445 } 0446 0447 void DebugSession::clearOutputBuffer() 0448 { 0449 m_buffer.clear(); 0450 } 0451 0452 void DebugSession::updateLocation() 0453 { 0454 qCDebug(KDEV_PYTHON_DEBUGGER) << "updating location"; 0455 InternalPdbCommand* cmd = new InternalPdbCommand(this, "locationUpdateReady", "where\n"); 0456 addCommand(cmd); 0457 } 0458 0459 void DebugSession::locationUpdateReady(QByteArray data) { 0460 qCDebug(KDEV_PYTHON_DEBUGGER) << "Got where information: " << data; 0461 QList<QByteArray> lines = data.split('\n'); 0462 if ( lines.length() >= 3 ) { 0463 lines.removeLast(); // prompt 0464 lines.removeLast(); // source line 0465 QString where = lines.last(); 0466 // > /bar/baz/foo.py(123)<module>() 0467 QRegExp m("^> (/.*\\.py)\\((\\d*)\\).*$"); 0468 m.setMinimal(true); 0469 m.exactMatch(where); 0470 setCurrentPosition(QUrl::fromLocalFile(m.capturedTexts().at(1)), m.capturedTexts().at(2).toInt() - 1 , "<unknown>"); 0471 qCDebug(KDEV_PYTHON_DEBUGGER) << "New position: " << m.capturedTexts().at(1) << m.capturedTexts().at(2).toInt() - 1 << m.capturedTexts() << where; 0472 } 0473 } 0474 0475 void DebugSession::stopDebugger() 0476 { 0477 m_commandQueue.clear(); 0478 InternalPdbCommand* cmd = new InternalPdbCommand(nullptr, nullptr, "quit\nquit\n"); 0479 addCommand(cmd); 0480 setState(StoppingState); 0481 if ( ! m_debuggerProcess->waitForFinished(200) ) { 0482 m_debuggerProcess->kill(); 0483 } 0484 qCDebug(KDEV_PYTHON_DEBUGGER) << "stopped debugger"; 0485 finalizeState(); 0486 } 0487 0488 void DebugSession::killDebuggerNow() 0489 { 0490 qCDebug(KDEV_PYTHON_DEBUGGER) << "killing debugger now"; 0491 m_debuggerProcess->kill(); 0492 finalizeState(); 0493 } 0494 0495 void DebugSession::finalizeState() 0496 { 0497 m_commandQueue.clear(); 0498 m_nextNotifyMethod = nullptr; 0499 m_nextNotifyObject.clear(); 0500 setState(IDebugSession::EndedState); 0501 } 0502 0503 DebugSession::~DebugSession() 0504 { 0505 m_debuggerProcess->kill(); 0506 } 0507 0508 void DebugSession::restartDebugger() 0509 { 0510 addSimpleUserCommand("run"); 0511 } 0512 0513 bool DebugSession::restartAvaliable() const 0514 { 0515 return false; 0516 } 0517 0518 KDevelop::IDebugSession::DebuggerState DebugSession::state() const 0519 { 0520 return m_state; 0521 } 0522 0523 0524 } 0525 0526 #include "moc_debugsession.cpp"