File indexing completed on 2024-04-28 04:38:57

0001 /*
0002     SPDX-FileCopyrightText: 2016 Aetf <aetf@unlimitedcodeworks.xyz>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "debugsession.h"
0008 
0009 #include "controllers/variable.h"
0010 #include "dbgglobal.h"
0011 #include "debuggerplugin.h"
0012 #include "debuglog.h"
0013 #include "lldbcommand.h"
0014 #include "mi/micommand.h"
0015 #include "stty.h"
0016 #include "stringhelpers.h"
0017 
0018 #include <debugger/breakpoint/breakpoint.h>
0019 #include <debugger/breakpoint/breakpointmodel.h>
0020 #include <execute/iexecuteplugin.h>
0021 #include <interfaces/icore.h>
0022 #include <interfaces/idebugcontroller.h>
0023 #include <interfaces/iuicontroller.h>
0024 #include <interfaces/ilaunchconfiguration.h>
0025 #include <sublime/message.h>
0026 #include <util/environmentprofilelist.h>
0027 
0028 #include <KConfigGroup>
0029 #include <KLocalizedString>
0030 #include <KMessageBox>
0031 #include <KMessageBox_KDevCompat>
0032 #include <KShell>
0033 
0034 #include <QApplication>
0035 #include <QDir>
0036 #include <QFileInfo>
0037 #include <QStandardPaths>
0038 #include <QGuiApplication>
0039 
0040 using namespace KDevMI::LLDB;
0041 using namespace KDevMI::MI;
0042 using namespace KDevMI;
0043 using namespace KDevelop;
0044 
0045 struct ExecRunHandler : public MICommandHandler
0046 {
0047     explicit ExecRunHandler(DebugSession *session, int maxRetry = 5)
0048         : m_session(session)
0049         , m_remainRetry(maxRetry)
0050         , m_activeCommands(1)
0051     {
0052     }
0053 
0054     void handle(const ResultRecord& r) override
0055     {
0056         --m_activeCommands;
0057         if (r.reason == QLatin1String("error")) {
0058             if (r.hasField(QStringLiteral("msg"))
0059                 && r[QStringLiteral("msg")].literal().contains(QLatin1String("Invalid process during debug session"))) {
0060                 // for some unknown reason, lldb-mi sometimes fails to start process
0061                 if (m_remainRetry && m_session) {
0062                     qCDebug(DEBUGGERLLDB) << "Retry starting";
0063                     --m_remainRetry;
0064                     // resend the command again.
0065                     ++m_activeCommands;
0066                     m_session->addCommand(ExecRun, QString(),
0067                                           this, // use *this as handler, so we can track error times
0068                                           CmdMaybeStartsRunning | CmdHandlesError);
0069                     return;
0070                 }
0071             }
0072             qCDebug(DEBUGGERLLDB) << "Failed to start inferior:"
0073                                   << "exceeded retry times or session become invalid";
0074             m_session->stopDebugger();
0075         }
0076         if (m_activeCommands == 0)
0077             delete this;
0078     }
0079 
0080     bool handlesError() override { return true; }
0081     bool autoDelete() override { return false; }
0082 
0083     QPointer<DebugSession> m_session;
0084     int m_remainRetry;
0085     int m_activeCommands;
0086 };
0087 
0088 DebugSession::DebugSession(LldbDebuggerPlugin *plugin)
0089     : MIDebugSession(plugin)
0090     , m_formatterPath()
0091 {
0092     m_breakpointController = new BreakpointController(this);
0093     m_variableController = new VariableController(this);
0094     m_frameStackModel = new LldbFrameStackModel(this);
0095 
0096     if (m_plugin) m_plugin->setupToolViews();
0097 
0098     connect(this, &DebugSession::stateChanged, this, &DebugSession::handleSessionStateChange);
0099 }
0100 
0101 DebugSession::~DebugSession()
0102 {
0103     if (m_plugin) m_plugin->unloadToolViews();
0104 }
0105 
0106 BreakpointController *DebugSession::breakpointController() const
0107 {
0108     return m_breakpointController;
0109 }
0110 
0111 VariableController *DebugSession::variableController() const
0112 {
0113     return m_variableController;
0114 }
0115 
0116 LldbFrameStackModel *DebugSession::frameStackModel() const
0117 {
0118     return m_frameStackModel;
0119 }
0120 
0121 LldbDebugger *DebugSession::createDebugger() const
0122 {
0123     return new LldbDebugger;
0124 }
0125 
0126 std::unique_ptr<MICommand> DebugSession::createCommand(MI::CommandType type, const QString& arguments,
0127                                                        MI::CommandFlags flags) const
0128 {
0129     // using protected ctor, cannot use make_unique
0130     return std::unique_ptr<MICommand>(new LldbCommand(type, arguments, flags));
0131 }
0132 
0133 std::unique_ptr<MICommand> DebugSession::createUserCommand(const QString& cmd) const
0134 {
0135     if (m_hasCorrectCLIOutput)
0136         return MIDebugSession::createUserCommand(cmd);
0137     auto msg = i18n("Attempting to execute user command on unsupported LLDB version");
0138     emit debuggerInternalOutput(msg);
0139     qCDebug(DEBUGGERLLDB) << "Attempting user command on unsupported LLDB version";
0140     return nullptr;
0141 }
0142 
0143 void DebugSession::setFormatterPath(const QString &path)
0144 {
0145     m_formatterPath = path;
0146 }
0147 
0148 void DebugSession::initializeDebugger()
0149 {
0150     //addCommand(MI::EnableTimings, "yes");
0151 
0152     // Check version
0153     addCommand(std::make_unique<CliCommand>(MI::NonMI, QStringLiteral("version"), this, &DebugSession::handleVersion));
0154 
0155     // load data formatter
0156     auto formatterPath = m_formatterPath;
0157     if (!QFileInfo(formatterPath).isFile()) {
0158         formatterPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0159                                                QStringLiteral("kdevlldb/formatters/all.py"));
0160     }
0161     if (!formatterPath.isEmpty()) {
0162         addCommand(MI::NonMI, QLatin1String("command script import ") + KShell::quoteArg(formatterPath));
0163     }
0164 
0165 
0166     // Treat char array as string
0167     addCommand(MI::GdbSet, QStringLiteral("print char-array-as-string on"));
0168 
0169     // set a larger term width.
0170     // TODO: set term-width to exact max column count in console view
0171     addCommand(MI::NonMI, QStringLiteral("settings set term-width 1024"));
0172 
0173     qCDebug(DEBUGGERLLDB) << "Initialized LLDB";
0174 }
0175 
0176 void DebugSession::configInferior(ILaunchConfiguration *cfg, IExecutePlugin *iexec, const QString &executable)
0177 {
0178     // Read Configuration values
0179     KConfigGroup grp = cfg->config();
0180 
0181     // Create target as early as possible, so we can do target specific configuration later
0182     QString filesymbols = Utils::quote(executable);
0183     bool remoteDebugging = grp.readEntry(Config::LldbRemoteDebuggingEntry, false);
0184     if (remoteDebugging) {
0185         auto connStr = grp.readEntry(Config::LldbRemoteServerEntry, QString());
0186         auto remoteDir = grp.readEntry(Config::LldbRemotePathEntry, QString());
0187         auto remoteExe = QDir(remoteDir).filePath(QFileInfo(executable).fileName());
0188 
0189         filesymbols += QLatin1String(" -r ") + Utils::quote(remoteExe);
0190 
0191         addCommand(MI::FileExecAndSymbols, filesymbols,
0192                    this, &DebugSession::handleFileExecAndSymbols,
0193                    CmdHandlesError);
0194 
0195         addCommand(MI::TargetSelect, QLatin1String("remote ") + connStr,
0196                    this, &DebugSession::handleTargetSelect, CmdHandlesError);
0197 
0198         // ensure executable is on remote end
0199         addCommand(MI::NonMI, QStringLiteral("platform mkdir -v 755 %0").arg(Utils::quote(remoteDir)));
0200         addCommand(MI::NonMI, QStringLiteral("platform put-file %0 %1")
0201                               .arg(Utils::quote(executable), Utils::quote(remoteExe)));
0202     } else {
0203         addCommand(MI::FileExecAndSymbols, filesymbols,
0204                    this, &DebugSession::handleFileExecAndSymbols,
0205                    CmdHandlesError);
0206     }
0207 
0208     raiseEvent(connected_to_program);
0209 
0210     // Set the environment variables has effect only after target created
0211     const EnvironmentProfileList environmentProfiles(KSharedConfig::openConfig());
0212     QString envProfileName = iexec->environmentProfileName(cfg);
0213     if (envProfileName.isEmpty()) {
0214         envProfileName = environmentProfiles.defaultProfileName();
0215     }
0216     const auto &envVariables = environmentProfiles.variables(envProfileName);
0217     if (!envVariables.isEmpty()) {
0218         QStringList vars;
0219         vars.reserve(envVariables.size());
0220         for (auto it = envVariables.constBegin(), ite = envVariables.constEnd(); it != ite; ++it) {
0221             vars.append(QStringLiteral("%0=%1").arg(it.key(), Utils::quote(it.value())));
0222         }
0223         // actually using lldb command 'settings set target.env-vars' which accepts multiple values
0224         addCommand(GdbSet, QLatin1String("environment ") + vars.join(QLatin1Char(' ')));
0225     }
0226 
0227     // Break on start: can't use "-exec-run --start" because in lldb-mi
0228     // the inferior stops without any notification
0229     bool breakOnStart = grp.readEntry(KDevMI::Config::BreakOnStartEntry, false);
0230     if (breakOnStart) {
0231         BreakpointModel* m = ICore::self()->debugController()->breakpointModel();
0232         bool found = false;
0233         const auto breakpoints = m->breakpoints();
0234         for (Breakpoint* b : breakpoints) {
0235             if (b->location() == QLatin1String("main")) {
0236                 found = true;
0237                 break;
0238             }
0239         }
0240         if (!found) {
0241             m->addCodeBreakpoint(QStringLiteral("main"));
0242         }
0243     }
0244 
0245     // Needed so that breakpoint widget has a chance to insert breakpoints.
0246     // FIXME: a bit hacky, as we're really not ready for new commands.
0247     setDebuggerStateOn(s_dbgBusy);
0248     raiseEvent(debugger_ready);
0249 
0250     qCDebug(DEBUGGERLLDB) << "Per inferior configuration done";
0251 }
0252 
0253 bool DebugSession::execInferior(ILaunchConfiguration *cfg, IExecutePlugin *, const QString &)
0254 {
0255     qCDebug(DEBUGGERLLDB) << "Executing inferior";
0256 
0257     KConfigGroup grp = cfg->config();
0258 
0259     // Start inferior
0260     bool remoteDebugging = grp.readEntry(Config::LldbRemoteDebuggingEntry, false);
0261     QUrl configLldbScript = grp.readEntry(Config::LldbConfigScriptEntry, QUrl());
0262     addCommand(std::make_unique<SentinelCommand>(
0263         [this, remoteDebugging, configLldbScript]() {
0264             // setup inferior I/O redirection
0265             if (!remoteDebugging) {
0266                 // FIXME: a hacky way to emulate tty setting on linux. Not sure if this provides all needed
0267                 // functionalities of a pty. Should make this conditional on other platforms.
0268 
0269                 // no need to quote, settings set takes 'raw' input
0270                 addCommand(MI::NonMI, QStringLiteral("settings set target.input-path %0").arg(m_tty->getSlave()));
0271                 addCommand(MI::NonMI, QStringLiteral("settings set target.output-path %0").arg(m_tty->getSlave()));
0272                 addCommand(MI::NonMI, QStringLiteral("settings set target.error-path %0").arg(m_tty->getSlave()));
0273             } else {
0274                 // what is the expected behavior for using external terminal when doing remote debugging?
0275             }
0276 
0277             // send breakpoints already in our breakpoint model to lldb
0278             auto bc = breakpointController();
0279             bc->initSendBreakpoints();
0280 
0281             qCDebug(DEBUGGERLLDB) << "Turn on delete duplicate mode";
0282             // turn on delete duplicate breakpoints model, so that breakpoints created by user command in
0283             // the script and returned as a =breakpoint-created notification won't get duplicated with the
0284             // one already in our model.
0285             // we will turn this model off once we first reach a paused state, and from that time on,
0286             // the user can create duplicated breakpoints using normal command.
0287             bc->setDeleteDuplicateBreakpoints(true);
0288             // run custom config script right before we starting the inferior,
0289             // so the user has the freedom to change everything.
0290             if (configLldbScript.isValid()) {
0291                 addCommand(MI::NonMI,
0292                            QLatin1String("command source -s 0 ") + KShell::quoteArg(configLldbScript.toLocalFile()));
0293             }
0294 
0295             addCommand(MI::ExecRun, QString(), new ExecRunHandler(this), CmdMaybeStartsRunning | CmdHandlesError);
0296         },
0297         CmdMaybeStartsRunning));
0298     return true;
0299 }
0300 
0301 bool DebugSession::loadCoreFile(ILaunchConfiguration *,
0302                                 const QString &debugee, const QString &corefile)
0303 {
0304     addCommand(MI::FileExecAndSymbols, debugee,
0305                this, &DebugSession::handleFileExecAndSymbols,
0306                CmdHandlesError);
0307     raiseEvent(connected_to_program);
0308 
0309     addCommand(std::make_unique<CliCommand>(NonMI, QLatin1String("target create -c ") + Utils::quote(corefile), this,
0310                                             &DebugSession::handleCoreFile, CmdHandlesError));
0311     return true;
0312 }
0313 
0314 void DebugSession::interruptDebugger()
0315 {
0316     if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown))
0317         return;
0318 
0319     addCommand(ExecInterrupt, QString(), CmdInterrupt);
0320     return;
0321 }
0322 
0323 void DebugSession::ensureDebuggerListening()
0324 {
0325     // lldb always uses async mode and prompt is always available.
0326     // no need to interrupt
0327     setDebuggerStateOff(s_dbgNotListening);
0328     // NOTE: there is actually a bug in lldb-mi that, when receiving SIGINT,
0329     // it would print '^done', which doesn't corresponds to any previous command.
0330     // This confuses our command queue.
0331 }
0332 
0333 void DebugSession::handleFileExecAndSymbols(const MI::ResultRecord& r)
0334 {
0335     if (r.reason == QLatin1String("error")) {
0336         const QString messageText = i18n("<b>Could not start debugger:</b><br />")+
0337             r[QStringLiteral("msg")].literal();
0338         auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0339         ICore::self()->uiController()->postMessage(message);
0340         stopDebugger();
0341     }
0342 }
0343 
0344 void DebugSession::handleTargetSelect(const MI::ResultRecord& r)
0345 {
0346     if (r.reason == QLatin1String("error")) {
0347         const QString messageText = i18n("<b>Error connecting to remote target:</b><br />")+
0348             r[QStringLiteral("msg")].literal();
0349         auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0350         ICore::self()->uiController()->postMessage(message);
0351         stopDebugger();
0352     }
0353 }
0354 
0355 void DebugSession::handleCoreFile(const QStringList &s)
0356 {
0357     qCDebug(DEBUGGERLLDB) << s;
0358     for (const auto &line : s) {
0359         if (line.startsWith(QLatin1String("error:"))) {
0360             const QString messageText = i18n("<b>Failed to load core file</b>"
0361                 "<p>Debugger reported the following error:"
0362                 "<p><tt>%1",
0363                 s.join(QLatin1Char('\n')));
0364             auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0365             ICore::self()->uiController()->postMessage(message);
0366             stopDebugger();
0367             return;
0368         }
0369     }
0370     // There's no "thread-group-started" notification from lldb-mi, so pretend we have received one.
0371     // see MIDebugSession::processNotification(const MI::AsyncRecord & async)
0372     setDebuggerStateOff(s_appNotStarted | s_programExited);
0373 
0374     setDebuggerStateOn(s_programExited | s_core);
0375 }
0376 
0377 void DebugSession::handleVersion(const QStringList& s)
0378 {
0379     m_hasCorrectCLIOutput = !s.isEmpty();
0380     if (!m_hasCorrectCLIOutput) {
0381         // No output from 'version' command. It's likely that
0382         // the lldb used is not patched for the CLI output
0383 
0384         if (!qobject_cast<QGuiApplication*>(qApp))  {
0385             //for unittest
0386             qFatal("You need a graphical application.");
0387         }
0388 
0389         auto ans = KMessageBox::warningTwoActions(
0390             qApp->activeWindow(),
0391             i18n("<b>Your lldb-mi version is unsupported, as it lacks an essential patch.</b><br/>"
0392                  "See https://llvm.org/bugs/show_bug.cgi?id=28026 for more information.<br/>"
0393                  "Debugger console will be disabled to prevent crash.<br/>"
0394                  "Do you want to continue?"),
0395             i18n("LLDB Version Unsupported"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(),
0396             QStringLiteral("unsupported-lldb-debugger"));
0397         if (ans == KMessageBox::SecondaryAction) {
0398             programFinished(QStringLiteral("Stopped because of unsupported LLDB version"));
0399             stopDebugger();
0400         }
0401         return;
0402     }
0403 
0404     qCDebug(DEBUGGERLLDB) << s.first();
0405 
0406 // minimal version is 3.8.1
0407 #ifdef Q_OS_OSX
0408     QRegularExpression rx(QStringLiteral("^lldb-(\\d+).(\\d+).(\\d+)\\b"), QRegularExpression::MultilineOption);
0409     // lldb 3.8.1 reports version 350.99.0 on OS X
0410     const int min_ver[] = {350, 99, 0};
0411 #else
0412     QRegularExpression rx(QStringLiteral("^lldb version (\\d+).(\\d+).(\\d+)\\b"), QRegularExpression::MultilineOption);
0413     const int min_ver[] = {3, 8, 1};
0414 #endif
0415 
0416     auto match = rx.match(s.first());
0417     int version[] = {0, 0, 0};
0418     if (match.hasMatch()) {
0419         for (int i = 0; i != 3; ++i) {
0420             version[i] = match.capturedRef(i+1).toInt();
0421         }
0422     }
0423 
0424     bool ok = true;
0425     for (int i = 0; i < 3; ++i) {
0426         if (version[i] < min_ver[i]) {
0427             ok = false;
0428             break;
0429         } else if (version[i] > min_ver[i]) {
0430             ok = true;
0431             break;
0432         }
0433     }
0434 
0435     if (!ok) {
0436         if (!qobject_cast<QGuiApplication*>(qApp))  {
0437             //for unittest
0438             qFatal("You need a graphical application.");
0439         }
0440 
0441         const QString messageText = i18n("<b>You need lldb-mi from LLDB 3.8.1 or higher.</b><br />"
0442             "You are using: %1", s.first());
0443         auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0444         ICore::self()->uiController()->postMessage(message);
0445         stopDebugger();
0446     }
0447 }
0448 
0449 void DebugSession::updateAllVariables()
0450 {
0451     // FIXME: this is only a workaround for lldb-mi doesn't provide -var-update changelist
0452     // for variables that have a python synthetic provider. Remove this after this is fixed
0453     // in the upstream.
0454 
0455     // re-fetch all toplevel variables, as -var-update doesn't work with data formatter
0456     // we have to pick out top level variables first, as refetching will delete child
0457     // variables.
0458     QList<LldbVariable*> toplevels;
0459     for (auto* variable : qAsConst(m_allVariables)) {
0460         auto *var = qobject_cast<LldbVariable*>(variable);
0461         if (var->topLevel()) {
0462             toplevels << var;
0463         }
0464     }
0465 
0466     for (auto var : qAsConst(toplevels)) {
0467         var->refetch();
0468     }
0469 }
0470 
0471 void DebugSession::handleSessionStateChange(IDebugSession::DebuggerState state)
0472 {
0473     if (state == IDebugSession::PausedState) {
0474         // session is paused, the user can input any commands now.
0475         // Turn off delete duplicate breakpoints mode, as the user
0476         // may intentionally want to do this.
0477         qCDebug(DEBUGGERLLDB) << "Turn off delete duplicate mode";
0478         breakpointController()->setDeleteDuplicateBreakpoints(false);
0479     }
0480 }
0481 
0482 #include "moc_debugsession.cpp"