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"