Crashreporting

This commit is contained in:
Vitaly Sverchinsky 2021-05-04 21:43:19 +03:00
parent 5c05f6b421
commit e8b186a9b4
24 changed files with 1363 additions and 19 deletions

View File

@ -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"

View File

@ -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" )

View File

@ -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" )

View File

@ -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:
$<IF:$<CONFIG:Release>,-U_DEBUG,-D_DEBUG=1>
$<IF:$<CONFIG:Debug>,-D_DEBUG=1,-U_DEBUG>
)
# Definitions controlled by the AUDACITY_BUILD_LEVEL switch
if( AUDACITY_BUILD_LEVEL EQUAL 0 )

View File

@ -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<std::string, std::string>& 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);
}

View File

@ -0,0 +1,45 @@
/*!********************************************************************
*
Audacity: A Digital Audio Editor
BreakpadConfigurer.h
Vitaly Sverchinsky
**********************************************************************/
#pragma once
#include <string>
#include <map>
//! 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<std::string, std::string> 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<std::string, std::string>& 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();
};

View File

@ -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="$<TARGET_FILE_NAME:crashreporter>"
)
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}" )

View File

@ -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}/$<CONFIG>"
-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}" )

View File

@ -0,0 +1,464 @@
#include "CrashReportApp.h"
#include <sstream>
#include <memory>
#include <wx/cmdline.h>
#include <wx/chartype.h>
#include <wx/artprov.h>
#include <wx/filename.h>
#include <wx/stdpaths.h>
#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 <locale>
#include <codecvt>
#include "client/windows/sender/crash_report_sender.h"
namespace
{
std::wstring ToPlatformString(const std::string& utf8)
{
return std::wstring_convert<std::codecvt_utf8<std::wstring::traits_type::char_type>, std::wstring::traits_type::char_type>().from_bytes(utf8);
}
bool SendMinidump(const std::string& url, const wxString& minidumpPath, const std::map<std::string, std::string>& arguments, const wxString& commentsFilePath)
{
std::map<std::wstring, std::wstring> files;
files[L"upload_file_minidump"] = minidumpPath.wc_str();
if (!commentsFilePath.empty())
{
files[wxFileName(commentsFilePath).GetFullName().wc_str()] = commentsFilePath.wc_str();
}
std::map<std::wstring, std::wstring> 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<std::string, std::string>& arguments, const wxString& commentsFilePath)
{
std::map<std::string, std::string> 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<std::string, std::string> 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<std::string, std::string> 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<uint32_t>::max());
google_breakpad::MinidumpMemoryList::set_max_regions(std::numeric_limits<uint32_t>::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<char> 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<bool(const wxString& comment)>& 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<wxLocale> 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;
});
}
}

View File

@ -0,0 +1,36 @@
/*!********************************************************************
*
Audacity: A Digital Audio Editor
CrashReportApp.h
Vitaly Sverchinsky
**********************************************************************/
#include <wx/wx.h>
#include <map>
#include <string>
//! 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<std::string, std::string> 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);

View File

@ -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 ",
" ",
" "
};

View File

@ -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 <errno.h>
#include <map>
#include <sstream>
#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<std::string, std::string>& 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<CrashReportContext*>(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<CrashReportContext*>(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<std::string, std::string>& 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
}

View File

@ -0,0 +1,43 @@
/*!********************************************************************
*
Audacity: A Digital Audio Editor
CrashReportContext.h
Vitaly Sverchinsky
**********************************************************************/
#pragma once
#include <string>
#include <map>
//!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<std::string, std::string>& 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);
};

View File

@ -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 <locale>
#include <codecvt>
#include <sstream>
#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<std::string, std::string>& 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<CrashReportContext*>(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<std::codecvt_utf8<wchar_t>, 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<std::string, std::string>& 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<std::codecvt_utf8<wchar_t>, wchar_t>().from_bytes(databasePath),
NULL,
UploadReport,
this,
google_breakpad::ExceptionHandler::HANDLER_ALL);
}

View File

@ -0,0 +1,47 @@
/*!********************************************************************
*
Audacity: A Digital Audio Editor
CrashReportContext.h
Vitaly Sverchinsky
**********************************************************************/
#pragma once
#include <string>
#include <map>
//!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<std::string, std::string>& 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);
};

View File

@ -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"),

View File

@ -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' |\

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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}

View File

@ -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();

View File

@ -1073,6 +1073,7 @@ list( APPEND LIBRARIES
libsoxr
portaudio-v19
sqlite
$<$<BOOL:${${_OPT}has_crashreports}>:crashreports>
$<$<BOOL:${USE_FFMPEG}>:ffmpeg>
$<$<BOOL:${USE_LIBID3TAG}>:libid3tag::libid3tag>
$<$<BOOL:${USE_LIBFLAC}>: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}" )