sentry.io integration

This commit is contained in:
Flyinghead 2022-12-20 21:52:46 +01:00
parent b6f73d96ca
commit f3a6fb7d8b
20 changed files with 527 additions and 42 deletions

View File

@ -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 != '' }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: [])
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {
}
}
}

View File

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

View File

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