1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-09-10 12:03:18 +02:00

troubleshooting: add configurable crash reporting with crashpad

This commit adds a basic crash-report system that can be optionally
configured to automatically send minidump crash-reports in addition
to product versions and a platform description including the OS
name and CPU architecture. Reports can be received at a configured
REST endpoint(POST). This endpoint URL can be configured using
a CMake variable `CRASH_REPORT_URL` which defaults to
"http://localhost:8080/submit".

- Introduces a new CMake option `ENABLE_CRASHREPORTS`, defaulting
  to OFF. This allows developers to enable crash reporting features
  at build time selectively. We also define a new macro with the
  same name to expose the state to QML in order to hide the UI
  components if needed.

- Implemented conditional inclusion of crashpad dependencies using
  `ENABLE_CRASHREPORTS`. If set, `ENABLE_CRASHPAD` is also enabled
  (other crash reporters exist and we may want to use them).

- 2 new application settings are added: `EnableCrashReporting` and
  `EnableAutomaticCrashReporting`. Default settings make it so
  crash-reports are generated but not automatically sent. With this
  default configuration, users will be prompted upon application
  start to confirm the report upload. Additionally, users may
  opt-in in order to have reports sent automatically at crash-time.

Gitlab: #1454
Change-Id: I53edab2dae210240a99272479381695fce1e221b
This commit is contained in:
Andreas Traczyk 2023-11-23 13:22:37 -05:00
parent 49d83fd937
commit 529b7cf529
18 changed files with 840 additions and 63 deletions

View file

@ -50,9 +50,13 @@ if(ENABLE_ASAN AND NOT MSVC)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
endif()
# Enable this option when building for production.
option(ENABLE_CRASHREPORTS "Enable crash reports" OFF)
# These values are exposed to QML and are better off being defined as values.
define_macro_with_value(WITH_WEBENGINE)
define_macro_with_value(APPSTORE)
define_macro_with_value(ENABLE_CRASHREPORTS)
# jami-core
if(NOT WITH_DAEMON_SUBMODULE)
@ -72,12 +76,6 @@ set(CLIENT_INCLUDE_DIRS, "")
set(CLIENT_LINK_DIRS, "")
set(CLIENT_LIBS, "")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT MSVC)
set(CMAKE_CXX_FLAGS_DEBUG "-Og -ggdb")
endif()
include(${PROJECT_SOURCE_DIR}/extras/build/cmake/contrib_tools.cmake)
set(EXTRA_PATCHES_DIR ${PROJECT_SOURCE_DIR}/extras/patches)
@ -87,6 +85,17 @@ list(APPEND QWINDOWKIT_OPTIONS
QWINDOWKIT_BUILD_STATIC ON
)
if(WIN32)
# Beta config
if(BETA)
message(STATUS "Beta config enabled")
add_definitions(-DBETA)
set(JAMI_OUTPUT_DIRECTORY_RELEASE ${PROJECT_SOURCE_DIR}/x64/Beta)
else()
set(JAMI_OUTPUT_DIRECTORY_RELEASE ${PROJECT_SOURCE_DIR}/x64/Release)
endif()
endif()
if(WIN32)
list(APPEND QWINDOWKIT_OPTIONS QWINDOWKIT_ENABLE_WINDOWS_SYSTEM_BORDERS OFF)
endif()
@ -110,6 +119,44 @@ add_fetch_content(
list(APPEND CLIENT_INCLUDE_DIRS ${QWindowKit_BINARY_DIR}/include)
list(APPEND CLIENT_LIBS QWindowKit::Quick)
# If ENABLE_CRASHREPORTS is enabled, we will use crashpad_cmake for now.
if(ENABLE_CRASHREPORTS)
set(ENABLE_CRASHPAD ON)
set(CRASH_REPORT_URL "http://localhost:8080/submit" CACHE STRING "URL for crash handler uploads")
endif()
add_definitions(-DCRASH_REPORT_URL="${CRASH_REPORT_URL}")
# Crash-report client: crashpad
if(ENABLE_CRASHPAD)
message(STATUS "Crashpad enabled for client")
if(WIN32)
set(CMAKE_OBJECT_PATH_MAX 256)
add_definitions(-DNOMINMAX)
endif()
add_fetch_content(
TARGET crashpad_cmake
URL https://github.com/TheAssemblyArmada/crashpad-cmake.git
BRANCH 80573adcc845071401c73c99eaec7fd9847d45fb
)
add_definitions(-DENABLE_CRASHPAD)
if (WIN32)
# This makes sure the console window doesn't show up when running the
# crashpad_handler executable.
set_target_properties(crashpad_handler PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS")
# Set the output directory for the crashpad_handler executable. On Windows,
# we use either the Release or Beta directory depending on the BETA option
# which is set above.
set_target_properties(crashpad_handler PROPERTIES
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${JAMI_OUTPUT_DIRECTORY_RELEASE}")
endif()
endif()
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT MSVC)
set(CMAKE_CXX_FLAGS_DEBUG "-Og -ggdb")
endif()
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
@ -323,7 +370,8 @@ set(COMMON_SOURCES
${APP_SRC_DIR}/imagedownloader.cpp
${APP_SRC_DIR}/pluginversionmanager.cpp
${APP_SRC_DIR}/connectioninfolistmodel.cpp
${APP_SRC_DIR}/pluginversionmanager.cpp)
${APP_SRC_DIR}/pluginversionmanager.cpp
)
set(COMMON_HEADERS
${APP_SRC_DIR}/global.h
@ -392,7 +440,10 @@ set(COMMON_HEADERS
${APP_SRC_DIR}/imagedownloader.h
${APP_SRC_DIR}/pluginversionmanager.h
${APP_SRC_DIR}/connectioninfolistmodel.h
${APP_SRC_DIR}/pttlistener.h)
${APP_SRC_DIR}/pttlistener.h
${APP_SRC_DIR}/crashreportclient.h
${APP_SRC_DIR}/crashreporter.h
)
# For libavutil/avframe.
set(LIBJAMI_CONTRIB_DIR "${DAEMON_DIR}/contrib")
@ -411,6 +462,15 @@ endif()
# Define PREFER_VULKAN to prefer Vulkan over the default API
# on GNU/Linux and Windows. Metal is always preferred on macOS.
if(ENABLE_CRASHREPORTS)
set(CRASHREPORT_CLIENT_DIR ${APP_SRC_DIR}/crashreportclients)
if(ENABLE_CRASHPAD)
list(APPEND CLIENT_LIBS crashpad_client)
list(APPEND COMMON_SOURCES ${CRASHREPORT_CLIENT_DIR}/crashpad.cpp)
list(APPEND COMMON_HEADERS ${CRASHREPORT_CLIENT_DIR}/crashpad.h)
endif()
endif()
if(MSVC)
set(WINDOWS_SYS_LIBS
windowsapp.lib
@ -456,16 +516,6 @@ if(MSVC)
set(JAMID_SRC_PATH ${DAEMON_DIR}/contrib/msvc/include)
set(GNUTLS_LIB ${DAEMON_DIR}/contrib/msvc/lib/x64/libgnutls.lib)
# Beta config
if(BETA)
message(STATUS "Beta config enabled")
add_definitions(-DBETA)
set(JAMI_OUTPUT_DIRECTORY_RELEASE ${PROJECT_SOURCE_DIR}/x64/Beta)
else()
set(JAMI_OUTPUT_DIRECTORY_RELEASE
${PROJECT_SOURCE_DIR}/x64/Release)
endif()
include_directories(
${JAMID_SRC_PATH}
${LIBCLIENT_SRC_DIR}

View file

@ -0,0 +1,9 @@
# python virtual environment
venv/
# python compiled files
*.pyc
# python cache
__pycache__
# example output
crash_reports/

View file

@ -0,0 +1,36 @@
# Crash report submission server examples
## Overview
This directory contains examples of crash report submission servers. These servers are responsible for receiving crash reports from clients and storing them. The examples are written in Python and use the Flask web framework.
## Running the examples
To run the examples, you need to have Python 3 installed. You can just use the virtual environment provided in this directory. To activate the virtual environment, run the following commands:
```
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
After activating the virtual environment, you can should be able to execute the example submission servers. To run the example submission server that uses the Crashpad format, run the following command:
```
python crashpad.py
```
## Metadata
The crash report submission servers expect the crash reports to contain a JSON object. The JSON object should contain the following basic metadata:
```
{
"build_id": "202410021437",
"client_sha": "77149ebd62",
"guid": "50c4218a-bcb9-48a9-8093-a06e6435cd61",
"jamicore_sha": "cbf8f0af6",
"platform": "Ubuntu 22.04.4 LTS_x86_64"
}
```
The `build_id` field is the build identifier of the client application. The `client_sha` field is the SHA-1 hash of the client application. The `guid` field is a unique identifier for the crash report. The `jamicore_sha` field is the SHA-1 hash of the Jami core library. The `platform` field is the platform on which the client application is running.

View file

@ -0,0 +1,51 @@
#!/usr/bin/env python3
import os
from flask import Flask, request
import json
app = Flask(__name__)
@app.route('/submit', methods=['POST'])
def submit():
try:
print("Received a crash report GUID: %s" % request.form.get('guid', 'No GUID provided'))
file_storage = request.files.get('upload_file_minidump')
dump_id = ""
if file_storage:
dump_id = file_storage.filename
# Create a directory to store the crash reports if it doesn't exist
base_path = 'crash_reports'
if not os.path.exists(base_path):
os.makedirs(base_path)
filepath = os.path.join(base_path, dump_id)
# Attempt to write the file, fail gracefully if it already exists
if os.path.exists(filepath):
print(f"File {filepath} already exists.")
return 'File already exists', 409
with open(filepath, 'wb') as f:
f.write(file_storage.read())
print(f"File saved successfully at {filepath}")
# Now save the metadata in {request.form} as separate filename <UID>.info.
# We assume the data is a JSON string.
metadata_filepath = os.path.join(base_path, f"{dump_id}.info")
with open(metadata_filepath, 'w') as f:
f.write(str(json.dumps(dict(request.form), indent=4)))
else:
print("No file found for the key 'upload_file_minidump'")
return 'No file found', 400
return 'Crash report received', 200
except OSError as e:
print(f"Error creating directory or writing file: {e}")
return 'Internal Server Error', 500
except Exception as e:
print(f"An unexpected error occurred: {e}")
return 'Internal Server Error', 500
if __name__ == '__main__':
app.run(port=8080, debug=True)

View file

@ -0,0 +1,5 @@
Flask==3.0.3
requests==2.24.0
markupsafe==2.1.1
itsdangerous==2.1.2
werkzeug==3.0.0

View file

@ -210,6 +210,22 @@ ApplicationWindow {
// Dbus error handler for Linux.
if (Qt.platform.os.toString() !== "windows" && Qt.platform.os.toString() !== "osx")
DBusErrorHandler.setActive(true);
// Handle potential crash recovery.
var crashedLastRun = crashReporter.getHasPendingReport();
if (crashedLastRun) {
// A crash was detected during the last session. We need to inform the user and offer to send a crash report.
var dlg = viewCoordinator.presentDialog(appWindow, "commoncomponents/ConfirmDialog.qml", {
"title": JamiStrings.crashReportTitle,
"textLabel": JamiStrings.crashReportMessage + "\n\n" + JamiStrings.crashReportMessageExtra,
"confirmLabel": JamiStrings.send,
"rejectLabel": JamiStrings.dontSend,
"textHAlign": Text.AlignLeft,
"textMaxWidth": 400,
});
dlg.accepted.connect(function () { crashReporter.uploadLastReport(); });
dlg.rejected.connect(function () { crashReporter.clearReports(); });
}
}
Loader {

View file

@ -24,8 +24,7 @@
#include <QCoreApplication>
#include <QLibraryInfo>
#include <locale.h>
#include <QDir>
const QString defaultDownloadPath = QStandardPaths::writableLocation(
QStandardPaths::DownloadLocation);

View file

@ -26,7 +26,6 @@
#include <QStandardPaths>
#include <QWindow> // for QWindow::AutomaticVisibility
#include <QSettings>
#include <QDir>
#include <QTranslator>
@ -74,7 +73,9 @@ extern const QString defaultDownloadPath;
X(ShowSendOption, false) \
X(EnablePtt, false) \
X(PttKeys, 32) \
X(UseFramelessWindow, USE_FRAMELESS_WINDOW_DEFAULT)
X(UseFramelessWindow, USE_FRAMELESS_WINDOW_DEFAULT) \
X(EnableCrashReporting, true) \
X(EnableAutomaticCrashReporting, false)
#if APPSTORE
#define KEYS COMMON_KEYS
#else

View file

@ -26,10 +26,15 @@ BaseModalDialog {
id: root
signal accepted
signal rejected
property string confirmLabel: ""
property string rejectLabel
property string textLabel: ""
property int textHAlign: Text.AlignHCenter
property real textMaxWidth: width - JamiTheme.preferredMarginSize * 4
autoClose: false
closeButtonVisible: false
button1.text: confirmLabel
button1.contentColorProvider: JamiTheme.redButtonColor
@ -37,8 +42,11 @@ BaseModalDialog {
close();
accepted();
}
button2.text: JamiStrings.optionCancel
button2.onClicked: close()
button2.text: rejectLabel ? rejectLabel : JamiStrings.optionCancel
button2.onClicked: {
close();
rejected();
}
button1Role: DialogButtonBox.AcceptRole
button2Role: DialogButtonBox.RejectRole
@ -50,7 +58,7 @@ BaseModalDialog {
id: labelAction
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: root.width - JamiTheme.preferredMarginSize * 4
Layout.maximumWidth: textMaxWidth
color: JamiTheme.textColor
text: root.textLabel
@ -58,7 +66,7 @@ BaseModalDialog {
font.pointSize: JamiTheme.textFontSize
font.kerning: true
horizontalAlignment: Text.AlignHCenter
horizontalAlignment: textHAlign
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}

131
src/app/crashreportclient.h Normal file
View file

@ -0,0 +1,131 @@
/*
* Copyright (C) 2024 Savoir-faire Linux Inc.
*
* 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 3 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/>.
*/
#pragma once
#include "version.h"
#include "version_info.h"
#include <QVariantMap>
class AppSettingsManager;
/**
* In the context of Jami where we employ GnuTLS, OpenSSL, and other cryptographic
* components as part of secure communication protocols, it is essential to configure
* crash reports with security in mind to prevent sensitive data from being exposed in crash
* reports. We must assume that attackers may attempt to exploit vulnerabilities based on
* stack data including values of cryptographic keys, certificates, etc. that may be used
* in some way to compromise the security of the user's account.
*
* We attempt to mitigate this risk by configuring crash reports to avoid collecting stack
* data beyond the offending function that caused the crash. We make the assumption that
* cryptographically sensitive data is not stored on the stack by 3rd party libraries.
*
* We also take care to avoid sending crash reports automatically and instead require user
* consent before uploading the last report.
*
* IMPORTANT: The opt-in approach is crucial, and the potential implications of transmitting
* these reports must be communicated to the user. The user should be informed that we cannot
* guarantee the security of the data in the crash report, even if we take steps to avoid
* leaking any sensitive information.
*
* We offer the following configuration options to enhance security:
*
* - (Option) EnableCrashReporting (default - true):
* An application settings allowing users to disable crash handling entirely.
*
* - (Option) EnableAutomaticCrashReporting (default - false):
* This setting allows users to opt-in to automatic crash reporting, which should be disabled
* by default. When the application crashes, the user should be prompted to upload the last
* crash report. If the user agrees, the report will be uploaded to the server. If this
* setting is enabled, no prompt will be shown, and the report will be uploaded automatically
* when the application crashes.
*
* Further considerations:
*
* - **Annotations**:
* Allows the inclusion of custom metadata in crash reports, such as the application version
* and build number, without exposing sensitive information. We must include this information
* to use the crash reports constructively.
*/
class CrashReportClient : public QObject
{
Q_OBJECT
public:
explicit CrashReportClient(AppSettingsManager* settingsManager, QObject* parent = nullptr)
: QObject(parent)
, settingsManager_(settingsManager)
, crashReportUrl_(CRASH_REPORT_URL)
{}
~CrashReportClient() = default;
virtual void syncHandlerWithSettings() = 0;
virtual void uploadLastReport() = 0;
virtual void clearReports() = 0;
// Used by the QML interface to query whether the application crashed last run.
bool getHasPendingReport()
{
// In builds that do not support crashpad, this will always return false, and
// thus will never trigger the dialog asking the user to upload the last report.
return crashedLastRun_;
}
protected:
// This function is used to toggle automatic crash reporting.
virtual void setUploadsEnabled(bool enabled) = 0;
// We will need to access the crash report related settings.
AppSettingsManager* settingsManager_;
// The endpoint URL that crash reports will be uploaded to.
QString crashReportUrl_;
// We store if the last run resulted in
bool crashedLastRun_ {false};
// This is the metadata that will be sent with each crash report.
// This data is required to correlate crash reports with the build so we can
// effectively load and analyze the mini-dumps.
QVariantMap metaData_ {
{"platform", QSysInfo::prettyProductName() + "_" + QSysInfo::currentCpuArchitecture()},
{"client_sha", APP_VERSION_STRING},
{"jamicore_sha", CORE_VERSION_STRING},
{"build_id", QString(VERSION_STRING)},
};
};
// Null implementation of the crash report client
class NullCrashReportClient : public CrashReportClient
{
Q_OBJECT
public:
explicit NullCrashReportClient(AppSettingsManager* settingsManager, QObject* parent = nullptr)
: CrashReportClient(settingsManager, parent)
{}
void syncHandlerWithSettings() override {}
void uploadLastReport() override {}
void clearReports() override {}
protected:
void setUploadsEnabled(bool enabled) override {}
};

View file

@ -0,0 +1,311 @@
/*
* Copyright (C) 2024 Savoir-faire Linux Inc.
*
* 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 3 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 "crashpad.h"
#include "appsettingsmanager.h"
#include "global.h"
#include <client/crash_report_database.h>
#include <client/settings.h>
#include <client/crashpad_info.h>
#include <QDir>
#include <QCoreApplication>
#include <QStandardPaths>
#include <QThreadPool>
#include <thread>
#if defined(OS_WIN)
#define FILEPATHSTR(Qs) Qs.toStdWString()
#define STRFILEPATH(Qs) QString::fromStdWString(Qs)
#define CRASHPAD_EXECUTABLE "crashpad_handler.exe"
#else
#define FILEPATHSTR(Qs) Qs.toStdString()
#define STRFILEPATH(Qs) QString::fromStdString(Qs)
#define CRASHPAD_EXECUTABLE "crashpad_handler"
#endif // OS_WIN
// We need the number of reports in the database to determine if the application crashed last time.
static int
getReportCount(base::FilePath dbPath)
{
auto database = crashpad::CrashReportDatabase::Initialize(base::FilePath(dbPath));
if (database == nullptr) {
return 0;
}
std::vector<crashpad::CrashReportDatabase::Report> completedReports;
database->GetCompletedReports(&completedReports);
return completedReports.size();
}
static void
clearCompletedReports(crashpad::CrashReportDatabase* database)
{
using namespace crashpad;
using OperationStatus = CrashReportDatabase::OperationStatus;
std::vector<CrashReportDatabase::Report> reports;
auto status = database->GetCompletedReports(&reports);
if (OperationStatus::kNoError != status) {
C_WARN << "Could not retrieve completed reports";
return;
}
for (const auto& report : reports) {
C_INFO.noquote() << QString("Deleting report: %1").arg(report.uuid.ToString().c_str());
status = database->DeleteReport(report.uuid);
if (OperationStatus::kNoError != status) {
C_WARN << "Failed to delete report";
}
}
}
CrashPadClient::CrashPadClient(AppSettingsManager* settingsManager, QObject* parent)
: CrashReportClient(settingsManager, parent)
{
try {
C_INFO << "Crashpad crash reporting enabled";
// We store the crashpad database in the application's local data.
const auto dataPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
if (dataPath.isEmpty()) {
throw std::runtime_error("Failed to retrieve writable location for AppLocalData");
}
dbPath_ = base::FilePath(FILEPATHSTR(QDir(dataPath).absoluteFilePath("crash_db")));
// Make sure the database directory exists.
if (!QDir().mkpath(STRFILEPATH(dbPath_.value()))) {
throw std::runtime_error("Failed to create crash database directory");
}
// The crashpad_handler executable is in the same directory as this executable.
const auto appBinPath = QCoreApplication::applicationDirPath();
if (appBinPath.isEmpty()) {
throw std::runtime_error("Failed to retrieve application directory path");
}
handlerPath_ = base::FilePath(FILEPATHSTR(QDir(appBinPath).filePath(CRASHPAD_EXECUTABLE)));
C_DBG << "Handler runtime path: " << handlerPath_.value();
// Check if the application crashed last time it was run by checking the crashpad database
// report count. If there is at least one report, we set the crashedLastRun_ flag to true.
// The flag will be queried by the QML interface to display a dialog. If the user accepts,
// the uploadLastReport function will be called, otherwise the reports will be cleared
// to avoid a build up of crash reports on the user's system.
using key = Settings::Key;
auto automaticReporting = settingsManager_->getValue(key::EnableAutomaticCrashReporting)
.toBool();
if (getReportCount(dbPath_) > 0 && !automaticReporting) {
crashedLastRun_ = true;
}
// If we crashed last time and need to send off a report, then uploadLastReport will call
// startHandler, and considering the `restartable` option for `StartHandler` is unused,
// and that restarting the handler will cause an assertion failure when debugging Linux,
// we will just not start the handler here in that case. The handler will be started in
// the clearReports function after the reports are cleared.
if (!crashedLastRun_) {
startHandler();
}
} catch (const std::exception& e) {
C_ERR << "Error initializing CrashPadClient: " << e.what();
} catch (...) {
C_ERR << "Unknown error initializing CrashPadClient";
}
}
CrashPadClient::~CrashPadClient()
{
// Remove any remaining stale crash reports.
// We use sleep to ensure that the reports are cleared after a forced upload,
// and it's possible that the reports are still being processed. This is a
// workaround for the lack of a synchronous `report-uploaded` signal/event.
if (auto database = crashpad::CrashReportDatabase::Initialize(base::FilePath(dbPath_))) {
clearCompletedReports(database.get());
}
}
void
CrashPadClient::startHandler()
{
std::vector<std::string> arguments;
// We disable rate-limiting because we want to upload crash reports as soon as possible.
// Perhaps we should enable rate-limiting in the future to avoid spamming the server, but
// we will need to investigate how that works in crashpad's implementation.
arguments.push_back("--no-rate-limit");
// We disable gzip compression because we want to be able to read the reports easily.
arguments.push_back("--no-upload-gzip");
// Convert the client metadata to map-string-string.
std::map<std::string, std::string> annotations;
Q_FOREACH (auto key, metaData_.keys()) {
annotations[key.toStdString()] = metaData_[key].toString().toStdString();
}
C_INFO << "Starting crashpad handler";
bool success = client_.StartHandler(handlerPath_, // handler
dbPath_, // database_dir
{}, // metrics_dir
crashReportUrl_.toStdString(), // url to upload reports
annotations, // Annotations
arguments, // Arguments
false, // restartable (this doesn't do anything)
false, // asynchronous_start (this doesn't do anything)
std::vector<base::FilePath>() // Attachments
);
if (!success) {
C_WARN << "Crashpad initialization failed";
return;
}
// Update the handler settings after starting the handler (we may have restarted it).
syncHandlerWithSettings();
}
void
CrashPadClient::syncHandlerWithSettings()
{
// Configure the crashpad handler with the settings from the settings manager.
using key = Settings::Key;
using namespace crashpad;
CrashpadInfo* crashpad_info = CrashpadInfo::GetCrashpadInfo();
// Optionally disable crashpad handler.
auto enableReportsAppSetting = settingsManager_->getValue(key::EnableCrashReporting).toBool();
crashpad_info->set_crashpad_handler_behavior(enableReportsAppSetting ? TriState::kEnabled
: TriState::kDisabled);
// Enable automatic crash reporting if the user has opted in.
auto automaticReporting = settingsManager_->getValue(key::EnableAutomaticCrashReporting).toBool();
setUploadsEnabled(automaticReporting);
}
void
CrashPadClient::setUploadsEnabled(bool enabled)
{
auto database = crashpad::CrashReportDatabase::Initialize(base::FilePath(dbPath_));
if (database != nullptr && database->GetSettings() != nullptr) {
database->GetSettings()->SetUploadsEnabled(enabled);
}
}
void
CrashPadClient::clearReports()
{
auto database = crashpad::CrashReportDatabase::Initialize(base::FilePath(dbPath_));
if (database == nullptr) {
return;
}
C_DBG << "Clearing completed crash reports";
const time_t secondsToWaitForReportLocks = 1;
database->CleanDatabase(secondsToWaitForReportLocks);
::clearCompletedReports(database.get());
// If the crashedLastRun_ flag is set, then we should follow up and start the handler.
// Refer to the comment in constructor for more information on why the handler wasn't
// started in the constructor if we crashed last time.
if (crashedLastRun_) {
startHandler();
}
}
void
CrashPadClient::uploadLastReport()
{
using namespace crashpad;
// Find the latest crash report.
auto database = CrashReportDatabase::Initialize(base::FilePath(dbPath_));
if (database == nullptr) {
C_WARN << "Crashpad database initialization failed";
return;
}
std::vector<CrashReportDatabase::Report> reports;
using OperationStatus = CrashReportDatabase::OperationStatus;
auto status = database->GetCompletedReports(&reports);
if (OperationStatus::kNoError != status) {
C_WARN << "Crashpad database GetCompletedReports failed";
return;
}
if (reports.empty()) {
C_WARN << "Crashpad database contains no completed reports";
return;
}
auto report = reports.back();
// Force the report to be uploaded (should change the report state to pending).
C_INFO << "Requesting report upload:" << report.uuid.ToString().c_str();
status = database->RequestUpload(report.uuid);
if (status != CrashReportDatabase::kNoError) {
// This may indicate that the report has already been removed from the database.
C_WARN << "Failed to request upload, status: " << status;
return;
}
// In this case, unless we restart the crashpad handler, the report won't
// be uploaded until the application is terminated.
startHandler();
// Let's wait for the report to be uploaded then clear all reports on a
// separate thread to avoid blocking the UI.
QThreadPool::globalInstance()->start([this, uuid = report.uuid]() {
auto database = CrashReportDatabase::Initialize(base::FilePath(dbPath_));
if (database == nullptr) {
C_WARN << "Crashpad database initialization failed";
return;
}
// Wait up to 3 seconds (~1.5s observed on Windows) for the report to be uploaded.
const int maxAttempts = 20;
int attempts = 0;
auto timeout = std::chrono::milliseconds(150);
C_INFO << "Waiting for report to be uploaded";
while (attempts++ < maxAttempts) {
CrashReportDatabase::Report report;
if (database->LookUpCrashReport(uuid, &report) == CrashReportDatabase::kNoError) {
if (report.uploaded
&& database->DeleteReport(uuid) == CrashReportDatabase::kNoError) {
C_INFO << "Report uploaded and deleted successfully";
return;
}
}
std::this_thread::sleep_for(timeout);
}
// Note: This usually indicates that the submission server is inaccessible.
C_WARN << "Failed to delete report. It may not have been uploaded successfully";
// If we failed to delete the report, we should still try to clear any dangling reports.
// This will prevent the database from growing indefinitely by removing at least the
// previous unsuccessfully removed report. In this case, we don't know if the report
// was successfully uploaded or not. This is a best-effort attempt.
::clearCompletedReports(database.get());
});
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (C) 2024 Savoir-faire Linux Inc.
*
* 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 3 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/>.
*/
#pragma once
#include "crashreportclient.h"
#include <client/crashpad_client.h>
class AppSettingsManager;
class CrashPadClient final : public CrashReportClient
{
Q_OBJECT
public:
explicit CrashPadClient(AppSettingsManager* settingsManager, QObject* parent = nullptr);
~CrashPadClient();
void syncHandlerWithSettings() override;
void uploadLastReport() override;
void clearReports() override;
protected:
void setUploadsEnabled(bool enabled) override;
private:
void startHandler();
crashpad::CrashpadClient client_;
base::FilePath dbPath_;
base::FilePath handlerPath_;
};

63
src/app/crashreporter.h Normal file
View file

@ -0,0 +1,63 @@
/*
* Copyright (C) 2024 Savoir-faire Linux Inc.
*
* 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 3 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/>.
*/
#pragma once
// Implementation choice
#include "crashreportclient.h"
#if not ENABLE_CRASHREPORTS
using CrashReportClientImpl = NullCrashReportClient;
#elif defined(ENABLE_CRASHPAD)
#include "crashreportclients/crashpad.h"
using CrashReportClientImpl = CrashPadClient;
#else
#pragma GCC error "No crash report client enabled, but reports are enabled."
#endif
#include <memory>
class CrashReporter : public QObject
{
Q_OBJECT
public:
explicit CrashReporter(AppSettingsManager* settingsManager, QObject* parent = nullptr)
: QObject(parent)
{
client_ = std::make_unique<CrashReportClientImpl>(settingsManager, this);
}
Q_INVOKABLE void syncHandlerWithSettings()
{
client_->syncHandlerWithSettings();
}
Q_INVOKABLE void uploadLastReport()
{
client_->uploadLastReport();
}
Q_INVOKABLE void clearReports()
{
client_->clearReports();
}
Q_INVOKABLE bool getHasPendingReport()
{
return client_->getHasPendingReport();
}
private:
std::unique_ptr<CrashReportClient> client_;
};

View file

@ -27,6 +27,7 @@
#include "connectivitymonitor.h"
#include "systemtray.h"
#include "previewengine.h"
#include "crashreporter.h"
#include <QWKQuick/qwkquickglobal.h>
@ -42,8 +43,6 @@
#include <QLibraryInfo>
#include <QQuickWindow>
#include <thread>
#ifdef Q_OS_WIN
#include <windows.h>
#endif
@ -183,11 +182,20 @@ MainApplication::~MainApplication()
{
engine_.reset();
lrcInstance_.reset();
// Allow the crash reporter to do implementation-specific cleanup before the application exits.
delete crashReporter_;
}
bool
MainApplication::init()
{
// Let's make sure we can provide postmortem debugging information prior
// to any other initialization. This won't do anything if crashpad isn't
// enabled.
settingsManager_ = new AppSettingsManager(this);
crashReporter_ = new CrashReporter(settingsManager_, this);
// This 2-phase initialisation prevents ephemeral instances from
// performing unnecessary tasks, like initializing the WebEngine.
engine_.reset(new QQmlApplicationEngine(this));
@ -195,7 +203,6 @@ MainApplication::init()
QWK::registerTypes(engine_.get());
connectivityMonitor_ = new ConnectivityMonitor(this);
settingsManager_ = new AppSettingsManager(this);
systemTray_ = new SystemTray(settingsManager_, this);
previewEngine_ = new PreviewEngine(connectivityMonitor_, this);
@ -425,6 +432,9 @@ MainApplication::initQmlLayer()
&screenInfo_,
this);
// Register the crash reporter as a context property in the QML engine.
engine_->rootContext()->setContextProperty("crashReporter", crashReporter_);
QUrl url = u"qrc:/MainApplicationWindow.qml"_qs;
#ifdef QT_DEBUG
if (parser_.isSet("test")) {

View file

@ -31,11 +31,10 @@
#include <QWindow>
#include <QCommandLineParser>
#include <memory>
class ConnectivityMonitor;
class AppSettingsManager;
class SystemTray;
class AppSettingsManager;
class CrashReporter;
class PreviewEngine;
// Provides information about the screen the app is displayed on
@ -123,6 +122,8 @@ private:
SystemTray* systemTray_;
AppSettingsManager* settingsManager_;
PreviewEngine* previewEngine_;
CrashReporter* crashReporter_;
ScreenInfo screenInfo_;
QCommandLineParser parser_;
};

View file

@ -46,7 +46,6 @@ Item {
// AboutPopUp
property string buildID: qsTr("Build ID")
property string version: qsTr("Version")
property string declarationYear: "© 2015-2024"
property string slogan: "Astarte"
property string declaration: qsTr('Jami, a GNU package, is software for universal and distributed peer-to-peer communication that respects the freedom and privacy of its users. Visit <a href="https://jami.net" style="color: ' + JamiTheme.buttonTintedBlue + '">jami.net</a>' + ' to learn more.')
@ -54,6 +53,11 @@ Item {
property string contribute: qsTr('Contribute')
property string feedback: qsTr('Feedback')
// Crash report popup
property string crashReportTitle: qsTr("Application Recovery")
property string crashReportMessage: qsTr("Jami has recovered from a crash. Would you like to send a crash report to help us fix the issue?")
property string crashReportMessageExtra: qsTr("Only essential data, including the app version, platform information, and a snapshot of the program's state at the time of the crash, will be shared.")
// AccountComboBox
property string displayQRCode: qsTr("Display QR code")
property string openSettings: qsTr("Open settings")
@ -769,6 +773,7 @@ Item {
property string shiftEnterNewLine: qsTr("Press Shift+Enter to insert a new line")
property string enterNewLine: qsTr("Press Enter to insert a new line")
property string send: qsTr("Send")
property string dontSend: qsTr("Don't send")
property string replyTo: qsTr("Reply to")
property string inReplyTo: qsTr("In reply to")
property string repliedTo: qsTr(" replied to")

View file

@ -280,6 +280,7 @@ registerTypes(QQmlEngine* engine,
auto videoProvider = new VideoProvider(lrcInstance->avModel(), app);
engine->rootContext()->setContextProperty("videoProvider", videoProvider);
engine->rootContext()->setContextProperty("ENABLE_CRASHREPORTS", ENABLE_CRASHREPORTS);
engine->rootContext()->setContextProperty("WITH_WEBENGINE", WITH_WEBENGINE);
engine->rootContext()->setContextProperty("APPSTORE", APPSTORE);
}

View file

@ -49,47 +49,80 @@ SettingsPageBase {
anchors.left: parent.left
anchors.leftMargin: JamiTheme.preferredSettingsMarginSize
RowLayout {
id: rawLayout
Text {
ColumnLayout {
width: parent.width
spacing: 10
ToggleSwitch {
id: enableCrashReports
visible: ENABLE_CRASHREPORTS
Layout.fillWidth: true
Layout.preferredHeight: 30
Layout.rightMargin: JamiTheme.preferredMarginSize
labelText: qsTr("Enable crash reports")
checked: UtilsAdapter.getAppValue(Settings.EnableCrashReporting)
text: JamiStrings.troubleshootText
font.pointSize: JamiTheme.settingsFontSize
font.kerning: true
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
color: JamiTheme.textColor
onSwitchToggled: {
UtilsAdapter.setAppValue(Settings.EnableCrashReporting, checked);
crashReporter.syncHandlerWithSettings();
}
}
MaterialButton {
id: enableTroubleshootingButton
ToggleSwitch {
id: enableAutomaticCrashReporting
visible: ENABLE_CRASHREPORTS
enabled: enableCrashReports.checked
Layout.fillWidth: true
labelText: qsTr("Automatically send crash reports")
checked: UtilsAdapter.getAppValue(Settings.EnableAutomaticCrashReporting)
TextMetrics {
id: enableTroubleshootingButtonTextSize
font.weight: Font.Bold
font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize
font.capitalization: Font.AllUppercase
text: enableTroubleshootingButton.text
onSwitchToggled: {
UtilsAdapter.setAppValue(Settings.EnableAutomaticCrashReporting, checked);
crashReporter.syncHandlerWithSettings();
}
}
RowLayout {
id: rawLayout
Text {
Layout.fillWidth: true
Layout.preferredHeight: 30
Layout.rightMargin: JamiTheme.preferredMarginSize
text: JamiStrings.troubleshootText
font.pointSize: JamiTheme.settingsFontSize
font.kerning: true
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
color: JamiTheme.textColor
}
Layout.alignment: Qt.AlignRight
MaterialButton {
id: enableTroubleshootingButton
preferredWidth: enableTroubleshootingButtonTextSize.width + 2 * JamiTheme.buttontextWizzardPadding
buttontextHeightMargin: JamiTheme.buttontextHeightMargin
TextMetrics {
id: enableTroubleshootingButtonTextSize
font.weight: Font.Bold
font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize
font.capitalization: Font.AllUppercase
text: enableTroubleshootingButton.text
}
primary: true
Layout.alignment: Qt.AlignRight
text: JamiStrings.troubleshootButton
toolTipText: JamiStrings.troubleshootButton
preferredWidth: enableTroubleshootingButtonTextSize.width + 2 * JamiTheme.buttontextWizzardPadding
buttontextHeightMargin: JamiTheme.buttontextHeightMargin
onClicked: {
LogViewWindowCreation.createlogViewWindowObject();
LogViewWindowCreation.showLogViewWindow();
primary: true
text: JamiStrings.troubleshootButton
toolTipText: JamiStrings.troubleshootButton
onClicked: {
LogViewWindowCreation.createlogViewWindowObject();
LogViewWindowCreation.showLogViewWindow();
}
}
}
}