From b13b27b4d29bec509ef263ed74720beea50c0122 Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Tue, 29 Dec 2020 12:42:36 +0100 Subject: [PATCH] Applications: Add CrashReporter :^) This is a simple application that can read a coredump file and display information regarding the crash, like the application's name and icon and a backtrace. It will be launched by CrashDaemon whenever a new coredump is available. Also, it's mostly written in GML! :^) Closes #400, but note that, unlike mentioned in that issue, this implementation doesn't ignore applications that "have been started in the terminal". That's just overcomplicating things, IMO. When my js(1) REPL segfaults, I want to see a backtrace! --- Applications/CMakeLists.txt | 1 + Applications/CrashDaemon/main.cpp | 15 ++ Applications/CrashReporter/CMakeLists.txt | 10 ++ .../CrashReporter/CrashReporterWindow.gml | 108 ++++++++++++ Applications/CrashReporter/main.cpp | 165 ++++++++++++++++++ Base/res/icons/16x16/app-crash-reporter.png | Bin 0 -> 5826 bytes Base/res/icons/32x32/app-crash-reporter.png | Bin 0 -> 7283 bytes 7 files changed, 299 insertions(+) create mode 100644 Applications/CrashReporter/CMakeLists.txt create mode 100644 Applications/CrashReporter/CrashReporterWindow.gml create mode 100644 Applications/CrashReporter/main.cpp create mode 100644 Base/res/icons/16x16/app-crash-reporter.png create mode 100644 Base/res/icons/32x32/app-crash-reporter.png diff --git a/Applications/CMakeLists.txt b/Applications/CMakeLists.txt index 90a95f5010..76d2ae304e 100644 --- a/Applications/CMakeLists.txt +++ b/Applications/CMakeLists.txt @@ -3,6 +3,7 @@ add_subdirectory(Browser) add_subdirectory(Calculator) add_subdirectory(Calendar) add_subdirectory(CrashDaemon) +add_subdirectory(CrashReporter) add_subdirectory(Debugger) add_subdirectory(DisplaySettings) add_subdirectory(FileManager) diff --git a/Applications/CrashDaemon/main.cpp b/Applications/CrashDaemon/main.cpp index b2f0d3debc..857415bdd6 100644 --- a/Applications/CrashDaemon/main.cpp +++ b/Applications/CrashDaemon/main.cpp @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include #include #include @@ -58,6 +60,18 @@ static void print_backtrace(const String& coredump_path) dbgln("{}", entry.to_string(true)); } +static void launch_crash_reporter(const String& coredump_path) +{ + pid_t child; + const char* argv[] = { "CrashReporter", coredump_path.characters(), nullptr, nullptr }; + if ((errno = posix_spawn(&child, "/bin/CrashReporter", nullptr, nullptr, const_cast(argv), environ))) { + perror("posix_spawn"); + } else { + if (disown(child) < 0) + perror("disown"); + } +} + int main() { static constexpr const char* coredumps_dir = "/tmp/coredump"; @@ -72,5 +86,6 @@ int main() dbgln("New coredump file: {}", coredump_path); wait_until_coredump_is_ready(coredump_path); print_backtrace(coredump_path); + launch_crash_reporter(coredump_path); } } diff --git a/Applications/CrashReporter/CMakeLists.txt b/Applications/CrashReporter/CMakeLists.txt new file mode 100644 index 0000000000..d391aa6b84 --- /dev/null +++ b/Applications/CrashReporter/CMakeLists.txt @@ -0,0 +1,10 @@ +compile_gml(CrashReporterWindow.gml CrashReporterWindowGML.h crash_reporter_window_gml) + + +set(SOURCES + main.cpp + CrashReporterWindowGML.h +) + +serenity_app(CrashReporter ICON app-crash-reporter) +target_link_libraries(CrashReporter LibCore LibCoreDump LibDesktop LibGUI) diff --git a/Applications/CrashReporter/CrashReporterWindow.gml b/Applications/CrashReporter/CrashReporterWindow.gml new file mode 100644 index 0000000000..7e2bd27cbd --- /dev/null +++ b/Applications/CrashReporter/CrashReporterWindow.gml @@ -0,0 +1,108 @@ +@GUI::Widget { + fill_with_background_color: true + + layout: @GUI::VerticalBoxLayout { + margins: [5, 5, 5, 5] + } + + @GUI::Widget { + vertical_size_policy: "Fixed" + preferred_height: 44 + + layout: @GUI::HorizontalBoxLayout { + spacing: 10 + } + + @GUI::ImageWidget { + name: "icon" + } + + @GUI::Label { + name: "description" + text_alignment: "CenterLeft" + } + } + + @GUI::Widget { + vertical_size_policy: "Fixed" + preferred_height: 18 + + layout: @GUI::HorizontalBoxLayout { + } + + @GUI::Label { + text: "Executable path:" + text_alignment: "CenterLeft" + horizontal_size_policy: "Fixed" + preferred_width: 90 + } + + @GUI::LinkLabel { + name: "executable_link" + text_alignment: "CenterLeft" + } + } + + @GUI::Widget { + vertical_size_policy: "Fixed" + preferred_height: 18 + + layout: @GUI::HorizontalBoxLayout { + } + + @GUI::Label { + text: "Coredump path:" + text_alignment: "CenterLeft" + horizontal_size_policy: "Fixed" + preferred_width: 90 + } + + @GUI::LinkLabel { + name: "coredump_link" + text_alignment: "CenterLeft" + } + } + + @GUI::Widget { + vertical_size_policy: "Fixed" + preferred_height: 18 + + layout: @GUI::HorizontalBoxLayout { + } + + @GUI::Label { + text: "Backtrace:" + text_alignment: "CenterLeft" + } + } + + @GUI::TextEditor { + name: "backtrace_text_editor" + mode: "ReadOnly" + } + + @GUI::Widget { + vertical_size_policy: "Fixed" + preferred_height: 32 + + layout: @GUI::HorizontalBoxLayout { + } + + // HACK: We need something like Layout::add_spacer() in GML! :^) + @GUI::Widget { + horizontal_size_policy: "Fixed" + vertical_size_policy: "Fill" + preferred_width: 378 + preferred_height: 0 + } + + @GUI::Button { + name: "close_button" + text: "Close" + horizontal_size_policy: "Fixed" + vertical_size_policy: "Fixed" + preferred_width: 70 + preferred_height: 22 + } + } +} diff --git a/Applications/CrashReporter/main.cpp b/Applications/CrashReporter/main.cpp new file mode 100644 index 0000000000..6d35cab725 --- /dev/null +++ b/Applications/CrashReporter/main.cpp @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2020, Linus Groh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + if (pledge("stdio shared_buffer accept cpath rpath unix fattr", nullptr) < 0) { + perror("pledge"); + return 1; + } + + const char* coredump_path = nullptr; + + Core::ArgsParser args_parser; + args_parser.set_general_help("Show information from an application crash coredump."); + args_parser.add_positional_argument(coredump_path, "Coredump path", "coredump-path"); + args_parser.parse(argc, argv); + + auto coredump = CoreDump::Reader::create(coredump_path); + if (!coredump) { + warnln("Could not open coredump '{}'", coredump_path); + return 1; + } + + auto app = GUI::Application::construct(argc, argv); + + if (pledge("stdio shared_buffer accept rpath unix", nullptr) < 0) { + perror("pledge"); + return 1; + } + + auto backtrace = coredump->backtrace(); + + String executable_path; + // FIXME: Maybe we should just embed the process's executable path + // in the coredump by itself so we don't have to extract it from the backtrace. + // Such a process section could also include the PID, which currently we'd have + // to parse from the filename. + if (!backtrace.entries().is_empty()) { + executable_path = backtrace.entries().last().object_name; + } else { + warnln("Could not determine executable path from coredump"); + return 1; + } + + if (unveil(executable_path.characters(), "r") < 0) { + perror("unveil"); + return 1; + } + + if (unveil("/res", "r") < 0) { + perror("unveil"); + return 1; + } + + if (unveil("/tmp/portal/launch", "rw") < 0) { + perror("unveil"); + return 1; + } + + if (unveil(nullptr, nullptr) < 0) { + perror("unveil"); + return 1; + } + + auto app_icon = GUI::Icon::default_icon("app-crash-reporter"); + + auto window = GUI::Window::construct(); + window->set_title("Crash Reporter"); + window->set_icon(app_icon.bitmap_for_size(16)); + window->set_resizable(false); + window->resize(460, 340); + window->center_on_screen(); + + auto& widget = window->set_main_widget(); + widget.load_from_gml(crash_reporter_window_gml); + + auto& icon_image_widget = static_cast(*widget.find_descendant_by_name("icon")); + icon_image_widget.set_bitmap(GUI::FileIconProvider::icon_for_executable(executable_path).bitmap_for_size(32)); + + auto app_name = LexicalPath(executable_path).basename(); + auto af = Desktop::AppFile::get_for_app(app_name); + if (af->is_valid()) + app_name = af->name(); + + auto& description_label = static_cast(*widget.find_descendant_by_name("description")); + description_label.set_text(String::formatted("\"{}\" has crashed!", app_name)); + + auto& executable_link_label = static_cast(*widget.find_descendant_by_name("executable_link")); + executable_link_label.set_text(LexicalPath::canonicalized_path(executable_path)); + executable_link_label.on_click = [&] { + Desktop::Launcher::open(URL::create_with_file_protocol(LexicalPath(executable_path).dirname())); + }; + + auto& coredump_link_label = static_cast(*widget.find_descendant_by_name("coredump_link")); + coredump_link_label.set_text(LexicalPath::canonicalized_path(coredump_path)); + coredump_link_label.on_click = [&] { + Desktop::Launcher::open(URL::create_with_file_protocol(LexicalPath(coredump_path).dirname())); + }; + + StringBuilder backtrace_builder; + auto first = true; + for (auto& entry : backtrace.entries()) { + if (first) + first = false; + else + backtrace_builder.append('\n'); + backtrace_builder.append(entry.to_string()); + } + + auto& backtrace_text_editor = static_cast(*widget.find_descendant_by_name("backtrace_text_editor")); + backtrace_text_editor.set_text(backtrace_builder.build()); + + auto& close_button = static_cast(*widget.find_descendant_by_name("close_button")); + close_button.on_click = [&](auto) { + app->quit(); + }; + + window->show(); + + return app->exec(); +} diff --git a/Base/res/icons/16x16/app-crash-reporter.png b/Base/res/icons/16x16/app-crash-reporter.png new file mode 100644 index 0000000000000000000000000000000000000000..8648098834cccdb4d69ba48c3ebf962d8a255c5e GIT binary patch literal 5826 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 literal 0 HcmV?d00001