// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "crashhandler.h" #include "crashhandlerdialog.h" #include "backtracecollector.h" #include "utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const char FileDistroInformation[] = "/etc/lsb-release"; static const char FileKernelVersion[] = "/proc/version"; static const char QtCreatorExecutable[] = "qtcreator"; static QString collectLinuxDistributionInfo() { return QString::fromLatin1(fileContents(QLatin1String(FileDistroInformation))); } static QString collectKernelVersionInfo() { return QString::fromLatin1(fileContents(QLatin1String(FileKernelVersion))); } // Convience class for interacting with exec() family of functions. class CExecList : public QVector { public: CExecList(const QStringList &list) { for (const QString &item : list) append(qstrdup(item.toLatin1().data())); append(0); } ~CExecList() { for (int i = 0; i < size(); ++i) delete[] value(i); } }; class CrashHandlerPrivate { public: CrashHandlerPrivate(pid_t pid, const QString &signalName, const QString &appName, CrashHandler *crashHandler) : pid(pid), creatorInPath(Utils::Environment::systemEnvironment().searchInPath(QLatin1String(QtCreatorExecutable))), dialog(crashHandler, signalName, appName) {} const pid_t pid; const Utils::FilePath creatorInPath; // Backup debugger. BacktraceCollector backtraceCollector; CrashHandlerDialog dialog; QStringList restartAppCommandLine; QStringList restartAppEnvironment; }; CrashHandler::CrashHandler(pid_t pid, const QString &signalName, const QString &appName, RestartCapability restartCap, QObject *parent) : QObject(parent), d(new CrashHandlerPrivate(pid, signalName, appName, this)) { connect(&d->backtraceCollector, &BacktraceCollector::error, this, &CrashHandler::onError); connect(&d->backtraceCollector, &BacktraceCollector::backtraceChunk, this, &CrashHandler::onBacktraceChunk); connect(&d->backtraceCollector, &BacktraceCollector::backtrace, this, &CrashHandler::onBacktraceFinished); d->dialog.appendDebugInfo(collectKernelVersionInfo()); d->dialog.appendDebugInfo(collectLinuxDistributionInfo()); if (restartCap == DisableRestart || !collectRestartAppData()) { d->dialog.disableRestartAppCheckBox(); if (d->creatorInPath.isEmpty()) d->dialog.disableDebugAppButton(); } d->dialog.show(); } CrashHandler::~CrashHandler() { delete d; } void CrashHandler::run() { d->backtraceCollector.run(d->pid); } void CrashHandler::onError(const QString &errorMessage) { d->dialog.setToFinalState(); QTextStream(stderr) << errorMessage; const QString text = QLatin1String("A problem occurred providing the backtrace. " "Please make sure to have the debugger \"gdb\" installed.\n"); d->dialog.appendDebugInfo(text); d->dialog.appendDebugInfo(errorMessage); } void CrashHandler::onBacktraceChunk(const QString &chunk) { d->dialog.appendDebugInfo(chunk); } void CrashHandler::onBacktraceFinished(const QString &backtrace) { d->dialog.setToFinalState(); // Select first line of relevant thread. // Example debugger output: // ... // [Current thread is 1 (Thread 0x7f1c33c79780 (LWP 975))] // ... // Thread 1 (Thread 0x7f1c33c79780 (LWP 975)): // ... const QRegularExpression rx(QLatin1String("\\[Current thread is (\\d+)")); const QRegularExpressionMatch match = rx.match(backtrace); if (!match.hasMatch()) return; const QString threadNumber = match.captured(1); const QString textToSelect = QString::fromLatin1("Thread %1").arg(threadNumber); d->dialog.selectLineWithContents(textToSelect); } void CrashHandler::openBugTracker() { QDesktopServices::openUrl(QUrl(QLatin1String(URL_BUGTRACKER))); } bool CrashHandler::collectRestartAppData() { const QString procDir = QString::fromLatin1("/proc/%1").arg(d->pid); // Get command line. // man 5 proc: /proc/[pid]/cmdline // The command-line arguments appear in this file as a set of strings separated by // null bytes ('\0'), with a further null byte after the last string. const QString procCmdFileName = procDir + QLatin1String("/cmdline"); QList commandLine = fileContents(procCmdFileName).split('\0'); if (commandLine.size() < 2) { qWarning("%s: Unexpected format in file '%s'.\n", Q_FUNC_INFO, qPrintable(procCmdFileName)); return false; } commandLine.removeLast(); for (const QByteArray &item : std::as_const(commandLine)) d->restartAppCommandLine.append(QString::fromLatin1(item)); // Get environment. // man 5 proc: /proc/[pid]/environ // The entries are separated by null bytes ('\0'), and there may be a null byte at the end. const QString procEnvFileName = procDir + QLatin1String("/environ"); QList environment = fileContents(procEnvFileName).split('\0'); if (environment.isEmpty()) { qWarning("%s: Unexpected format in file '%s'.\n", Q_FUNC_INFO, qPrintable(procEnvFileName)); return false; } if (environment.last().isEmpty()) environment.removeLast(); for (const QByteArray &item : std::as_const(environment)) d->restartAppEnvironment.append(QString::fromLatin1(item)); return true; } void CrashHandler::runCommand(QStringList commandLine, QStringList environment, WaitMode waitMode) { // TODO: If QTBUG-2284 is resolved, use QProcess::startDetached() here. // We can't use QProcess::startDetached because of bug // // QTBUG-2284 // QProcess::startDetached does not support setting an environment for the new process // // therefore, we use fork-exec. pid_t pid = fork(); switch (pid) { case -1: // error qFatal("%s: fork() failed.", Q_FUNC_INFO); break; case 0: { // child CExecList argv(commandLine); CExecList envp(environment); qDebug("Running\n"); for (int i = 0; argv[i]; ++i) qDebug(" %s", argv[i]); if (!environment.isEmpty()) { qDebug("\nwith environment:\n"); for (int i = 0; envp[i]; ++i) qDebug(" %s", envp[i]); } // The standards pipes must be open, otherwise the application will // receive a SIGPIPE as soon as these are used. if (freopen("/dev/null", "r", stdin) == 0) qFatal("%s: freopen() failed for stdin: %s.\n", Q_FUNC_INFO, strerror(errno)); if (freopen("/dev/null", "w", stdout) == 0) qFatal("%s: freopen() failed for stdout: %s.\n", Q_FUNC_INFO, strerror(errno)); if (freopen("/dev/null", "w", stderr) == 0) qFatal("%s: freopen() failed for stderr: %s.\n.", Q_FUNC_INFO, strerror(errno)); if (environment.isEmpty()) execvp(argv[0], argv.data()); else execvpe(argv[0], argv.data(), envp.data()); _exit(EXIT_FAILURE); } default: // parent if (waitMode == WaitForExit) { while (true) { int status; if (waitpid(pid, &status, 0) == -1) { if (errno == EINTR) // Signal handler of QProcess for SIGCHLD was triggered. continue; perror("waitpid() failed unexpectedly"); } if (WIFEXITED(status)) { qDebug("Child exited with exit code %d.", WEXITSTATUS(status)); break; } else if (WIFSIGNALED(status)) { qDebug("Child terminated by signal %d.", WTERMSIG(status)); break; } } } break; } } void CrashHandler::restartApplication() { runCommand(d->restartAppCommandLine, d->restartAppEnvironment, DontWaitForExit); } void CrashHandler::debugApplication() { // User requested to debug the app while our debugger is running. if (d->backtraceCollector.isRunning()) { if (!d->dialog.runDebuggerWhileBacktraceNotFinished()) return; if (d->backtraceCollector.isRunning()) { d->backtraceCollector.disconnect(); d->backtraceCollector.kill(); d->dialog.setToFinalState(); d->dialog.appendDebugInfo(tr("\n\nCollecting backtrace aborted by user.")); QCoreApplication::processEvents(); // Show the last appended output immediately. } } // Prepare command. QString executable = d->creatorInPath.toString(); if (executable.isEmpty() && !d->restartAppCommandLine.isEmpty()) executable = d->restartAppCommandLine.at(0); const QStringList commandLine = QStringList({executable, "-debug", QString::number(d->pid)}); QStringList environment; if (!d->restartAppEnvironment.isEmpty()) environment = d->restartAppEnvironment; // The UI is blocked/frozen anyway, so hide the dialog while debugging. d->dialog.hide(); runCommand(commandLine, environment, WaitForExit); d->dialog.show(); }