sentry.io integration
This commit is contained in:
parent
b6f73d96ca
commit
f3a6fb7d8b
|
@ -34,6 +34,8 @@ jobs:
|
|||
- name: Gradle
|
||||
working-directory: shell/android-studio
|
||||
run: ./gradlew assembleRelease --parallel
|
||||
env:
|
||||
SENTRY_UPLOAD_URL: ${{ secrets.SENTRY_UPLOAD_URL }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
@ -71,6 +73,24 @@ jobs:
|
|||
run: aws s3 sync shell/android-studio/flycast/build/outputs/apk/release s3://flycast-builds/android/${GITHUB_REF#refs/}-$GITHUB_SHA --acl public-read --exclude='*.json' --follow-symlinks
|
||||
if: ${{ steps.aws-credentials.outputs.aws-account-id != '' }}
|
||||
|
||||
- name: Upload to S3 (symbols)
|
||||
run: aws s3 sync symbols s3://flycast-symbols/android --follow-symlinks
|
||||
if: ${{ steps.aws-credentials.outputs.aws-account-id != '' }}
|
||||
- name: Setup Sentry CLI
|
||||
uses: mathieu-bour/setup-sentry-cli@1.2.0
|
||||
env:
|
||||
SENTRY_TOKEN: ${{ secrets.SENTRY_TOKEN }}
|
||||
with:
|
||||
url: https://sentry.io
|
||||
token: ${{ env.SENTRY_TOKEN }}
|
||||
organization: flycast
|
||||
project: minidump
|
||||
if: ${{ env.SENTRY_TOKEN != '' }}
|
||||
|
||||
- name: Upload symbols to Sentry
|
||||
run: |
|
||||
VERSION=$(git describe --tags --always)
|
||||
sentry-cli releases new "$VERSION"
|
||||
sentry-cli releases set-commits "$VERSION" --auto
|
||||
sentry-cli upload-dif symbols
|
||||
shell: bash
|
||||
env:
|
||||
SENTRY_TOKEN: ${{ secrets.SENTRY_TOKEN }}
|
||||
if: ${{ env.SENTRY_TOKEN != '' }}
|
||||
|
|
|
@ -84,8 +84,10 @@ jobs:
|
|||
|
||||
- name: CMake
|
||||
run: |
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.config.buildType }} -DCMAKE_INSTALL_PREFIX=artifact ${{ matrix.config.cmakeArgs }}
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.config.buildType }} -DCMAKE_INSTALL_PREFIX=artifact ${{ matrix.config.cmakeArgs }} -DSENTRY_UPLOAD_URL=${{ env.SENTRY_UPLOAD_URL }}
|
||||
cmake --build build --config ${{ matrix.config.buildType }} --target install
|
||||
env:
|
||||
SENTRY_UPLOAD_URL: ${{ secrets.SENTRY_UPLOAD_URL }}
|
||||
|
||||
- name: Unit Tests
|
||||
run: |
|
||||
|
@ -96,10 +98,8 @@ jobs:
|
|||
|
||||
- name: Dump symbols
|
||||
run: |
|
||||
core/deps/breakpad/bin/dump_syms artifact/bin/flycast.exe > flycast.exe.sym 2>/dev/null
|
||||
BUILD_ID=`head -1 flycast.exe.sym | awk '{ print $4 }'`
|
||||
mkdir -p symbols/flycast.exe/$BUILD_ID
|
||||
mv flycast.exe.sym symbols/flycast.exe/$BUILD_ID
|
||||
mkdir -p symbols
|
||||
core/deps/breakpad/bin/dump_syms artifact/bin/flycast.exe > symbols/flycast.sym 2>/dev/null
|
||||
strip artifact/bin/flycast.exe
|
||||
if: matrix.config.name == 'x86_64-w64-mingw32'
|
||||
|
||||
|
@ -134,7 +134,25 @@ jobs:
|
|||
shell: bash
|
||||
if: ${{ steps.aws-credentials.outputs.aws-account-id != '' }}
|
||||
|
||||
- name: Upload symbols to S3 (Windows-MinGW, macOS)
|
||||
run: aws s3 sync symbols s3://flycast-symbols/${{ matrix.config.destDir }} --follow-symlinks
|
||||
- name: Setup Sentry CLI
|
||||
uses: mathieu-bour/setup-sentry-cli@1.2.0
|
||||
env:
|
||||
SENTRY_TOKEN: ${{ secrets.SENTRY_TOKEN }}
|
||||
with:
|
||||
url: https://sentry.io
|
||||
token: ${{ env.SENTRY_TOKEN }}
|
||||
organization: flycast
|
||||
project: minidump
|
||||
if: ${{ env.SENTRY_TOKEN != '' }}
|
||||
|
||||
- name: Upload symbols to Sentry (Windows, macOS)
|
||||
run: |
|
||||
VERSION=$(git describe --tags --always)
|
||||
sentry-cli releases new "$VERSION"
|
||||
sentry-cli releases set-commits "$VERSION" --auto
|
||||
sentry-cli upload-dif symbols
|
||||
shell: bash
|
||||
if: ${{ steps.aws-credentials.outputs.aws-account-id != '' && (matrix.config.name == 'x86_64-w64-mingw32' || matrix.config.name == 'apple-darwin') }}
|
||||
env:
|
||||
SENTRY_TOKEN: ${{ secrets.SENTRY_TOKEN }}
|
||||
if: ${{ env.SENTRY_TOKEN != '' && (matrix.config.name == 'x86_64-w64-mingw32' || matrix.config.name == 'apple-darwin') }}
|
||||
|
|
@ -23,6 +23,7 @@ option(USE_OPENGL "Use OpenGL API" ON)
|
|||
option(USE_VIDEOCORE "RPI: use the legacy Broadcom GLES libraries" OFF)
|
||||
option(APPLE_BREAKPAD "macOS: Build breakpad client and dump symbols" OFF)
|
||||
option(ENABLE_GDB_SERVER "Build with GDB debugging support" OFF)
|
||||
option(SENTRY_UPLOAD_URL "Sentry upload URL" "")
|
||||
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/shell/cmake")
|
||||
|
||||
|
@ -194,6 +195,10 @@ if(IOS)
|
|||
GLES_SILENCE_DEPRECATION)
|
||||
endif()
|
||||
|
||||
if(NOT ${SENTRY_UPLOAD_URL} STREQUAL "")
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE SENTRY_UPLOAD=${SENTRY_UPLOAD_URL})
|
||||
endif()
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE core core/deps core/deps/stb core/khronos core/deps/json)
|
||||
if(LIBRETRO)
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE shell/libretro)
|
||||
|
@ -258,36 +263,30 @@ if(NOT LIBRETRO)
|
|||
if(${CMAKE_GENERATOR} MATCHES "^Xcode.*")
|
||||
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
||||
COMMAND sleep 20
|
||||
COMMAND mkdir -p ../symbols
|
||||
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/build/breakpad/dump_syms
|
||||
-a x86_64
|
||||
-g ${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/Flycast.app.dSYM
|
||||
${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/Flycast.app/Contents/MacOS/Flycast > Flycast.sym
|
||||
COMMAND mkdir -p ../symbols/Flycast/`head -1 Flycast.sym | awk '{ print $4 }'`
|
||||
COMMAND mv Flycast.sym ../symbols/Flycast/`head -1 Flycast.sym | awk '{ print $4 }'`
|
||||
${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/Flycast.app/Contents/MacOS/Flycast > ../symbols/Flycast-x64.sym
|
||||
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/build/breakpad/dump_syms
|
||||
-a arm64
|
||||
-g ${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/Flycast.app.dSYM
|
||||
${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/Flycast.app/Contents/MacOS/Flycast > Flycast.sym
|
||||
COMMAND mkdir -p ../symbols/Flycast/`head -1 Flycast.sym | awk '{ print $4 }'`
|
||||
COMMAND mv Flycast.sym ../symbols/Flycast/`head -1 Flycast.sym | awk '{ print $4 }'`
|
||||
${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/Flycast.app/Contents/MacOS/Flycast > ../symbols/Flycast-arm64.sym
|
||||
)
|
||||
else()
|
||||
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
||||
COMMAND sleep 20
|
||||
COMMAND mkdir -p ../symbols
|
||||
COMMAND dsymutil ${CMAKE_CURRENT_BINARY_DIR}/Flycast.app/Contents/MacOS/Flycast
|
||||
-o ${CMAKE_CURRENT_BINARY_DIR}/Flycast.app.dSYM
|
||||
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/build/breakpad/dump_syms
|
||||
-a x86_64
|
||||
-g ${CMAKE_CURRENT_BINARY_DIR}/Flycast.app.dSYM
|
||||
${CMAKE_CURRENT_BINARY_DIR}/Flycast.app/Contents/MacOS/Flycast > Flycast.sym
|
||||
COMMAND mkdir -p ../symbols/Flycast/`head -1 Flycast.sym | awk '{ print $4 }'`
|
||||
COMMAND mv Flycast.sym ../symbols/Flycast/`head -1 Flycast.sym | awk '{ print $4 }'`
|
||||
${CMAKE_CURRENT_BINARY_DIR}/Flycast.app/Contents/MacOS/Flycast > ../symbols/Flycast-x64.sym
|
||||
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/build/breakpad/dump_syms
|
||||
-a arm64
|
||||
-g ${CMAKE_CURRENT_BINARY_DIR}/Flycast.app.dSYM
|
||||
${CMAKE_CURRENT_BINARY_DIR}/Flycast.app/Contents/MacOS/Flycast > Flycast.sym
|
||||
COMMAND mkdir -p ../symbols/Flycast/`head -1 Flycast.sym | awk '{ print $4 }'`
|
||||
COMMAND mv Flycast.sym ../symbols/Flycast/`head -1 Flycast.sym | awk '{ print $4 }'`
|
||||
${CMAKE_CURRENT_BINARY_DIR}/Flycast.app/Contents/MacOS/Flycast > ../symbols/Flycast-arm64.sym
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
@ -121,6 +121,7 @@ Option<bool> OpenGlChecks("OpenGlChecks", false, "validate");
|
|||
|
||||
Option<std::vector<std::string>, false> ContentPath("Dreamcast.ContentPath");
|
||||
Option<bool, false> HideLegacyNaomiRoms("Dreamcast.HideLegacyNaomiRoms", true);
|
||||
Option<bool> UploadCrashLogs("UploadCrashLogs", true);
|
||||
|
||||
// Network
|
||||
|
||||
|
|
|
@ -482,6 +482,7 @@ extern Option<bool> OpenGlChecks;
|
|||
|
||||
extern Option<std::vector<std::string>, false> ContentPath;
|
||||
extern Option<bool, false> HideLegacyNaomiRoms;
|
||||
extern Option<bool> UploadCrashLogs;
|
||||
|
||||
// Network
|
||||
|
||||
|
|
|
@ -297,7 +297,9 @@ std::vector<std::string> find_system_data_dirs()
|
|||
#if defined(USE_BREAKPAD)
|
||||
static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* context, bool succeeded)
|
||||
{
|
||||
printf("Minidump saved to '%s'\n", descriptor.path());
|
||||
if (succeeded)
|
||||
registerCrash(descriptor.directory(), descriptor.path());
|
||||
|
||||
return succeeded;
|
||||
}
|
||||
#endif
|
||||
|
@ -310,6 +312,8 @@ int main(int argc, char* argv[])
|
|||
//appletSetFocusHandlingMode(AppletFocusHandlingMode_NoSuspend);
|
||||
#endif
|
||||
#if defined(USE_BREAKPAD)
|
||||
auto async = std::async(std::launch::async, uploadCrashes, "/tmp");
|
||||
|
||||
google_breakpad::MinidumpDescriptor descriptor("/tmp");
|
||||
google_breakpad::ExceptionHandler eh(descriptor, nullptr, dumpCallback, nullptr, true, -1);
|
||||
#endif
|
||||
|
|
|
@ -20,6 +20,9 @@
|
|||
#include "stdclass.h"
|
||||
#include "cfg/cfg.h"
|
||||
#include "cfg/option.h"
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace hostfs
|
||||
{
|
||||
|
@ -145,3 +148,71 @@ std::string getBiosFontPath()
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
#ifdef USE_BREAKPAD
|
||||
|
||||
#include "rend/boxart/http_client.h"
|
||||
#include "version.h"
|
||||
|
||||
#define FLYCAST_CRASH_LIST "flycast-crashes.txt"
|
||||
|
||||
void registerCrash(const std::string& directory, const char *path)
|
||||
{
|
||||
FILE *f = nowide::fopen((directory + "/" FLYCAST_CRASH_LIST).c_str(), "at");
|
||||
if (f != nullptr)
|
||||
{
|
||||
fprintf(f, "%s", path);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
void uploadCrashes(const std::string& directory)
|
||||
{
|
||||
FILE *f = nowide::fopen((directory + "/" FLYCAST_CRASH_LIST).c_str(), "rt");
|
||||
if (f == nullptr)
|
||||
return;
|
||||
http::init();
|
||||
char line[256];
|
||||
bool uploadFailure = false;
|
||||
while (fgets(line, sizeof(line), f) != nullptr)
|
||||
{
|
||||
char *p = line + strlen(line) - 1;
|
||||
if (*p == '\n')
|
||||
*p = '\0';
|
||||
if (file_exists(line))
|
||||
{
|
||||
#ifdef SENTRY_UPLOAD
|
||||
#define STRINGIZE(x) #x
|
||||
if (config::UploadCrashLogs)
|
||||
{
|
||||
NOTICE_LOG(COMMON, "Uploading minidump %s", line);
|
||||
std::vector<http::PostField> fields;
|
||||
fields.emplace_back("upload_file_minidump", std::string(line), "application/octet-stream");
|
||||
fields.emplace_back("flycast_version", std::string(GIT_VERSION));
|
||||
// TODO log, config, gpu/driver
|
||||
int rc = http::post(STRINGIZE(SENTRY_UPLOAD), fields);
|
||||
if (rc >= 200 && rc < 300)
|
||||
nowide::remove(line);
|
||||
else
|
||||
uploadFailure = true;
|
||||
}
|
||||
else
|
||||
#undef STRINGIZE
|
||||
#endif
|
||||
{
|
||||
nowide::remove(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
http::term();
|
||||
fclose(f);
|
||||
if (!uploadFailure)
|
||||
nowide::remove((directory + "/" FLYCAST_CRASH_LIST).c_str());
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void registerCrash(const std::string& directory, const char *path) {}
|
||||
void uploadCrashes(const std::string& directory) {}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -29,6 +29,17 @@ u32 static inline bitscanrev(u32 v)
|
|||
#endif
|
||||
}
|
||||
|
||||
u32 static inline bitscanrev64(u64 v)
|
||||
{
|
||||
#ifdef __GNUC__
|
||||
return 63 - __builtin_clzll(v);
|
||||
#else
|
||||
unsigned long rv;
|
||||
_BitScanReverse64(&rv, (__int64)v);
|
||||
return rv;
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace hostfs
|
||||
{
|
||||
std::string getVmuPath(const std::string& port);
|
||||
|
@ -135,3 +146,5 @@ static inline void freeAligned(void *p)
|
|||
#endif
|
||||
}
|
||||
|
||||
void registerCrash(const std::string& directory, const char *path);
|
||||
void uploadCrashes(const std::string& directory);
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
#ifdef _WIN32
|
||||
#ifndef TARGET_UWP
|
||||
#include "stdclass.h"
|
||||
#include <windows.h>
|
||||
#include <wininet.h>
|
||||
|
||||
|
@ -67,6 +68,105 @@ int get(const std::string& url, std::vector<u8>& content, std::string& contentTy
|
|||
return 200;
|
||||
}
|
||||
|
||||
int post(const std::string& url, const std::vector<PostField>& fields)
|
||||
{
|
||||
static const std::string boundary("----flycast-boundary-8304529454");
|
||||
|
||||
std::string content;
|
||||
for (const PostField& field : fields)
|
||||
{
|
||||
content += "--" + boundary + "\r\n";
|
||||
content += "Content-Disposition: form-data; name=\"" + field.name + '"';
|
||||
if (!field.contentType.empty())
|
||||
{
|
||||
size_t pos = get_last_slash_pos(field.value);
|
||||
std::string filename;
|
||||
if (pos == std::string::npos)
|
||||
filename = field.value;
|
||||
else
|
||||
filename = field.value.substr(pos + 1);
|
||||
content += "; filename=\"" + filename + '"';
|
||||
}
|
||||
content += "\r\n";
|
||||
if (!field.contentType.empty())
|
||||
content += "Content-Type: " + field.contentType + "\r\n";
|
||||
content += "\r\n";
|
||||
|
||||
if (field.contentType.empty())
|
||||
{
|
||||
content += field.value;
|
||||
}
|
||||
else
|
||||
{
|
||||
FILE *f = nowide::fopen(field.value.c_str(), "rb");
|
||||
if (f == nullptr) {
|
||||
WARN_LOG(NETWORK, "Can't open mime file %s", field.value.c_str());
|
||||
return 500;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
size_t size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
std::vector<char> data;
|
||||
data.resize(size);
|
||||
size_t read = fread(data.data(), 1, size, f);
|
||||
if (read != size)
|
||||
{
|
||||
fclose(f);
|
||||
WARN_LOG(NETWORK, "Truncated read on mime file %s: %d -> %d", field.value.c_str(), (int)size, (int)read);
|
||||
return 500;
|
||||
}
|
||||
fclose(f);
|
||||
content += std::string(&data[0], size);
|
||||
}
|
||||
content += "\r\n";
|
||||
}
|
||||
content += "--" + boundary + "--\r\n";
|
||||
|
||||
char scheme[16], host[256], path[256];
|
||||
URL_COMPONENTS components{};
|
||||
components.dwStructSize = sizeof(components);
|
||||
components.lpszScheme = scheme;
|
||||
components.dwSchemeLength = sizeof(scheme) / sizeof(scheme[0]);
|
||||
components.lpszHostName = host;
|
||||
components.dwHostNameLength = sizeof(host) / sizeof(host[0]);
|
||||
components.lpszUrlPath = path;
|
||||
components.dwUrlPathLength = sizeof(path) / sizeof(path[0]);
|
||||
|
||||
if (!InternetCrackUrlA(url.c_str(), url.length(), 0, &components))
|
||||
return 500;
|
||||
|
||||
bool https = !strcmp(scheme, "https");
|
||||
|
||||
int rc = 500;
|
||||
HINTERNET ic = InternetConnect(hInet, host, components.nPort, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
|
||||
if (ic == NULL)
|
||||
return rc;
|
||||
|
||||
HINTERNET hreq = HttpOpenRequest(ic, "POST", path, NULL, NULL, NULL, https ? INTERNET_FLAG_SECURE : 0, 0);
|
||||
if (hreq == NULL) {
|
||||
InternetCloseHandle(ic);
|
||||
return rc;
|
||||
}
|
||||
std::string header("Content-Type: multipart/form-data; boundary=" + boundary);
|
||||
if (!HttpSendRequest(hreq, header.c_str(), -1, &content[0], content.length()))
|
||||
WARN_LOG(NETWORK, "HttpSendRequest Error %d", GetLastError());
|
||||
else
|
||||
{
|
||||
DWORD status;
|
||||
DWORD size = sizeof(status);
|
||||
DWORD index = 0;
|
||||
if (!HttpQueryInfo(hreq, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &status, &size, &index))
|
||||
WARN_LOG(NETWORK, "HttpQueryInfo Error %d", GetLastError());
|
||||
else
|
||||
rc = status;
|
||||
}
|
||||
|
||||
InternetCloseHandle(hreq);
|
||||
InternetCloseHandle(ic);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
void term()
|
||||
{
|
||||
if (hInet != NULL)
|
||||
|
@ -127,6 +227,45 @@ int get(const std::string& url, std::vector<u8>& content, std::string& contentTy
|
|||
return (int)httpCode;
|
||||
}
|
||||
|
||||
int post(const std::string& url, const std::vector<PostField>& fields)
|
||||
{
|
||||
CURL *curl = curl_easy_init();
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Flycast/1.0");
|
||||
curl_easy_setopt(curl, CURLOPT_AUTOREFERER, 1);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
|
||||
curl_mime *mime = curl_mime_init(curl);
|
||||
for (const auto& field : fields)
|
||||
{
|
||||
curl_mimepart *part = curl_mime_addpart(mime);
|
||||
curl_mime_name(part, field.name.c_str());
|
||||
if (field.contentType.empty()) {
|
||||
curl_mime_data(part, field.value.c_str(), CURL_ZERO_TERMINATED);
|
||||
}
|
||||
else {
|
||||
curl_mime_filedata(part, field.value.c_str());
|
||||
curl_mime_type(part, field.contentType.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
long httpCode = 500;
|
||||
if (res == CURLE_OK)
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
return (int)httpCode;
|
||||
|
||||
}
|
||||
|
||||
void term()
|
||||
{
|
||||
curl_global_cleanup();
|
||||
|
|
|
@ -35,6 +35,21 @@ static inline int get(const std::string& url, std::vector<u8>& content) {
|
|||
return get(url, content, contentType);
|
||||
}
|
||||
|
||||
struct PostField
|
||||
{
|
||||
PostField() = default;
|
||||
PostField(const std::string& name, const std::string& value)
|
||||
: name(name), value(value) { }
|
||||
PostField(const std::string& name, const std::string& filePath, const std::string& contentType)
|
||||
: name(name), value(filePath), contentType(contentType) { }
|
||||
|
||||
std::string name;
|
||||
std::string value; // contains file path if contentType isn't empty
|
||||
std::string contentType;
|
||||
};
|
||||
|
||||
int post(const std::string& url, const std::vector<PostField>& fields);
|
||||
|
||||
static inline bool success(int status) {
|
||||
return status >= 200 && status < 300;
|
||||
}
|
||||
|
|
|
@ -2191,6 +2191,10 @@ static void gui_display_settings()
|
|||
}
|
||||
ImGui::SameLine();
|
||||
ShowHelpMarker("Log debug information to flycast.log");
|
||||
#ifdef SENTRY_UPLOAD
|
||||
OptionCheckbox("Automatically Report Crashes", config::UploadCrashLogs,
|
||||
"Automatically upload crash reports to sentry.io to help in troubleshooting. No personal information is included.");
|
||||
#endif
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::EndTabItem();
|
||||
|
|
|
@ -731,6 +731,17 @@ static bool dumpCallback(const wchar_t* dump_path,
|
|||
wchar_t s[MAX_PATH + 32];
|
||||
_snwprintf(s, ARRAY_SIZE(s), L"Minidump saved to '%s\\%s.dmp'", dump_path, minidump_id);
|
||||
::OutputDebugStringW(s);
|
||||
|
||||
nowide::stackstring path;
|
||||
if (path.convert(dump_path))
|
||||
{
|
||||
std::string directory = path.c_str();
|
||||
if (path.convert(minidump_id))
|
||||
{
|
||||
std::string fullPath = directory + '\\' + std::string(path.c_str()) + ".dmp";
|
||||
registerCrash(directory, fullPath.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
|
@ -859,6 +870,12 @@ int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi
|
|||
wchar_t tempDir[MAX_PATH + 1];
|
||||
GetTempPathW(MAX_PATH + 1, tempDir);
|
||||
|
||||
nowide::stackstring nws;
|
||||
static std::string tempDir8;
|
||||
if (nws.convert(tempDir))
|
||||
tempDir8 = nws.c_str();
|
||||
auto async = std::async(std::launch::async, uploadCrashes, tempDir8);
|
||||
|
||||
static google_breakpad::CustomInfoEntry custom_entries[] = {
|
||||
google_breakpad::CustomInfoEntry(L"prod", L"Flycast"),
|
||||
google_breakpad::CustomInfoEntry(L"ver", L"" GIT_VERSION),
|
||||
|
|
|
@ -11,6 +11,11 @@ def getVersionName = { ->
|
|||
return stdout.toString().trim()
|
||||
}
|
||||
|
||||
def getSentryUrl = { ->
|
||||
def url = System.env.SENTRY_UPLOAD_URL
|
||||
return url == null ? "" : url
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 29
|
||||
|
||||
|
@ -24,7 +29,7 @@ android {
|
|||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DANDROID_ARM_MODE=arm"
|
||||
arguments "-DANDROID_ARM_MODE=arm", "-DSENTRY_UPLOAD_URL=" + getSentryUrl()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,11 +65,16 @@ android {
|
|||
buildFeatures {
|
||||
prefab true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/DEPENDENCIES'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'com.google.oboe:oboe:1.6.1'
|
||||
implementation 'org.apache.commons:commons-lang3:3.12.0'
|
||||
implementation 'org.apache.httpcomponents.client5:httpclient5:5.0.3'
|
||||
implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
|
||||
}
|
||||
|
|
|
@ -91,6 +91,7 @@ public abstract class BaseGLActivity extends Activity implements ActivityCompat.
|
|||
Emulator.setCurrentActivity(this);
|
||||
|
||||
OuyaController.init(this);
|
||||
new HttpClient().nativeInit();
|
||||
|
||||
String home_directory = prefs.getString(Config.pref_home, "");
|
||||
String result = JNIdc.initEnvironment((Emulator)getApplicationContext(), getFilesDir().getAbsolutePath(), home_directory,
|
||||
|
@ -115,7 +116,6 @@ public abstract class BaseGLActivity extends Activity implements ActivityCompat.
|
|||
}
|
||||
Log.i("flycast", "Environment initialized");
|
||||
installButtons();
|
||||
new HttpClient().nativeInit();
|
||||
setStorageDirectories();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !storagePermissionGranted) {
|
||||
|
|
|
@ -20,7 +20,16 @@ package com.reicast.emulator.emu;
|
|||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.hc.client5.http.classic.methods.HttpPost;
|
||||
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||
import org.apache.hc.core5.http.ContentType;
|
||||
import org.apache.hc.core5.http.HttpEntity;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
|
@ -28,6 +37,8 @@ import java.net.MalformedURLException;
|
|||
import java.net.URL;
|
||||
|
||||
public class HttpClient {
|
||||
private CloseableHttpClient httpClient;
|
||||
|
||||
// Called from native code
|
||||
public int openUrl(String url_string, byte[][] content, String[] contentType)
|
||||
{
|
||||
|
@ -59,6 +70,40 @@ public class HttpClient {
|
|||
Log.e("flycast", "I/O error", e);
|
||||
} catch (SecurityException e) {
|
||||
Log.e("flycast", "Security error", e);
|
||||
} catch (Throwable t) {
|
||||
Log.e("flycast", "Unknown error", t);
|
||||
}
|
||||
return 500;
|
||||
}
|
||||
|
||||
public int post(String urlString, String[] fieldNames, String[] fieldValues, String[] contentTypes)
|
||||
{
|
||||
try {
|
||||
if (httpClient == null)
|
||||
httpClient = HttpClients.createDefault();
|
||||
HttpPost httpPost = new HttpPost(urlString);
|
||||
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
|
||||
for (int i = 0; i < fieldNames.length; i++) {
|
||||
if (contentTypes[i].isEmpty())
|
||||
builder.addTextBody(fieldNames[i], fieldValues[i], ContentType.TEXT_PLAIN);
|
||||
else {
|
||||
File file = new File(fieldValues[i]);
|
||||
builder.addBinaryBody(fieldNames[i], file, ContentType.create(contentTypes[i]), file.getName());
|
||||
}
|
||||
}
|
||||
HttpEntity multipart = builder.build();
|
||||
httpPost.setEntity(multipart);
|
||||
CloseableHttpResponse response = httpClient.execute(httpPost);
|
||||
|
||||
return response.getCode();
|
||||
} catch (MalformedURLException e) {
|
||||
Log.e("flycast", "Malformed URL", e);
|
||||
} catch (IOException e) {
|
||||
Log.e("flycast", "I/O error", e);
|
||||
} catch (SecurityException e) {
|
||||
Log.e("flycast", "Security error", e);
|
||||
} catch (Throwable t) {
|
||||
Log.e("flycast", "Unknown error", t);
|
||||
}
|
||||
return 500;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include "rend/mainui.h"
|
||||
#include "cfg/option.h"
|
||||
#include "stdclass.h"
|
||||
#include "oslib/oslib.h"
|
||||
#ifdef USE_BREAKPAD
|
||||
#include "client/linux/handler/exception_handler.h"
|
||||
#endif
|
||||
|
@ -145,29 +146,26 @@ void os_SetWindowText(char const *Text)
|
|||
static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* context, bool succeeded)
|
||||
{
|
||||
if (succeeded)
|
||||
{
|
||||
__android_log_print(ANDROID_LOG_ERROR, "Flycast", "Minidump saved to '%s'\n", descriptor.path());
|
||||
registerCrash(descriptor.directory(), descriptor.path());
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
static void *uploadCrashThread(void *p)
|
||||
{
|
||||
uploadCrashes(*(std::string *)p);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static google_breakpad::ExceptionHandler *exceptionHandler;
|
||||
|
||||
#endif
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL Java_com_reicast_emulator_emu_JNIdc_initEnvironment(JNIEnv *env, jobject obj, jobject emulator, jstring filesDirectory, jstring homeDirectory, jstring locale)
|
||||
{
|
||||
#if defined(USE_BREAKPAD)
|
||||
if (exceptionHandler == nullptr)
|
||||
{
|
||||
jstring directory = homeDirectory != NULL && env->GetStringLength(homeDirectory) > 0 ? homeDirectory : filesDirectory;
|
||||
const char *jchar = env->GetStringUTFChars(directory, 0);
|
||||
std::string path(jchar);
|
||||
env->ReleaseStringUTFChars(directory, jchar);
|
||||
google_breakpad::MinidumpDescriptor descriptor(path);
|
||||
exceptionHandler = new google_breakpad::ExceptionHandler(descriptor, nullptr, dumpCallback, nullptr, true, -1);
|
||||
}
|
||||
#endif
|
||||
// Initialize platform-specific stuff
|
||||
common_linux_setup();
|
||||
|
||||
bool first_init = false;
|
||||
|
||||
// Keep reference to global JVM and Emulator objects
|
||||
|
@ -180,6 +178,27 @@ extern "C" JNIEXPORT jstring JNICALL Java_com_reicast_emulator_emu_JNIdc_initEnv
|
|||
g_emulator = env->NewGlobalRef(emulator);
|
||||
saveAndroidSettingsMid = env->GetMethodID(env->GetObjectClass(emulator), "SaveAndroidSettings", "(Ljava/lang/String;)V");
|
||||
}
|
||||
|
||||
#if defined(USE_BREAKPAD)
|
||||
if (exceptionHandler == nullptr)
|
||||
{
|
||||
jstring directory = homeDirectory != NULL && env->GetStringLength(homeDirectory) > 0 ? homeDirectory : filesDirectory;
|
||||
const char *jchar = env->GetStringUTFChars(directory, 0);
|
||||
std::string path(jchar);
|
||||
env->ReleaseStringUTFChars(directory, jchar);
|
||||
|
||||
static std::string crashPath;
|
||||
crashPath = path;
|
||||
cThread uploadThread(uploadCrashThread, &crashPath);
|
||||
uploadThread.Start();
|
||||
|
||||
google_breakpad::MinidumpDescriptor descriptor(path);
|
||||
exceptionHandler = new google_breakpad::ExceptionHandler(descriptor, nullptr, dumpCallback, nullptr, true, -1);
|
||||
}
|
||||
#endif
|
||||
// Initialize platform-specific stuff
|
||||
common_linux_setup();
|
||||
|
||||
// Set home directory based on User config
|
||||
if (homeDirectory != NULL)
|
||||
{
|
||||
|
|
|
@ -22,6 +22,7 @@ namespace http {
|
|||
|
||||
static jobject HttpClient;
|
||||
static jmethodID openUrlMid;
|
||||
static jmethodID postMid;
|
||||
|
||||
void init() {
|
||||
}
|
||||
|
@ -58,6 +59,37 @@ namespace http {
|
|||
return httpStatus;
|
||||
}
|
||||
|
||||
int post(const std::string &url, const std::vector<PostField>& fields)
|
||||
{
|
||||
JNIEnv *env = jvm_attacher.getEnv();
|
||||
jstring jurl = env->NewStringUTF(url.c_str());
|
||||
jclass stringClass = env->FindClass("java/lang/String");
|
||||
jobjectArray names = env->NewObjectArray(fields.size(), stringClass, NULL);
|
||||
jobjectArray values = env->NewObjectArray(fields.size(), stringClass, NULL);
|
||||
jobjectArray contentTypes = env->NewObjectArray(fields.size(), stringClass, NULL);
|
||||
|
||||
for (size_t i = 0; i < fields.size(); i++)
|
||||
{
|
||||
jstring js = env->NewStringUTF(fields[i].name.c_str());
|
||||
env->SetObjectArrayElement(names, i, js);
|
||||
env->DeleteLocalRef(js);
|
||||
js = env->NewStringUTF(fields[i].value.c_str());
|
||||
env->SetObjectArrayElement(values, i, js);
|
||||
env->DeleteLocalRef(js);
|
||||
js = env->NewStringUTF(fields[i].contentType.c_str());
|
||||
env->SetObjectArrayElement(contentTypes, i, js);
|
||||
env->DeleteLocalRef(js);
|
||||
}
|
||||
|
||||
int httpStatus = env->CallIntMethod(HttpClient, postMid, jurl, names, values, contentTypes);
|
||||
env->DeleteLocalRef(jurl);
|
||||
env->DeleteLocalRef(names);
|
||||
env->DeleteLocalRef(values);
|
||||
env->DeleteLocalRef(contentTypes);
|
||||
|
||||
return httpStatus;
|
||||
}
|
||||
|
||||
void term() {
|
||||
}
|
||||
|
||||
|
@ -67,4 +99,5 @@ extern "C" JNIEXPORT void JNICALL Java_com_reicast_emulator_emu_HttpClient_nativ
|
|||
{
|
||||
http::HttpClient = env->NewGlobalRef(obj);
|
||||
http::openUrlMid = env->GetMethodID(env->GetObjectClass(obj), "openUrl", "(Ljava/lang/String;[[B[Ljava/lang/String;)I");
|
||||
http::postMid = env->GetMethodID(env->GetObjectClass(obj), "post", "(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)I");
|
||||
}
|
||||
|
|
|
@ -46,10 +46,71 @@ int get(const std::string& url, std::vector<u8>& content, std::string& contentTy
|
|||
return [httpResponse statusCode];
|
||||
}
|
||||
|
||||
int post(const std::string& url, const std::vector<PostField>& fields)
|
||||
{
|
||||
NSString *nsurl = [NSString stringWithCString:url.c_str()
|
||||
encoding:[NSString defaultCStringEncoding]];
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:nsurl]];
|
||||
[request setHTTPMethod:@"POST"];
|
||||
[request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
|
||||
[request setHTTPShouldHandleCookies:NO];
|
||||
|
||||
NSString *boundary = @"----flycast-boundary-7192397596";
|
||||
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
|
||||
[request setValue:contentType forHTTPHeaderField: @"Content-Type"];
|
||||
|
||||
NSMutableData *body = [NSMutableData data];
|
||||
for (const PostField& field : fields)
|
||||
{
|
||||
NSString *value = [NSString stringWithCString:field.value.c_str()
|
||||
encoding:[NSString defaultCStringEncoding]];
|
||||
[body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
[body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"", [NSString stringWithCString:field.name.c_str()
|
||||
encoding:[NSString defaultCStringEncoding]]] dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
if (!field.contentType.empty())
|
||||
{
|
||||
[body appendData:[[NSString stringWithFormat:@"; filename=\"%@\"\r\n", value] dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
[body appendData:[[NSString stringWithFormat:@"Content-Type: %@", [NSString stringWithCString:field.contentType.c_str()
|
||||
encoding:[NSString defaultCStringEncoding]]] dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
}
|
||||
[body appendData:[@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
|
||||
if (field.contentType.empty())
|
||||
{
|
||||
[body appendData:[value dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
}
|
||||
else
|
||||
{
|
||||
NSError* error = nil;
|
||||
NSData *filedata = [NSData dataWithContentsOfFile:value options:0 error:&error];
|
||||
if (error != nil)
|
||||
return 500;
|
||||
[body appendData:filedata];
|
||||
}
|
||||
[body appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
}
|
||||
[body appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
|
||||
[request setHTTPBody:body];
|
||||
NSString *postLength = [NSString stringWithFormat:@"%ld", (unsigned long)[body length]];
|
||||
[request setValue:postLength forHTTPHeaderField:@"Content-Length"];
|
||||
|
||||
NSURLResponse *response = nil;
|
||||
NSError *error = nil;
|
||||
[NSURLConnection sendSynchronousRequest:request
|
||||
returningResponse:&response
|
||||
error:&error];
|
||||
if (error != nil)
|
||||
return 500;
|
||||
|
||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
||||
return [httpResponse statusCode];
|
||||
}
|
||||
|
||||
void init() {
|
||||
}
|
||||
|
||||
void term() {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
#include "SDLMain.h"
|
||||
#include <sys/param.h> /* for MAXPATHLEN */
|
||||
#include <unistd.h>
|
||||
#include <future>
|
||||
#include "rend/gui.h"
|
||||
#include "oslib/oslib.h"
|
||||
|
||||
#ifdef USE_BREAKPAD
|
||||
#include "client/mac/handler/exception_handler.h"
|
||||
|
@ -245,7 +247,13 @@ static void CustomApplicationMain (int argc, char **argv)
|
|||
#ifdef USE_BREAKPAD
|
||||
static bool dumpCallback(const char *dump_dir, const char *minidump_id, void *context, bool succeeded)
|
||||
{
|
||||
printf("Minidump saved to '%s/%s.dmp'\n", dump_dir, minidump_id);
|
||||
if (succeeded)
|
||||
{
|
||||
char path[512];
|
||||
sprintf(path, "%s/%s.dmp", dump_dir, minidump_id);
|
||||
printf("Minidump saved to '%s'\n", path);
|
||||
registerCrash(dump_dir, path);
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
#endif
|
||||
|
@ -302,6 +310,8 @@ static bool dumpCallback(const char *dump_dir, const char *minidump_id, void *co
|
|||
- (void) applicationDidFinishLaunching: (NSNotification *) note
|
||||
{
|
||||
#ifdef USE_BREAKPAD
|
||||
auto async = std::async(std::launch::async, uploadCrashes, "/tmp");
|
||||
|
||||
google_breakpad::ExceptionHandler eh("/tmp", NULL, dumpCallback, NULL, true, NULL);
|
||||
task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS, MACH_PORT_NULL, EXCEPTION_DEFAULT, 0);
|
||||
#endif
|
||||
|
|
|
@ -89,4 +89,9 @@ int get(const std::string& url, std::vector<u8>& content, std::string& contentTy
|
|||
}
|
||||
}
|
||||
|
||||
int post(const std::string& url, const std::vector<PostField>& fields) {
|
||||
// not implemented
|
||||
return 500;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue