drkonqibackends.cpp 9.97 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
    Copyright (C) 2009  George Kiagiadakis <gkiagia@users.sourceforge.net>

    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, see <http://www.gnu.org/licenses/>.
*/
#include "drkonqibackends.h"

#include <cstdlib>
#include <cerrno>
#include <sys/types.h>
#include <signal.h>

24
25
#include <QTimer>
#include <QDir>
26
#include <QRegularExpression>
27
28
29
30
31

#include <KConfig>
#include <KConfigGroup>
#include <KCrash>
#include <QStandardPaths>
Laurent Montel's avatar
Laurent Montel committed
32
#include "drkonqi_debug.h"
33
34
35
36
37
38
39

#include "crashedapplication.h"
#include "debugger.h"
#include "debuggermanager.h"
#include "backtracegenerator.h"
#include "drkonqi.h"

René J.V. Bertin's avatar
René J.V. Bertin committed
40
41
42
43
#ifdef Q_OS_MACOS
#include <AvailabilityMacros.h>
#endif

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
AbstractDrKonqiBackend::~AbstractDrKonqiBackend()
{
}

bool AbstractDrKonqiBackend::init()
{
    m_crashedApplication = constructCrashedApplication();
    m_debuggerManager = constructDebuggerManager();
    return true;
}


KCrashBackend::KCrashBackend()
    : QObject(), AbstractDrKonqiBackend(), m_state(ProcessRunning)
{
}

KCrashBackend::~KCrashBackend()
{
    continueAttachedProcess();
}

bool KCrashBackend::init()
{
    AbstractDrKonqiBackend::init();

    //check whether the attached process exists and whether we have permissions to inspect it
    if (crashedApplication()->pid() <= 0) {
Laurent Montel's avatar
Laurent Montel committed
72
        qCWarning(DRKONQI_LOG) << "Invalid pid specified";
73
74
75
76
77
78
79
        return false;
    }

#if !defined(Q_OS_WIN32)
    if (::kill(crashedApplication()->pid(), 0) < 0) {
        switch (errno) {
        case EPERM:
Laurent Montel's avatar
Laurent Montel committed
80
            qCWarning(DRKONQI_LOG) << "DrKonqi doesn't have permissions to inspect the specified process";
81
82
            break;
        case ESRCH:
Laurent Montel's avatar
Laurent Montel committed
83
            qCWarning(DRKONQI_LOG) << "The specified process does not exist.";
84
85
86
87
88
89
90
91
92
93
94
            break;
        default:
            break;
        }
        return false;
    }

    //--keeprunning means: generate backtrace instantly and let the process continue execution
    if(DrKonqi::isKeepRunning()) {
        stopAttachedProcess();
        debuggerManager()->backtraceGenerator()->start();
95
        connect(debuggerManager(), &DebuggerManager::debuggerFinished, this, &KCrashBackend::continueAttachedProcess);
96
    } else {
97
98
        connect(debuggerManager(), &DebuggerManager::debuggerStarting, this, &KCrashBackend::onDebuggerStarting);
        connect(debuggerManager(), &DebuggerManager::debuggerFinished, this, &KCrashBackend::onDebuggerFinished);
99
100
101
102
103
104

        //stop the process to avoid high cpu usage by other threads (bug 175362).
        //if the process was started by kdeinit, we need to wait a bit for KCrash
        //to reach the alarm(0); call in kdeui/util/kcrash.cpp line 406 or else
        //if we stop it before this call, pending alarm signals will kill the
        //process when we try to continue it.
105
        QTimer::singleShot(2000, this, &KCrashBackend::stopAttachedProcess);
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
    }
#endif

    //Handle drkonqi crashes
    s_pid = crashedApplication()->pid(); //copy pid for use by the crash handler, so that it is safer
    KCrash::setEmergencySaveFunction(emergencySaveFunction);

    return true;
}

CrashedApplication *KCrashBackend::constructCrashedApplication()
{
    CrashedApplication *a = new CrashedApplication(this);
    a->m_datetime = QDateTime::currentDateTime();
    a->m_name = DrKonqi::programName();
Laurent Montel's avatar
Laurent Montel committed
121
122
    a->m_version = DrKonqi::appVersion();
    a->m_reportAddress = BugReportAddress(DrKonqi::bugAddress());
123
124
125
126
127
128
    a->m_pid = DrKonqi::pid();
    a->m_signalNumber = DrKonqi::signal();
    a->m_restarted = DrKonqi::isRestarted();
    a->m_thread = DrKonqi::thread();

    //try to determine the executable that crashed
129
130
131
    const QString procPath(QStringLiteral("/proc/%1").arg(a->m_pid));
    const QString exeProcPath(procPath + QStringLiteral("/exe"));
    if (QFileInfo(exeProcPath).exists()) {
132
        //on linux, the fastest and most reliable way is to get the path from /proc
Laurent Montel's avatar
Laurent Montel committed
133
        qCDebug(DRKONQI_LOG) << "Using /proc to determine executable path";
134
        const QString exePath = QFile::symLinkTarget(exeProcPath);
135

136
        a->m_executable.setFile(exePath);
137
        if (DrKonqi::isKdeinit() ||
138
            a->m_executable.fileName().startsWith(QLatin1String("python")) ) {
139
140
            a->m_fakeBaseName = DrKonqi::appName();
        }
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

        QDir mapFilesDir(procPath + QStringLiteral("/map_files"));
        mapFilesDir.setFilter(mapFilesDir.filter() | QDir::System); // proc is system!

        // "/bin/foo (deleted)" is how the kernel tells us that a file has been deleted since
        // it was mmap'd.
        QRegularExpression expression(QStringLiteral("(?<path>.+) \\(deleted\\)$"));
        // For the map_files we filter only .so files to ensure that
        // we don't trip over cache files or the like, as a result we
        // manually need to check if the main exe was deleted and add
        // it.
        // NB: includes .so* and .py* since we also implicitly support snakes to
        //   a degree
        QRegularExpression soExpression(QStringLiteral("(?<path>.+\\.(so|py)([^/]*)) \\(deleted\\)$"));

        bool hasDeletedFiles = false;

        const auto exeMatch = expression.match(exePath);
        if (exeMatch.isValid() && exeMatch.hasMatch()) {
            hasDeletedFiles = true;
        }

        const auto list = mapFilesDir.entryInfoList();
        for (auto it = list.constBegin(); !hasDeletedFiles && it != list.constEnd(); ++it) {
            const auto match = soExpression.match(it->symLinkTarget());
            if (!match.isValid() || !match.hasMatch()) {
                continue;
            }
169
170
171
172
173
            const QString path = match.captured(QStringLiteral("path"));
            if (path.startsWith(QStringLiteral("/memfd"))) {
                // Qml.so's JIT shows up under memfd. This is a false positive against our regex.
                continue;
            }
174
175
176
177
178
179
            hasDeletedFiles = true;
        }

        a->m_hasDeletedFiles = hasDeletedFiles;

        qCDebug(DRKONQI_LOG) << "exe" << exePath << "has deleted files:" << hasDeletedFiles;
180
181
    } else {
        if ( DrKonqi::isKdeinit() ) {
182
            a->m_executable = QFileInfo(QStandardPaths::findExecutable(QStringLiteral("kdeinit5")));
183
184
185
186
187
188
189
190
191
192
193
194
195
196
            a->m_fakeBaseName = DrKonqi::appName();
        } else {
            QFileInfo execPath(DrKonqi::appName());
            if ( execPath.isAbsolute() ) {
                a->m_executable = execPath;
            } else if ( !DrKonqi::appPath().isEmpty() ) {
                QDir execDir(DrKonqi::appPath());
                a->m_executable = execDir.absoluteFilePath(execPath.fileName());
            } else {
                a->m_executable = QFileInfo(QStandardPaths::findExecutable(execPath.fileName()));
            }
        }
    }

Laurent Montel's avatar
Laurent Montel committed
197
198
    qCDebug(DRKONQI_LOG) << "Executable is:" << a->m_executable.absoluteFilePath();
    qCDebug(DRKONQI_LOG) << "Executable exists:" << a->m_executable.exists();
199
200
201
202
203
204

    return a;
}

DebuggerManager *KCrashBackend::constructDebuggerManager()
{
205
    QList<Debugger> internalDebuggers = Debugger::availableInternalDebuggers(QStringLiteral("KCrash"));
206
    KConfigGroup config(KSharedConfig::openConfig(), "DrKonqi");
René J.V. Bertin's avatar
René J.V. Bertin committed
207
208
209
#if defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED > 1070
    QString defaultDebuggerName = config.readEntry("Debugger", QStringLiteral("lldb"));
#elif !defined(Q_OS_WIN)
210
    QString defaultDebuggerName = config.readEntry("Debugger", QStringLiteral("gdb"));
211
#else
patrick pereira's avatar
patrick pereira committed
212
    QString defaultDebuggerName = config.readEntry("Debugger", QStringLiteral("cdb"));
213
214
215
216
#endif

    Debugger firstKnownGoodDebugger, preferredDebugger;
    foreach (const Debugger & debugger, internalDebuggers) {
217
218
219
        qCDebug(DRKONQI_LOG) << "Check debugger if"
                             << debugger.displayName() << "[" << debugger.codeName() << "]"
                             << "is installed:" << debugger.isInstalled();
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
        if (!firstKnownGoodDebugger.isValid() && debugger.isInstalled()) {
            firstKnownGoodDebugger = debugger;
        }
        if (debugger.codeName() == defaultDebuggerName) {
            preferredDebugger = debugger;
        }
        if (firstKnownGoodDebugger.isValid() && preferredDebugger.isValid()) {
            break;
        }
    }

    if (!preferredDebugger.isInstalled()) {
        if (firstKnownGoodDebugger.isValid()) {
            preferredDebugger = firstKnownGoodDebugger;
        } else {
Laurent Montel's avatar
Laurent Montel committed
235
            qCWarning(DRKONQI_LOG) << "Unable to find an internal debugger that can work with the KCrash backend";
236
237
238
        }
    }

239
    qCDebug(DRKONQI_LOG) << "Using debugger:" << preferredDebugger.codeName();
240
    return new DebuggerManager(preferredDebugger, Debugger::availableExternalDebuggers(QStringLiteral("KCrash")), this);
241
242
243
244
245
}

void KCrashBackend::stopAttachedProcess()
{
    if (m_state == ProcessRunning) {
Laurent Montel's avatar
Laurent Montel committed
246
        qCDebug(DRKONQI_LOG) << "Sending SIGSTOP to process";
247
248
249
250
251
252
253
254
        ::kill(crashedApplication()->pid(), SIGSTOP);
        m_state = ProcessStopped;
    }
}

void KCrashBackend::continueAttachedProcess()
{
    if (m_state == ProcessStopped) {
Laurent Montel's avatar
Laurent Montel committed
255
        qCDebug(DRKONQI_LOG) << "Sending SIGCONT to process";
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
        ::kill(crashedApplication()->pid(), SIGCONT);
        m_state = ProcessRunning;
    }
}

void KCrashBackend::onDebuggerStarting()
{
    continueAttachedProcess();
    m_state = DebuggerRunning;
}

void KCrashBackend::onDebuggerFinished()
{
    m_state = ProcessRunning;
    stopAttachedProcess();
}

//static
qint64 KCrashBackend::s_pid = 0;

//static
void KCrashBackend::emergencySaveFunction(int signal)
{
    // In case drkonqi itself crashes, we need to get rid of the process being debugged,
    // so we kill it, no matter what its state was.
    Q_UNUSED(signal);
    ::kill(s_pid, SIGKILL);
}