From e8b186a9b4da333d6a72d0726086b00fd47889d7 Mon Sep 17 00:00:00 2001 From: Vitaly Sverchinsky Date: Tue, 4 May 2021 21:43:19 +0300 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFCrashreporting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cmake_build.yml | 15 + CMakeLists.txt | 17 + cmake-proxies/CMakeLists.txt | 4 + .../cmake-modules/AudacityFunctions.cmake | 2 +- crashreports/BreakpadConfigurer.cpp | 51 ++ crashreports/BreakpadConfigurer.h | 45 ++ crashreports/CMakeLists.txt | 53 ++ crashreports/crashreporter/CMakeLists.txt | 41 ++ crashreports/crashreporter/CrashReportApp.cpp | 464 ++++++++++++++++++ crashreports/crashreporter/CrashReportApp.h | 36 ++ crashreports/crashreporter/warning.xpm | 138 ++++++ .../internal/unix/CrashReportContext.cpp | 155 ++++++ .../internal/unix/CrashReportContext.h | 43 ++ .../internal/win32/CrashReportContext.cpp | 165 +++++++ .../internal/win32/CrashReportContext.h | 47 ++ libraries/lib-strings/FutureStrings.h | 15 - locale/update_po_files.sh | 2 +- scripts/ci/build.sh | 18 + scripts/ci/configure.sh | 6 + scripts/ci/environment.sh | 2 +- scripts/ci/linux/split_debug_symbols.sh | 8 + scripts/ci/upload_debug_symbols.sh | 19 + src/AudacityApp.cpp | 31 +- src/CMakeLists.txt | 5 + 24 files changed, 1363 insertions(+), 19 deletions(-) create mode 100644 crashreports/BreakpadConfigurer.cpp create mode 100644 crashreports/BreakpadConfigurer.h create mode 100644 crashreports/CMakeLists.txt create mode 100644 crashreports/crashreporter/CMakeLists.txt create mode 100644 crashreports/crashreporter/CrashReportApp.cpp create mode 100644 crashreports/crashreporter/CrashReportApp.h create mode 100644 crashreports/crashreporter/warning.xpm create mode 100644 crashreports/internal/unix/CrashReportContext.cpp create mode 100644 crashreports/internal/unix/CrashReportContext.h create mode 100644 crashreports/internal/win32/CrashReportContext.cpp create mode 100644 crashreports/internal/win32/CrashReportContext.h create mode 100644 scripts/ci/linux/split_debug_symbols.sh create mode 100644 scripts/ci/upload_debug_symbols.sh diff --git a/.github/workflows/cmake_build.yml b/.github/workflows/cmake_build.yml index 3592f95eb..7afdcd91a 100644 --- a/.github/workflows/cmake_build.yml +++ b/.github/workflows/cmake_build.yml @@ -69,6 +69,7 @@ jobs: SENTRY_DSN_KEY: ${{ secrets.SENTRY_DSN_KEY }} SENTRY_HOST: ${{ secrets.SENTRY_HOST }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + CRASH_REPORT_URL: ${{ secrets.CRASH_REPORT_URL }} run: | exec bash "scripts/ci/configure.sh" @@ -76,6 +77,20 @@ jobs: run: | exec bash "scripts/ci/build.sh" + - name: Upload debug symbols + if: startsWith(github.ref, 'refs/tags/release-') + env: + SENTRY_DSN_KEY: ${{ secrets.SENTRY_DSN_KEY }} + SENTRY_HOST: ${{ secrets.SENTRY_HOST }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG_SLUG: ${{ secrets.SENTRY_ORG_SLUG }} + SENTRY_PROJECT_SLUG: ${{ secrets.SENTRY_PROJECT_SLUG }} + #this is required to run sentry's get-cli script on platforms where 'sudo' is not available + INSTALL_DIR: ${{ github.workspace }} + run: | + exec bash "scripts/ci/upload_debug_symbols.sh" + - name: Install run: | exec bash "scripts/ci/install.sh" diff --git a/CMakeLists.txt b/CMakeLists.txt index fee909b3f..189d05e08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -171,6 +171,7 @@ include( TestBigEndian ) set_from_env(SENTRY_DSN_KEY) set_from_env(SENTRY_HOST) set_from_env(SENTRY_PROJECT) +set_from_env(CRASH_REPORT_URL) cmake_dependent_option( ${_OPT}has_sentry_reporting @@ -180,6 +181,14 @@ cmake_dependent_option( Off ) +cmake_dependent_option( + ${_OPT}has_crashreports + "Enables crash reporting for Audacity" + On + "${_OPT}has_networking;DEFINED CRASH_REPORT_URL" + Off +) + # Determine 32-bit or 64-bit target if( CMAKE_C_COMPILER_ID MATCHES "MSVC" AND CMAKE_VS_PLATFORM_NAME MATCHES "Win64|x64" ) set( IS_64BIT ON ) @@ -217,6 +226,11 @@ elseif( CMAKE_SYSTEM_NAME MATCHES "Darwin" ) endif() message( STATUS " MacOS SDK: ${CMAKE_OSX_SYSROOT}" ) message( STATUS ) + + if(${_OPT}has_crashreports) + set(CMAKE_XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT "dwarf-with-dsym") + endif() + endif() # Try to get the current commit information @@ -485,6 +499,9 @@ add_subdirectory( "cmake-proxies" ) resolve_conan_dependencies() add_subdirectory( "help" ) +if(${_OPT}has_crashreports) +add_subdirectory( "crashreports" ) +endif() add_subdirectory( "images" ) add_subdirectory( "libraries" ) add_subdirectory( "locale" ) diff --git a/cmake-proxies/CMakeLists.txt b/cmake-proxies/CMakeLists.txt index 5048cf92c..1b4e57512 100644 --- a/cmake-proxies/CMakeLists.txt +++ b/cmake-proxies/CMakeLists.txt @@ -18,6 +18,10 @@ add_conan_lib( expat:shared=True ) +if(${_OPT}has_crashreports) + add_conan_lib(breakpad breakpad/0.1 REQUIRED) +endif() + set( wx_zlib "zlib" ) set( wx_png "libpng" ) diff --git a/cmake-proxies/cmake-modules/AudacityFunctions.cmake b/cmake-proxies/cmake-modules/AudacityFunctions.cmake index 199fcb878..b1f7eeebb 100644 --- a/cmake-proxies/cmake-modules/AudacityFunctions.cmake +++ b/cmake-proxies/cmake-modules/AudacityFunctions.cmake @@ -262,7 +262,7 @@ function( audacity_append_common_compiler_options var use_pch ) # Define/undefine _DEBUG # Yes, -U to /U too as needed for Windows: - $,-U_DEBUG,-D_DEBUG=1> + $,-D_DEBUG=1,-U_DEBUG> ) # Definitions controlled by the AUDACITY_BUILD_LEVEL switch if( AUDACITY_BUILD_LEVEL EQUAL 0 ) diff --git a/crashreports/BreakpadConfigurer.cpp b/crashreports/BreakpadConfigurer.cpp new file mode 100644 index 000000000..4a56049da --- /dev/null +++ b/crashreports/BreakpadConfigurer.cpp @@ -0,0 +1,51 @@ +/*!******************************************************************** +* + Audacity: A Digital Audio Editor + + BreakpadConfigurer.cpp + + Vitaly Sverchinsky + + **********************************************************************/ + +#include "BreakpadConfigurer.h" + +#if defined(WIN32) +#include "internal/win32/CrashReportContext.h" +#else +#include "internal/unix/CrashReportContext.h" +#endif + +BreakpadConfigurer& BreakpadConfigurer::SetDatabasePathUTF8(const std::string& pathUTF8) +{ + mDatabasePathUTF8 = pathUTF8; + return *this; +} + +BreakpadConfigurer& BreakpadConfigurer::SetReportURL(const std::string& reportURL) +{ + mReportURL = reportURL; + return *this; +} + +BreakpadConfigurer& BreakpadConfigurer::SetParameters(const std::map& parameters) +{ + mParameters = parameters; + return *this; +} + +BreakpadConfigurer& BreakpadConfigurer::SetSenderPathUTF8(const std::string& pathUTF8) +{ + mSenderPathUTF8 = pathUTF8; + return *this; +} + +void BreakpadConfigurer::Start() +{ + static CrashReportContext context{}; + bool ok = context.SetSenderPathUTF8(mSenderPathUTF8); + ok = ok && context.SetReportURL(mReportURL); + ok = ok && context.SetParameters(mParameters); + if (ok) + context.StartHandler(mDatabasePathUTF8); +} diff --git a/crashreports/BreakpadConfigurer.h b/crashreports/BreakpadConfigurer.h new file mode 100644 index 000000000..de4129486 --- /dev/null +++ b/crashreports/BreakpadConfigurer.h @@ -0,0 +1,45 @@ +/*!******************************************************************** +* + Audacity: A Digital Audio Editor + + BreakpadConfigurer.h + + Vitaly Sverchinsky + + **********************************************************************/ + +#pragma once + +#include +#include + +//! This class is used to configure Breakpad's handler before start. +/*! Typically handler should be started as early as possible. +* BreakpadConfigurer may be a short living object, it is used to configure +* Breakpad handler, and run it with BreakpadConfigurer::Start() method, +* It's expected that Start() will be called once during application +* lifetime, any calls to Set* methods after handler is started will be ignored. +* The handler itself simply starts crash sender program, passing all the details +* (path crash dump, report url, parameters...) as a command-line arguments to it. +* Please read official documentation for details: +* https://chromium.googlesource.com/breakpad/breakpad +*/ +class BreakpadConfigurer final +{ + std::string mDatabasePathUTF8; + std::string mSenderPathUTF8; + std::string mReportURL; + std::map mParameters; +public: + //! Sets the directory where crashreports will be stored (should have rw permission) + BreakpadConfigurer& SetDatabasePathUTF8(const std::string& pathUTF8); + //! Sets report URL to the crash reporting server (URL-Encoded, optional) + BreakpadConfigurer& SetReportURL(const std::string& reportURL); + //! Sets an additional parameters that should be sent to a crash reporting server (ASCII encoded) + BreakpadConfigurer& SetParameters(const std::map& parameters); + //! Sets a path to a directory where crash reporter sending program is located + BreakpadConfigurer& SetSenderPathUTF8(const std::string& pathUTF8); + + //! Starts the handler + void Start(); +}; diff --git a/crashreports/CMakeLists.txt b/crashreports/CMakeLists.txt new file mode 100644 index 000000000..a49d1076b --- /dev/null +++ b/crashreports/CMakeLists.txt @@ -0,0 +1,53 @@ +# This module provides an interface to configure and start Breakpad handler +# in a platform independent way. + +set(TARGET crashreports) +set(TARGET_ROOT ${CMAKE_CURRENT_SOURCE_DIR}) + +message( STATUS "========== Configuring ${TARGET} ==========" ) + +add_library(${TARGET} STATIC) + +set(SOURCES "") +set(INCLUDES INTERFACE ./) +set(LIBRARIES "") +set(DEFINIES "") + +# also adding Crash Reporting dialog +add_subdirectory(crashreporter) + +list(APPEND SOURCES + PRIVATE + BreakpadConfigurer.h + BreakpadConfigurer.cpp +) +list(APPEND LIBRARIES + PRIVATE + breakpad::client +) +list(APPEND DEFINES + PUBLIC + -DUSE_BREAKPAD + PRIVATE + -DCRASHREPORTER_PROGRAM_NAME="$" +) + +if(WIN32) + list(APPEND SOURCES + PRIVATE + internal/win32/CrashReportContext.h + internal/win32/CrashReportContext.cpp + ) +elseif(UNIX) + list(APPEND SOURCES + PRIVATE + internal/unix/CrashReportContext.h + internal/unix/CrashReportContext.cpp) +endif() + +target_include_directories(${TARGET} ${INCLUDES}) +target_sources(${TARGET} ${SOURCES}) +target_link_libraries(${TARGET} ${LIBRARIES}) +target_compile_definitions(${TARGET} ${DEFINES}) + +organize_source( "${TARGET_ROOT}" "" "${SOURCES}" ) \ No newline at end of file diff --git a/crashreports/crashreporter/CMakeLists.txt b/crashreports/crashreporter/CMakeLists.txt new file mode 100644 index 000000000..a8e0b9703 --- /dev/null +++ b/crashreports/crashreporter/CMakeLists.txt @@ -0,0 +1,41 @@ +#Adds a Crash Reporting dialog which may be invoked by a crashing program + +set(TARGET crashreporter) +set(TARGET_ROOT ${CMAKE_CURRENT_SOURCE_DIR}) + +message( STATUS "========== Configuring ${TARGET} ==========" ) + +set(SOURCES + PRIVATE + warning.xpm + CrashReportApp.h + CrashReportApp.cpp + ) + +add_executable(${TARGET}) +target_sources(${TARGET} ${SOURCES}) +target_link_libraries(${TARGET} breakpad::processor breakpad::sender wxwidgets::wxwidgets) + +if(WIN32) + set_target_properties(${TARGET} PROPERTIES WIN32_EXECUTABLE ON) +endif() + +if( CMAKE_SYSTEM_NAME MATCHES "Darwin" ) + add_custom_command( + TARGET + ${TARGET} + COMMAND + ${CMAKE_COMMAND} -D SRC="${_EXEDIR}/crashreporter" + -D DST="${_PKGLIB}" + -D WXWIN="${_SHARED_PROXY_BASE_PATH}/$" + -P ${AUDACITY_MODULE_PATH}/CopyLibs.cmake + POST_BUILD + ) +elseif(UNIX) + target_compile_definitions(${TARGET} PRIVATE -DINSTALL_PREFIX="${CMAKE_INSTALL_PREFIX}") + install(TARGETS ${TARGET} RUNTIME) +endif() + +set_target_property_all( ${TARGET} RUNTIME_OUTPUT_DIRECTORY "${_EXEDIR}" ) + +organize_source( "${TARGET_ROOT}" "" "${SOURCES}" ) diff --git a/crashreports/crashreporter/CrashReportApp.cpp b/crashreports/crashreporter/CrashReportApp.cpp new file mode 100644 index 000000000..ac5c8b2b1 --- /dev/null +++ b/crashreports/crashreporter/CrashReportApp.cpp @@ -0,0 +1,464 @@ +#include "CrashReportApp.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include "google_breakpad/processor/basic_source_line_resolver.h" +#include "google_breakpad/processor/minidump_processor.h" +#include "google_breakpad/processor/process_state.h" +#include "google_breakpad/processor/minidump.h" +#include "processor/stackwalk_common.h" + +#include "warning.xpm" + +//Temporary solution until lib-strings is added +#define XC(msg, ctx) (wxGetTranslation(msg, wxEmptyString, ctx)) + +#if defined(_WIN32) +#include +#include +#include "client/windows/sender/crash_report_sender.h" + +namespace +{ + std::wstring ToPlatformString(const std::string& utf8) + { + return std::wstring_convert, std::wstring::traits_type::char_type>().from_bytes(utf8); + } + + bool SendMinidump(const std::string& url, const wxString& minidumpPath, const std::map& arguments, const wxString& commentsFilePath) + { + std::map files; + files[L"upload_file_minidump"] = minidumpPath.wc_str(); + if (!commentsFilePath.empty()) + { + files[wxFileName(commentsFilePath).GetFullName().wc_str()] = commentsFilePath.wc_str(); + } + + std::map parameters; + for (auto& p : arguments) + { + parameters[ToPlatformString(p.first)] = ToPlatformString(p.second); + } + + google_breakpad::CrashReportSender sender(L""); + + auto result = sender.SendCrashReport( + ToPlatformString(url), + parameters, + files, + nullptr + ); + return result == google_breakpad::RESULT_SUCCEEDED; + } +} +#else + +#include "common/linux/http_upload.h" + +namespace +{ + bool SendMinidump(const std::string& url, const wxString& minidumpPath, const std::map& arguments, const wxString& commentsFilePath) + { + std::map files; + files["upload_file_minidump"] = minidumpPath.ToStdString(); + if (!commentsFilePath.empty()) + { + files["comments.txt"] = commentsFilePath.ToStdString(); + } + + std::string response, error; + bool success = google_breakpad::HTTPUpload::SendRequest( + url, + arguments, + files, + std::string(), + std::string(), + std::string(), + &response, + NULL, + &error); + + return success; + } +} + +#endif + +IMPLEMENT_APP(CrashReportApp); +namespace +{ + std::map parseArguments(const std::string& str) + { + int TOKEN_IDENTIFIER{ 0 }; + constexpr int TOKEN_EQ{ 1 }; + constexpr int TOKEN_COMMA{ 2 }; + constexpr int TOKEN_VALUE{ 3 }; + + int i = 0; + + std::string key; + int state = TOKEN_COMMA; + std::map result; + while (true) + { + if (str[i] == 0) + break; + else if (isspace(str[i])) + ++i; + else if (isalpha(str[i])) + { + if (state != TOKEN_COMMA) + throw std::logic_error("malformed parameters string: unexpected identifier"); + + int begin = i; + while (isalnum(str[i])) + ++i; + + key = str.substr(begin, i - begin); + state = TOKEN_IDENTIFIER; + } + else if (str[i] == '=') + { + if (state != TOKEN_IDENTIFIER) + throw std::logic_error("malformed parameters string: unexpected '=' symbol"); + ++i; + state = TOKEN_EQ; + } + else if (str[i] == '\"') + { + if (state != TOKEN_EQ) + throw std::logic_error("malformed parameters string: unexpected '\"' symbol"); + + int begin = ++i; + while (true) + { + if (str[i] == 0) + throw std::logic_error("unterminated string literal"); + else if (str[i] == '\"') + { + if (i > begin) + result[key] = str.substr(begin, i - begin); + else + result[key] = std::string(); + ++i; + state = TOKEN_VALUE; + break; + } + ++i; + } + } + else if (str[i] == ',') + { + if (state != TOKEN_VALUE) + throw std::logic_error("malformed parameters string: unexpected ',' symbol"); + state = TOKEN_COMMA; + ++i; + } + else + throw std::logic_error("malformed parameters string"); + } + if (state != TOKEN_VALUE) + throw std::logic_error("malformed parameters string"); + + return result; + } + + void PrintMinidump(google_breakpad::Minidump& minidump) + { + google_breakpad::BasicSourceLineResolver resolver; + google_breakpad::MinidumpProcessor minidumpProcessor(nullptr, &resolver); + google_breakpad::MinidumpThreadList::set_max_threads(std::numeric_limits::max()); + google_breakpad::MinidumpMemoryList::set_max_regions(std::numeric_limits::max()); + + google_breakpad::ProcessState processState; + + if (minidumpProcessor.Process(&minidump, &processState) != google_breakpad::PROCESS_OK) + { + printf("Failed to process minidump"); + } + else + { + google_breakpad::PrintProcessState(processState, true, &resolver); + } + } + + wxString MakeDumpString(google_breakpad::Minidump& minidump, const wxString& temp) + { +#if _WIN32 + auto stream = _wfreopen(temp.wc_str(), L"w+", stdout); +#else + auto stream = freopen(temp.utf8_str().data(), "w+", stdout); +#endif + if (stream == NULL) + throw std::runtime_error("Failed to print minidump: cannot open temp file"); + PrintMinidump(minidump); + fflush(stdout); + + auto length = ftell(stream); + std::vector bytes(length); + fseek(stream, 0, SEEK_SET); + fread(&bytes[0], 1, length, stream); + fclose(stream); + +#if _WIN32 + _wremove(temp.wc_str()); +#else + remove(temp.utf8_str().data()); +#endif + + return wxString::From8BitData(&bytes[0], bytes.size()); + } + + wxString MakeHeaderString(google_breakpad::Minidump& minidump) + { + if (auto exception = minidump.GetException()) + { + if (auto rawException = exception->exception()) + { + // i18n-hint C++ programming assertion + return wxString::Format(_("Exception code 0x%x"), rawException->exception_record.exception_code); + } + else + { + // i18n-hint C++ programming assertion + return _("Unknown exception"); + } + } + else if (auto assertion = minidump.GetAssertion()) + { + auto expression = assertion->expression(); + if (!expression.empty()) + { + return expression; + } + } + return _("Unknown error"); + } + + void DoShowCrashReportFrame(const wxString& header, const wxString& dump, const std::function& onSend) + { + static constexpr int MaxUserCommentLength = 2000; + + auto frame = new wxFrame( + nullptr, + wxID_ANY, + _("Problem Report for Audacity"), + wxDefaultPosition, + wxDefaultSize, + wxDEFAULT_FRAME_STYLE & ~(wxRESIZE_BORDER | wxMAXIMIZE_BOX)//disable frame resize + ); + frame->SetOwnBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_FRAMEBK)); + + auto mainLayout = new wxBoxSizer(wxVERTICAL); + + auto headerText = new wxStaticText(frame, wxID_ANY, header); + headerText->SetFont(wxFont(wxFontInfo().Bold())); + + auto headerLayout = new wxBoxSizer(wxHORIZONTAL); + headerLayout->Add(new wxStaticBitmap(frame, wxID_ANY, wxIcon(warning))); + headerLayout->AddSpacer(5); + headerLayout->Add(headerText, wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); + + auto buttonsLayout = new wxBoxSizer(wxHORIZONTAL); + + wxTextCtrl* commentCtrl = nullptr; + if (onSend != nullptr) + { + commentCtrl = new wxTextCtrl(frame, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(500, 100), wxTE_MULTILINE); + commentCtrl->SetMaxLength(MaxUserCommentLength); + } + + if (onSend != nullptr) + { + auto okButton = new wxButton(frame, wxID_ANY, XC("&Don't send", "crash reporter button")); + auto sendButton = new wxButton(frame, wxID_ANY, XC("&Send", "crash reporter button")); + + okButton->Bind(wxEVT_BUTTON, [frame](wxCommandEvent&) + { + frame->Close(true); + }); + sendButton->Bind(wxEVT_BUTTON, [frame, commentCtrl, onSend](wxCommandEvent&) + { + if (onSend(commentCtrl->GetValue())) + { + frame->Close(true); + } + }); + + buttonsLayout->Add(okButton); + buttonsLayout->AddSpacer(5); + buttonsLayout->Add(sendButton); + } + else + { + auto okButton = new wxButton(frame, wxID_OK, wxT("OK")); + okButton->Bind(wxEVT_BUTTON, [frame](wxCommandEvent&) + { + frame->Close(true); + }); + buttonsLayout->Add(okButton); + } + + mainLayout->Add(headerLayout, wxSizerFlags().Border(wxALL)); + if (onSend != nullptr) + { + mainLayout->AddSpacer(5); + mainLayout->Add(new wxStaticText(frame, wxID_ANY, _("Click \"Send\" to submit the report to Audacity. This information is collected anonymously.")), wxSizerFlags().Border(wxALL)); + } + mainLayout->AddSpacer(10); + mainLayout->Add(new wxStaticText(frame, wxID_ANY, _("Problem details")), wxSizerFlags().Border(wxALL)); + + auto dumpTextCtrl = new wxTextCtrl(frame, wxID_ANY, dump, wxDefaultPosition, wxSize(500, 300), wxTE_RICH | wxTE_READONLY | wxTE_MULTILINE | wxTE_DONTWRAP); + dumpTextCtrl->SetFont(wxFont(wxFontInfo().Family(wxFONTFAMILY_TELETYPE))); + dumpTextCtrl->ShowPosition(0);//scroll to top + mainLayout->Add(dumpTextCtrl, wxSizerFlags().Border(wxALL).Expand()); + + if (onSend != nullptr) + { + mainLayout->AddSpacer(10); + mainLayout->Add(new wxStaticText(frame, wxID_ANY, _("Comments")), wxSizerFlags().Border(wxALL)); + mainLayout->Add(commentCtrl, wxSizerFlags().Border(wxALL).Expand()); + } + + mainLayout->Add(buttonsLayout, wxSizerFlags().Border(wxALL).Align(wxALIGN_RIGHT)); + frame->SetSizerAndFit(mainLayout); + + frame->Show(true); + } +} + + +bool CrashReportApp::OnInit() +{ + if (!wxApp::OnInit()) + return false; + + if (mSilent) + { + if (!mURL.empty()) + SendMinidump(mURL, mMinidumpPath, mArguments, wxEmptyString); + } + else + { + static std::unique_ptr sLocale(new wxLocale(wxLANGUAGE_DEFAULT)); +#if defined(__WXOSX__) + sLocale->AddCatalogLookupPathPrefix(wxT("../Resources")); +#elif defined(__WXMSW__) + sLocale->AddCatalogLookupPathPrefix(wxT("Languages")); +#elif defined(__WXGTK__) + sLocale->AddCatalogLookupPathPrefix(wxT("./locale")); + sLocale->AddCatalogLookupPathPrefix(wxString::Format(wxT("%s/share/locale"), wxT(INSTALL_PREFIX))); +#endif + sLocale->AddCatalog("audacity"); + sLocale->AddCatalog("wxstd"); + + google_breakpad::Minidump minidump(mMinidumpPath.ToStdString(), false); + if (minidump.Read()) + { + SetExitOnFrameDelete(true); + + wxFileName temp(mMinidumpPath); + temp.SetExt("tmp"); + + try + { + ShowCrashReport(MakeHeaderString(minidump), MakeDumpString(minidump, temp.GetFullPath())); + } + catch (std::exception& e) + { + wxMessageBox(e.what()); + return false; + } + return true; + } + } + return false; +} + +void CrashReportApp::OnInitCmdLine(wxCmdLineParser& parser) +{ + static const wxCmdLineEntryDesc cmdLineEntryDesc[] = + { + { wxCMD_LINE_SWITCH, "h", "help", "Display help on the command line parameters", wxCMD_LINE_VAL_NONE, wxCMD_LINE_OPTION_HELP }, + { wxCMD_LINE_SWITCH, "s", "silent", "Send without displaying the confirmation dialog" }, + { wxCMD_LINE_OPTION, "u", "url", "Crash report server URL", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL }, + { wxCMD_LINE_OPTION, "a", "args", "A set of arguments to send", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL }, + { wxCMD_LINE_PARAM, NULL, NULL, "path to minidump file", wxCMD_LINE_VAL_STRING, wxCMD_LINE_OPTION_MANDATORY }, + { wxCMD_LINE_NONE } + }; + + parser.SetDesc(cmdLineEntryDesc); + + wxApp::OnInitCmdLine(parser); +} + +bool CrashReportApp::OnCmdLineParsed(wxCmdLineParser& parser) +{ + wxString url; + wxString arguments; + if (parser.Found("u", &url)) + { + mURL = url.ToStdString(); + } + if (parser.Found("a", &arguments)) + { + try + { + mArguments = parseArguments(arguments.ToStdString()); + } + catch (std::exception& e) + { + wxMessageBox(e.what()); + return false; + } + } + mMinidumpPath = parser.GetParam(0); + mSilent = parser.Found("s"); + + return wxApp::OnCmdLineParsed(parser); +} + +void CrashReportApp::ShowCrashReport(const wxString& header, const wxString& text) +{ + if (mURL.empty()) + { + DoShowCrashReportFrame(header, text, nullptr); + } + else + { + DoShowCrashReportFrame(header, text, [this](const wxString& comments) + { + wxString commentsFilePath; + if (!comments.empty()) + { + wxFileName temp(mMinidumpPath); + temp.SetName(temp.GetName() + "-comments"); + temp.SetExt("txt"); + commentsFilePath = temp.GetFullPath(); + wxFile file; + if (file.Open(commentsFilePath, wxFile::write)) + { + file.Write(comments); + file.Close(); + } + } + + auto result = SendMinidump(mURL, mMinidumpPath, mArguments, commentsFilePath); + if (!commentsFilePath.empty()) + wxRemoveFile(commentsFilePath); + + if (!result) + { + wxMessageBox(_("Failed to send crash report")); + } + return result; + }); + } +} diff --git a/crashreports/crashreporter/CrashReportApp.h b/crashreports/crashreporter/CrashReportApp.h new file mode 100644 index 000000000..a5710e331 --- /dev/null +++ b/crashreports/crashreporter/CrashReportApp.h @@ -0,0 +1,36 @@ +/*!******************************************************************** +* + Audacity: A Digital Audio Editor + + CrashReportApp.h + + Vitaly Sverchinsky + + **********************************************************************/ + +#include +#include +#include + +//! Crash reporter GUI application +/*! Used to send crash reports to a remote server, or view them. + * Shows brief report content, and allows user to send report to developers. + * Reporting URL and other parameters are specified as a command line arguments. + */ +class CrashReportApp final : public wxApp +{ + std::string mURL; + wxString mMinidumpPath; + std::map mArguments; + + bool mSilent{ false }; +public: + bool OnInit() override; + void OnInitCmdLine(wxCmdLineParser& parser) override; + bool OnCmdLineParsed(wxCmdLineParser& parser) override; + +private: + void ShowCrashReport(const wxString& header, const wxString& text); +}; + +DECLARE_APP(CrashReportApp); diff --git a/crashreports/crashreporter/warning.xpm b/crashreports/crashreporter/warning.xpm new file mode 100644 index 000000000..ae82c4e0f --- /dev/null +++ b/crashreports/crashreporter/warning.xpm @@ -0,0 +1,138 @@ +/* XPM */ +static const char *warning[] = { +/* columns rows colors chars-per-pixel */ +"24 24 108 2 ", +" c None", +". c black", +"X c #010100", +"o c #020200", +"O c #020201", +"+ c #070601", +"@ c #070701", +"# c #0E0C02", +"$ c #151204", +"% c #1C1804", +"& c #1F1A05", +"* c #261F00", +"= c #262000", +"- c #262005", +"; c #292305", +": c #2D2705", +"> c #312A06", +", c #403705", +"< c #413806", +"1 c #8F7300", +"2 c #957700", +"3 c #BA9500", +"4 c #B99600", +"5 c #E3B800", +"6 c #E5BA00", +"7 c #FEBB0B", +"8 c #FFBC08", +"9 c #FFBA0C", +"0 c #FFBF10", +"q c #FFBF11", +"w c #FFC000", +"e c #FFC100", +"r c #FFC202", +"t c #FFC400", +"y c #FFC500", +"u c #FFC700", +"i c #FFC307", +"p c #FFC800", +"a c #FFC900", +"s c #FECA00", +"d c #FFCB00", +"f c #FFCC00", +"g c #FFCD00", +"h c #FFCA04", +"j c #FFCF04", +"k c #FFC20B", +"l c #FFC00D", +"z c #FFC20D", +"x c #FFC00E", +"c c #FFCF0D", +"v c #FED004", +"b c #FFD104", +"n c #FFD00D", +"m c #FFD10D", +"M c #FFD20D", +"N c #FEC116", +"B c #FFCB14", +"V c #FFC61C", +"C c #FFCA1F", +"Z c #FFD215", +"A c #FFD11C", +"S c #FFD11D", +"D c #FFD31D", +"F c #FFD41D", +"G c #E6C327", +"H c #E7C527", +"J c #FFC621", +"K c #FFC525", +"L c #FFC624", +"P c #FFCC21", +"I c #FFCA25", +"U c #FFC927", +"Y c #FFCB26", +"T c #FFCC27", +"R c #FFCE27", +"E c #FFC22B", +"W c #FFC52A", +"Q c #FFC62C", +"! c #F2CE29", +"~ c #F3CF29", +"^ c #FFC828", +"/ c #FFCE28", +"( c #FFCC2A", +") c #FFCC2C", +"_ c #FFD423", +"` c #FFD623", +"' c #FFD226", +"] c #FFD527", +"[ c #F6D129", +"{ c #FFD129", +"} c #FFD229", +"| c #FFD22A", +" . c #FFD32A", +".. c #FDD42A", +"X. c #FCD52B", +"o. c #FFD42A", +"O. c #FFD52A", +"+. c #FFD52B", +"@. c #FFD62A", +"#. c #FFD62B", +"$. c #FFD72B", +"%. c #FFD828", +"&. c #FFD92B", +"*. c #FFDA2B", +"=. c #FFDA2C", +"-. c #FFC633", +";. c #FFC830", +":. c #FFCA31", +/* pixels */ +" ", +" -.E ", +" / R ", +" U +.+.) ", +" ;.| +.+.| W ", +" U +.[ [ +.R ", +" ;.+.H X X G | J ", +" R +.! X X ! +.R ", +" L +.+.+.@ @ +.+.+.^ ", +" | +.+.=.# # *.+.+.| ", +" / +.+.+.*.$ $ *.+.+.+.U ", +" L ' ] ] ] *.% % *.] ] ] ' Q ", +" C ` ` ` ` ` - ; ` ` ` ` ` P ", +" J D D D D D D : > F D D D D D V ", +" B Z Z Z Z Z Z < , Z Z Z Z Z Z B ", +" k c n n n n n n n n n n n n n n c x ", +" q h j j j j j j j b b j j j j j j j h x ", +" i f f f f f f f 6 = * 5 f f f f f f f w ", +" 8 p p p p p p p p 3 X X 3 p p p f f p p p q ", +" w p p p p p p p p p 2 1 p p p p p p p p p w ", +"9 u u u u u u u u u u u u u u u u u u u u u u 7 ", +"N u u u u u u u u u u u u u u u u u u u u u u x ", +" ", +" " +}; diff --git a/crashreports/internal/unix/CrashReportContext.cpp b/crashreports/internal/unix/CrashReportContext.cpp new file mode 100644 index 000000000..a366097f8 --- /dev/null +++ b/crashreports/internal/unix/CrashReportContext.cpp @@ -0,0 +1,155 @@ +/*!******************************************************************** +* + Audacity: A Digital Audio Editor + + CrashReportContext.cpp + + Vitaly Sverchinsky + + Some parts of the code are designed to operate while app is crashing, + so there may be some restrictions on heap usage. For more information + please read Breakpad documentation. + + **********************************************************************/ +#include "CrashReportContext.h" + +#include +#include +#include + +#if defined(__APPLE__) +#include "client/mac/handler/exception_handler.h" +#else +#include "client/linux/handler/exception_handler.h" +#endif + +bool SendReport(CrashReportContext* c, const char* minidumpPath) +{ + auto pid = fork(); + if(pid == 0) + { + if(c->mParameters[0] != 0) + { + execl(c->mSenderPath, CRASHREPORTER_PROGRAM_NAME, "-a", c->mParameters, "-u", c->mReportURL, minidumpPath, NULL); + } + else + { + execl(c->mSenderPath, CRASHREPORTER_PROGRAM_NAME, "-u", c->mReportURL, minidumpPath, NULL); + } + fprintf(stderr, "Failed to start handler: %s\n", strerror(errno)); + abort(); + } + return pid != -1; +} + +namespace +{ + + //converts parameters map to a string, so that the Crash Reporting program + //would understand them + std::string StringifyParameters(const std::map& parameters) + { + std::stringstream stream; + + std::size_t parameterIndex = 0; + std::size_t parametersCount = parameters.size(); + for (auto& pair : parameters) + { + stream << pair.first.c_str() << "=\"" << pair.second.c_str() << "\""; + ++parameterIndex; + if (parameterIndex < parametersCount) + stream << ","; + } + return stream.str(); + } + + //copy contents of src to a raw char dest buffer, + //returns false if dest is not large enough + bool StrcpyChecked(char* dest, size_t destsz, const std::string& src) + { + if(src.length() < destsz) + { + memcpy(dest, src.c_str(), src.length()); + dest[src.length()] = '\0'; + return true; + } + return false; + } + +#if defined(__APPLE__) + + static constexpr size_t MaxDumpPathLength{ 4096 }; + static char DumpPath[MaxDumpPathLength]; + + bool DumpCallback(const char* dump_dir, const char* minidump_id, void* context, bool succeeded) + { + if(succeeded) + { + const int PathDumpLength = strlen(dump_dir) + strlen("/") + strlen(minidump_id) + strlen(".dmp"); + if(PathDumpLength < MaxDumpPathLength) + { + strcpy(DumpPath, dump_dir); + strcat(DumpPath, "/"); + strcat(DumpPath, minidump_id); + strcat(DumpPath, ".dmp"); + auto crashReportContext = static_cast(context); + return SendReport(crashReportContext, DumpPath); + } + return false; + } + return succeeded; + } +#else + bool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* context, bool succeeded) + { + if(succeeded) + { + auto crashReportContext = static_cast(context); + return SendReport(crashReportContext, descriptor.path()); + } + return succeeded; + } +#endif +} + +bool CrashReportContext::SetSenderPathUTF8(const std::string& path) +{ + return StrcpyChecked(mSenderPath, MaxBufferLength, path + "/" + CRASHREPORTER_PROGRAM_NAME); +} + +bool CrashReportContext::SetReportURL(const std::string& url) +{ + return StrcpyChecked(mReportURL, MaxBufferLength, url); +} + +bool CrashReportContext::SetParameters(const std::map& p) +{ + auto str = StringifyParameters(p); + return StrcpyChecked(mParameters, MaxBufferLength, str); +} + +void CrashReportContext::StartHandler(const std::string& databasePath) +{ + //intentinal leak: error hooks may be useful while application is terminating + //CrashReportContext data should be alive too... +#if(__APPLE__) + static auto handler = new google_breakpad::ExceptionHandler( + databasePath, + nullptr, + DumpCallback, + this, + true, + nullptr + ); +#else + google_breakpad::MinidumpDescriptor descriptor(databasePath); + static auto handler = new google_breakpad::ExceptionHandler( + descriptor, + nullptr, + DumpCallback, + this, + true, + -1 + ); +#endif +} diff --git a/crashreports/internal/unix/CrashReportContext.h b/crashreports/internal/unix/CrashReportContext.h new file mode 100644 index 000000000..43abae9d6 --- /dev/null +++ b/crashreports/internal/unix/CrashReportContext.h @@ -0,0 +1,43 @@ +/*!******************************************************************** +* + Audacity: A Digital Audio Editor + + CrashReportContext.h + + Vitaly Sverchinsky + + **********************************************************************/ + +#pragma once + +#include +#include + +//!This object is for internal usage. +/*! Simple POD type, holds user data required to start handler. + * Fields are initialized with Set* methods, + * which may return false if internal buffer isn't large enough + * to store value passed as an argument. + * After initialization call StartHandler providing path to the + * database, where minidumps will be stored. + */ +class CrashReportContext +{ + static constexpr size_t MaxBufferLength{ 2048 }; + + char mSenderPath[MaxBufferLength]{}; + char mReportURL[MaxBufferLength]{}; + char mParameters[MaxBufferLength]{}; + +public: + + bool SetSenderPathUTF8(const std::string& path); + bool SetReportURL(const std::string& url); + bool SetParameters(const std::map& p); + + void StartHandler(const std::string& databasePath); + +private: + //helper function which need access to a private data, but should not be exposed to a public class interface + friend bool SendReport(CrashReportContext* ctx, const char* minidumpPath); +}; diff --git a/crashreports/internal/win32/CrashReportContext.cpp b/crashreports/internal/win32/CrashReportContext.cpp new file mode 100644 index 000000000..f03be9ff5 --- /dev/null +++ b/crashreports/internal/win32/CrashReportContext.cpp @@ -0,0 +1,165 @@ +/*!******************************************************************** +* + Audacity: A Digital Audio Editor + + CrashReportContext.cpp + + Vitaly Sverchinsky + + Some parts of the code are designed to operate while app is crashing, + so there may be some restrictions on heap usage. For more information + please read Breakpad documentation. + + **********************************************************************/ + +#include "CrashReportContext.h" + +#include +#include +#include +#include "client/windows/handler/exception_handler.h" + +namespace +{ + //copy src(null-terminated) to dst, + //returns false if dest is not large enough + bool StrcpyChecked(wchar_t* dest, size_t destsz, const wchar_t* src) + { + auto len = wcslen(src); + if (len < destsz) + { + memcpy(dest, src, sizeof(wchar_t) * len); + dest[len] = 0; + return true; + } + return false; + } + + //appends src(null-terminated) to dest, destsz is the total dest buffer size, not remaining + //returns false if dest is not large enough + bool StrcatChecked(wchar_t* dest, size_t destsz, const wchar_t* src) + { + auto srclen = wcslen(src); + auto dstlen = wcslen(dest); + if (srclen + dstlen < destsz) + { + memcpy(dest + dstlen, src, sizeof(wchar_t) * srclen); + dest[srclen + dstlen] = 0; + return true; + } + return false; + } + + //converts parameters map to a string, so that the Crash Reporting program + //would understand them + std::string StringifyParameters(const std::map& parameters) + { + std::stringstream stream; + + std::size_t parameterIndex = 0; + std::size_t parametersCount = parameters.size(); + for (auto& pair : parameters) + { + stream << pair.first.c_str() << "=\\\"" << pair.second.c_str() << "\\\""; + ++parameterIndex; + if (parameterIndex < parametersCount) + stream << ","; + } + return stream.str(); + } +} + +bool MakeCommand(CrashReportContext* c, const wchar_t* path, const wchar_t* id) +{ + //utility path + auto ok = StrcpyChecked(c->mCommand, CrashReportContext::MaxCommandLength, L"\""); + ok = ok && StrcatChecked(c->mCommand, CrashReportContext::MaxCommandLength, c->mSenderPath); + ok = ok && StrcatChecked(c->mCommand, CrashReportContext::MaxCommandLength, L"\""); + + //parameters: /p "..." + if (ok && c->mParameters[0] != 0) + { + ok = ok && StrcatChecked(c->mCommand, CrashReportContext::MaxCommandLength, L" /a \""); + ok = ok && StrcatChecked(c->mCommand, CrashReportContext::MaxCommandLength, c->mParameters); + ok = ok && StrcatChecked(c->mCommand, CrashReportContext::MaxCommandLength, L"\""); + } + //crash report URL: /u https://... + ok = ok && StrcatChecked(c->mCommand, CrashReportContext::MaxBufferLength, L" /u \""); + ok = ok && StrcatChecked(c->mCommand, CrashReportContext::MaxBufferLength, c->mReportURL); + ok = ok && StrcatChecked(c->mCommand, CrashReportContext::MaxBufferLength, L"\" "); + //minidump path: path/to/minidump.dmp + ok = ok && StrcatChecked(c->mCommand, CrashReportContext::MaxCommandLength, L" \""); + ok = ok && StrcatChecked(c->mCommand, CrashReportContext::MaxCommandLength, path); + ok = ok && StrcatChecked(c->mCommand, CrashReportContext::MaxCommandLength, L"\\"); + ok = ok && StrcatChecked(c->mCommand, CrashReportContext::MaxCommandLength, id); + ok = ok && StrcatChecked(c->mCommand, CrashReportContext::MaxCommandLength, L".dmp\""); + return ok; +} + +bool SendReport(CrashReportContext* c, const wchar_t* path, const wchar_t* id) +{ + if (!MakeCommand(c, path, id)) + return false; + + STARTUPINFOW si; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_SHOW; + + PROCESS_INFORMATION pi; + ZeroMemory(&pi, sizeof(pi)); + + if (CreateProcessW(NULL, c->mCommand, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) + { + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return true; + } + else + return false; +} + +bool UploadReport( + const wchar_t* dump_path, + const wchar_t* minidump_id, + void* context, + EXCEPTION_POINTERS* /*exinfo*/, + MDRawAssertionInfo* /*assertion*/, + bool succeeded) +{ + CrashReportContext* crashReportContext = static_cast(context); + if (!SendReport(crashReportContext, dump_path, minidump_id)) + return false; + return succeeded; +} + +bool CrashReportContext::SetSenderPathUTF8(const std::string& path) +{ + auto fullpath = path + "\\" + CRASHREPORTER_PROGRAM_NAME; + return StrcpyChecked(mSenderPath, MaxBufferLength, std::wstring_convert, wchar_t>().from_bytes(fullpath).c_str()); +} + +bool CrashReportContext::SetReportURL(const std::string& url) +{ + return StrcpyChecked(mReportURL, MaxBufferLength, std::wstring(url.begin(), url.end()).c_str()); +} + +bool CrashReportContext::SetParameters(const std::map& p) +{ + auto str = StringifyParameters(p); + return StrcpyChecked(mParameters, MaxBufferLength, std::wstring(str.begin(), str.end()).c_str()); +} + +void CrashReportContext::StartHandler(const std::string& databasePath) +{ + //intentinal leak: error hooks may be useful while application is terminating + //CrashReportContext data should be alive too... + static auto handler = new google_breakpad::ExceptionHandler( + std::wstring_convert, wchar_t>().from_bytes(databasePath), + NULL, + UploadReport, + this, + google_breakpad::ExceptionHandler::HANDLER_ALL); +} + diff --git a/crashreports/internal/win32/CrashReportContext.h b/crashreports/internal/win32/CrashReportContext.h new file mode 100644 index 000000000..723bc088e --- /dev/null +++ b/crashreports/internal/win32/CrashReportContext.h @@ -0,0 +1,47 @@ +/*!******************************************************************** +* + Audacity: A Digital Audio Editor + + CrashReportContext.h + + Vitaly Sverchinsky + + **********************************************************************/ + +#pragma once + +#include +#include + +//!This object is for internal usage. +/*! Simple POD type, holds user data required to start handler. + * Fields are initialized with Set* methods, + * which may return false if internal buffer isn't large enough + * to store value passed as an argument. + * After initialization call StartHandler providing path to the + * database, where minidumps will be stored. + */ +class CrashReportContext final +{ + static constexpr size_t MaxBufferLength{ 2048 }; + static constexpr size_t MaxCommandLength{ 8192 }; + + wchar_t mSenderPath[MaxBufferLength]{}; + wchar_t mReportURL[MaxBufferLength]{}; + wchar_t mParameters[MaxBufferLength]{}; + + //this is a buffer where the command will be built at runtime + wchar_t mCommand[MaxCommandLength]{}; + +public: + bool SetSenderPathUTF8(const std::string& path); + bool SetReportURL(const std::string& path); + bool SetParameters(const std::map& p); + + void StartHandler(const std::string& databasePath); + +private: + //helper functions which need access to a private data, but should not be exposed to a public class interface + friend bool MakeCommand(CrashReportContext* ctx, const wchar_t* path, const wchar_t* id); + friend bool SendReport(CrashReportContext* ctx, const wchar_t* path, const wchar_t* id); +}; diff --git a/libraries/lib-strings/FutureStrings.h b/libraries/lib-strings/FutureStrings.h index 3c4fe3fa3..f63569992 100644 --- a/libraries/lib-strings/FutureStrings.h +++ b/libraries/lib-strings/FutureStrings.h @@ -96,21 +96,6 @@ Some example strings are also given first, to document the syntax. // //////////////////////////////////////////////// End examples #endif - -// Crash & error report window - -XO("Problem Report for Audacity"), -XO("Problem details"), -// i18n-hint C++ programming exception -XO("Exception code 0x%x"), -// i18n-hint C++ programming exception -XO("Unknown exception"), -// i18n-hint C++ programming assertion -XO("Unknown assertion"), -XO("Unknown error"), -XO("Failed to send crash report"), - - // Update version dialog XC("Update Audacity", "update dialog"), XC("&Skip", "update dialog"), diff --git a/locale/update_po_files.sh b/locale/update_po_files.sh index 85ccd60a8..5d2701ca1 100755 --- a/locale/update_po_files.sh +++ b/locale/update_po_files.sh @@ -2,7 +2,7 @@ # Run this script with locale as the current directory set -o errexit echo ";; Recreating audacity.pot using .h, .cpp and .mm files" -for path in ../modules/mod-* ../libraries/lib-* ../include ../src ; do +for path in ../modules/mod-* ../libraries/lib-* ../include ../src ../crashreports ; do find $path -name \*.h -o -name \*.cpp -o -name \*.mm done | LANG=c sort | \ sed -E 's/\.\.\///g' |\ diff --git a/scripts/ci/build.sh b/scripts/ci/build.sh index 48262b3f2..582743552 100755 --- a/scripts/ci/build.sh +++ b/scripts/ci/build.sh @@ -20,3 +20,21 @@ fi # Build Audacity cmake --build build -j "${cpus}" --config "${AUDACITY_BUILD_TYPE}" + +BIN_OUTPUT_DIR=build/bin/${AUDACITY_BUILD_TYPE} +SYMBOLS_OUTPUT_DIR=debug + +mkdir ${SYMBOLS_OUTPUT_DIR} + +if [[ "${OSTYPE}" == msys* ]]; then # Windows + # copy PDBs to debug folder... + find ${BIN_OUTPUT_DIR} -name '*.pdb' | xargs -I % cp % ${SYMBOLS_OUTPUT_DIR} + # and remove debug symbol files from the file tree before archieving + find ${BIN_OUTPUT_DIR} -name '*.iobj' -o -name '*.ipdb' -o -name '*.pdb' -o -name '*.ilk' | xargs rm -f +elif [[ "${OSTYPE}" == darwin* ]]; then # macOS + find ${BIN_OUTPUT_DIR} -name '*.dSYM' | xargs -J % mv % ${SYMBOLS_OUTPUT_DIR} +else # Linux & others + chmod +x scripts/ci/linux/split_debug_symbols.sh + find ${BIN_OUTPUT_DIR} -type f -executable -o -name '*.so' | xargs -n 1 scripts/ci/linux/split_debug_symbols.sh + find ${BIN_OUTPUT_DIR} -name '*.debug' | xargs -I % mv % ${SYMBOLS_OUTPUT_DIR} +fi diff --git a/scripts/ci/configure.sh b/scripts/ci/configure.sh index a01644f50..22402ac6a 100755 --- a/scripts/ci/configure.sh +++ b/scripts/ci/configure.sh @@ -17,6 +17,10 @@ cmake_args=( ) if [[ "${AUDACITY_CMAKE_GENERATOR}" == "Visual Studio"* ]]; then + cmake_args+=( + # skip unneeded configurations + -D CMAKE_CONFIGURATION_TYPES="${AUDACITY_BUILD_TYPE}" + ) case "${AUDACITY_ARCH_LABEL}" in 32bit) cmake_args+=( -A Win32 ) ;; 64bit) cmake_args+=( -A x64 ) ;; @@ -24,6 +28,8 @@ if [[ "${AUDACITY_CMAKE_GENERATOR}" == "Visual Studio"* ]]; then esac elif [[ "${AUDACITY_CMAKE_GENERATOR}" == Xcode* ]]; then cmake_args+=( + # skip unneeded configurations + -D CMAKE_CONFIGURATION_TYPES="${AUDACITY_BUILD_TYPE}" -T buildsystem=1 ) fi diff --git a/scripts/ci/environment.sh b/scripts/ci/environment.sh index fd426eccc..907c1a956 100644 --- a/scripts/ci/environment.sh +++ b/scripts/ci/environment.sh @@ -21,5 +21,5 @@ gh_export CONAN_USER_HOME_SHORT="${repository_root}/conan-home/short" gh_export GIT_HASH="$(git show -s --format='%H')" gh_export GIT_HASH_SHORT="$(git show -s --format='%h')" -gh_export AUDACITY_BUILD_TYPE="Release" +gh_export AUDACITY_BUILD_TYPE="RelWithDebInfo" gh_export AUDACITY_INSTALL_PREFIX="${repository_root}/build/install" diff --git a/scripts/ci/linux/split_debug_symbols.sh b/scripts/ci/linux/split_debug_symbols.sh new file mode 100644 index 000000000..d200e4ebe --- /dev/null +++ b/scripts/ci/linux/split_debug_symbols.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -x +objcopy --only-keep-debug --compress-debug-section=zlib "${1}" "${1}.debug" +if [ -f "${1}.debug" ]; then + objcopy --strip-debug --strip-unneeded "${1}" + objcopy --add-gnu-debuglink="${1}.debug" "${1}" +fi diff --git a/scripts/ci/upload_debug_symbols.sh b/scripts/ci/upload_debug_symbols.sh new file mode 100644 index 000000000..2adcfad06 --- /dev/null +++ b/scripts/ci/upload_debug_symbols.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +((${BASH_VERSION%%.*} >= 4)) || { echo >&2 "$0: Error: Please upgrade Bash."; exit 1; } + +set -euxo pipefail + +# download sentry-cli +# TODO: currently this script downloads binaries and install them +# each time job is started, workarounds? +curl -sL https://sentry.io/get-cli/ | bash + +#Where debug symbols will go +UPLOAD_URL=https://${SENTRY_HOST}/api/${SENTRY_PROJECT}/minidump/?sentry_key=${SENTRY_DSN_KEY} + +SYMBOLS=$(find debug | xargs) + +${INSTALL_DIR}/sentry-cli --auth-token ${SENTRY_AUTH_TOKEN} --url ${UPLOAD_URL} \ + --org ${SENTRY_ORG_SLUG}} \ + --project ${SENTRY_PROJECT_SLUG} ${SYMBOLS} diff --git a/src/AudacityApp.cpp b/src/AudacityApp.cpp index 6e6c29a9a..14f2e5e3c 100644 --- a/src/AudacityApp.cpp +++ b/src/AudacityApp.cpp @@ -127,6 +127,10 @@ It handles initialization and termination by subclassing wxApp. #include "import/Import.h" +#if defined(USE_BREAKPAD) +#include "BreakpadConfigurer.h" +#endif + #ifdef EXPERIMENTAL_SCOREALIGN #include "effects/ScoreAlignDialog.h" #endif @@ -382,6 +386,29 @@ void PopulatePreferences() gPrefs->Flush(); } +#if defined(USE_BREAKPAD) +void InitBreakpad() +{ + wxFileName databasePath; + databasePath.SetPath(wxStandardPaths::Get().GetUserLocalDataDir()); + databasePath.AppendDir("crashreports"); + databasePath.Mkdir(wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL); + + if(databasePath.DirExists()) + { + BreakpadConfigurer configurer; + configurer.SetDatabasePathUTF8(databasePath.GetPath().ToUTF8().data()) + .SetSenderPathUTF8(wxFileName(wxStandardPaths::Get().GetExecutablePath()).GetPath().ToUTF8().data()) + #if defined(CRASH_REPORT_URL) + .SetReportURL(CRASH_REPORT_URL) + #endif + .SetParameters({ + { "version", wxString(AUDACITY_VERSION_STRING).ToUTF8().data() } + }) + .Start(); + } +} +#endif } static bool gInited = false; @@ -996,8 +1023,10 @@ bool AudacityApp::OnExceptionInMainLoop() AudacityApp::AudacityApp() { +#if defined(USE_BREAKPAD) + InitBreakpad(); // Do not capture crashes in debug builds -#if !defined(_DEBUG) +#elif !defined(_DEBUG) #if defined(HAS_CRASH_REPORT) #if defined(wxUSE_ON_FATAL_EXCEPTION) && wxUSE_ON_FATAL_EXCEPTION wxHandleFatalExceptions(); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3a71d5a6b..b749b80f4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1073,6 +1073,7 @@ list( APPEND LIBRARIES libsoxr portaudio-v19 sqlite + $<$:crashreports> $<$:ffmpeg> $<$:libid3tag::libid3tag> $<$:libflac> @@ -1292,6 +1293,10 @@ else() ) endif() +if(CRASH_REPORT_URL) + list(APPEND DEFINES PRIVATE -DCRASH_REPORT_URL="${CRASH_REPORT_URL}") +endif() + set_target_property_all( ${TARGET} RUNTIME_OUTPUT_NAME ${AUDACITY_NAME} ) organize_source( "${TARGET_ROOT}/.." "include" "${HEADERS}" )