From f5cfb3aabfebd64934ffb67a1d178c8bda56af59 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 28 Feb 2024 17:24:15 +0000 Subject: [PATCH] Add support for query thread user-time --- bindings/gumjs/gumquickthread.c | 10 + bindings/gumjs/gumv8thread.cpp | 10 + gum/backend-darwin/gumprocess-darwin.c | 18 ++ gum/backend-freebsd/gumprocess-freebsd.c | 16 ++ gum/backend-linux/gumprocess-linux.c | 25 +++ gum/backend-qnx/gumprocess-qnx.c | 6 + gum/backend-windows/gumprocess-windows.c | 43 ++++ gum/gumprocess.c | 1 + gum/gumprocess.h | 1 + tests/core/process.c | 133 +++++++++++ tests/gumjs/script.c | 270 +++++++++++++++++++++++ 11 files changed, 533 insertions(+) diff --git a/bindings/gumjs/gumquickthread.c b/bindings/gumjs/gumquickthread.c index ae0e80ee4..05fb59bf6 100644 --- a/bindings/gumjs/gumquickthread.c +++ b/bindings/gumjs/gumquickthread.c @@ -17,11 +17,13 @@ enum _GumBacktracerType GUMJS_DECLARE_FUNCTION (gumjs_thread_backtrace) GUMJS_DECLARE_FUNCTION (gumjs_thread_sleep) +GUMJS_DECLARE_FUNCTION (gumjs_thread_get_user_time) static const JSCFunctionListEntry gumjs_thread_entries[] = { JS_CFUNC_DEF ("_backtrace", 0, gumjs_thread_backtrace), JS_CFUNC_DEF ("sleep", 0, gumjs_thread_sleep), + JS_CFUNC_DEF ("getUserTime", 0, gumjs_thread_get_user_time), }; static const JSCFunctionListEntry gumjs_backtracer_entries[] = @@ -159,3 +161,11 @@ GUMJS_DEFINE_FUNCTION (gumjs_thread_sleep) return JS_UNDEFINED; } + +GUMJS_DEFINE_FUNCTION (gumjs_thread_get_user_time) +{ + guint64 user_time = gum_thead_get_user_time (); + + return JS_NewFloat64 (ctx, ((double) user_time) / G_USEC_PER_SEC); +} + diff --git a/bindings/gumjs/gumv8thread.cpp b/bindings/gumjs/gumv8thread.cpp index 84c41ed8e..d5580b974 100644 --- a/bindings/gumjs/gumv8thread.cpp +++ b/bindings/gumjs/gumv8thread.cpp @@ -15,11 +15,13 @@ using namespace v8; GUMJS_DECLARE_FUNCTION (gumjs_thread_backtrace) GUMJS_DECLARE_FUNCTION (gumjs_thread_sleep) +GUMJS_DECLARE_FUNCTION (gumjs_thread_get_user_time) static const GumV8Function gumjs_thread_functions[] = { { "_backtrace", gumjs_thread_backtrace }, { "sleep", gumjs_thread_sleep }, + { "getUserTime", gumjs_thread_get_user_time}, { NULL, NULL } }; @@ -171,3 +173,11 @@ GUMJS_DEFINE_FUNCTION (gumjs_thread_sleep) g_usleep (delay * G_USEC_PER_SEC); } } + +GUMJS_DEFINE_FUNCTION (gumjs_thread_get_user_time) +{ + guint64 user_time = gum_thead_get_user_time (); + + info.GetReturnValue ().Set ((double) user_time / G_USEC_PER_SEC); +} + diff --git a/gum/backend-darwin/gumprocess-darwin.c b/gum/backend-darwin/gumprocess-darwin.c index a66f9da13..f649a2ab5 100644 --- a/gum/backend-darwin/gumprocess-darwin.c +++ b/gum/backend-darwin/gumprocess-darwin.c @@ -686,6 +686,24 @@ gum_thread_resume (GumThreadId thread_id, #endif } +guint64 +gum_thead_get_user_time (void) +{ + mach_port_t port; + thread_basic_info_data_t info; + mach_msg_type_number_t info_count = THREAD_BASIC_INFO_COUNT; + G_GNUC_UNUSED kern_return_t kr; + + port = mach_thread_self (); + kr = thread_info (port, THREAD_BASIC_INFO, + (thread_info_t) &info, &info_count); + g_assert (kr == KERN_SUCCESS); + mach_port_deallocate (mach_task_self (), port); + + return ((guint64) info.user_time.seconds * G_USEC_PER_SEC) + + info.user_time.microseconds; +} + gboolean gum_module_load (const gchar * module_name, GError ** error) diff --git a/gum/backend-freebsd/gumprocess-freebsd.c b/gum/backend-freebsd/gumprocess-freebsd.c index 770abf25b..f6156a47b 100644 --- a/gum/backend-freebsd/gumprocess-freebsd.c +++ b/gum/backend-freebsd/gumprocess-freebsd.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -716,6 +717,21 @@ gum_thread_resume (GumThreadId thread_id, } } +guint64 +gum_thead_get_user_time (void) +{ + guint64 user_time = 0; + struct rusage usage; + + if (getrusage (RUSAGE_THREAD, &usage) == 0) + { + user_time = (usage.ru_utime.tv_sec * G_USEC_PER_SEC) + + usage.ru_utime.tv_usec; + } + + return user_time; +} + gboolean gum_module_load (const gchar * module_name, GError ** error) diff --git a/gum/backend-linux/gumprocess-linux.c b/gum/backend-linux/gumprocess-linux.c index dddbf6084..f7fdd2753 100644 --- a/gum/backend-linux/gumprocess-linux.c +++ b/gum/backend-linux/gumprocess-linux.c @@ -48,6 +48,8 @@ #ifdef HAVE_SYS_USER_H # include #endif +#include +#include #define GUM_PAGE_START(value, page_size) \ (GUM_ADDRESS (value) & ~GUM_ADDRESS (page_size - 1)) @@ -87,6 +89,9 @@ #ifndef NT_PRSTATUS # define NT_PRSTATUS 1 #endif +#ifndef RUSAGE_THREAD +# define RUSAGE_THREAD 1 +#endif #define GUM_TEMP_FAILURE_RETRY(expression) \ ({ \ @@ -1548,6 +1553,26 @@ gum_thread_resume (GumThreadId thread_id, } } +/** + * gum_thead_get_user_time: + * + * Returns: the time the thread has spend executing in user-mode in micro-seconds + */ +guint64 +gum_thead_get_user_time (void) +{ + guint64 user_time = 0; + struct rusage usage; + + if (getrusage (RUSAGE_THREAD, &usage) == 0) + { + user_time = (usage.ru_utime.tv_sec * G_USEC_PER_SEC) + + usage.ru_utime.tv_usec; + } + + return user_time; +} + gboolean gum_module_load (const gchar * module_name, GError ** error) diff --git a/gum/backend-qnx/gumprocess-qnx.c b/gum/backend-qnx/gumprocess-qnx.c index 2fa04aebb..1c0d1e397 100644 --- a/gum/backend-qnx/gumprocess-qnx.c +++ b/gum/backend-qnx/gumprocess-qnx.c @@ -508,6 +508,12 @@ gum_thread_resume (GumThreadId thread_id, } } +guint64 +gum_thead_get_user_time (void) +{ + return 0; +} + gboolean gum_module_load (const gchar * module_name, GError ** error) diff --git a/gum/backend-windows/gumprocess-windows.c b/gum/backend-windows/gumprocess-windows.c index 7e350be4c..b2291f953 100644 --- a/gum/backend-windows/gumprocess-windows.c +++ b/gum/backend-windows/gumprocess-windows.c @@ -27,6 +27,9 @@ typedef void (WINAPI * GumGetCurrentThreadStackLimitsFunc) ( PULONG_PTR low_limit, PULONG_PTR high_limit); typedef struct _GumEnumerateSymbolsContext GumEnumerateSymbolsContext; typedef struct _GumFindExportContext GumFindExportContext; +typedef BOOL (WINAPI * GetThreadTimesFunc) (HANDLE ThreadHandle, + LPFILETIME lpCreationTime, LPFILETIME lpExitTime, LPFILETIME lpKernelTime, + LPFILETIME lpUserTime); struct _GumEnumerateSymbolsContext { @@ -637,6 +640,46 @@ gum_thread_resume (GumThreadId thread_id, } } +guint64 +gum_thead_get_user_time (void) +{ + static gboolean initialized = FALSE; + static GetThreadTimesFunc get_thread_times = NULL; + + HMODULE mod; + FILETIME creationTime; + FILETIME exitTime; + FILETIME kernelTime; + FILETIME userTime; + guint64 result; + + if (!initialized) + { + initialized = TRUE; + + mod = GetModuleHandle (_T ("kernel32.dll")); + if (mod == NULL) + return 0; + + get_thread_times =(GetThreadTimesFunc) GetProcAddress (mod, + "GetThreadTimes"); + } + + if (get_thread_times == NULL) + return 0; + + if (!get_thread_times (GetCurrentThread (), &creationTime, &exitTime, + &kernelTime, &userTime)) + { + return 0; + } + + result = (((guint64) userTime.dwHighDateTime) << 32) + userTime.dwLowDateTime; + + /* Timings on Windows are to 100-nanosecond granularity. Convert to u-secs */ + return result / 10; +} + gboolean gum_module_load (const gchar * module_name, GError ** error) diff --git a/gum/gumprocess.c b/gum/gumprocess.c index 5d78a0964..a3bfbb8c1 100644 --- a/gum/gumprocess.c +++ b/gum/gumprocess.c @@ -9,6 +9,7 @@ #include "gum-init.h" #include "gumcloak.h" +#include "gumstalker.h" typedef struct _GumEmitThreadsContext GumEmitThreadsContext; typedef struct _GumResolveModulePointerContext GumResolveModulePointerContext; diff --git a/gum/gumprocess.h b/gum/gumprocess.h index 0b6bb9c27..828ce2351 100644 --- a/gum/gumprocess.h +++ b/gum/gumprocess.h @@ -230,6 +230,7 @@ GUM_API gint gum_thread_get_system_error (void); GUM_API void gum_thread_set_system_error (gint value); GUM_API gboolean gum_thread_suspend (GumThreadId thread_id, GError ** error); GUM_API gboolean gum_thread_resume (GumThreadId thread_id, GError ** error); +GUM_API guint64 gum_thead_get_user_time (void); GUM_API gboolean gum_module_load (const gchar * module_name, GError ** error); GUM_API gboolean gum_module_ensure_initialized (const gchar * module_name); GUM_API void gum_module_enumerate_imports (const gchar * module_name, diff --git a/tests/core/process.c b/tests/core/process.c index 85c6e4fd0..8404f6414 100644 --- a/tests/core/process.c +++ b/tests/core/process.c @@ -36,6 +36,9 @@ TESTLIST_BEGIN (process) TESTENTRY (process_threads) TESTENTRY (process_threads_exclude_cloaked) TESTENTRY (process_threads_should_include_name) + TESTENTRY (process_threads_get_user_time) + TESTENTRY (process_threads_get_user_time_by_id_self) + TESTENTRY (process_threads_get_user_time_by_id_other) TESTENTRY (process_modules) TESTENTRY (process_ranges) TESTENTRY (process_ranges_exclude_cloaked) @@ -128,6 +131,9 @@ struct _ExportSearch #endif static gboolean check_thread_enumeration_testable (void); +static guint64 gum_thead_get_user_time_by_id (GumThreadId thread_id); +static void gum_get_user_time (const GumCpuContext * cpu_context, + gpointer user_data); static gpointer probe_thread (gpointer data); static void inspect_thread_ranges (void); @@ -150,6 +156,7 @@ static gboolean process_potential_export_search_result ( static GThread * create_sleeping_dummy_thread_sync (const gchar * name, volatile gboolean * done, GumThreadId * thread_id); static gpointer sleeping_dummy (gpointer data); +static void do_work (void); static gboolean thread_found_cb (const GumThreadDetails * details, gpointer user_data); static gboolean thread_check_cb (const GumThreadDetails * details, @@ -290,6 +297,114 @@ check_thread_enumeration_testable (void) return TRUE; } +TESTCASE (process_threads_get_user_time) +{ + guint64 user_time_a, user_time_b; + + if (!check_thread_enumeration_testable ()) + return; + + do_work (); + user_time_a = gum_thead_get_user_time (); + + do_work (); + user_time_b = gum_thead_get_user_time (); +#if defined (HAVE_LINUX) || defined (HAVE_DARWIN) || defined (HAVE_FREEBSD) \ + || defined (HAVE_WINDOWS) + g_assert_cmpuint (user_time_a, !=, 0); + g_assert_cmpuint (user_time_b, >, user_time_a); +#else + g_assert_cmpuint (user_time_a, ==, 0); + g_assert_cmpuint (user_time_b, ==, 0); +#endif +} + +TESTCASE (process_threads_get_user_time_by_id_self) +{ + GumThreadId tid = gum_process_get_current_thread_id (); + guint64 user_time_a, user_time_b; + + if (!check_thread_enumeration_testable ()) + return; + + do_work (); + user_time_a = gum_thead_get_user_time_by_id (tid); + + do_work (); + user_time_b = gum_thead_get_user_time_by_id (tid); +#if defined (HAVE_LINUX) || defined (HAVE_DARWIN) || defined (HAVE_FREEBSD) \ + || defined (HAVE_WINDOWS) + g_assert_cmpuint (user_time_a, !=, 0); + g_assert_cmpuint (user_time_b, >, user_time_a); +#else + g_assert_cmpuint (user_time_a, ==, 0); + g_assert_cmpuint (user_time_b, ==, 0); +#endif +} + +TESTCASE (process_threads_get_user_time_by_id_other) +{ + volatile gboolean done = FALSE; + GThread * thread; + GumThreadDetails d = { 0, }; + guint64 user_time_a, user_time_b; + + if (!check_thread_enumeration_testable ()) + return; + + thread = create_sleeping_dummy_thread_sync ("user_time", &done, &d.id); + + /* Sleep for a short while to let the other thread wake and run */ + g_usleep (250000); + user_time_a = gum_thead_get_user_time_by_id (d.id); + + /* Sleep for a short while to let the other thread wake and run */ + g_usleep (250000); + user_time_b = gum_thead_get_user_time_by_id (d.id); + +#if defined (HAVE_LINUX) || defined (HAVE_DARWIN) || defined (HAVE_FREEBSD) \ + || defined (HAVE_WINDOWS) + g_assert_cmpuint (user_time_a, !=, 0); + g_assert_cmpuint (user_time_b, >, user_time_a); +#else + g_assert_cmpuint (user_time_b, ==, 0); + g_assert_cmpuint (user_time_a, ==, 0); +#endif + + done = TRUE; + g_thread_join (thread); +} + +static guint64 +gum_thead_get_user_time_by_id (GumThreadId thread_id) +{ + guint64 user_time = 0; + GumStalker * stalker = NULL; + + if (thread_id == gum_process_get_current_thread_id ()) + { + return gum_thead_get_user_time (); + } + + stalker = gum_stalker_new (); + + gum_stalker_run_on_thread_sync (stalker, thread_id, gum_get_user_time, + &user_time); + + while (gum_stalker_garbage_collect (stalker)) + g_usleep (10000); + + g_object_unref (stalker); + return user_time; +} + +static void +gum_get_user_time (const GumCpuContext * cpu_context, gpointer user_data) +{ + guint64 * user_time = (guint64 *) user_data; + *user_time = gum_thead_get_user_time (); +} + TESTCASE (process_modules) { TestForEachContext ctx; @@ -1156,6 +1271,8 @@ sleeping_dummy (gpointer data) pthread_setname_np (pthread_self (), sync_data->name); #endif + do_work (); + g_mutex_lock (&sync_data->mutex); sync_data->started = TRUE; sync_data->thread_id = gum_process_get_current_thread_id (); @@ -1168,6 +1285,22 @@ sleeping_dummy (gpointer data) return NULL; } +static void +do_work (void) +{ + GTimer * timer = g_timer_new (); + + g_timer_start (timer); + /* Do some work and use some CPU cycles */ + static guint no_opt = 0; + while (g_timer_elapsed (timer, NULL) < 0.1) + { + no_opt = no_opt + 1; + } + + g_timer_destroy (timer); +} + static gboolean thread_found_cb (const GumThreadDetails * details, gpointer user_data) diff --git a/tests/gumjs/script.c b/tests/gumjs/script.c index d0a13ce16..a9ab375fb 100644 --- a/tests/gumjs/script.c +++ b/tests/gumjs/script.c @@ -11,6 +11,8 @@ #include "script-fixture.c" +#define NUM_THREADS (10) + TESTLIST_BEGIN (script) TESTENTRY (invalid_script_should_return_null) TESTENTRY (strict_mode_should_be_enforced) @@ -201,6 +203,9 @@ TESTLIST_BEGIN (script) TESTENTRY (process_threads_can_be_enumerated) TESTENTRY (process_threads_can_be_enumerated_legacy_style) TESTENTRY (process_threads_have_names) + TESTENTRY (process_threads_get_user_time) + TESTENTRY (process_threads_get_user_time_other_thread) + TESTENTRY (process_threads_find_busy_thread) TESTENTRY (process_modules_can_be_enumerated) TESTENTRY (process_modules_can_be_enumerated_legacy_style) TESTENTRY (process_module_can_be_looked_up_from_address) @@ -510,6 +515,7 @@ TESTLIST_END () typedef int (* TargetFunctionInt) (int arg); typedef struct _GumInvokeTargetContext GumInvokeTargetContext; typedef struct _GumNamedSleeperContext GumNamedSleeperContext; +typedef struct _GumHotNamedSleeperContext GumHotNamedSleeperContext; typedef struct _TestRunOnThreadSyncContext TestRunOnThreadSyncContext; typedef struct _GumCrashExceptorContext GumCrashExceptorContext; typedef struct _TestTrigger TestTrigger; @@ -528,6 +534,14 @@ struct _GumNamedSleeperContext GAsyncQueue * sleeper_messages; }; +struct _GumHotNamedSleeperContext +{ + GAsyncQueue * controller_messages; + GAsyncQueue * sleeper_messages; + GumThreadId id; + gboolean hot; +}; + struct _TestRunOnThreadSyncContext { GMutex mutex; @@ -590,6 +604,9 @@ static gpointer run_stalked_through_target_function (gpointer data); static gpointer sleeping_dummy (gpointer data); G_GNUC_UNUSED static gpointer named_sleeper (gpointer data); +static gpointer user_time (gpointer data); +static void do_work (void); +static gpointer hot_user_time (gpointer data); static GThread * create_sleeping_dummy_thread_sync (gboolean * done, GumThreadId * thread_id); static gpointer sleeping_dummy_func (gpointer data); @@ -5173,6 +5190,259 @@ named_sleeper (gpointer data) return NULL; } +TESTCASE (process_threads_get_user_time) +{ + double user_time_a = 0, user_time_b = 0; + +#ifdef HAVE_LINUX + if (!check_exception_handling_testable ()) + return; +#endif + +#ifdef HAVE_MIPS + if (!g_test_slow ()) + { + g_print (" "); + return; + } +#endif + + COMPILE_AND_LOAD_SCRIPT ( + "function delay() {" + " const start = new Date();" + " while (new Date() - start < 100);" + "}" + "delay();" + "const time_a = Thread.getUserTime();" + "delay();" + "const time_b = Thread.getUserTime();" + GUM_PTR_CONST".writeDouble(time_a);" + GUM_PTR_CONST".writeDouble(time_b);", + &user_time_a, &user_time_b + ); + EXPECT_NO_MESSAGES (); + +#if defined (HAVE_LINUX) || defined (HAVE_DARWIN) || defined (HAVE_FREEBSD) \ + || defined (HAVE_WINDOWS) + g_assert_true (user_time_a != 0); + g_assert_true (user_time_b > user_time_a); +#else + g_assert_cmpuint (user_time_a, ==, 0); + g_assert_cmpuint (user_time_b, ==, 0); +#endif +} + +TESTCASE (process_threads_get_user_time_other_thread) +{ +#if defined (HAVE_LINUX) && !defined (HAVE_PTHREAD_SETNAME_NP) + g_print (" "); +#else + GumNamedSleeperContext ctx; + GThread * thread; + double user_time_a = 0, user_time_b = 0; + +# ifdef HAVE_LINUX + if (!check_exception_handling_testable ()) + return; +# endif + +# ifdef HAVE_MIPS + if (!g_test_slow ()) + { + g_print (" "); + return; + } +# endif + + ctx.controller_messages = g_async_queue_new (); + ctx.sleeper_messages = g_async_queue_new (); + + thread = g_thread_new ("user-time", user_time, &ctx); + g_assert_cmpstr (g_async_queue_pop (ctx.controller_messages), ==, "ready1"); + + COMPILE_AND_LOAD_SCRIPT ( + "const tid = Process.enumerateThreads()" + " .find(t => t.name === 'user-time')" + " .id;" + "Process.runOnThread(tid, () => {" + " const time = Thread.getUserTime();" + " "GUM_PTR_CONST".writeDouble(time);" + "});", + &user_time_a + ); + EXPECT_NO_MESSAGES (); + + g_async_queue_push (ctx.sleeper_messages, "done1"); + g_assert_cmpstr (g_async_queue_pop (ctx.controller_messages), ==, "ready2"); + + COMPILE_AND_LOAD_SCRIPT ( + "const tid = Process.enumerateThreads()" + " .find(t => t.name === 'user-time')" + " .id;" + "Process.runOnThread(tid, () => {" + " const time = Thread.getUserTime();" + " "GUM_PTR_CONST".writeDouble(time);" + "});", + &user_time_b + ); + EXPECT_NO_MESSAGES (); + + g_async_queue_push (ctx.sleeper_messages, "done2"); + + g_thread_join (thread); + + g_async_queue_unref (ctx.sleeper_messages); + g_async_queue_unref (ctx.controller_messages); + +# if defined (HAVE_LINUX) || defined (HAVE_DARWIN) || defined (HAVE_FREEBSD) \ + || defined (HAVE_WINDOWS) + g_assert_true (user_time_a != 0); + g_assert_true (user_time_b > user_time_a); +# else + g_assert_cmpuint (user_time_a, ==, 0); + g_assert_cmpuint (user_time_b, ==, 0); +# endif +#endif +} + +static gpointer +user_time (gpointer data) +{ + GumNamedSleeperContext * ctx = data; + + /* + * On Linux g_thread_new() may not actually set the thread name, which is due + * to GLib potentially having been prebuilt against an old libc. Therefore we + * set the name manually using pthreads. + */ +#if defined (HAVE_LINUX) && defined (HAVE_PTHREAD_SETNAME_NP) + pthread_setname_np (pthread_self (), "user-time"); +#endif + + do_work (); + g_async_queue_push (ctx->controller_messages, "ready1"); + g_assert_cmpstr (g_async_queue_pop (ctx->sleeper_messages), ==, "done1"); + + do_work (); + g_async_queue_push (ctx->controller_messages, "ready2"); + g_assert_cmpstr (g_async_queue_pop (ctx->sleeper_messages), ==, "done2"); + + return NULL; +} + +static void +do_work (void) +{ + GTimer * timer = g_timer_new (); + + g_timer_start (timer); + /* Do some work and use some CPU cycles */ + static guint no_opt = 0; + while (g_timer_elapsed (timer, NULL) < 0.1) + { + no_opt = no_opt + 1; + } + + g_timer_destroy (timer); +} + +TESTCASE (process_threads_find_busy_thread) +{ +#if defined (HAVE_LINUX) && !defined (HAVE_PTHREAD_SETNAME_NP) + g_print (" "); +#else + GumHotNamedSleeperContext ctx[NUM_THREADS]; + GThread * thread[NUM_THREADS]; + +# ifdef HAVE_LINUX + if (!check_exception_handling_testable ()) + return; +# endif + +# ifdef HAVE_MIPS + if (!g_test_slow ()) + { + g_print (" "); + return; + } +# endif + + guint rand = g_random_int_range (0, 10); + + for (guint i = 0; i < NUM_THREADS; i++) + { + ctx[i].controller_messages = g_async_queue_new (); + ctx[i].sleeper_messages = g_async_queue_new (); + ctx[i].hot = (i == rand); + + thread[i] = g_thread_new ("user-time", hot_user_time, &ctx[i]); + g_assert_cmpstr (g_async_queue_pop (ctx[i].controller_messages), ==, + "ready1"); + + EXPECT_NO_MESSAGES (); + g_async_queue_push (ctx[i].sleeper_messages, "done1"); + + g_assert_cmpstr (g_async_queue_pop (ctx[i].controller_messages), ==, + "ready2"); + } + + COMPILE_AND_LOAD_SCRIPT ( + "const times = Process.enumerateThreads()" + " .filter(t => t.name === 'user-time')" + " .map(t => Process.runOnThread(t.id, () => {" + " return {" + " 'id' : t.id," + " 'time' : Thread.getUserTime()" + " };" + " }));" + "Promise.all(times).then((t) => {" + " const result = t.sort((a,b) => b.time - a.time)[0].id;" + " send(result);" + "});" + ); + + for (guint i = 0; i < NUM_THREADS; i++) + { + g_async_queue_push (ctx[i].sleeper_messages, "done2"); + g_thread_join (thread[i]); + g_async_queue_unref (ctx[i].sleeper_messages); + g_async_queue_unref (ctx[i].controller_messages); + } + +# if defined (HAVE_LINUX) || defined (HAVE_DARWIN) || defined (HAVE_FREEBSD) \ + || defined (HAVE_WINDOWS) + EXPECT_SEND_MESSAGE_WITH ("%" G_GSIZE_MODIFIER "u", ctx[rand].id); +# endif +#endif +} + +static gpointer +hot_user_time (gpointer data) +{ + GumHotNamedSleeperContext * ctx = data; + + /* + * On Linux g_thread_new() may not actually set the thread name, which is due + * to GLib potentially having been prebuilt against an old libc. Therefore we + * set the name manually using pthreads. + */ +#ifdef HAVE_LINUX + pthread_setname_np (pthread_self (), "user-time"); +#endif + + ctx->id = gum_process_get_current_thread_id (); + + g_async_queue_push (ctx->controller_messages, "ready1"); + g_assert_cmpstr (g_async_queue_pop (ctx->sleeper_messages), ==, "done1"); + + if (ctx->hot) + do_work (); + g_async_queue_push (ctx->controller_messages, "ready2"); + g_assert_cmpstr (g_async_queue_pop (ctx->sleeper_messages), ==, "done2"); + + return NULL; +} + TESTCASE (process_modules_can_be_enumerated) { COMPILE_AND_LOAD_SCRIPT (