File: debugsession.cpp

package info (click to toggle)
kdevelop 4%3A5.3.1-3
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 52,544 kB
  • sloc: cpp: 254,897; python: 3,380; sh: 1,271; ansic: 657; xml: 221; php: 95; makefile: 36; lisp: 13; sed: 12
file content (496 lines) | stat: -rw-r--r-- 18,257 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
/*
 * LLDB Debugger Support
 * Copyright 2016  Aetf <aetf@unlimitedcodeworks.xyz>
 *
 * 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 <http://www.gnu.org/licenses/>.
 *
 */

#include "debugsession.h"

#include "controllers/variable.h"
#include "dbgglobal.h"
#include "debuggerplugin.h"
#include "debuglog.h"
#include "lldbcommand.h"
#include "mi/micommand.h"
#include "stty.h"
#include "stringhelpers.h"

#include <debugger/breakpoint/breakpoint.h>
#include <debugger/breakpoint/breakpointmodel.h>
#include <execute/iexecuteplugin.h>
#include <interfaces/icore.h>
#include <interfaces/idebugcontroller.h>
#include <interfaces/ilaunchconfiguration.h>
#include <util/environmentprofilelist.h>
#include <qtcompat_p.h>

#include <KConfigGroup>
#include <KLocalizedString>
#include <KMessageBox>
#include <KShell>

#include <QApplication>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
#include <QGuiApplication>

using namespace KDevMI::LLDB;
using namespace KDevMI::MI;
using namespace KDevMI;
using namespace KDevelop;

struct ExecRunHandler : public MICommandHandler
{
    explicit ExecRunHandler(DebugSession *session, int maxRetry = 5)
        : m_session(session)
        , m_remainRetry(maxRetry)
        , m_activeCommands(1)
    {
    }

    void handle(const ResultRecord& r) override
    {
        --m_activeCommands;
        if (r.reason == QLatin1String("error")) {
            if (r.hasField(QStringLiteral("msg"))
                && r[QStringLiteral("msg")].literal().contains(QLatin1String("Invalid process during debug session"))) {
                // for some unknown reason, lldb-mi sometimes fails to start process
                if (m_remainRetry && m_session) {
                    qCDebug(DEBUGGERLLDB) << "Retry starting";
                    --m_remainRetry;
                    // resend the command again.
                    ++m_activeCommands;
                    m_session->addCommand(ExecRun, QString(),
                                          this, // use *this as handler, so we can track error times
                                          CmdMaybeStartsRunning | CmdHandlesError);
                    return;
                }
            }
            qCDebug(DEBUGGERLLDB) << "Failed to start inferior:"
                                  << "exceeded retry times or session become invalid";
            m_session->stopDebugger();
        }
        if (m_activeCommands == 0)
            delete this;
    }

    bool handlesError() override { return true; }
    bool autoDelete() override { return false; }

    QPointer<DebugSession> m_session;
    int m_remainRetry;
    int m_activeCommands;
};

DebugSession::DebugSession(LldbDebuggerPlugin *plugin)
    : MIDebugSession(plugin)
    , m_formatterPath()
{
    m_breakpointController = new BreakpointController(this);
    m_variableController = new VariableController(this);
    m_frameStackModel = new LldbFrameStackModel(this);

    if (m_plugin) m_plugin->setupToolViews();

    connect(this, &DebugSession::stateChanged, this, &DebugSession::handleSessionStateChange);
}

DebugSession::~DebugSession()
{
    if (m_plugin) m_plugin->unloadToolViews();
}

BreakpointController *DebugSession::breakpointController() const
{
    return m_breakpointController;
}

VariableController *DebugSession::variableController() const
{
    return m_variableController;
}

LldbFrameStackModel *DebugSession::frameStackModel() const
{
    return m_frameStackModel;
}

LldbDebugger *DebugSession::createDebugger() const
{
    return new LldbDebugger;
}

MICommand *DebugSession::createCommand(MI::CommandType type, const QString& arguments,
                                       MI::CommandFlags flags) const
{
    return new LldbCommand(type, arguments, flags);
}

MICommand *DebugSession::createUserCommand(const QString& cmd) const
{
    if (m_hasCorrectCLIOutput)
        return MIDebugSession::createUserCommand(cmd);
    auto msg = i18n("Attempting to execute user command on unsupported LLDB version");
    emit debuggerInternalOutput(msg);
    qCDebug(DEBUGGERLLDB) << "Attempting user command on unsupported LLDB version";
    return nullptr;
}

void DebugSession::setFormatterPath(const QString &path)
{
    m_formatterPath = path;
}

void DebugSession::initializeDebugger()
{
    //addCommand(MI::EnableTimings, "yes");

    // Check version
    addCommand(new CliCommand(MI::NonMI, QStringLiteral("version"), this, &DebugSession::handleVersion));

    // load data formatter
    auto formatterPath = m_formatterPath;
    if (!QFileInfo(formatterPath).isFile()) {
        formatterPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
                                               QStringLiteral("kdevlldb/formatters/all.py"));
    }
    if (!formatterPath.isEmpty()) {
        addCommand(MI::NonMI, QLatin1String("command script import ") + KShell::quoteArg(formatterPath));
    }


    // Treat char array as string
    addCommand(MI::GdbSet, QStringLiteral("print char-array-as-string on"));

    // set a larger term width.
    // TODO: set term-width to exact max column count in console view
    addCommand(MI::NonMI, QStringLiteral("settings set term-width 1024"));

    qCDebug(DEBUGGERLLDB) << "Initialized LLDB";
}

void DebugSession::configInferior(ILaunchConfiguration *cfg, IExecutePlugin *iexec, const QString &executable)
{
    // Read Configuration values
    KConfigGroup grp = cfg->config();

    // Create target as early as possible, so we can do target specific configuration later
    QString filesymbols = Utils::quote(executable);
    bool remoteDebugging = grp.readEntry(Config::LldbRemoteDebuggingEntry, false);
    if (remoteDebugging) {
        auto connStr = grp.readEntry(Config::LldbRemoteServerEntry, QString());
        auto remoteDir = grp.readEntry(Config::LldbRemotePathEntry, QString());
        auto remoteExe = QDir(remoteDir).filePath(QFileInfo(executable).fileName());

        filesymbols += QLatin1String(" -r ") + Utils::quote(remoteExe);

        addCommand(MI::FileExecAndSymbols, filesymbols,
                   this, &DebugSession::handleFileExecAndSymbols,
                   CmdHandlesError);

        addCommand(MI::TargetSelect, QLatin1String("remote ") + connStr,
                   this, &DebugSession::handleTargetSelect, CmdHandlesError);

        // ensure executable is on remote end
        addCommand(MI::NonMI, QStringLiteral("platform mkdir -v 755 %0").arg(Utils::quote(remoteDir)));
        addCommand(MI::NonMI, QStringLiteral("platform put-file %0 %1")
                              .arg(Utils::quote(executable), Utils::quote(remoteExe)));
    } else {
        addCommand(MI::FileExecAndSymbols, filesymbols,
                   this, &DebugSession::handleFileExecAndSymbols,
                   CmdHandlesError);
    }

    raiseEvent(connected_to_program);

    // Set the environment variables has effect only after target created
    const EnvironmentProfileList environmentProfiles(KSharedConfig::openConfig());
    QString envProfileName = iexec->environmentProfileName(cfg);
    if (envProfileName.isEmpty()) {
        envProfileName = environmentProfiles.defaultProfileName();
    }
    const auto &envVariables = environmentProfiles.variables(envProfileName);
    if (!envVariables.isEmpty()) {
        QStringList vars;
        vars.reserve(envVariables.size());
        for (auto it = envVariables.constBegin(), ite = envVariables.constEnd(); it != ite; ++it) {
            vars.append(QStringLiteral("%0=%1").arg(it.key(), Utils::quote(it.value())));
        }
        // actually using lldb command 'settings set target.env-vars' which accepts multiple values
        addCommand(GdbSet, QLatin1String("environment ") + vars.join(QLatin1Char(' ')));
    }

    // Break on start: can't use "-exec-run --start" because in lldb-mi
    // the inferior stops without any notification
    bool breakOnStart = grp.readEntry(KDevMI::Config::BreakOnStartEntry, false);
    if (breakOnStart) {
        BreakpointModel* m = ICore::self()->debugController()->breakpointModel();
        bool found = false;
        foreach (Breakpoint *b, m->breakpoints()) {
            if (b->location() == QLatin1String("main")) {
                found = true;
                break;
            }
        }
        if (!found) {
            m->addCodeBreakpoint(QStringLiteral("main"));
        }
    }

    // Needed so that breakpoint widget has a chance to insert breakpoints.
    // FIXME: a bit hacky, as we're really not ready for new commands.
    setDebuggerStateOn(s_dbgBusy);
    raiseEvent(debugger_ready);

    qCDebug(DEBUGGERLLDB) << "Per inferior configuration done";
}

bool DebugSession::execInferior(ILaunchConfiguration *cfg, IExecutePlugin *, const QString &)
{
    qCDebug(DEBUGGERLLDB) << "Executing inferior";

    KConfigGroup grp = cfg->config();

    // Start inferior
    bool remoteDebugging = grp.readEntry(Config::LldbRemoteDebuggingEntry, false);
    QUrl configLldbScript = grp.readEntry(Config::LldbConfigScriptEntry, QUrl());
    addCommand(new SentinelCommand([this, remoteDebugging, configLldbScript]() {
        // setup inferior I/O redirection
        if (!remoteDebugging) {
            // FIXME: a hacky way to emulate tty setting on linux. Not sure if this provides all needed
            // functionalities of a pty. Should make this conditional on other platforms.

            // no need to quote, settings set takes 'raw' input
            addCommand(MI::NonMI, QStringLiteral("settings set target.input-path %0").arg(m_tty->getSlave()));
            addCommand(MI::NonMI, QStringLiteral("settings set target.output-path %0").arg(m_tty->getSlave()));
            addCommand(MI::NonMI, QStringLiteral("settings set target.error-path %0").arg(m_tty->getSlave()));
        } else {
            // what is the expected behavior for using external terminal when doing remote debugging?
        }

        // send breakpoints already in our breakpoint model to lldb
        auto bc = breakpointController();
        bc->initSendBreakpoints();

        qCDebug(DEBUGGERLLDB) << "Turn on delete duplicate mode";
        // turn on delete duplicate breakpoints model, so that breakpoints created by user command in
        // the script and returned as a =breakpoint-created notification won't get duplicated with the
        // one already in our model.
        // we will turn this model off once we first reach a paused state, and from that time on,
        // the user can create duplicated breakpoints using normal command.
        bc->setDeleteDuplicateBreakpoints(true);
        // run custom config script right before we starting the inferior,
        // so the user has the freedom to change everything.
        if (configLldbScript.isValid()) {
            addCommand(MI::NonMI, QLatin1String("command source -s 0 ") + KShell::quoteArg(configLldbScript.toLocalFile()));
        }

        addCommand(MI::ExecRun, QString(), new ExecRunHandler(this),
                   CmdMaybeStartsRunning | CmdHandlesError);
    }, CmdMaybeStartsRunning));
    return true;
}

bool DebugSession::loadCoreFile(ILaunchConfiguration *,
                                const QString &debugee, const QString &corefile)
{
    addCommand(MI::FileExecAndSymbols, debugee,
               this, &DebugSession::handleFileExecAndSymbols,
               CmdHandlesError);
    raiseEvent(connected_to_program);

    addCommand(new CliCommand(NonMI, QLatin1String("target create -c ") + Utils::quote(corefile),
                              this, &DebugSession::handleCoreFile,
                              CmdHandlesError));
    return true;
}

void DebugSession::interruptDebugger()
{
    if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown))
        return;

    addCommand(ExecInterrupt, QString(), CmdInterrupt);
    return;
}

void DebugSession::ensureDebuggerListening()
{
    // lldb always uses async mode and prompt is always available.
    // no need to interrupt
    setDebuggerStateOff(s_dbgNotListening);
    // NOTE: there is actually a bug in lldb-mi that, when receiving SIGINT,
    // it would print '^done', which doesn't coresponds to any previous command.
    // This confuses our command queue.
}

void DebugSession::handleFileExecAndSymbols(const MI::ResultRecord& r)
{
    if (r.reason == QLatin1String("error")) {
        KMessageBox::error(
            qApp->activeWindow(),
            i18n("<b>Could not start debugger:</b><br />")+
            r[QStringLiteral("msg")].literal(),
            i18n("Startup error"));
        stopDebugger();
    }
}

void DebugSession::handleTargetSelect(const MI::ResultRecord& r)
{
    if (r.reason == QLatin1String("error")) {
        KMessageBox::error(qApp->activeWindow(),
            i18n("<b>Error connecting to remote target:</b><br />")+
            r[QStringLiteral("msg")].literal(),
            i18n("Startup error"));
        stopDebugger();
    }
}

void DebugSession::handleCoreFile(const QStringList &s)
{
    qCDebug(DEBUGGERLLDB) << s;
    for (const auto &line : s) {
        if (line.startsWith(QLatin1String("error:"))) {
            KMessageBox::error(
                qApp->activeWindow(),
                i18n("<b>Failed to load core file</b>"
                "<p>Debugger reported the following error:"
                "<p><tt>%1",
                s.join(QLatin1Char('\n'))),
                i18n("Startup error"));
            stopDebugger();
            return;
        }
    }
    // There's no "thread-group-started" notification from lldb-mi, so pretend we have received one.
    // see MIDebugSession::processNotification(const MI::AsyncRecord & async)
    setDebuggerStateOff(s_appNotStarted | s_programExited);

    setDebuggerStateOn(s_programExited | s_core);
}

void DebugSession::handleVersion(const QStringList& s)
{
    m_hasCorrectCLIOutput = !s.isEmpty();
    if (!m_hasCorrectCLIOutput) {
        // No output from 'version' command. It's likely that
        // the lldb used is not patched for the CLI output

        if (!qobject_cast<QGuiApplication*>(qApp))  {
            //for unittest
            qFatal("You need a graphical application.");
        }

        auto ans = KMessageBox::warningYesNo(
            qApp->activeWindow(),
            i18n("<b>Your lldb-mi version is unsupported, as it lacks an essential patch.</b><br/>"
                 "See https://llvm.org/bugs/show_bug.cgi?id=28026 for more information.<br/>"
                 "Debugger console will be disabled to prevent crash.<br/>"
                 "Do you want to continue?"),
            i18n("LLDB Version Unsupported"),
            KStandardGuiItem::yes(),
            KStandardGuiItem::no(),
            QStringLiteral("unsupported-lldb-debugger"));
        if (ans == KMessageBox::ButtonCode::No) {
            programFinished(QStringLiteral("Stopped because of unsupported LLDB version"));
            stopDebugger();
        }
        return;
    }

    qCDebug(DEBUGGERLLDB) << s.first();

// minimal version is 3.8.1
#ifdef Q_OS_OSX
    QRegularExpression rx(QStringLiteral("^lldb-(\\d+).(\\d+).(\\d+)\\b"), QRegularExpression::MultilineOption);
    // lldb 3.8.1 reports version 350.99.0 on OS X
    const int min_ver[] = {350, 99, 0};
#else
    QRegularExpression rx(QStringLiteral("^lldb version (\\d+).(\\d+).(\\d+)\\b"), QRegularExpression::MultilineOption);
    const int min_ver[] = {3, 8, 1};
#endif

    auto match = rx.match(s.first());
    int version[] = {0, 0, 0};
    if (match.hasMatch()) {
        for (int i = 0; i != 3; ++i) {
            version[i] = match.captured(i+1).toInt();
        }
    }

    bool ok = true;
    for (int i = 0; i < 3; ++i) {
        if (version[i] < min_ver[i]) {
            ok = false;
            break;
        } else if (version[i] > min_ver[i]) {
            ok = true;
            break;
        }
    }

    if (!ok) {
        if (!qobject_cast<QGuiApplication*>(qApp))  {
            //for unittest
            qFatal("You need a graphical application.");
        }

        KMessageBox::error(
            qApp->activeWindow(),
            i18n("<b>You need lldb-mi from LLDB 3.8.1 or higher.</b><br />"
            "You are using: %1", s.first()),
            i18n("LLDB Error"));
        stopDebugger();
    }
}

void DebugSession::updateAllVariables()
{
    // FIXME: this is only a workaround for lldb-mi doesn't provide -var-update changelist
    // for variables that have a python synthetic provider. Remove this after this is fixed
    // in the upstream.

    // re-fetch all toplevel variables, as -var-update doesn't work with data formatter
    // we have to pick out top level variables first, as refetching will delete child
    // variables.
    QList<LldbVariable*> toplevels;
    for (auto it = m_allVariables.begin(), ite = m_allVariables.end(); it != ite; ++it) {
        LldbVariable *var = qobject_cast<LldbVariable*>(it.value());
        if (var->topLevel()) {
            toplevels << var;
        }
    }

    for (auto var : qAsConst(toplevels)) {
        var->refetch();
    }
}

void DebugSession::handleSessionStateChange(IDebugSession::DebuggerState state)
{
    if (state == IDebugSession::PausedState) {
        // session is paused, the user can input any commands now.
        // Turn off delete duplicate breakpoints mode, as the user
        // may intentionaly want to do this.
        qCDebug(DEBUGGERLLDB) << "Turn off delete duplicate mode";
        breakpointController()->setDeleteDuplicateBreakpoints(false);
    }
}