diff --git a/kdevplatform/shell/tests/test_documentcontroller.cpp b/kdevplatform/shell/tests/test_documentcontroller.cpp index d833e959cf..54603f7939 100644 --- a/kdevplatform/shell/tests/test_documentcontroller.cpp +++ b/kdevplatform/shell/tests/test_documentcontroller.cpp @@ -1,197 +1,197 @@ /* Unit tests for DocumentController.* Copyright 2011 Damien Flament This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "test_documentcontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; void TestDocumentController::initTestCase() { - AutoTestShell::init(); + AutoTestShell::init({{}}); // do not load plugins at all TestCore::initialize(); Core::self()->languageController()->backgroundParser()->disableProcessing(); m_subject = Core::self()->documentController(); } void TestDocumentController::init() { Core::self()->documentControllerInternal()->initialize(); // create temp files m_file1.setFileTemplate(m_tempDir.path() + "/tmp_XXXXXX.txt"); m_file2.setFileTemplate(m_tempDir.path() + "/tmp_XXXXXX.txt"); if(!m_file1.open() || !m_file2.open()) { QFAIL("Can't create temp files"); } // pre-conditions QVERIFY(m_subject->openDocuments().empty()); QVERIFY(m_subject->documentForUrl(QUrl()) == nullptr); QVERIFY(m_subject->activeDocument() == nullptr); } void TestDocumentController::cleanup() { // ensure there are not opened documents for next test foreach(IDocument* document, m_subject->openDocuments()) { document->close(IDocument::Discard); } Core::self()->documentControllerInternal()->cleanup(); } void TestDocumentController::cleanupTestCase() { TestCore::shutdown(); m_tempDir.remove(); } void TestDocumentController::testOpeningNewDocumentFromText() { qRegisterMetaType("KDevelop::IDocument*"); QSignalSpy createdSpy(m_subject, SIGNAL(textDocumentCreated(KDevelop::IDocument*))); QVERIFY(createdSpy.isValid()); QSignalSpy openedSpy(m_subject, SIGNAL(documentOpened(KDevelop::IDocument*))); QVERIFY(openedSpy.isValid()); IDocument* document = m_subject->openDocumentFromText(QLatin1String("")); QVERIFY(document != nullptr); QCOMPARE(createdSpy.count(), 1); QCOMPARE(openedSpy.count(), 1); QVERIFY(!m_subject->openDocuments().empty()); QVERIFY(m_subject->documentForUrl(document->url()) == document); QVERIFY(m_subject->activeDocument() == document); } void TestDocumentController::testOpeningDocumentFromUrl() { QUrl url = QUrl::fromLocalFile(m_file1.fileName()); IDocument* document = m_subject->openDocument(url); QVERIFY(document != nullptr); } void TestDocumentController::testSaveSomeDocuments() { // create documents QTemporaryDir dir; IDocument *document1 = m_subject->openDocument(createFile(dir, QStringLiteral("foo"))); IDocument *document2 = m_subject->openDocument(createFile(dir, QStringLiteral("bar"))); QCOMPARE(document1->state(), IDocument::Clean); QCOMPARE(document2->state(), IDocument::Clean); // edit both documents document1->textDocument()->insertText(KTextEditor::Cursor(), QStringLiteral("some text")); document2->textDocument()->insertText(KTextEditor::Cursor(), QStringLiteral("some text")); QCOMPARE(document1->state(), IDocument::Modified); QCOMPARE(document2->state(), IDocument::Modified); // save one document (Silent == don't ask user) m_subject->saveSomeDocuments(QList() << document1, IDocument::Silent); QCOMPARE(document1->state(), IDocument::Clean); QCOMPARE(document2->state(), IDocument::Modified); } void TestDocumentController::testSaveAllDocuments() { // create documents QTemporaryDir dir; IDocument *document1 = m_subject->openDocument(createFile(dir, QStringLiteral("foo"))); IDocument *document2 = m_subject->openDocument(createFile(dir, QStringLiteral("bar"))); QCOMPARE(document1->state(), IDocument::Clean); QCOMPARE(document2->state(), IDocument::Clean); // edit documents document1->textDocument()->insertText(KTextEditor::Cursor(), QStringLiteral("some text")); document2->textDocument()->insertText(KTextEditor::Cursor(), QStringLiteral("some text")); QCOMPARE(document1->state(), IDocument::Modified); QCOMPARE(document2->state(), IDocument::Modified); // save documents m_subject->saveAllDocuments(IDocument::Silent); QCOMPARE(document1->state(), IDocument::Clean); QCOMPARE(document2->state(), IDocument::Clean); } void TestDocumentController::testCloseAllDocuments() { // create documents m_subject->openDocumentFromText(QLatin1String("")); m_subject->openDocumentFromText(QLatin1String("")); QVERIFY(!m_subject->openDocuments().empty()); m_subject->closeAllDocuments(); QVERIFY(m_subject->openDocuments().empty()); } QUrl TestDocumentController::createFile(const QTemporaryDir& dir, const QString& filename) { QFile file(dir.path() + filename); bool success = file.open(QIODevice::WriteOnly | QIODevice::Text); if(!success) { QWARN(QString("Failed to create file: " + dir.path() + filename).toLatin1().data()); return QUrl(); } file.close(); return QUrl::fromLocalFile(dir.path() + filename); } void TestDocumentController::testEmptyUrl() { const auto first = DocumentController::nextEmptyDocumentUrl(); QVERIFY(DocumentController::isEmptyDocumentUrl(first)); QCOMPARE(DocumentController::nextEmptyDocumentUrl(), first); auto doc = m_subject->openDocumentFromText(QString()); QCOMPARE(doc->url(), first); const auto second = DocumentController::nextEmptyDocumentUrl(); QVERIFY(first != second); QVERIFY(DocumentController::isEmptyDocumentUrl(second)); QVERIFY(!DocumentController::isEmptyDocumentUrl(QUrl())); QVERIFY(!DocumentController::isEmptyDocumentUrl(QUrl(QStringLiteral("http://foo.org")))); QVERIFY(!DocumentController::isEmptyDocumentUrl(QUrl(QStringLiteral("http://foo.org/test")))); QVERIFY(!DocumentController::isEmptyDocumentUrl(QUrl::fromLocalFile(QStringLiteral("/")))); QVERIFY(!DocumentController::isEmptyDocumentUrl(QUrl::fromLocalFile(QStringLiteral("/test")))); } QTEST_MAIN(TestDocumentController); diff --git a/plugins/debuggercommon/CMakeLists.txt b/plugins/debuggercommon/CMakeLists.txt index 2bd2795ea2..f6b395f592 100644 --- a/plugins/debuggercommon/CMakeLists.txt +++ b/plugins/debuggercommon/CMakeLists.txt @@ -1,91 +1,92 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevdebuggercommon\") set(debuggercommon_SRCS mi/mi.cpp mi/milexer.cpp mi/miparser.cpp mi/micommand.cpp mi/micommandqueue.cpp + dbgglobal.cpp dialogs/selectcoredialog.cpp # debug session & debugger midebugger.cpp midebugsession.cpp midebuggerplugin.cpp midebugjobs.cpp # controllers mibreakpointcontroller.cpp miframestackmodel.cpp mivariablecontroller.cpp mivariable.cpp stringhelpers.cpp stty.cpp # tool views widgets/debuggerconsoleview.cpp widgets/disassemblewidget.cpp # register registers/registersview.cpp registers/registercontroller.cpp registers/registersmanager.cpp registers/registercontroller_x86.cpp registers/registercontroller_arm.cpp registers/modelsmanager.cpp registers/converters.cpp ) if(KF5SysGuard_FOUND) list(APPEND debuggercommon_SRCS dialogs/processselection.cpp ) endif() ecm_qt_declare_logging_category(debuggercommon_SRCS HEADER debuglog.h IDENTIFIER DEBUGGERCOMMON CATEGORY_NAME "kdevelop.plugins.common" ) ki18n_wrap_ui(debuggercommon_SRCS dialogs/selectcoredialog.ui widgets/debuggerconsoleview.ui widgets/selectaddressdialog.ui registers/registersview.ui ) # Use old behavior (ignore the visibility properties for static libraries, object # libraries, and executables without exports) on target kdevdebuggercommon (so the # default public visibility is used). # kdevdebuggercommon is used by target test_gdb which is added by ecm_add_test, # which doesn't set CMP0063 so old behavior is used. # If kdevdebuggercommon honors visibility properties (set to hidden), it will cause # linker warnings about direct access to global weak symbol when link against test_gdb if(NOT CMAKE_VERSION VERSION_LESS "3.3") cmake_policy(SET CMP0063 OLD) endif() add_library(kdevdebuggercommon STATIC ${debuggercommon_SRCS}) target_link_libraries(kdevdebuggercommon PUBLIC KDev::Debugger KDev::OutputView KDev::Sublime PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets KDev::Util KDev::Language KDev::IExecute ) target_include_directories(kdevdebuggercommon PUBLIC ${CMAKE_CURRENT_BINARY_DIR} ) if(KF5SysGuard_FOUND) target_link_libraries(kdevdebuggercommon PUBLIC KF5::ProcessUi ) endif() kde_target_enable_exceptions(kdevdebuggercommon PUBLIC) if(BUILD_TESTING) add_subdirectory(tests) endif() diff --git a/plugins/debuggercommon/dbgglobal.cpp b/plugins/debuggercommon/dbgglobal.cpp new file mode 100644 index 0000000000..7d1982848f --- /dev/null +++ b/plugins/debuggercommon/dbgglobal.cpp @@ -0,0 +1,22 @@ +/* This file is part of the KDE project + Copyright (C) 2018 Kevin Funk + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. +*/ + +#include "dbgglobal.h" + +#include "moc_dbgglobal.cpp" diff --git a/plugins/debuggercommon/dbgglobal.h b/plugins/debuggercommon/dbgglobal.h index 2744e2f8fe..9cf0ca9e31 100644 --- a/plugins/debuggercommon/dbgglobal.h +++ b/plugins/debuggercommon/dbgglobal.h @@ -1,90 +1,97 @@ /*************************************************************************** dbgglobal.h ------------------- begin : Sun Aug 8 1999 copyright : (C) 1999 by John Birch email : jbb@kdevelop.org ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef _DBGGLOBAL_H_ #define _DBGGLOBAL_H_ +#include #include namespace KDevMI { +#if QT_VERSION >= QT_VERSION_CHECK(5,8,0) +Q_NAMESPACE +#endif enum DBGStateFlag { s_none = 0, s_dbgNotStarted = 1 << 0, s_appNotStarted = 1 << 1, s_programExited = 1 << 2, s_attached = 1 << 3, s_core = 1 << 4, /// Set when 'slotStopDebugger' started executing, to avoid /// entering that function several times. s_shuttingDown = 1 << 6, s_dbgBusy = 1 << 8, s_appRunning = 1 << 9, /// Set when we suspect GDB to be in a state where it does not listen for new commands /// while the inferior is running s_dbgNotListening = 1 << 10, s_interruptSent = 1 << 11, /// Once GDB is completely idle, send an automatic ExecContinue to resume from an interruption /// by CmdImmediately commands s_automaticContinue = 1 << 12, /// Set when the debugger failed to start s_dbgFailedStart = 1 << 13, }; +#if QT_VERSION >= QT_VERSION_CHECK(5,8,0) +Q_ENUM_NS(DBGStateFlag) +#endif Q_DECLARE_FLAGS(DBGStateFlags, DBGStateFlag) enum DataType { typeUnknown, typeValue, typePointer, typeReference, typeStruct, typeArray, typeQString, typeWhitespace, typeName }; // FIXME: find a more appropriate place for these strings. Possibly a place specific to debugger backend namespace Config { static const char StartWithEntry[] = "Start With"; // FIXME: break on start isn't exposed in the UI for GDB static const char BreakOnStartEntry[] = "Break on Start"; } namespace GDB { namespace Config { static const char GdbPathEntry[] = "GDB Path"; static const char DebuggerShellEntry[] = "Debugger Shell"; static const char RemoteGdbConfigEntry[] = "Remote GDB Config Script"; static const char RemoteGdbShellEntry[] = "Remote GDB Shell Script"; static const char RemoteGdbRunEntry[] = "Remote GDB Run Script"; static const char StaticMembersEntry[] = "Display Static Members"; static const char DemangleNamesEntry[] = "Display Demangle Names"; static const char AllowForcedBPEntry[] = "Allow Forced Breakpoint Set"; } } namespace LLDB { namespace Config { static const char LldbExecutableEntry[] = "LLDB Executable"; static const char LldbArgumentsEntry[] = "LLDB Arguments"; static const char LldbEnvironmentEntry[] = "LLDB Environment"; static const char LldbInheritSystemEnvEntry[] = "LLDB Inherit System Env"; static const char LldbConfigScriptEntry[] = "LLDB Config Script"; static const char LldbRemoteDebuggingEntry[] = "LLDB Remote Debugging"; static const char LldbRemoteServerEntry[] = "LLDB Remote Server"; static const char LldbRemotePathEntry[] = "LLDB Remote Path"; } } } // end of namespace KDevMI Q_DECLARE_OPERATORS_FOR_FLAGS(KDevMI::DBGStateFlags) #endif // _DBGGLOBAL_H_ diff --git a/plugins/debuggercommon/midebugger.cpp b/plugins/debuggercommon/midebugger.cpp index d46f8f7c10..0ebb4cd22e 100644 --- a/plugins/debuggercommon/midebugger.cpp +++ b/plugins/debuggercommon/midebugger.cpp @@ -1,371 +1,373 @@ /* * Low level GDB interface. * * Copyright 1999 John Birch * Copyright 2007 Vladimir Prus * Copyright 2016 Aetf * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "midebugger.h" #include "debuglog.h" #include "mi/micommand.h" #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #endif // #define DEBUG_NO_TRY //to get a backtrace to where the exception was thrown using namespace KDevMI; using namespace KDevMI::MI; MIDebugger::MIDebugger(QObject* parent) : QObject(parent) , m_process(nullptr) , m_currentCmd(nullptr) { m_process = new KProcess(this); m_process->setOutputChannelMode(KProcess::SeparateChannels); connect(m_process, &KProcess::readyReadStandardOutput, this, &MIDebugger::readyReadStandardOutput); connect(m_process, &KProcess::readyReadStandardError, this, &MIDebugger::readyReadStandardError); connect(m_process, static_cast(&KProcess::finished), this, &MIDebugger::processFinished); connect(m_process, static_cast(&KProcess::error), this, &MIDebugger::processErrored); } MIDebugger::~MIDebugger() { // prevent Qt warning: QProcess: Destroyed while process is still running. if (m_process && m_process->state() == QProcess::Running) { disconnect(m_process, static_cast(&KProcess::error), this, &MIDebugger::processErrored); m_process->kill(); m_process->waitForFinished(10); } } void MIDebugger::execute(MICommand* command) { m_currentCmd = command; QString commandText = m_currentCmd->cmdToSend(); qCDebug(DEBUGGERCOMMON) << "SEND:" << commandText.trimmed(); QByteArray commandUtf8 = commandText.toUtf8(); m_process->write(commandUtf8, commandUtf8.length()); command->markAsSubmitted(); QString prettyCmd = m_currentCmd->cmdToSend(); prettyCmd.remove( QRegExp("set prompt \032.\n") ); prettyCmd = "(gdb) " + prettyCmd; if (m_currentCmd->isUserCommand()) emit userCommandOutput(prettyCmd); else emit internalCommandOutput(prettyCmd); } bool MIDebugger::isReady() const { return m_currentCmd == nullptr; } void MIDebugger::interrupt() { #ifndef Q_OS_WIN int pid = m_process->pid(); if (pid != 0) { ::kill(pid, SIGINT); } #else SetConsoleCtrlHandler(nullptr, true); GenerateConsoleCtrlEvent(0, 0); SetConsoleCtrlHandler(nullptr, false); #endif } MICommand* MIDebugger::currentCommand() const { return m_currentCmd; } void MIDebugger::kill() { m_process->kill(); } void MIDebugger::readyReadStandardOutput() { m_process->setReadChannel(QProcess::StandardOutput); m_buffer += m_process->readAll(); for (;;) { /* In MI mode, all messages are exactly one line. See if we have any complete lines in the buffer. */ int i = m_buffer.indexOf('\n'); if (i == -1) break; QByteArray reply(m_buffer.left(i)); m_buffer = m_buffer.mid(i+1); processLine(reply); } } void MIDebugger::readyReadStandardError() { m_process->setReadChannel(QProcess::StandardError); emit debuggerInternalOutput(QString::fromUtf8(m_process->readAll())); } void MIDebugger::processLine(const QByteArray& line) { - qCDebug(DEBUGGERCOMMON) << "Debugger (" << m_process->pid() <<") output: " << line; + if (line != "(gdb) ") { + qCDebug(DEBUGGERCOMMON) << "Debugger output (pid =" << m_process->pid() << "): " << line; + } FileSymbol file; file.contents = line; std::unique_ptr r(m_parser.parse(&file)); if (!r) { // simply ignore the invalid MI message because both gdb and lldb // sometimes produces invalid messages that can be safely ignored. qCDebug(DEBUGGERCOMMON) << "Invalid MI message:" << line; // We don't consider the current command done. // So, if a command results in unparseable reply, // we'll just wait for the "right" reply, which might // never come. However, marking the command as // done in this case is even more risky. // It's probably possible to get here if we're debugging // natively without PTY, though this is uncommon case. return; } #ifndef DEBUG_NO_TRY try { #endif switch(r->kind) { case MI::Record::Result: { MI::ResultRecord& result = static_cast(*r); // it's still possible for the user to issue a MI command, // emit correct signal if (m_currentCmd && m_currentCmd->isUserCommand()) { emit userCommandOutput(QString::fromUtf8(line) + '\n'); } else { emit internalCommandOutput(QString::fromUtf8(line) + '\n'); } // protect against wild replies that sometimes returned from gdb without a pending command if (!m_currentCmd) { qCWarning(DEBUGGERCOMMON) << "Received a result without a pending command"; throw std::runtime_error("Received a result without a pending command"); } else if (m_currentCmd->token() != result.token) { std::stringstream ss; ss << "Received a result with token not matching pending command. " << "Pending: " << m_currentCmd->token() << "Received: " << result.token; qCWarning(DEBUGGERCOMMON) << ss.str().c_str(); throw std::runtime_error(ss.str()); } // GDB doc: "running" and "exit" are status codes equivalent to "done" if (result.reason == QLatin1String("done") || result.reason == QLatin1String("running") || result.reason == QLatin1String("exit")) { qCDebug(DEBUGGERCOMMON) << "Result token is" << result.token; m_currentCmd->markAsCompleted(); qCDebug(DEBUGGERCOMMON) << "Command successful, times " << m_currentCmd->totalProcessingTime() << m_currentCmd->queueTime() << m_currentCmd->gdbProcessingTime(); m_currentCmd->invokeHandler(result); } else if (result.reason == QLatin1String("error")) { qCDebug(DEBUGGERCOMMON) << "Handling error"; m_currentCmd->markAsCompleted(); qCDebug(DEBUGGERCOMMON) << "Command error, times" << m_currentCmd->totalProcessingTime() << m_currentCmd->queueTime() << m_currentCmd->gdbProcessingTime(); // Some commands want to handle errors themself. if (m_currentCmd->handlesError() && m_currentCmd->invokeHandler(result)) { qCDebug(DEBUGGERCOMMON) << "Invoked custom handler\n"; // Done, nothing more needed } else emit error(result); } else { qCDebug(DEBUGGERCOMMON) << "Unhandled result code: " << result.reason; } delete m_currentCmd; m_currentCmd = nullptr; emit ready(); break; } case MI::Record::Async: { MI::AsyncRecord& async = static_cast(*r); switch (async.subkind) { case MI::AsyncRecord::Exec: { // Prefix '*'; asynchronous state changes of the target if (async.reason == QLatin1String("stopped")) { emit programStopped(async); } else if (async.reason == QLatin1String("running")) { emit programRunning(); } else { qCDebug(DEBUGGERCOMMON) << "Unhandled exec notification: " << async.reason; } break; } case MI::AsyncRecord::Notify: { // Prefix '='; supplementary information that we should handle (new breakpoint etc.) emit notification(async); break; } case MI::AsyncRecord::Status: { // Prefix '+'; GDB documentation: // On-going status information about progress of a slow operation; may be ignored break; } } break; } case MI::Record::Stream: { MI::StreamRecord& s = static_cast(*r); if (s.subkind == MI::StreamRecord::Target) { emit applicationOutput(s.message); } else if (s.subkind == MI::StreamRecord::Console) { if (m_currentCmd && m_currentCmd->isUserCommand()) emit userCommandOutput(s.message); else emit internalCommandOutput(s.message); if (m_currentCmd) m_currentCmd->newOutput(s.message); } else { emit debuggerInternalOutput(s.message); } emit streamRecord(s); break; } case MI::Record::Prompt: break; } #ifndef DEBUG_NO_TRY } catch(const std::exception& e) { KMessageBox::detailedSorry( qApp->activeWindow(), i18nc("Internal debugger error", "

The debugger component encountered internal error while " "processing reply from gdb. Please submit a bug report. " "The debug session will now end to prevent potential crash"), i18n("The exception is: %1\n" "The MI response is: %2", e.what(), QString::fromLatin1(line)), i18n("Internal debugger error")); emit exited(true, e.what()); } #endif } void MIDebugger::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { qCDebug(DEBUGGERCOMMON) << "Debugger FINISHED\n"; bool abnormal = exitCode != 0 || exitStatus != QProcess::NormalExit; emit userCommandOutput(QStringLiteral("Process exited\n")); emit exited(abnormal, i18n("Process exited")); } void MIDebugger::processErrored(QProcess::ProcessError error) { qCDebug(DEBUGGERCOMMON) << "Debugger ERRORED" << error; if(error == QProcess::FailedToStart) { KMessageBox::information( qApp->activeWindow(), i18n("Could not start debugger." "

Could not run '%1'. " "Make sure that the path name is specified correctly.", m_debuggerExecutable), i18n("Could not start debugger")); emit userCommandOutput(QStringLiteral("Process failed to start\n")); emit exited(true, i18n("Process failed to start")); } else if (error == QProcess::Crashed) { KMessageBox::error( qApp->activeWindow(), i18n("Debugger crashed." "

The debugger process '%1' crashed.
" "Because of that the debug session has to be ended.
" "Try to reproduce the crash without KDevelop and report a bug.
", m_debuggerExecutable), i18n("Debugger crashed")); emit userCommandOutput(QStringLiteral("Process crashed\n")); emit exited(true, i18n("Process crashed")); } } diff --git a/plugins/debuggercommon/midebugsession.cpp b/plugins/debuggercommon/midebugsession.cpp index feec2456e6..4bf0abebe0 100644 --- a/plugins/debuggercommon/midebugsession.cpp +++ b/plugins/debuggercommon/midebugsession.cpp @@ -1,1317 +1,1316 @@ /* * Common code for debugger support * * Copyright 1999-2001 John Birch * Copyright 2001 by Bernd Gehrmann * Copyright 2006 Vladimir Prus * Copyright 2007 Hamish Rodda * Copyright 2009 Niko Sams * Copyright 2016 Aetf * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "midebugsession.h" #include "debuglog.h" #include "midebugger.h" #include "mivariable.h" #include "mi/mi.h" #include "mi/micommand.h" #include "mi/micommandqueue.h" #include "stty.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; using namespace KDevMI; using namespace KDevMI::MI; MIDebugSession::MIDebugSession(MIDebuggerPlugin *plugin) : m_procLineMaker(new ProcessLineMaker(this)) , m_commandQueue(new CommandQueue) , m_sessionState(NotStartedState) , m_debugger(nullptr) , m_debuggerState(s_dbgNotStarted | s_appNotStarted) , m_stateReloadInProgress(false) , m_stateReloadNeeded(false) , m_tty(nullptr) , m_hasCrashed(false) , m_sourceInitFile(true) , m_plugin(plugin) { // setup signals connect(m_procLineMaker, &ProcessLineMaker::receivedStdoutLines, this, &MIDebugSession::inferiorStdoutLines); connect(m_procLineMaker, &ProcessLineMaker::receivedStderrLines, this, &MIDebugSession::inferiorStderrLines); // forward tty output to process line maker connect(this, &MIDebugSession::inferiorTtyStdout, m_procLineMaker, &ProcessLineMaker::slotReceivedStdout); connect(this, &MIDebugSession::inferiorTtyStderr, m_procLineMaker, &ProcessLineMaker::slotReceivedStderr); // FIXME: see if this still works //connect(statusBarIndicator, SIGNAL(doubleClicked()), // controller, SLOT(explainDebuggerStatus())); // FIXME: reimplement / re-enable //connect(this, SIGNAL(addWatchVariable(QString)), controller->variables(), SLOT(slotAddWatchVariable(QString))); //connect(this, SIGNAL(evaluateExpression(QString)), controller->variables(), SLOT(slotEvaluateExpression(QString))); } MIDebugSession::~MIDebugSession() { qCDebug(DEBUGGERCOMMON) << "Destroying MIDebugSession"; // Deleting the session involves shutting down gdb nicely. // When were attached to a process, we must first detach so that the process // can continue running as it was before being attached. gdb is quite slow to // detach from a process, so we must process events within here to get a "clean" // shutdown. if (!debuggerStateIsOn(s_dbgNotStarted)) { stopDebugger(); } } IDebugSession::DebuggerState MIDebugSession::state() const { return m_sessionState; } QMap & MIDebugSession::variableMapping() { return m_allVariables; } MIVariable* MIDebugSession::findVariableByVarobjName(const QString &varobjName) const { if (m_allVariables.count(varobjName) == 0) return nullptr; return m_allVariables.value(varobjName); } void MIDebugSession::markAllVariableDead() { for (auto i = m_allVariables.begin(), e = m_allVariables.end(); i != e; ++i) { i.value()->markAsDead(); } m_allVariables.clear(); } bool MIDebugSession::restartAvaliable() const { if (debuggerStateIsOn(s_attached) || debuggerStateIsOn(s_core)) { return false; } else { return true; } } bool MIDebugSession::startDebugger(ILaunchConfiguration *cfg) { qCDebug(DEBUGGERCOMMON) << "Starting new debugger instance"; if (m_debugger) { qCWarning(DEBUGGERCOMMON) << "m_debugger object still exists"; delete m_debugger; m_debugger = nullptr; } m_debugger = createDebugger(); m_debugger->setParent(this); // output signals connect(m_debugger, &MIDebugger::applicationOutput, this, [this](const QString &output) { auto lines = output.split(QRegularExpression(QStringLiteral("[\r\n]")), QString::SkipEmptyParts); for (auto &line : lines) { int p = line.length(); while (p >= 1 && (line[p-1] == '\r' || line[p-1] == '\n')) p--; if (p != line.length()) line.remove(p, line.length() - p); } emit inferiorStdoutLines(lines); }); connect(m_debugger, &MIDebugger::userCommandOutput, this, &MIDebugSession::debuggerUserCommandOutput); connect(m_debugger, &MIDebugger::internalCommandOutput, this, &MIDebugSession::debuggerInternalCommandOutput); connect(m_debugger, &MIDebugger::debuggerInternalOutput, this, &MIDebugSession::debuggerInternalOutput); // state signals connect(m_debugger, &MIDebugger::programStopped, this, &MIDebugSession::inferiorStopped); connect(m_debugger, &MIDebugger::programRunning, this, &MIDebugSession::inferiorRunning); // internal handlers connect(m_debugger, &MIDebugger::ready, this, &MIDebugSession::slotDebuggerReady); connect(m_debugger, &MIDebugger::exited, this, &MIDebugSession::slotDebuggerExited); connect(m_debugger, &MIDebugger::programStopped, this, &MIDebugSession::slotInferiorStopped); connect(m_debugger, &MIDebugger::programRunning, this, &MIDebugSession::slotInferiorRunning); connect(m_debugger, &MIDebugger::notification, this, &MIDebugSession::processNotification); // start the debugger. Do this after connecting all signals so that initial // debugger output, and important events like the debugger died are reported. QStringList extraArguments; if (!m_sourceInitFile) extraArguments << QStringLiteral("--nx"); auto config = cfg ? cfg->config() // FIXME: this is only used when attachToProcess or examineCoreFile. // Change to use a global launch configuration when calling : KConfigGroup(KSharedConfig::openConfig(), "GDB Config"); if (!m_debugger->start(config, extraArguments)) { // debugger failed to start, ensure debugger and session state are correctly updated. setDebuggerStateOn(s_dbgFailedStart); return false; } // FIXME: here, we should wait until the debugger is up and waiting for input. // Then, clear s_dbgNotStarted // It's better to do this right away so that the state bit is always correct. setDebuggerStateOff(s_dbgNotStarted); // Initialise debugger. At this stage debugger is sitting wondering what to do, // and to whom. initializeDebugger(); qCDebug(DEBUGGERCOMMON) << "Debugger instance started"; return true; } bool MIDebugSession::startDebugging(ILaunchConfiguration* cfg, IExecutePlugin* iexec) { qCDebug(DEBUGGERCOMMON) << "Starting new debug session"; Q_ASSERT(cfg); Q_ASSERT(iexec); // Ensure debugger is started first if (debuggerStateIsOn(s_appNotStarted)) { emit showMessage(i18n("Running program"), 1000); } if (debuggerStateIsOn(s_dbgNotStarted)) { if (!startDebugger(cfg)) return false; } if (debuggerStateIsOn(s_shuttingDown)) { qCDebug(DEBUGGERCOMMON) << "Tried to run when debugger shutting down"; return false; } // Only dummy err here, actual erros have been checked already in the job and we don't get here if there were any QString err; QString executable = iexec->executable(cfg, err).toLocalFile(); configInferior(cfg, iexec, executable); // Set up the tty for the inferior bool config_useExternalTerminal = iexec->useTerminal(cfg); QString config_ternimalName = iexec->terminal(cfg); if (!config_ternimalName.isEmpty()) { // the external terminal cmd contains additional arguments, just get the terminal name config_ternimalName = KShell::splitArgs(config_ternimalName).first(); } m_tty.reset(new STTY(config_useExternalTerminal, config_ternimalName)); if (!config_useExternalTerminal) { connect(m_tty.get(), &STTY::OutOutput, this, &MIDebugSession::inferiorTtyStdout); connect(m_tty.get(), &STTY::ErrOutput, this, &MIDebugSession::inferiorTtyStderr); } QString tty(m_tty->getSlave()); #ifndef Q_OS_WIN if (tty.isEmpty()) { KMessageBox::information(qApp->activeWindow(), m_tty->lastError(), i18n("warning")); m_tty.reset(nullptr); return false; } #endif addCommand(InferiorTtySet, tty); // Change the working directory to the correct one QString dir = iexec->workingDirectory(cfg).toLocalFile(); if (dir.isEmpty()) { dir = QFileInfo(executable).absolutePath(); } addCommand(EnvironmentCd, '"' + dir + '"'); // Set the run arguments QStringList arguments = iexec->arguments(cfg, err); if (!arguments.isEmpty()) addCommand(ExecArguments, KShell::joinArgs(arguments)); // Do other debugger specific config options and actually start the inferior program if (!execInferior(cfg, iexec, executable)) { return false; } QString config_startWith = cfg->config().readEntry(Config::StartWithEntry, QStringLiteral("ApplicationOutput")); if (config_startWith == QLatin1String("GdbConsole")) { emit raiseDebuggerConsoleViews(); } else if (config_startWith == QLatin1String("FrameStack")) { emit raiseFramestackViews(); } else { // ApplicationOutput is raised in DebugJob (by setting job to Verbose/Silent) } return true; } // FIXME: use same configuration process as startDebugging bool MIDebugSession::attachToProcess(int pid) { qCDebug(DEBUGGERCOMMON) << "Attach to process" << pid; emit showMessage(i18n("Attaching to process %1", pid), 1000); if (debuggerStateIsOn(s_dbgNotStarted)) { // FIXME: use global launch configuration rather than nullptr if (!startDebugger(nullptr)) { return false; } } setDebuggerStateOn(s_attached); //set current state to running, after attaching we will get *stopped response setDebuggerStateOn(s_appRunning); addCommand(TargetAttach, QString::number(pid), this, &MIDebugSession::handleTargetAttach, CmdHandlesError); addCommand(new SentinelCommand(breakpointController(), &MIBreakpointController::initSendBreakpoints)); raiseEvent(connected_to_program); emit raiseFramestackViews(); return true; } void MIDebugSession::handleTargetAttach(const MI::ResultRecord& r) { if (r.reason == QLatin1String("error")) { KMessageBox::error( qApp->activeWindow(), i18n("Could not attach debugger:
")+ r[QStringLiteral("msg")].literal(), i18n("Startup error")); stopDebugger(); } } bool MIDebugSession::examineCoreFile(const QUrl &debugee, const QUrl &coreFile) { emit showMessage(i18n("Examining core file %1", coreFile.toLocalFile()), 1000); if (debuggerStateIsOn(s_dbgNotStarted)) { // FIXME: use global launch configuration rather than nullptr if (!startDebugger(nullptr)) { return false; } } // FIXME: support non-local URLs if (!loadCoreFile(nullptr, debugee.toLocalFile(), coreFile.toLocalFile())) { return false; } raiseEvent(program_state_changed); return true; } #define ENUM_NAME(o,e,v) (o::staticMetaObject.enumerator(o::staticMetaObject.indexOfEnumerator(#e)).valueToKey((v))) void MIDebugSession::setSessionState(DebuggerState state) { qCDebug(DEBUGGERCOMMON) << "Session state changed to" << ENUM_NAME(IDebugSession, DebuggerState, state) << "(" << state << ")"; if (state != m_sessionState) { m_sessionState = state; emit stateChanged(state); } } bool MIDebugSession::debuggerStateIsOn(DBGStateFlags state) const { return m_debuggerState & state; } DBGStateFlags MIDebugSession::debuggerState() const { return m_debuggerState; } void MIDebugSession::setDebuggerStateOn(DBGStateFlags stateOn) { DBGStateFlags oldState = m_debuggerState; debuggerStateChange(m_debuggerState, m_debuggerState | stateOn); m_debuggerState |= stateOn; handleDebuggerStateChange(oldState, m_debuggerState); } void MIDebugSession::setDebuggerStateOff(DBGStateFlags stateOff) { DBGStateFlags oldState = m_debuggerState; debuggerStateChange(m_debuggerState, m_debuggerState & ~stateOff); m_debuggerState &= ~stateOff; handleDebuggerStateChange(oldState, m_debuggerState); } void MIDebugSession::setDebuggerState(DBGStateFlags newState) { DBGStateFlags oldState = m_debuggerState; debuggerStateChange(m_debuggerState, newState); m_debuggerState = newState; handleDebuggerStateChange(oldState, m_debuggerState); } void MIDebugSession::debuggerStateChange(DBGStateFlags oldState, DBGStateFlags newState) { int delta = oldState ^ newState; if (delta) { QString out; #define STATE_CHECK(name) \ do { \ if (delta & name) { \ out += ((newState & name) ? " +" : " -"); \ out += #name; \ delta &= ~name; \ } \ } while (0) STATE_CHECK(s_dbgNotStarted); STATE_CHECK(s_appNotStarted); STATE_CHECK(s_programExited); STATE_CHECK(s_attached); STATE_CHECK(s_core); STATE_CHECK(s_shuttingDown); STATE_CHECK(s_dbgBusy); STATE_CHECK(s_appRunning); STATE_CHECK(s_dbgNotListening); STATE_CHECK(s_automaticContinue); #undef STATE_CHECK for (unsigned int i = 0; delta != 0 && i < 32; ++i) { if (delta & (1 << i)) { delta &= ~(1 << i); out += ((1 << i) & newState) ? " +" : " -"; out += QString::number(i); } } - qCDebug(DEBUGGERCOMMON) << "Debugger state change:" << out; } } void MIDebugSession::handleDebuggerStateChange(DBGStateFlags oldState, DBGStateFlags newState) { QString message; DebuggerState oldSessionState = state(); DebuggerState newSessionState = oldSessionState; DBGStateFlags changedState = oldState ^ newState; if (newState & s_dbgNotStarted) { if (changedState & s_dbgNotStarted) { message = i18n("Debugger stopped"); emit finished(); } if (oldSessionState != NotStartedState || newState & s_dbgFailedStart) { newSessionState = EndedState; } } else { if (newState & s_appNotStarted) { if (oldSessionState == NotStartedState || oldSessionState == StartingState) { newSessionState = StartingState; } else { newSessionState = StoppedState; } } else if (newState & s_programExited) { if (changedState & s_programExited) { message = i18n("Process exited"); } newSessionState = StoppedState; } else if (newState & s_appRunning) { if (changedState & s_appRunning) { message = i18n("Application is running"); } newSessionState = ActiveState; } else { if (changedState & s_appRunning) { message = i18n("Application is paused"); } newSessionState = PausedState; } } // And now? :-) - qCDebug(DEBUGGERCOMMON) << "Debugger state changed to: " << newState << message; + qCDebug(DEBUGGERCOMMON) << "Debugger state changed to:" << newState << message << "- changes:" << changedState; if (!message.isEmpty()) emit showMessage(message, 3000); emit debuggerStateChanged(oldState, newState); // must be last, since it can lead to deletion of the DebugSession if (newSessionState != oldSessionState) { setSessionState(newSessionState); } } void MIDebugSession::restartDebugger() { // We implement restart as kill + slotRun, as opposed as plain "run" // command because kill + slotRun allows any special logic in slotRun // to apply for restart. // // That includes: // - checking for out-of-date project // - special setup for remote debugging. // // Had we used plain 'run' command, restart for remote debugging simply // would not work. if (!debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) { // FIXME: s_dbgBusy or m_debugger->isReady()? if (debuggerStateIsOn(s_dbgBusy)) { interruptDebugger(); } // The -exec-abort is not implemented in gdb // addCommand(ExecAbort); addCommand(NonMI, QStringLiteral("kill")); } run(); } void MIDebugSession::stopDebugger() { if (debuggerStateIsOn(s_dbgNotStarted)) { // we are force to stop even before debugger started, just reset qCDebug(DEBUGGERCOMMON) << "Stopping debugger when it's not started"; return; } m_commandQueue->clear(); qCDebug(DEBUGGERCOMMON) << "try stopping debugger"; if (debuggerStateIsOn(s_shuttingDown) || !m_debugger) return; setDebuggerStateOn(s_shuttingDown); qCDebug(DEBUGGERCOMMON) << "stopping debugger"; // Get debugger's attention if it's busy. We need debugger to be at the // command line so we can stop it. if (!m_debugger->isReady()) { qCDebug(DEBUGGERCOMMON) << "debugger busy on shutdown - interrupting"; interruptDebugger(); } // If the app is attached then we release it here. This doesn't stop // the app running. if (debuggerStateIsOn(s_attached)) { addCommand(TargetDetach); emit debuggerUserCommandOutput(QStringLiteral("(gdb) detach\n")); } // Now try to stop debugger running. addCommand(GdbExit); emit debuggerUserCommandOutput(QStringLiteral("(gdb) quit")); // We cannot wait forever, kill gdb after 5 seconds if it's not yet quit QPointer guarded_this(this); QTimer::singleShot(5000, [guarded_this](){ if (guarded_this) { if (!guarded_this->debuggerStateIsOn(s_programExited) && guarded_this->debuggerStateIsOn(s_shuttingDown)) { qCDebug(DEBUGGERCOMMON) << "debugger not shutdown - killing"; guarded_this->m_debugger->kill(); guarded_this->setDebuggerState(s_dbgNotStarted | s_appNotStarted); guarded_this->raiseEvent(debugger_exited); } } }); emit reset(); } void MIDebugSession::interruptDebugger() { Q_ASSERT(m_debugger); // Explicitly send the interrupt in case something went wrong with the usual // ensureGdbListening logic. m_debugger->interrupt(); addCommand(ExecInterrupt, QString(), CmdInterrupt); } void MIDebugSession::run() { if (debuggerStateIsOn(s_appNotStarted|s_dbgNotStarted|s_shuttingDown)) return; addCommand(MI::ExecContinue, QString(), CmdMaybeStartsRunning); } void MIDebugSession::runToCursor() { if (IDocument* doc = ICore::self()->documentController()->activeDocument()) { KTextEditor::Cursor cursor = doc->cursorPosition(); if (cursor.isValid()) runUntil(doc->url(), cursor.line() + 1); } } void MIDebugSession::jumpToCursor() { if (IDocument* doc = ICore::self()->documentController()->activeDocument()) { KTextEditor::Cursor cursor = doc->cursorPosition(); if (cursor.isValid()) jumpTo(doc->url(), cursor.line() + 1); } } void MIDebugSession::stepOver() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecNext, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepIntoInstruction() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecStepInstruction, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepInto() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecStep, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepOverInstruction() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecNextInstruction, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepOut() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecFinish, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::runUntil(const QUrl& url, int line) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (!url.isValid()) { addCommand(ExecUntil, QString::number(line), CmdMaybeStartsRunning | CmdTemporaryRun); } else { addCommand(ExecUntil, QStringLiteral("%1:%2").arg(url.toLocalFile()).arg(line), CmdMaybeStartsRunning | CmdTemporaryRun); } } void MIDebugSession::runUntil(const QString& address) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (!address.isEmpty()) { addCommand(ExecUntil, QStringLiteral("*%1").arg(address), CmdMaybeStartsRunning | CmdTemporaryRun); } } void MIDebugSession::jumpTo(const QUrl& url, int line) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (url.isValid()) { addCommand(NonMI, QStringLiteral("tbreak %1:%2").arg(url.toLocalFile()).arg(line)); addCommand(NonMI, QStringLiteral("jump %1:%2").arg(url.toLocalFile()).arg(line)); } } void MIDebugSession::jumpToMemoryAddress(const QString& address) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (!address.isEmpty()) { addCommand(NonMI, QStringLiteral("tbreak *%1").arg(address)); addCommand(NonMI, QStringLiteral("jump *%1").arg(address)); } } void MIDebugSession::addUserCommand(const QString& cmd) { auto usercmd = createUserCommand(cmd); if (!usercmd) return; queueCmd(usercmd); // User command can theoreticall modify absolutely everything, // so need to force a reload. // We can do it right now, and don't wait for user command to finish // since commands used to reload all view will be executed after // user command anyway. if (!debuggerStateIsOn(s_appNotStarted) && !debuggerStateIsOn(s_programExited)) raiseEvent(program_state_changed); } MICommand *MIDebugSession::createUserCommand(const QString &cmd) const { MICommand *res = nullptr; if (!cmd.isEmpty() && cmd[0].isDigit()) { // Add a space to the beginning, so debugger won't get confused if the // command starts with a number (won't mix it up with command token added) res = new UserCommand(MI::NonMI, QLatin1Char(' ') + cmd); } else { res = new UserCommand(MI::NonMI, cmd); } return res; } MICommand *MIDebugSession::createCommand(CommandType type, const QString& arguments, CommandFlags flags) const { return new MICommand(type, arguments, flags); } void MIDebugSession::addCommand(MICommand* cmd) { queueCmd(cmd); } void MIDebugSession::addCommand(MI::CommandType type, const QString& arguments, MI::CommandFlags flags) { queueCmd(createCommand(type, arguments, flags)); } void MIDebugSession::addCommand(MI::CommandType type, const QString& arguments, MI::MICommandHandler *handler, MI::CommandFlags flags) { auto cmd = createCommand(type, arguments, flags); cmd->setHandler(handler); queueCmd(cmd); } void MIDebugSession::addCommand(MI::CommandType type, const QString& arguments, const MI::FunctionCommandHandler::Function& callback, MI::CommandFlags flags) { auto cmd = createCommand(type, arguments, flags); cmd->setHandler(callback); queueCmd(cmd); } // Fairly obvious that we'll add whatever command you give me to a queue // Not quite so obvious though is that if we are going to run again. then any // information requests become redundent and must be removed. // We also try and run whatever command happens to be at the head of // the queue. void MIDebugSession::queueCmd(MICommand *cmd) { if (debuggerStateIsOn(s_dbgNotStarted)) { KMessageBox::information( qApp->activeWindow(), i18n("Gdb command sent when debugger is not running
" "The command was:
%1", cmd->initialString()), i18n("Internal error")); return; } if (m_stateReloadInProgress) cmd->setStateReloading(true); m_commandQueue->enqueue(cmd); qCDebug(DEBUGGERCOMMON) << "QUEUE: " << cmd->initialString() << (m_stateReloadInProgress ? "(state reloading)" : "") << m_commandQueue->count() << "pending"; bool varCommandWithContext= (cmd->type() >= MI::VarAssign && cmd->type() <= MI::VarUpdate && cmd->type() != MI::VarDelete); bool stackCommandWithContext = (cmd->type() >= MI::StackInfoDepth && cmd->type() <= MI::StackListLocals); if (varCommandWithContext || stackCommandWithContext) { if (cmd->thread() == -1) qCDebug(DEBUGGERCOMMON) << "\t--thread will be added on execution"; if (cmd->frame() == -1) qCDebug(DEBUGGERCOMMON) << "\t--frame will be added on execution"; } setDebuggerStateOn(s_dbgBusy); raiseEvent(debugger_busy); executeCmd(); } void MIDebugSession::executeCmd() { Q_ASSERT(m_debugger); if (debuggerStateIsOn(s_dbgNotListening) && m_commandQueue->haveImmediateCommand()) { // We may have to call this even while a command is currently executing, because // debugger can get into a state where a command such as ExecRun does not send a response // while the inferior is running. ensureDebuggerListening(); } if (!m_debugger->isReady()) return; MICommand* currentCmd = m_commandQueue->nextCommand(); if (!currentCmd) return; if (currentCmd->flags() & (CmdMaybeStartsRunning | CmdInterrupt)) { setDebuggerStateOff(s_automaticContinue); } if (currentCmd->flags() & CmdMaybeStartsRunning) { // GDB can be in a state where it is listening for commands while the program is running. // However, when we send a command such as ExecContinue in this state, GDB may return to // the non-listening state without acknowledging that the ExecContinue command has even // finished, let alone sending a new notification about the program's running state. // So let's be extra cautious about ensuring that we will wake GDB up again if required. setDebuggerStateOn(s_dbgNotListening); } bool varCommandWithContext= (currentCmd->type() >= MI::VarAssign && currentCmd->type() <= MI::VarUpdate && currentCmd->type() != MI::VarDelete); bool stackCommandWithContext = (currentCmd->type() >= MI::StackInfoDepth && currentCmd->type() <= MI::StackListLocals); if (varCommandWithContext || stackCommandWithContext) { // Most var commands should be executed in the context // of the selected thread and frame. if (currentCmd->thread() == -1) currentCmd->setThread(frameStackModel()->currentThread()); if (currentCmd->frame() == -1) currentCmd->setFrame(frameStackModel()->currentFrame()); } QString commandText = currentCmd->cmdToSend(); bool bad_command = false; QString message; int length = commandText.length(); // No i18n for message since it's mainly for debugging. if (length == 0) { // The command might decide it's no longer necessary to send // it. if (SentinelCommand* sc = dynamic_cast(currentCmd)) { qCDebug(DEBUGGERCOMMON) << "SEND: sentinel command, not sending"; sc->invokeHandler(); } else { qCDebug(DEBUGGERCOMMON) << "SEND: command " << currentCmd->initialString() << "changed its mind, not sending"; } delete currentCmd; executeCmd(); return; } else { if (commandText[length-1] != '\n') { bad_command = true; message = QStringLiteral("Debugger command does not end with newline"); } } if (bad_command) { KMessageBox::information(qApp->activeWindow(), i18n("Invalid debugger command
%1", message), i18n("Invalid debugger command")); executeCmd(); return; } m_debugger->execute(currentCmd); } void MIDebugSession::ensureDebuggerListening() { Q_ASSERT(m_debugger); // Note: we don't use interruptDebugger() here since // we don't want to queue more commands before queuing a command m_debugger->interrupt(); setDebuggerStateOn(s_interruptSent); if (debuggerStateIsOn(s_appRunning)) setDebuggerStateOn(s_automaticContinue); setDebuggerStateOff(s_dbgNotListening); } void MIDebugSession::destroyCmds() { m_commandQueue->clear(); } // FIXME: I don't fully remember what is the business with // m_stateReloadInProgress and whether we can lift it to the // generic level. void MIDebugSession::raiseEvent(event_t e) { if (e == program_exited || e == debugger_exited) { m_stateReloadInProgress = false; } if (e == program_state_changed) { m_stateReloadInProgress = true; qCDebug(DEBUGGERCOMMON) << "State reload in progress\n"; } IDebugSession::raiseEvent(e); if (e == program_state_changed) { m_stateReloadInProgress = false; } } bool KDevMI::MIDebugSession::hasCrashed() const { return m_hasCrashed; } void MIDebugSession::slotDebuggerReady() { Q_ASSERT(m_debugger); m_stateReloadInProgress = false; executeCmd(); if (m_debugger->isReady()) { /* There is nothing in the command queue and no command is currently executing. */ if (debuggerStateIsOn(s_automaticContinue)) { if (!debuggerStateIsOn(s_appRunning)) { qCDebug(DEBUGGERCOMMON) << "Posting automatic continue"; addCommand(ExecContinue, QString(), CmdMaybeStartsRunning); } setDebuggerStateOff(s_automaticContinue); return; } if (m_stateReloadNeeded && !debuggerStateIsOn(s_appRunning)) { qCDebug(DEBUGGERCOMMON) << "Finishing program stop"; // Set to false right now, so that if 'actOnProgramPauseMI_part2' // sends some commands, we won't call it again when handling replies // from that commands. m_stateReloadNeeded = false; reloadProgramState(); } qCDebug(DEBUGGERCOMMON) << "No more commands"; setDebuggerStateOff(s_dbgBusy); raiseEvent(debugger_ready); } } void MIDebugSession::slotDebuggerExited(bool abnormal, const QString &msg) { /* Technically speaking, GDB is likely not to kill the application, and we should have some backup mechanism to make sure the application is killed by KDevelop. But even if application stays around, we no longer can control it in any way, so mark it as exited. */ setDebuggerStateOn(s_appNotStarted); setDebuggerStateOn(s_dbgNotStarted); setDebuggerStateOn(s_programExited); setDebuggerStateOff(s_shuttingDown); if (!msg.isEmpty()) emit showMessage(msg, 3000); if (abnormal) { /* The error is reported to user in MIDebugger now. KMessageBox::information( KDevelop::ICore::self()->uiController()->activeMainWindow(), i18n("Debugger exited abnormally" "

This is likely a bug in GDB. " "Examine the gdb output window and then stop the debugger"), i18n("Debugger exited abnormally")); */ // FIXME: not sure if the following still applies. // Note: we don't stop the debugger here, becuse that will hide gdb // window and prevent the user from finding the exact reason of the // problem. } /* FIXME: raiseEvent is handled across multiple places where we explicitly * stop/kill the debugger, a better way is to let the debugger itself report * its exited event. */ // raiseEvent(debugger_exited); } void MIDebugSession::slotInferiorStopped(const MI::AsyncRecord& r) { /* By default, reload all state on program stop. */ m_stateReloadNeeded = true; setDebuggerStateOff(s_appRunning); setDebuggerStateOff(s_dbgNotListening); QString reason; if (r.hasField(QStringLiteral("reason"))) reason = r[QStringLiteral("reason")].literal(); if (reason == QLatin1String("exited-normally") || reason == QLatin1String("exited")) { if (r.hasField(QStringLiteral("exit-code"))) { programNoApp(i18n("Exited with return code: %1", r["exit-code"].literal())); } else { programNoApp(i18n("Exited normally")); } m_stateReloadNeeded = false; return; } if (reason == QLatin1String("exited-signalled")) { programNoApp(i18n("Exited on signal %1", r["signal-name"].literal())); m_stateReloadNeeded = false; return; } if (reason == QLatin1String("watchpoint-scope")) { QString number = r[QStringLiteral("wpnum")].literal(); // FIXME: shuld remove this watchpoint // But first, we should consider if removing all // watchpoinst on program exit is the right thing to // do. addCommand(ExecContinue, QString(), CmdMaybeStartsRunning); m_stateReloadNeeded = false; return; } bool wasInterrupt = false; if (reason == QLatin1String("signal-received")) { QString name = r[QStringLiteral("signal-name")].literal(); QString user_name = r[QStringLiteral("signal-meaning")].literal(); // SIGINT is a "break into running program". // We do this when the user set/mod/clears a breakpoint but the // application is running. // And the user does this to stop the program also. if (name == QLatin1String("SIGINT") && debuggerStateIsOn(s_interruptSent)) { wasInterrupt = true; } else { // Whenever we have a signal raised then tell the user, but don't // end the program as we want to allow the user to look at why the // program has a signal that's caused the prog to stop. // Continuing from SIG FPE/SEGV will cause a "Cannot ..." and // that'll end the program. programFinished(i18n("Program received signal %1 (%2)", name, user_name)); m_hasCrashed = true; } } if (!reason.contains(QLatin1String("exited"))) { // FIXME: we should immediately update the current thread and // frame in the framestackmodel, so that any user actions // are in that thread. However, the way current framestack model // is implemented, we can't change thread id until we refresh // the entire list of threads -- otherwise we might set a thread // id that is not already in the list, and it will be upset. //Indicates if program state should be reloaded immediately. bool updateState = false; if (r.hasField(QStringLiteral("frame"))) { const MI::Value& frame = r[QStringLiteral("frame")]; QString file, line, addr; if (frame.hasField(QStringLiteral("fullname"))) file = frame[QStringLiteral("fullname")].literal();; if (frame.hasField(QStringLiteral("line"))) line = frame[QStringLiteral("line")].literal(); if (frame.hasField(QStringLiteral("addr"))) addr = frame[QStringLiteral("addr")].literal(); // gdb counts lines from 1 and we don't setCurrentPosition(QUrl::fromLocalFile(file), line.toInt() - 1, addr); updateState = true; } if (updateState) { reloadProgramState(); } } setDebuggerStateOff(s_interruptSent); if (!wasInterrupt) setDebuggerStateOff(s_automaticContinue); } void MIDebugSession::slotInferiorRunning() { setDebuggerStateOn(s_appRunning); raiseEvent(program_running); if (m_commandQueue->haveImmediateCommand() || (m_debugger->currentCommand() && (m_debugger->currentCommand()->flags() & (CmdImmediately | CmdInterrupt)))) { ensureDebuggerListening(); } else { setDebuggerStateOn(s_dbgNotListening); } } void MIDebugSession::processNotification(const MI::AsyncRecord & async) { if (async.reason == QLatin1String("thread-group-started")) { setDebuggerStateOff(s_appNotStarted | s_programExited); } else if (async.reason == QLatin1String("thread-group-exited")) { setDebuggerStateOn(s_programExited); } else if (async.reason == QLatin1String("library-loaded")) { // do nothing } else if (async.reason == QLatin1String("breakpoint-created")) { breakpointController()->notifyBreakpointCreated(async); } else if (async.reason == QLatin1String("breakpoint-modified")) { breakpointController()->notifyBreakpointModified(async); } else if (async.reason == QLatin1String("breakpoint-deleted")) { breakpointController()->notifyBreakpointDeleted(async); } else { qCDebug(DEBUGGERCOMMON) << "Unhandled notification: " << async.reason; } } void MIDebugSession::reloadProgramState() { raiseEvent(program_state_changed); m_stateReloadNeeded = false; } // There is no app anymore. This can be caused by program exiting // an invalid program specified or ... // gdb is still running though, but only the run command (may) make sense // all other commands are disabled. void MIDebugSession::programNoApp(const QString& msg) { qCDebug(DEBUGGERCOMMON) << msg; setDebuggerState(s_appNotStarted | s_programExited | (m_debuggerState & s_shuttingDown)); destroyCmds(); // The application has existed, but it's possible that // some of application output is still in the pipe. We use // different pipes to communicate with gdb and to get application // output, so "exited" message from gdb might have arrived before // last application output. Get this last bit. // Note: this method can be called when we open an invalid // core file. In that case, tty_ won't be set. if (m_tty){ m_tty->readRemaining(); // Tty is no longer usable, delete it. Without this, QSocketNotifier // will continuously bomd STTY with signals, so we need to either disable // QSocketNotifier, or delete STTY. The latter is simpler, since we can't // reuse it for future debug sessions anyway. m_tty.reset(nullptr); } stopDebugger(); raiseEvent(program_exited); raiseEvent(debugger_exited); emit showMessage(msg, 0); programFinished(msg); } void MIDebugSession::programFinished(const QString& msg) { QString m = QStringLiteral("*** %0 ***").arg(msg.trimmed()); emit inferiorStderrLines(QStringList(m)); /* Also show message in gdb window, so that users who prefer to look at gdb window know what's up. */ emit debuggerUserCommandOutput(m); } void MIDebugSession::explainDebuggerStatus() { MICommand* currentCmd_ = m_debugger->currentCommand(); QString information = i18np("1 command in queue\n", "%1 commands in queue\n", m_commandQueue->count()) + i18ncp("Only the 0 and 1 cases need to be translated", "1 command being processed by gdb\n", "%1 commands being processed by gdb\n", (currentCmd_ ? 1 : 0)) + i18n("Debugger state: %1\n", m_debuggerState); if (currentCmd_) { QString extra = i18n("Current command class: '%1'\n" "Current command text: '%2'\n" "Current command original text: '%3'\n", typeid(*currentCmd_).name(), currentCmd_->cmdToSend(), currentCmd_->initialString()); information += extra; } KMessageBox::information(qApp->activeWindow(), information, i18n("Debugger status")); } // There is no app anymore. This can be caused by program exiting // an invalid program specified or ... // gdb is still running though, but only the run command (may) make sense // all other commands are disabled. void MIDebugSession::handleNoInferior(const QString& msg) { qCDebug(DEBUGGERCOMMON) << msg; setDebuggerState(s_appNotStarted | s_programExited | (debuggerState() & s_shuttingDown)); destroyCmds(); // The application has existed, but it's possible that // some of application output is still in the pipe. We use // different pipes to communicate with gdb and to get application // output, so "exited" message from gdb might have arrived before // last application output. Get this last bit. // Note: this method can be called when we open an invalid // core file. In that case, tty_ won't be set. if (m_tty){ m_tty->readRemaining(); // Tty is no longer usable, delete it. Without this, QSocketNotifier // will continuously bomd STTY with signals, so we need to either disable // QSocketNotifier, or delete STTY. The latter is simpler, since we can't // reuse it for future debug sessions anyway. m_tty.reset(nullptr); } stopDebugger(); raiseEvent(program_exited); raiseEvent(debugger_exited); emit showMessage(msg, 0); handleInferiorFinished(msg); } void MIDebugSession::handleInferiorFinished(const QString& msg) { QString m = QStringLiteral("*** %0 ***").arg(msg.trimmed()); emit inferiorStderrLines(QStringList(m)); /* Also show message in gdb window, so that users who prefer to look at gdb window know what's up. */ emit debuggerUserCommandOutput(m); } // FIXME: connect to debugger's slot. void MIDebugSession::defaultErrorHandler(const MI::ResultRecord& result) { QString msg = result[QStringLiteral("msg")].literal(); if (msg.contains(QLatin1String("No such process"))) { setDebuggerState(s_appNotStarted|s_programExited); raiseEvent(program_exited); return; } KMessageBox::information( qApp->activeWindow(), i18n("Debugger error" "

Debugger reported the following error:" "

%1", result["msg"].literal()), i18n("Debugger error")); // Error most likely means that some change made in GUI // was not communicated to the gdb, so GUI is now not // in sync with gdb. Resync it. // // Another approach is to make each widget reload it content // on errors from commands that it sent, but that's too complex. // Errors are supposed to happen rarely, so full reload on error // is not a big deal. Well, maybe except for memory view, but // it's no auto-reloaded anyway. // // Also, don't reload state on errors appeared during state // reloading! if (!m_debugger->currentCommand()->stateReloading()) raiseEvent(program_state_changed); } void MIDebugSession::setSourceInitFile(bool enable) { m_sourceInitFile = enable; }