diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 9527b32d0..a805b6857 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -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 != '' }} diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index caee4ff60..c8465f23b 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -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') }} \ No newline at end of file + env: + SENTRY_TOKEN: ${{ secrets.SENTRY_TOKEN }} + if: ${{ env.SENTRY_TOKEN != '' && (matrix.config.name == 'x86_64-w64-mingw32' || matrix.config.name == 'apple-darwin') }} + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 18c8007c3..b4bcc7c8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}/$/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 ) 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() diff --git a/core/cfg/option.cpp b/core/cfg/option.cpp index a825a3ba7..6f046bb12 100644 --- a/core/cfg/option.cpp +++ b/core/cfg/option.cpp @@ -121,6 +121,7 @@ Option OpenGlChecks("OpenGlChecks", false, "validate"); Option, false> ContentPath("Dreamcast.ContentPath"); Option HideLegacyNaomiRoms("Dreamcast.HideLegacyNaomiRoms", true); +Option UploadCrashLogs("UploadCrashLogs", true); // Network diff --git a/core/cfg/option.h b/core/cfg/option.h index af25b3555..33b1b8319 100644 --- a/core/cfg/option.h +++ b/core/cfg/option.h @@ -482,6 +482,7 @@ extern Option OpenGlChecks; extern Option, false> ContentPath; extern Option HideLegacyNaomiRoms; +extern Option UploadCrashLogs; // Network diff --git a/core/linux-dist/main.cpp b/core/linux-dist/main.cpp index b93b9421c..be6c6a195 100644 --- a/core/linux-dist/main.cpp +++ b/core/linux-dist/main.cpp @@ -297,7 +297,9 @@ std::vector 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 diff --git a/core/oslib/oslib.cpp b/core/oslib/oslib.cpp index e20267743..1239ed996 100644 --- a/core/oslib/oslib.cpp +++ b/core/oslib/oslib.cpp @@ -20,6 +20,9 @@ #include "stdclass.h" #include "cfg/cfg.h" #include "cfg/option.h" +#ifndef _WIN32 +#include +#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 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 diff --git a/core/oslib/oslib.h b/core/oslib/oslib.h index 3ffb874a5..1cc0174a8 100644 --- a/core/oslib/oslib.h +++ b/core/oslib/oslib.h @@ -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); diff --git a/core/rend/boxart/http_client.cpp b/core/rend/boxart/http_client.cpp index 128a155f5..2efac6218 100644 --- a/core/rend/boxart/http_client.cpp +++ b/core/rend/boxart/http_client.cpp @@ -22,6 +22,7 @@ #ifdef _WIN32 #ifndef TARGET_UWP +#include "stdclass.h" #include #include @@ -67,6 +68,105 @@ int get(const std::string& url, std::vector& content, std::string& contentTy return 200; } +int post(const std::string& url, const std::vector& 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 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& content, std::string& contentTy return (int)httpCode; } +int post(const std::string& url, const std::vector& 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(); diff --git a/core/rend/boxart/http_client.h b/core/rend/boxart/http_client.h index d2c780fcd..a2ac06e8f 100644 --- a/core/rend/boxart/http_client.h +++ b/core/rend/boxart/http_client.h @@ -35,6 +35,21 @@ static inline int get(const std::string& url, std::vector& 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& fields); + static inline bool success(int status) { return status >= 200 && status < 300; } diff --git a/core/rend/gui.cpp b/core/rend/gui.cpp index f4a7f2862..bbfd11abe 100644 --- a/core/rend/gui.cpp +++ b/core/rend/gui.cpp @@ -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(); diff --git a/core/windows/winmain.cpp b/core/windows/winmain.cpp index 8a5b1a5d2..6738645ca 100644 --- a/core/windows/winmain.cpp +++ b/core/windows/winmain.cpp @@ -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), diff --git a/shell/android-studio/flycast/build.gradle b/shell/android-studio/flycast/build.gradle index e7860dcbc..f3df711d1 100644 --- a/shell/android-studio/flycast/build.gradle +++ b/shell/android-studio/flycast/build.gradle @@ -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: []) } diff --git a/shell/android-studio/flycast/src/main/java/com/reicast/emulator/BaseGLActivity.java b/shell/android-studio/flycast/src/main/java/com/reicast/emulator/BaseGLActivity.java index b8e11c201..320b79b2c 100644 --- a/shell/android-studio/flycast/src/main/java/com/reicast/emulator/BaseGLActivity.java +++ b/shell/android-studio/flycast/src/main/java/com/reicast/emulator/BaseGLActivity.java @@ -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) { diff --git a/shell/android-studio/flycast/src/main/java/com/reicast/emulator/emu/HttpClient.java b/shell/android-studio/flycast/src/main/java/com/reicast/emulator/emu/HttpClient.java index 0da392d70..2b82eb9f4 100644 --- a/shell/android-studio/flycast/src/main/java/com/reicast/emulator/emu/HttpClient.java +++ b/shell/android-studio/flycast/src/main/java/com/reicast/emulator/emu/HttpClient.java @@ -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; } diff --git a/shell/android-studio/flycast/src/main/jni/src/Android.cpp b/shell/android-studio/flycast/src/main/jni/src/Android.cpp index 4f9e0b49c..f0fac0f97 100644 --- a/shell/android-studio/flycast/src/main/jni/src/Android.cpp +++ b/shell/android-studio/flycast/src/main/jni/src/Android.cpp @@ -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) { diff --git a/shell/android-studio/flycast/src/main/jni/src/http_client.h b/shell/android-studio/flycast/src/main/jni/src/http_client.h index 3cedefebe..b1e6c43b9 100644 --- a/shell/android-studio/flycast/src/main/jni/src/http_client.h +++ b/shell/android-studio/flycast/src/main/jni/src/http_client.h @@ -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& 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"); } diff --git a/shell/apple/common/http_client.mm b/shell/apple/common/http_client.mm index 23de92548..e1f411533 100644 --- a/shell/apple/common/http_client.mm +++ b/shell/apple/common/http_client.mm @@ -46,10 +46,71 @@ int get(const std::string& url, std::vector& content, std::string& contentTy return [httpResponse statusCode]; } +int post(const std::string& url, const std::vector& 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() { } -} \ No newline at end of file +} diff --git a/shell/apple/emulator-osx/emulator-osx/SDLMain.mm b/shell/apple/emulator-osx/emulator-osx/SDLMain.mm index ee3122060..6a7240224 100644 --- a/shell/apple/emulator-osx/emulator-osx/SDLMain.mm +++ b/shell/apple/emulator-osx/emulator-osx/SDLMain.mm @@ -8,7 +8,9 @@ #include "SDLMain.h" #include /* for MAXPATHLEN */ #include +#include #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 diff --git a/shell/uwp/http_client.cpp b/shell/uwp/http_client.cpp index 27b18ba23..b04586189 100644 --- a/shell/uwp/http_client.cpp +++ b/shell/uwp/http_client.cpp @@ -89,4 +89,9 @@ int get(const std::string& url, std::vector& content, std::string& contentTy } } +int post(const std::string& url, const std::vector& fields) { + // not implemented + return 500; +} + }