There's no reliable way to test these changes end-to-end because we'd need to check the outgoing network traffic from Glean, but that relies on Glean's scheduling of sending the pings. Differential Revision: https://phabricator.services.mozilla.com/D290267
406 lines
14 KiB
C++
406 lines
14 KiB
C++
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
#include <android/log.h>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <jni.h>
|
|
#include <optional>
|
|
|
|
#define LOG(level, message, ...) \
|
|
__android_log_print(ANDROID_LOG_##level, "NativeCrashTools", message, \
|
|
__VA_ARGS__)
|
|
|
|
namespace {
|
|
|
|
// The base of a utf16-encoded string, used for Java string interop.
|
|
struct Utf16String {
|
|
const uint16_t* chars;
|
|
size_t len;
|
|
};
|
|
|
|
// The upload function signature used in crashtools_crashping_init.
|
|
// This corresponds to the `UploadFn` type in `export/lib.rs`.
|
|
typedef int32_t (*crashtools_upload_fn)(const Utf16String& url,
|
|
const uint8_t* body, size_t body_len,
|
|
const Utf16String (*headers)[2],
|
|
size_t headers_len);
|
|
|
|
// External symbols from `./export/lib.rs`
|
|
extern "C" {
|
|
void crashtools_init(void);
|
|
int32_t crashtools_upload_exception(bool non_fatal, const Utf16String& message);
|
|
extern const int32_t CRASHTOOLS_UPLOAD_FATAL_ERROR;
|
|
Utf16String crashtools_crashping_init(const Utf16String& data_dir,
|
|
const Utf16String& app_id,
|
|
const Utf16String* build_id,
|
|
const Utf16String* display_version,
|
|
bool upload_enabled,
|
|
crashtools_upload_fn upload_fn);
|
|
Utf16String crashtools_analyze_minidump(const Utf16String& minidump_path,
|
|
const Utf16String& extras_path,
|
|
bool all_threads);
|
|
void crashtools_free_string(Utf16String result);
|
|
|
|
void crashtools_send_ping(const Utf16String& extras);
|
|
void crashtools_set_ping_collection_enabled(bool enabled);
|
|
void crashtools_test_metric_values_before_next_send(
|
|
void (*cb)(void* obj, const Utf16String& json), void (*drop)(void* obj),
|
|
void* obj);
|
|
}
|
|
|
|
// Create a Java string from a utf16-encoded string.
|
|
jstring createJString(JNIEnv* env, const Utf16String& s) {
|
|
return env->NewString(s.chars, s.len);
|
|
}
|
|
|
|
// A utf16-encoded string local to Java.
|
|
struct LocalString : Utf16String {
|
|
LocalString(JNIEnv* env, jstring str) : env(env), str(str) {
|
|
chars = env->GetStringChars(str, nullptr);
|
|
len = env->GetStringLength(str);
|
|
}
|
|
|
|
~LocalString() { env->ReleaseStringChars(str, chars); }
|
|
|
|
LocalString(const LocalString&) = delete;
|
|
LocalString& operator=(const LocalString&) = delete;
|
|
|
|
JNIEnv* env;
|
|
jstring str;
|
|
};
|
|
|
|
// A utf16-encoded string from Rust ("foreign" to Java), taking ownership of
|
|
// the data.
|
|
struct ForeignString : Utf16String {
|
|
explicit ForeignString(Utf16String s) : Utf16String(s) {}
|
|
~ForeignString() { crashtools_free_string(*this); }
|
|
|
|
ForeignString(const ForeignString&) = delete;
|
|
ForeignString& operator=(const ForeignString&) = delete;
|
|
|
|
explicit operator bool() const { return chars != nullptr; }
|
|
|
|
jstring to_jstring(JNIEnv* env) const { return createJString(env, *this); }
|
|
};
|
|
|
|
JavaVM* jvm;
|
|
jclass nativeCrashToolsClass;
|
|
|
|
// Create a JNIEnv local to the current thread, attaching the JVM if necessary.
|
|
//
|
|
// When destructed, the JVM will be detached if it wasn't originally attached.
|
|
class LocalJNIEnv {
|
|
JNIEnv* env;
|
|
bool detached;
|
|
|
|
public:
|
|
LocalJNIEnv() {
|
|
auto status = jvm->GetEnv((void**)&env, JNI_VERSION_1_6);
|
|
detached = status == JNI_EDETACHED;
|
|
if (detached) {
|
|
status = jvm->AttachCurrentThread(&env, NULL);
|
|
}
|
|
if (status != JNI_OK) {
|
|
LOG(FATAL, "failed to get JNI env: error code %d", status);
|
|
}
|
|
}
|
|
|
|
~LocalJNIEnv() {
|
|
if (detached) {
|
|
// Check for an exception and log it.
|
|
if (env != nullptr) {
|
|
jthrowable exc = env->ExceptionOccurred();
|
|
env->ExceptionClear();
|
|
|
|
if (exc != nullptr) {
|
|
#define LOG_PENDING_EXCEPTION(message) \
|
|
LOG(FATAL, "pending exception in thread: %s", message)
|
|
auto excClass = env->GetObjectClass(exc);
|
|
auto toStringMethodID =
|
|
env->GetMethodID(excClass, "toString", "()Ljava/lang/String;");
|
|
|
|
if (toStringMethodID == nullptr) {
|
|
LOG_PENDING_EXCEPTION(
|
|
"(failed to get toString() method for exception)");
|
|
env->ExceptionClear();
|
|
} else {
|
|
jstring excString =
|
|
(jstring)env->CallObjectMethod(exc, toStringMethodID);
|
|
if (excString != nullptr) {
|
|
auto excChars = env->GetStringUTFChars(excString, nullptr);
|
|
if (excChars != nullptr) {
|
|
// We are optimistic that the string doesn't contain the edge
|
|
// cases of the Java modified utf8, however if it does we'll
|
|
// just end up with some garbled output.
|
|
LOG_PENDING_EXCEPTION(excChars);
|
|
env->ReleaseStringUTFChars(excString, excChars);
|
|
} else {
|
|
LOG_PENDING_EXCEPTION(
|
|
"(failed to get exception string characters)");
|
|
}
|
|
} else {
|
|
LOG_PENDING_EXCEPTION("(exception toString() returned null)");
|
|
}
|
|
}
|
|
#undef LOG_PENDING_EXCEPTION
|
|
}
|
|
}
|
|
jvm->DetachCurrentThread();
|
|
}
|
|
}
|
|
|
|
JNIEnv* operator->() const { return env; }
|
|
|
|
operator JNIEnv*() const { return env; }
|
|
|
|
explicit operator bool() const { return env != nullptr; }
|
|
};
|
|
|
|
void throw_java_exception(JNIEnv* env, const char* class_name,
|
|
const char* message) {
|
|
__android_log_print(ANDROID_LOG_DEBUG, "NativeCrashTools",
|
|
"throwing Java exception %s: %s", class_name, message);
|
|
auto clazz = env->FindClass(class_name);
|
|
if (clazz == nullptr) {
|
|
return;
|
|
}
|
|
if (env->ThrowNew(clazz, message) != 0) {
|
|
__android_log_print(
|
|
ANDROID_LOG_FATAL, "NativeCrashTools",
|
|
"failed to throw Java exception with class %s and message %s",
|
|
class_name, message);
|
|
}
|
|
}
|
|
|
|
#define THROW_JAVA(class_name, message) \
|
|
do { \
|
|
throw_java_exception(env, class_name, message); \
|
|
return; \
|
|
} while (false)
|
|
|
|
// The upload function called from the Rust code. This will call the static
|
|
// NativeCrashTools.postRequest() method.
|
|
//
|
|
// This returns the HTTP status code from sending the ping, or forwards the
|
|
// crashtools_upload_exception return value.
|
|
int32_t upload_fn(const Utf16String& url, const uint8_t* body, size_t body_len,
|
|
const Utf16String (*headers)[2], size_t headers_len) {
|
|
#define UPLOAD_ASSERT(val) \
|
|
do { \
|
|
if (!(val)) return CRASHTOOLS_UPLOAD_FATAL_ERROR; \
|
|
} while (false)
|
|
LocalJNIEnv env;
|
|
UPLOAD_ASSERT(env);
|
|
|
|
// Create Java values to pass to the static Java method.
|
|
auto urlStr = createJString(env, url);
|
|
UPLOAD_ASSERT(urlStr);
|
|
|
|
auto bodyArr = env->NewByteArray(body_len);
|
|
// For some reason the JNI docs don't indicate that `NewByteArray` will throw
|
|
// an `OutOfMemoryError` on failure (like many other allocating functions
|
|
// do), however it undoubtedly does (and e.g. openJDK source code shows it
|
|
// does).
|
|
UPLOAD_ASSERT(bodyArr);
|
|
env->SetByteArrayRegion(bodyArr, 0, body_len, (const jbyte*)body);
|
|
// We don't need to check for `SetByteArrayRegion` throwing an exception, we
|
|
// are guaranteed to be in the region.
|
|
|
|
// Create a LinkedHashMap for the headers.
|
|
auto mapClass = env->FindClass("java/util/LinkedHashMap");
|
|
UPLOAD_ASSERT(mapClass);
|
|
auto classConstructorID = env->GetMethodID(mapClass, "<init>", "(I)V");
|
|
UPLOAD_ASSERT(classConstructorID);
|
|
auto headersMap = env->NewObject(mapClass, classConstructorID, headers_len);
|
|
UPLOAD_ASSERT(headersMap);
|
|
auto mapPutMethodID = env->GetMethodID(
|
|
mapClass, "put",
|
|
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
|
|
UPLOAD_ASSERT(mapPutMethodID);
|
|
|
|
for (size_t i = 0; i < headers_len; i++) {
|
|
auto key = createJString(env, headers[i][0]);
|
|
UPLOAD_ASSERT(key);
|
|
auto value = createJString(env, headers[i][1]);
|
|
UPLOAD_ASSERT(value);
|
|
env->CallObjectMethod(headersMap, mapPutMethodID, key, value);
|
|
UPLOAD_ASSERT(!env->ExceptionCheck());
|
|
}
|
|
|
|
// Call the static postRequest method.
|
|
auto postRequestMethodID =
|
|
env->GetStaticMethodID(nativeCrashToolsClass, "postRequest",
|
|
"(Ljava/lang/String;[BLjava/util/Map;)I");
|
|
UPLOAD_ASSERT(postRequestMethodID);
|
|
jint ret = env->CallStaticIntMethod(
|
|
nativeCrashToolsClass, postRequestMethodID, urlStr, bodyArr, headersMap);
|
|
|
|
// If any exception occurred in the called method, catch it and change the
|
|
// return value appropriately.
|
|
{
|
|
jthrowable exc = env->ExceptionOccurred();
|
|
env->ExceptionClear();
|
|
|
|
if (exc != nullptr) {
|
|
auto isNonFatal = false;
|
|
|
|
// Assume IOExceptions are some sort of network error that may not be
|
|
// permanent.
|
|
{
|
|
auto ioExceptionClass = env->FindClass("java/io/IOException");
|
|
// Clear exception in case FindClass fails.
|
|
env->ExceptionClear();
|
|
isNonFatal =
|
|
ioExceptionClass && env->IsInstanceOf(exc, ioExceptionClass);
|
|
}
|
|
|
|
// Pass the exception message to the handler.
|
|
auto excClass = env->GetObjectClass(exc);
|
|
auto toStringMethodID =
|
|
env->GetMethodID(excClass, "toString", "()Ljava/lang/String;");
|
|
UPLOAD_ASSERT(toStringMethodID);
|
|
jstring excString = (jstring)env->CallObjectMethod(exc, toStringMethodID);
|
|
UPLOAD_ASSERT(!env->ExceptionCheck());
|
|
|
|
LocalString excLocalString(env, excString);
|
|
return crashtools_upload_exception(isNonFatal, excLocalString);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
#undef UPLOAD_ASSERT
|
|
}
|
|
|
|
#define KOTLIN_CLOSURE_CLASS "kotlin/jvm/functions/Function1"
|
|
|
|
// Call a Kotlin closure with the given String argument.
|
|
void call_closure(void* obj, const Utf16String& value) {
|
|
LocalJNIEnv env;
|
|
if (!env) return;
|
|
auto kotlinFunction1Class = env->FindClass(KOTLIN_CLOSURE_CLASS);
|
|
// We require the callback to be a kotlin closure when we store it, so no
|
|
// need to check the result of `FindClass` nor check with `IsInstanceOf`.
|
|
auto invokeMethod = env->GetMethodID(
|
|
kotlinFunction1Class, "invoke", "(Ljava/lang/Object;)Ljava/lang/Object;");
|
|
if (!invokeMethod) return;
|
|
auto arg = createJString(env, value);
|
|
if (!arg) return;
|
|
env->CallObjectMethod(static_cast<jobject>(obj), invokeMethod, arg);
|
|
}
|
|
|
|
// Drop a Java callback (the function drops any globally-refed object).
|
|
void drop_closure(void* obj) {
|
|
LocalJNIEnv env;
|
|
if (!env) return;
|
|
env->DeleteGlobalRef(static_cast<jobject>(obj));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" {
|
|
// Static init when the library is first loaded. We store the jvm to be able to
|
|
// attach to threads later.
|
|
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
|
|
crashtools_init();
|
|
jvm = vm;
|
|
return JNI_VERSION_1_6;
|
|
}
|
|
|
|
// The init Java static method which configures the data path and build id.
|
|
JNIEXPORT void Java_mozilla_components_lib_crash_NativeCrashTools_nativeInit(
|
|
JNIEnv* env, jclass clazz, jstring data_path, jstring app_id,
|
|
jstring build_id, jstring display_version, jboolean ping_upload_enabled) {
|
|
LocalString data_str(env, data_path);
|
|
LocalString app_id_str(env, app_id);
|
|
|
|
// Store the NativeCrashTools class for use in callbacks.
|
|
nativeCrashToolsClass = (jclass)env->NewGlobalRef(clazz);
|
|
if (nativeCrashToolsClass == nullptr) {
|
|
THROW_JAVA("java/lang/OutOfMemoryError",
|
|
"failed to create a NativeCrashTools global ref");
|
|
}
|
|
|
|
std::optional<LocalString> build_id_str;
|
|
if (build_id != nullptr) {
|
|
build_id_str.emplace(env, build_id);
|
|
}
|
|
|
|
std::optional<LocalString> display_version_str;
|
|
if (display_version != nullptr) {
|
|
display_version_str.emplace(env, build_id);
|
|
}
|
|
|
|
if (auto result = ForeignString(crashtools_crashping_init(
|
|
data_str, app_id_str, build_id_str ? &*build_id_str : nullptr,
|
|
display_version_str ? &*display_version_str : nullptr,
|
|
ping_upload_enabled, upload_fn))) {
|
|
auto result_jstring = result.to_jstring(env);
|
|
const char* utf8Chars = nullptr;
|
|
if (!result_jstring ||
|
|
!(utf8Chars = env->GetStringUTFChars(result_jstring, nullptr))) {
|
|
env->ExceptionClear();
|
|
THROW_JAVA("java/io/IOException",
|
|
"failed to initialize crashping library (and failed to get "
|
|
"related error string)");
|
|
}
|
|
throw_java_exception(env, "java/io/IOException", utf8Chars);
|
|
env->ReleaseStringUTFChars(result_jstring, utf8Chars);
|
|
return;
|
|
}
|
|
}
|
|
|
|
JNIEXPORT jstring JNICALL
|
|
Java_mozilla_components_lib_crash_NativeCrashTools_nativeAnalyzeMinidump(
|
|
JNIEnv* env, jobject obj, jstring minidump_path, jstring extras_path,
|
|
jboolean all_threads) {
|
|
LocalString minidump_str(env, minidump_path);
|
|
LocalString extras_str(env, extras_path);
|
|
if (auto error = ForeignString(
|
|
crashtools_analyze_minidump(minidump_str, extras_str, all_threads))) {
|
|
return error.to_jstring(env);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
JNIEXPORT void JNICALL
|
|
Java_mozilla_components_lib_crash_NativeCrashTools_nativeSendPing(
|
|
JNIEnv* env, jobject obj, jstring extras) {
|
|
LocalString extras_str(env, extras);
|
|
crashtools_send_ping(extras_str);
|
|
}
|
|
|
|
JNIEXPORT void JNICALL
|
|
Java_mozilla_components_lib_crash_NativeCrashTools_nativeTestMetricValuesBeforeNextSend(
|
|
JNIEnv* env, jobject obj, jobject cb) {
|
|
// Add a global reference to the callback object. We pass ownership to the
|
|
// Rust code, which will call `drop_closure` when appropriate to release the
|
|
// reference.
|
|
if (cb == nullptr) {
|
|
THROW_JAVA("java/lang/IllegalArgumentException",
|
|
"callback must not be null");
|
|
}
|
|
auto kotlinFunction1Class = env->FindClass(KOTLIN_CLOSURE_CLASS);
|
|
if (!kotlinFunction1Class) return;
|
|
if (!env->IsInstanceOf(cb, kotlinFunction1Class)) {
|
|
THROW_JAVA("java/lang/IllegalArgumentException",
|
|
"callback must be a Kotlin closure taking one argument");
|
|
}
|
|
|
|
cb = env->NewGlobalRef(cb);
|
|
if (cb == nullptr) {
|
|
THROW_JAVA("java/lang/OutOfMemoryError",
|
|
"failed to create a global ref of the callback argument");
|
|
}
|
|
crashtools_test_metric_values_before_next_send(call_closure, drop_closure,
|
|
cb);
|
|
}
|
|
|
|
JNIEXPORT void
|
|
Java_mozilla_components_lib_crash_NativeCrashTools_nativeSetPingCollectionEnabled(
|
|
JNIEnv* env, jobject obj, jboolean enabled) {
|
|
crashtools_set_ping_collection_enabled(enabled);
|
|
}
|
|
}
|