Commit 903281b3 authored by Nikolai Kosjar's avatar Nikolai Kosjar

Linux: Add a crash handler providing a backtrace for debug builds.

Use case: You're working with a debug version of Qt Creator and you're
interested in getting a backtrace displayed as soon as Qt Creator
crashes without searching for the core file, starting your debugger, ...

Once a 'serious signal' (currently SIGILL, SIGFPE, SIGSEGV, SIGBUS,
SIGPIPE) is delivered, a popup displays the following debug information:

 - Qt Creator version (same as in the about dialog)
 - Kernel version (uname -a)
 - GNU/Linux Distribution (/etc/lsb-release)
 - Backtrace (by gdb)

Please note that this crash handler is built and used only in debug mode
on GNU/Linux. It's solely meant as a convenience tool for the developer.

In contrast to the breakpad integration, this crash handler operates
'offline'. There is no network i/o involved.

Change-Id: Idcfb1bf1ad68942615ecfe0dffc0d03154455049
Reviewed-by: default avatarChristian Kandeler <christian.kandeler@digia.com>
Reviewed-by: default avatarhjk <qthjk@ovi.com>
parent 6d06d47a
......@@ -77,6 +77,7 @@ Project {
"src/plugins/welcome/welcome.qbs",
"src/share/share.qbs",
"src/tools/qtcdebugger/qtcdebugger.qbs",
"src/tools/qtcreatorcrashhandler/qtcreatorcrashhandler.qbs",
"src/tools/qtpromaker/qtpromaker.qbs",
"src/plugins/cpaster/frontend/frontend.qbs"
]
......@@ -178,6 +179,14 @@ Project {
]
}
Group {
condition: qbs.targetOS == "linux"
files: [
"src/tools/qtcreatorcrashhandler/crashhandlersetup.cpp",
"src/tools/qtcreatorcrashhandler/crashhandlersetup.h"
]
}
Group {
condition: qbs.targetOS == "windows"
files: [
......
......@@ -6,6 +6,13 @@ TARGET = $$IDE_APP_TARGET
DESTDIR = $$IDE_APP_PATH
SOURCES += main.cpp
linux-* {
# Build only in debug mode.
debug_and_release|CONFIG(debug, debug|release) {
HEADERS += ../tools/qtcreatorcrashhandler/crashhandlersetup.h
SOURCES += ../tools/qtcreatorcrashhandler/crashhandlersetup.cpp
}
}
include(../rpath.pri)
......
......@@ -29,6 +29,7 @@
**************************************************************************/
#include "qtsingleapplication.h"
#include "../tools/qtcreatorcrashhandler/crashhandlersetup.h"
#include <app/app_version.h>
#include <extensionsystem/iplugin.h>
......@@ -228,6 +229,8 @@ int main(int argc, char **argv)
const int threadCount = QThreadPool::globalInstance()->maxThreadCount();
QThreadPool::globalInstance()->setMaxThreadCount(qMax(4, 2 * threadCount));
setupCrashHandler(); // Display a backtrace once a serious signal is delivered.
#ifdef ENABLE_QT_BREAKPAD
QtSystemExceptionHandler systemExceptionHandler;
#endif
......@@ -432,5 +435,7 @@ int main(int argc, char **argv)
QTimer::singleShot(100, &pluginManager, SLOT(startTests()));
#endif
return app.exec();
const int r = app.exec();
cleanupCrashHandler();
return r;
}
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: http://www.qt-project.org/
**
**
** GNU Lesser General Public License Usage
**
** This file may be used under the terms of the GNU Lesser General Public
** License version 2.1 as published by the Free Software Foundation and
** appearing in the file LICENSE.LGPL included in the packaging of this file.
** Please review the following information to ensure the GNU Lesser General
** Public License version 2.1 requirements will be met:
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** Other Usage
**
** Alternatively, this file may be used in accordance with the terms and
** conditions contained in a signed written agreement between you and Nokia.
**
**
**************************************************************************/
#include "backtracecollector.h"
#include <QDebug>
#include <QScopedPointer>
#include <QTemporaryFile>
const char GdbBatchCommands[] =
"set height 0\n"
"set width 0\n"
"thread\n"
"thread apply all backtrace full\n";
class BacktraceCollectorPrivate
{
public:
BacktraceCollectorPrivate() : errorOccurred(false) {}
bool errorOccurred;
QScopedPointer<QTemporaryFile> commandFile;
QProcess debugger;
QString output;
};
BacktraceCollector::BacktraceCollector(QObject *parent) :
QObject(parent), d(new BacktraceCollectorPrivate)
{
connect(&d->debugger, SIGNAL(finished(int,QProcess::ExitStatus)),
SLOT(onDebuggerFinished(int,QProcess::ExitStatus)));
connect(&d->debugger, SIGNAL(error(QProcess::ProcessError)),
SLOT(onDebuggerError(QProcess::ProcessError)));
connect(&d->debugger, SIGNAL(readyRead()), SLOT(onDebuggerOutputAvailable()));
d->debugger.setProcessChannelMode(QProcess::MergedChannels);
}
BacktraceCollector::~BacktraceCollector()
{
delete d;
}
void BacktraceCollector::run(Q_PID pid)
{
d->debugger.start(QLatin1String("gdb"), QStringList()
<< QLatin1String("--nw") // Do not use a window interface.
<< QLatin1String("--nx") // Do not read .gdbinit file.
<< QLatin1String("--batch") // Exit after processing options.
<< QLatin1String("--command") << createTemporaryCommandFile()
<< QLatin1String("--pid") << QString::number(pid)
);
}
void BacktraceCollector::onDebuggerFinished(int exitCode, QProcess::ExitStatus /*exitStatus*/)
{
if (d->errorOccurred) {
emit error(QLatin1String("QProcess: ") + d->debugger.errorString());
return;
}
if (exitCode != 0) {
emit error(QString::fromLatin1("Debugger exited with code %1.").arg(exitCode));
return;
}
emit backtrace(d->output);
}
void BacktraceCollector::onDebuggerError(QProcess::ProcessError /*error*/)
{
d->errorOccurred = true;
}
QString BacktraceCollector::createTemporaryCommandFile()
{
d->commandFile.reset(new QTemporaryFile);
if (!d->commandFile->open()) {
emit error(QLatin1String("Error: Could not create temporary command file."));
return QString();
}
if (d->commandFile->write(GdbBatchCommands) == -1) {
emit error(QLatin1String("Error: Could not write temporary command file."));
return QString();
}
d->commandFile->close();
return d->commandFile->fileName();
}
void BacktraceCollector::onDebuggerOutputAvailable()
{
const QString newChunk = d->debugger.readAll();
d->output.append(newChunk);
emit backtraceChunk(newChunk);
}
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: http://www.qt-project.org/
**
**
** GNU Lesser General Public License Usage
**
** This file may be used under the terms of the GNU Lesser General Public
** License version 2.1 as published by the Free Software Foundation and
** appearing in the file LICENSE.LGPL included in the packaging of this file.
** Please review the following information to ensure the GNU Lesser General
** Public License version 2.1 requirements will be met:
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** Other Usage
**
** Alternatively, this file may be used in accordance with the terms and
** conditions contained in a signed written agreement between you and Nokia.
**
**
**************************************************************************/
#ifndef BACKTRACECOLLECTOR_H
#define BACKTRACECOLLECTOR_H
#include <QProcess>
class BacktraceCollectorPrivate;
class BacktraceCollector : public QObject
{
Q_OBJECT
public:
explicit BacktraceCollector(QObject *parent = 0);
~BacktraceCollector();
void run(Q_PID pid);
signals:
void error(const QString &errorMessage);
void backtrace(const QString &backtrace);
void backtraceChunk(const QString &chunk);
private slots:
void onDebuggerOutputAvailable();
void onDebuggerFinished(int exitCode, QProcess::ExitStatus exitStatus);
void onDebuggerError(QProcess::ProcessError err);
private:
QString createTemporaryCommandFile();
BacktraceCollectorPrivate *d;
};
#endif // BACKTRACECOLLECTOR_H
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: http://www.qt-project.org/
**
**
** GNU Lesser General Public License Usage
**
** This file may be used under the terms of the GNU Lesser General Public
** License version 2.1 as published by the Free Software Foundation and
** appearing in the file LICENSE.LGPL included in the packaging of this file.
** Please review the following information to ensure the GNU Lesser General
** Public License version 2.1 requirements will be met:
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** Other Usage
**
** Alternatively, this file may be used in accordance with the terms and
** conditions contained in a signed written agreement between you and Nokia.
**
**
**************************************************************************/
#include "crashhandler.h"
#include "crashhandlerdialog.h"
#include "backtracecollector.h"
#include "utils.h"
#include <QApplication>
#include <QDebug>
#include <QDesktopServices>
#include <QFile>
#include <QRegExp>
#include <QTextStream>
#include <QUrl>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
static const char FileDistroInformation[] = "/etc/lsb-release";
static const char FileKernelVersion[] = "/proc/version";
static QString collectLinuxDistributionInfo()
{
return QString::fromLatin1(fileContents(QLatin1String(FileDistroInformation)));
}
static QString collectKernelVersionInfo()
{
return QString::fromLatin1(fileContents(QLatin1String(FileKernelVersion)));
}
class CrashHandlerPrivate
{
public:
CrashHandlerPrivate(pid_t pid, CrashHandler *crashHandler)
: pid(pid), dialog(crashHandler), argv(0), envp(0) {}
~CrashHandlerPrivate()
{
if (argv) {
for (int i = 0; argv[i]; ++i)
delete[] argv[i];
}
if (envp) {
for (int i = 0; envp[i]; ++i)
delete[] envp[i];
}
free(argv);
free(envp);
}
pid_t pid;
BacktraceCollector backtraceCollector;
CrashHandlerDialog dialog;
// For restarting the process.
char **argv;
char **envp;
};
CrashHandler::CrashHandler(pid_t pid, QObject *parent)
: QObject(parent), d(new CrashHandlerPrivate(pid, this))
{
connect(&d->backtraceCollector, SIGNAL(error(QString)), SLOT(onError(QString)));
connect(&d->backtraceCollector, SIGNAL(backtraceChunk(QString)), SLOT(onBacktraceChunk(QString)));
connect(&d->backtraceCollector, SIGNAL(backtrace(QString)), SLOT(onBacktraceFinished(QString)));
d->dialog.appendDebugInfo(collectKernelVersionInfo());
d->dialog.appendDebugInfo(collectLinuxDistributionInfo());
d->dialog.show();
if (!collectRestartAppData()) // If we can't restart the app properly, ...
d->dialog.disableRestartAppButton();
}
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("There occured a problem 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);
QTextStream out(stdout);
out << 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)):
// ...
QRegExp rx("\\[Current thread is (\\d+)");
const int pos = rx.indexIn(backtrace);
if (pos == -1)
return;
const QString threadNumber = rx.cap(1);
const QString textToSelect = QString::fromLatin1("Thread %1").arg(threadNumber);
d->dialog.selectLineWithContents(textToSelect);
}
void CrashHandler::openBugTracker()
{
QDesktopServices::openUrl(QUrl(URL_BUGTRACKER));
}
bool CrashHandler::collectRestartAppData()
{
const QString procDir = QString::fromLatin1("/proc/%1").arg(d->pid);
// Construct d->argv.
// 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<QByteArray> cmdEntries = fileContents(procCmdFileName).split('\0');
if (cmdEntries.size() < 2) {
qWarning("%s: Unexpected format in file '%s'.\n", Q_FUNC_INFO, qPrintable(procCmdFileName));
return false;
}
cmdEntries.removeLast();
char * const executable = qstrdup(qPrintable(cmdEntries.takeFirst()));
d->argv = (char **) malloc(sizeof(char*) * (cmdEntries.size() + 2));
if (d->argv == 0)
qFatal("%s: malloc() failed.\n", Q_FUNC_INFO);
d->argv[0] = executable;
int i;
for (i = 1; i <= cmdEntries.size(); ++i)
d->argv[i] = qstrdup(cmdEntries.at(i-1));
d->argv[i] = 0;
// Construct d->envp.
// 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<QByteArray> envEntries = fileContents(procEnvFileName).split('\0');
if (envEntries.isEmpty()) {
qWarning("%s: Unexpected format in file '%s'.\n", Q_FUNC_INFO, qPrintable(procEnvFileName));
return false;
}
if (envEntries.last().isEmpty())
envEntries.removeLast();
d->envp = (char **) malloc(sizeof(char*) * (envEntries.size() + 1));
if (d->envp == 0)
qFatal("%s: malloc() failed.\n", Q_FUNC_INFO);
for (i = 0; i < envEntries.size(); ++i)
d->envp[i] = qstrdup(envEntries.at(i));
d->envp[i] = 0;
return true;
}
void CrashHandler::restartApplication()
{
// TODO: If QTBUG-2284 is resolved, use QProcess::startDetached() here.
// Close the crash handler and start the process again with same environment and
// command line arguments.
//
// 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
qDebug("Restarting Qt Creator with\n");
for (int i = 0; d->argv[i]; ++i)
qDebug(" %s", d->argv[i]);
qDebug("\nand environment\n");
for (int i = 0; d->envp[i]; ++i)
qDebug(" %s", d->envp[i]);
// The standards pipes must be open, otherwise the restarted Qt Creator 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));
execve(d->argv[0], d->argv, d->envp);
_exit(EXIT_FAILURE);
default: // parent
qApp->quit();
break;
}
}
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: http://www.qt-project.org/
**
**
** GNU Lesser General Public License Usage
**
** This file may be used under the terms of the GNU Lesser General Public
** License version 2.1 as published by the Free Software Foundation and
** appearing in the file LICENSE.LGPL included in the packaging of this file.
** Please review the following information to ensure the GNU Lesser General
** Public License version 2.1 requirements will be met:
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** Other Usage
**
** Alternatively, this file may be used in accordance with the terms and
** conditions contained in a signed written agreement between you and Nokia.
**
**
**************************************************************************/
#ifndef CRASHHANDLER_H
#define CRASHHANDLER_H
#include <QObject>
QT_BEGIN_NAMESPACE
class QString;
QT_END_NAMESPACE
class ApplicationInfo;
class CrashHandlerPrivate;
class CrashHandler : public QObject
{
Q_OBJECT
public:
explicit CrashHandler(pid_t pid, QObject *parent = 0);
~CrashHandler();
void run();
public slots:
void onError(const QString &errorMessage);
void onBacktraceChunk(const QString &chunk);
void onBacktraceFinished(const QString &backtrace);
void openBugTracker();
void restartApplication();
private:
bool collectRestartAppData();
CrashHandlerPrivate *d;
};
#endif // CRASHHANDLER_H
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: http://www.qt-project.org/
**
**
** GNU Lesser General Public License Usage
**
** This file may be used under the terms of the GNU Lesser General Public
** License version 2.1 as published by the Free Software Foundation and
** appearing in the file LICENSE.LGPL included in the packaging of this file.
** Please review the following information to ensure the GNU Lesser General
** Public License version 2.1 requirements will be met:
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** Other Usage
**
** Alternatively, this file may be used in accordance with the terms and
** conditions contained in a signed written agreement between you and Nokia.
**
**
**************************************************************************/
#include "crashhandler.h"
#include "crashhandlerdialog.h"
#include "ui_crashhandlerdialog.h"
#include "utils.h"
#include <app/app_version.h>
#include <QClipboard>
#include <QIcon>
CrashHandlerDialog::CrashHandlerDialog(CrashHandler *handler, QWidget *parent) :
QDialog(parent),
m_crashHandler(handler),
m_ui(new Ui::CrashHandlerDialog)
{
m_ui->setupUi(this);
m_ui->introLabel->setTextFormat(Qt::RichText);
m_ui->introLabel->setOpenExternalLinks(true);
m_ui->debugInfoEdit->setReadOnly(true);
m_ui->progressBar->setMinimum(0);
m_ui->progressBar->setMaximum(0);
const QStyle * const style = QApplication::style();
m_ui->closeButton->setIcon(style->standardIcon(QStyle::SP_DialogCloseButton));
const int iconSize = style->pixelMetric(QStyle::PM_MessageBoxIconSize, 0);
QIcon icon = style->standardIcon(QStyle::SP_MessageBoxCritical);
m_ui->iconLabel->setPixmap(icon.pixmap(iconSize, iconSize));
connect(m_ui->copyToClipBoardButton, SIGNAL(clicked()), this, SLOT(copyToClipboardClicked()));
connect(m_ui->reportBugButton, SIGNAL(clicked()), m_crashHandler, SLOT(openBugTracker()));
connect(m_ui->restartAppButton, SIGNAL(clicked()), m_crashHandler, SLOT(restartApplication()));
connect(m_ui->closeButton, SIGNAL(clicked()), qApp, SLOT(quit()));
setApplicationInfo();
}
CrashHandlerDialog::~CrashHandlerDialog()
{
delete m_ui;
}
void CrashHandlerDialog::setToFinalState()
{
m_ui->progressBar->hide();
m_ui->copyToClipBoardButton->setEnabled(true);
m_ui->reportBugButton->setEnabled(true);
}
void CrashHandlerDialog::disableRestartAppButton()
{
m_ui->restartAppButton->setDisabled(true);
}
void CrashHandlerDialog::setApplicationInfo()
{
const QString ideName = QLatin1String("Qt Creator");
const QString contents = tr(
"<p><b>%1 has closed unexpectedly.</b></p>"
"<p>Please file a <a href='%2'>bug report</a> with the debug information provided below.</p>")
.arg(ideName, QLatin1String(URL_BUGTRACKER));
m_ui->introLabel->setText(contents);
QString revision;
#ifdef IDE_REVISION
revision = tr(" from revision %1").arg(QString::fromLatin1(Core::Constants::IDE_REVISION_STR).left(10));
#endif
const QString versionInformation = tr(
"%1 %2