Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture native thread states #1384

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bugsnag-android-core/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<ID>LongParameterList:DeviceWithState.kt$DeviceWithState$( buildInfo: DeviceBuildInfo, jailbroken: Boolean?, id: String?, locale: String?, totalMemory: Long?, runtimeVersions: MutableMap&lt;String, Any>, /** * The number of free bytes of storage available on the device */ var freeDisk: Long?, /** * The number of free bytes of memory available on the device */ var freeMemory: Long?, /** * The orientation of the device when the event occurred: either portrait or landscape */ var orientation: String?, /** * The timestamp on the device when the event occurred */ var time: Date? )</ID>
<ID>LongParameterList:EventFilenameInfo.kt$EventFilenameInfo.Companion$( obj: Any, uuid: String = UUID.randomUUID().toString(), apiKey: String?, timestamp: Long = System.currentTimeMillis(), config: ImmutableConfig, isLaunching: Boolean? = null )</ID>
<ID>LongParameterList:EventStorageModule.kt$EventStorageModule$( contextModule: ContextModule, configModule: ConfigModule, dataCollectionModule: DataCollectionModule, bgTaskService: BackgroundTaskService, trackerModule: TrackerModule, systemServiceModule: SystemServiceModule, notifier: Notifier )</ID>
<ID>LongParameterList:StateEvent.kt$StateEvent.Install$( @JvmField val apiKey: String, @JvmField val autoDetectNdkCrashes: Boolean, @JvmField val appVersion: String?, @JvmField val buildUuid: String?, @JvmField val releaseStage: String?, @JvmField val lastRunInfoPath: String, @JvmField val consecutiveLaunchCrashes: Int )</ID>
<ID>LongParameterList:StateEvent.kt$StateEvent.Install$( @JvmField val apiKey: String, @JvmField val autoDetectNdkCrashes: Boolean, @JvmField val appVersion: String?, @JvmField val buildUuid: String?, @JvmField val releaseStage: String?, @JvmField val lastRunInfoPath: String, @JvmField val consecutiveLaunchCrashes: Int, @JvmField val sendThreads: ThreadSendPolicy )</ID>
<ID>LongParameterList:ThreadState.kt$ThreadState$( stackTraces: MutableMap&lt;java.lang.Thread, Array&lt;StackTraceElement>>, currentThread: java.lang.Thread, exc: Throwable?, isUnhandled: Boolean, projectPackages: Collection&lt;String>, logger: Logger )</ID>
<ID>MagicNumber:DefaultDelivery.kt$DefaultDelivery$299</ID>
<ID>MagicNumber:DefaultDelivery.kt$DefaultDelivery$429</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ internal class ClientObservable : BaseObservable() {
conf.buildUuid,
conf.releaseStage,
lastRunInfoPath,
consecutiveLaunchCrashes
consecutiveLaunchCrashes,
conf.sendThreads
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ sealed class StateEvent { // JvmField allows direct field access optimizations
@JvmField val buildUuid: String?,
@JvmField val releaseStage: String?,
@JvmField val lastRunInfoPath: String,
@JvmField val consecutiveLaunchCrashes: Int
@JvmField val consecutiveLaunchCrashes: Int,
@JvmField val sendThreads: ThreadSendPolicy
) : StateEvent()

object DeliverPending : StateEvent()
Expand Down
2 changes: 1 addition & 1 deletion bugsnag-plugin-android-ndk/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComplexMethod:NativeBridge.kt$NativeBridge$override fun onStateChange(event: StateEvent)</ID>
<ID>LongParameterList:NativeBridge.kt$NativeBridge$( apiKey: String, reportingDirectory: String, lastRunInfoPath: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, is32bit: Boolean, appVersion: String, buildUuid: String, releaseStage: String )</ID>
<ID>LongParameterList:NativeBridge.kt$NativeBridge$( apiKey: String, reportingDirectory: String, lastRunInfoPath: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, is32bit: Boolean, threadSendPolicy: Int )</ID>
<ID>NestedBlockDepth:NativeBridge.kt$NativeBridge$private fun deliverPendingReports()</ID>
<ID>TooManyFunctions:NativeBridge.kt$NativeBridge : StateObserver</ID>
</CurrentIssues>
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[{"id":1234,"name":"Binder 1","state":"R","type":"c"}]
[{"id":1234,"name":"Binder 1","state":"Running","type":"c"}]
1 change: 1 addition & 0 deletions bugsnag-plugin-android-ndk/src/main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ add_library( # Specifies the name of the library.
jni/utils/stack_unwinder_simple.c
jni/utils/serializer.c
jni/utils/string.c
jni/utils/threads.c
jni/deps/parson/parson.c
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ class NativeBridge : StateObserver {
autoDetectNdkCrashes: Boolean,
apiLevel: Int,
is32bit: Boolean,
appVersion: String,
buildUuid: String,
releaseStage: String
threadSendPolicy: Int
)

external fun startedSession(
Expand Down Expand Up @@ -172,9 +170,7 @@ class NativeBridge : StateObserver {
arg.autoDetectNdkCrashes,
Build.VERSION.SDK_INT,
is32bit,
makeSafe(arg.appVersion ?: ""),
makeSafe(arg.buildUuid ?: ""),
makeSafe(arg.releaseStage ?: "")
arg.sendThreads.ordinal
)
installed.set(true)
}
Expand Down
4 changes: 3 additions & 1 deletion bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ void bsg_update_next_run_info(bsg_environment *env) {
JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_install(
JNIEnv *env, jobject _this, jstring _api_key, jstring _event_path,
jstring _last_run_info_path, jint consecutive_launch_crashes,
jboolean auto_detect_ndk_crashes, jint _api_level, jboolean is32bit) {
jboolean auto_detect_ndk_crashes, jint _api_level, jboolean is32bit,
jint send_threads) {
bsg_environment *bugsnag_env = calloc(1, sizeof(bsg_environment));
bsg_set_unwind_types((int)_api_level, (bool)is32bit,
&bugsnag_env->signal_unwind_style,
Expand All @@ -143,6 +144,7 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_install(
htonl(47) == 47; // potentially too clever, see man 3 htonl
bugsnag_env->report_header.version = BUGSNAG_EVENT_VERSION;
bugsnag_env->consecutive_launch_crashes = consecutive_launch_crashes;
bugsnag_env->send_threads = send_threads;

// copy event path to env struct
const char *event_path = bsg_safe_get_string_utf_chars(env, _event_path);
Expand Down
6 changes: 6 additions & 0 deletions bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ typedef struct {
bool crash_handled;

bsg_on_error on_error;

/**
* Controls whether we should capture and serialize the state of all threads
* at the time of an error.
*/
bsg_thread_send_policy send_threads;
} bsg_environment;

bsg_unwinder bsg_configured_unwind_style();
Expand Down
8 changes: 7 additions & 1 deletion bugsnag-plugin-android-ndk/src/main/jni/event.h
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,15 @@ typedef struct {
typedef struct {
pid_t id;
char name[16];
char state;
char state[13];
} bsg_thread;

typedef enum {
SEND_THREADS_ALWAYS = 0,
SEND_THREADS_UNHANDLED_ONLY = 1,
SEND_THREADS_NEVER = 2
} bsg_thread_send_policy;

typedef struct {
bsg_notifier notifier;
bsg_app_info app;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "../utils/crash_info.h"
#include "../utils/serializer.h"
#include "../utils/string.h"
#include "../utils/threads.h"
/**
* Previously installed termination handler
*/
Expand Down Expand Up @@ -95,6 +96,13 @@ void bsg_handle_cpp_terminate() {
bsg_unwind_stack(bsg_global_env->unwind_style,
bsg_global_env->next_event.error.stacktrace, NULL, NULL);

if (bsg_global_env->send_threads != SEND_THREADS_NEVER) {
bsg_global_env->next_event.thread_count = bsg_capture_thread_states(
bsg_global_env->next_event.threads, BUGSNAG_THREADS_MAX);
} else {
bsg_global_env->next_event.thread_count = 0;
}

std::type_info *tinfo = __cxxabiv1::__cxa_current_exception_type();
if (tinfo != NULL) {
bsg_strncpy(bsg_global_env->next_event.error.errorClass,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "../utils/crash_info.h"
#include "../utils/serializer.h"
#include "../utils/string.h"
#include "../utils/threads.h"
#define BSG_HANDLED_SIGNAL_COUNT 6

/**
Expand Down Expand Up @@ -185,6 +186,13 @@ void bsg_handle_signal(int signum, siginfo_t *info,
bsg_global_env->signal_unwind_style,
bsg_global_env->next_event.error.stacktrace, info, user_context);

if (bsg_global_env->send_threads != SEND_THREADS_NEVER) {
bsg_global_env->next_event.thread_count = bsg_capture_thread_states(
bsg_global_env->next_event.threads, BUGSNAG_THREADS_MAX);
} else {
bsg_global_env->next_event.thread_count = 0;
}

for (int i = 0; i < BSG_HANDLED_SIGNAL_COUNT; i++) {
const int signal = bsg_native_signals[i];
if (signal == signum) {
Expand Down
5 changes: 1 addition & 4 deletions bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c
Original file line number Diff line number Diff line change
Expand Up @@ -855,18 +855,15 @@ void bsg_serialize_threads(const bugsnag_event *event, JSON_Array *threads) {
return;
}

char status_buffer[2];
status_buffer[1] = 0; // null terminator
for (int index = 0; index < event->thread_count; index++) {
JSON_Value *thread_val = json_value_init_object();
JSON_Object *json_thread = json_value_get_object(thread_val);
json_array_append_value(threads, thread_val);

const bsg_thread *thread = &event->threads[index];
status_buffer[0] = thread->state;
json_object_set_number(json_thread, "id", (double)thread->id);
json_object_set_string(json_thread, "name", thread->name);
json_object_set_string(json_thread, "state", status_buffer);
json_object_set_string(json_thread, "state", thread->state);
json_object_set_string(json_thread, "type", "c");
}
}
Expand Down
199 changes: 199 additions & 0 deletions bugsnag-plugin-android-ndk/src/main/jni/utils/threads.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#include <dirent.h>
#include <fcntl.h>
#include <sys/syscall.h>
#include <unistd.h>

#include "string.h"
#include "threads.h"

/*
* We read thread states by scanning the tid list in `/proc/self/task`. Each
* subdirectory here represents a task (tid) within the current application, and
* each of these contains a `stat` file with the information we require.
*
* This behaviour needs to be async / signal-safe, which blocks us from using
* many of the standard libc filesystem functions. To work-around this we resort
* to Linux syscalls for the directory listing.
*/

#define TASK_STAT_PATH_PREFIX "/proc/self/task/"
// we don't want the null-terminator in the length
#define TASK_STAT_PATH_PREFIX_LEN (sizeof(TASK_STAT_PATH_PREFIX) - 1)
#define TASK_STAT_PATH_SUFFIX "/stat"

// in theory we could optimise this - since all paths have a common prefix
static void path_for_tid_stat(char *dest, const char *tid) {
size_t tidlen = bsg_strlen(tid);
size_t remaining = MAX_STAT_PATH_LENGTH;
bsg_strncpy(dest, TASK_STAT_PATH_PREFIX, remaining);
remaining -= TASK_STAT_PATH_PREFIX_LEN;
bsg_strncpy(&dest[TASK_STAT_PATH_PREFIX_LEN], tid, remaining);
remaining -= tidlen;
bsg_strncpy(&dest[TASK_STAT_PATH_PREFIX_LEN + tidlen], TASK_STAT_PATH_SUFFIX,
remaining);
}

/**
* Possible task states as defined in:
* https://github.com/torvalds/linux/blob/v5.13/fs/proc/array.c#L132-L143
*/
static const char *const task_state_array[] = {
"R running", "S sleeping", "D disk sleep", "T stopped", "t tracing stop",
"X dead", "Z zombie", "P parked", "I idle",
};

static void get_task_state_description(const char code, char *dest) {
for (size_t index = 0; index < (sizeof(task_state_array) / sizeof(char *));
index++) {
if (task_state_array[index][0] == code) {
bsg_strcpy(dest, &task_state_array[index][2]);
return;
}
}

// if we reached here we failed to map the code, store the single-character
// code as a fallback
dest[0] = code;
dest[1] = '\0';
}

/**
* Parses the content of a /proc/self/task/{tid}/stat file, as documented in:
* https://man7.org/linux/man-pages/man5/proc.5.html
*
* We are only interested in the first three fields which are the:
* 1) TID (numeric)
* 2) Name (in parenthesis)
* 3) State (single character)
*
* Unfortunately thread names can contain spaces, making tools like `strtok`
* unsuitable for this process. As a result we parse using a simple single pass
* state-based loop.
*
* *WARNING* `content` is modified by this method under normal operation
*
* @return true if the thread-data was parsed, false if not
*/
static bool parse_stat_content(bsg_thread *dest, char *content, size_t len) {
enum stat_parse_state {
PARSE_ID,
PARSE_NAME_START,
PARSE_NAME_CONTENT,
PARSE_NAME_END,
PARSE_STATUS,
PARSE_DONE
};

enum stat_parse_state state = PARSE_ID;

size_t i = 0;
size_t name_length = 0;
while (i < len) {
char current = content[i];

switch (state) {
case PARSE_ID:
if (current == ' ') {
// we create a terminator for the numeric parse
content[i] = '\0';
// atoi is async-safe, strtol is not guaranteed to be
dest->id = atoi(content);
state = PARSE_NAME_START;
}
break;
case PARSE_NAME_START:
if (current == '(') {
state = PARSE_NAME_CONTENT;
}
break;
case PARSE_NAME_CONTENT:
if (current == ')') {
state = PARSE_NAME_END;
} else if (name_length < sizeof(dest->name)) {
dest->name[name_length] = current;
name_length++;
}
break;
case PARSE_NAME_END:
dest->name[name_length] = '\0';
if (current == ' ') {
state = PARSE_STATUS;
}
break;
case PARSE_STATUS:
get_task_state_description(current, dest->state);
state = PARSE_DONE;
break;
case PARSE_DONE:
goto end;
}

i += 1;
}

end:
// success if we hit the DONE marker
return state == PARSE_DONE;
}

/*
* Reads the /stat file fields we are looking for (TID, Name, State) into `dest`
* with a signal-safe approach.
*/
static bool read_thread_state(bsg_thread *dest, const char *tid) {
// we filter out anything not numeric
if (tid[0] < '0' || tid[0] > '9') {
return false;
}

char filename[MAX_STAT_PATH_LENGTH];
path_for_tid_stat(filename, tid);
// the content buffer for the stat file data, in the format:
// {tid} ({name}) {status}
// {tid} = integer TID value
// {name} = thread name char[16]
// {status} = single character
char content_buffer[64];
int stat_fd = open(filename, O_RDONLY);

if (stat_fd == 0) {
return false;
}

size_t len = read(stat_fd, content_buffer, sizeof(content_buffer));
bool parse_success = parse_stat_content(dest, content_buffer, len);
close(stat_fd);
return parse_success;
}

size_t bsg_capture_thread_states(bsg_thread *threads, size_t max_threads) {
size_t total_thread_count = 0;
struct dirent64 *entry;
char buffer[1024];
int available, offset;

int task_dir_fd = open("/proc/self/task", O_RDONLY | O_DIRECTORY);
if (task_dir_fd == 0) {
return 0;
}

while (total_thread_count < max_threads) {
available = syscall(SYS_getdents64, task_dir_fd, buffer, sizeof(buffer));
if (available <= 0) {
break;
}

for (offset = 0; offset < available && total_thread_count < max_threads;) {
entry = (struct dirent64 *)(buffer + offset);
if (read_thread_state(&threads[total_thread_count], entry->d_name)) {
total_thread_count += 1;
}

offset += entry->d_reclen;
}
}

close(task_dir_fd);

return total_thread_count;
}
Loading