diff --git a/android/app/src/main/cpp/iperf_jni.c b/android/app/src/main/cpp/iperf_jni.c index a19a7c7..2a8dc03 100644 --- a/android/app/src/main/cpp/iperf_jni.c +++ b/android/app/src/main/cpp/iperf_jni.c @@ -4,6 +4,13 @@ * This file exposes iperf3's API to Java/Kotlin via JNI, enabling * the Flutter app to run iperf3 tests natively inside the app process * (no Process.start needed, no SELinux restrictions). + * + * SIGNAL HANDLING NOTE: + * iperf3's run() function (called from iperf_run_client()) uses + * setjmp/longjmp for graceful signal handling. The JNI bridge must + * replicate this setup — otherwise iperf_catch_sigend() installs + * handlers that longjmp to an unset buffer, causing undefined + * behaviour (typically a SIGSEGV or hang). */ #include @@ -11,6 +18,8 @@ #include #include #include +#include +#include #include #include #include @@ -159,6 +168,62 @@ static void free_argv(char **argv, int argc) { free(argv); } +/* ------------------------------------------------------------------ */ +/* Internal: clean up after iperf_run_client returns (normal path). */ +/* Must be called only on the normal return path (NOT after longjmp). */ +/* ------------------------------------------------------------------ */ +static void iperf_cleanup_and_signal(JNIEnv *env, + struct iperf_run_state *state, + char **argv, int argc, + int ret, int stopped) { + /* Wait for reader thread to finish */ + pthread_join(state->reader_thread, NULL); + + /* Flush and close output */ + fflush(state->outfile); + fclose(state->outfile); + + /* Clean up iperf */ + iperf_free_test(g_test); + g_test = NULL; + + /* Free argv */ + free_argv(argv, argc); + + /* Signal result to Java */ + if (stopped) { + signal_output(state, "[iPerf JNI] Test was stopped by user.\n"); + } else if (ret < 0) { + char errbuf[256]; + snprintf(errbuf, sizeof(errbuf), + "[iPerf JNI] Test failed: %s\n", + iperf_strerror(i_errno)); + signal_error(state, errbuf); + } else { + signal_output(state, "[iPerf JNI] Test completed successfully.\n"); + } + + /* Signal completion to Java */ + signal_complete(state); + + /* Free global ref and state struct */ + (*env)->DeleteGlobalRef(env, state->callback_ref); + free(state); +} + +/* ------------------------------------------------------------------ */ +/* Per-call signal handling state. */ +/* Must be static so it survives across the longjmp back to setjmp. */ +/* ------------------------------------------------------------------ */ +static jmp_buf g_sigend_jmp_buf; +static volatile int g_signed_sig = 0; + +/* Signal handler — mirrors main.c::sigend_handler */ +static void sigend_handler(int sig) { + g_signed_sig = sig; + longjmp(g_sigend_jmp_buf, 1); +} + /* ------------------------------------------------------------------ */ /* JNI: Start an iperf3 test with the given arguments. */ /* Signature: void runIperfLive(String[] args, IperfCallback cb) */ @@ -179,26 +244,47 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive( iperf_defaults(test); g_test = test; - /* Convert Java String[] -> char** argv */ - int argc = 0; - char **argv = java_string_array_to_argv(env, arguments, &argc); + /* + * Convert Java String[] -> char** argv, prepending "iperf3" as argv[0]. + * iperf_parse_arguments() expects argv[0] to be the program name; + * passing args directly from Dart means argv[0] would be "-c", + * which iperf3 misinterprets as the program name, not the client flag. + */ + int dart_argc = (*env)->GetArrayLength(env, arguments); + int total_argc = dart_argc + 1; /* +1 for "iperf3" */ + char **argv = malloc((total_argc + 1) * sizeof(char *)); if (!argv) { - const char *err = "Failed to parse arguments"; - signal_error((struct iperf_run_state *) 0, err); + const char *err = "Failed to allocate argv"; + LOGE("%s", err); iperf_free_test(test); g_test = NULL; return; } + argv[0] = strdup("iperf3"); + for (int i = 0; i < dart_argc; i++) { + jstring jstr = (jstring) (*env)->GetObjectArrayElement(env, arguments, i); + const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL); + if (cstr) { + argv[i + 1] = strdup(cstr); + (*env)->ReleaseStringUTFChars(env, jstr, cstr); + } else { + argv[i + 1] = strdup(""); + } + (*env)->DeleteLocalRef(env, jstr); + } + argv[total_argc] = NULL; /* NULL-terminated for iperf */ + + /* Set a reasonable connect timeout (30s) to avoid hanging on DNS resolution. + * Default iperf3 connect timeout is 120s which is far too long. */ + test->settings->connect_timeout = 30000; /* Parse arguments */ - if (iperf_parse_arguments(test, argc, argv) < 0) { + if (iperf_parse_arguments(test, total_argc, argv) < 0) { char errbuf[256]; - snprintf(errbuf, sizeof(errbuf), "Argument parse error: %s", - iperf_strerror(i_errno)); - free_argv(argv, argc); - - signal_error((struct iperf_run_state *) 0, errbuf); - + snprintf(errbuf, sizeof(errbuf), + "Argument parse error: %s", iperf_strerror(i_errno)); + LOGE("%s", errbuf); + free_argv(argv, total_argc); iperf_free_test(test); g_test = NULL; return; @@ -207,7 +293,7 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive( /* Create a pipe to capture iperf's stdout/stderr */ int pipefd[2]; if (pipe(pipefd) != 0) { - free_argv(argv, argc); + free_argv(argv, total_argc); const char *err = "Failed to create output pipe"; LOGE("pipe() failed: %s", strerror(errno)); @@ -222,7 +308,7 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive( struct iperf_run_state *state = calloc(1, sizeof(struct iperf_run_state)); if (!state) { - free_argv(argv, argc); + free_argv(argv, total_argc); close(pipefd[0]); close(pipefd[1]); iperf_free_test(test); @@ -253,60 +339,72 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive( test->outfile = state->outfile; setvbuf(test->outfile, NULL, _IOLBF, 0); /* Line-buffered */ - /* Notify Java that the test is starting */ - signal_output(state, "[iPerf JNI] Starting iPerf3 client...\n"); + /* + * Set up signal handling BEFORE calling iperf_run_client(). + * iperf3's run() function uses setjmp/longjmp for signal handling. + * - setjmp returns 0 on first call (normal path) + * - setjmp returns non-zero when called via longjmp (signal path) + * + * Both paths share the same `state`, `argv`, `argc` variables, + * so the stack frame is preserved across the longjmp. + */ + if (setjmp(g_sigend_jmp_buf) == 0) { + /* Normal path — run the test */ + signal(SIGPIPE, SIG_IGN); /* Ignore SIGPIPE for socket errors */ + iperf_catch_sigend(sigend_handler); - /* Spawn reader thread to forward iperf output to Java */ - if (pthread_create(&state->reader_thread, NULL, - reader_thread_func, state) != 0) { - free_argv(argv, argc); - close(pipefd[0]); - close(pipefd[1]); - const char *err = "Failed to start output reader thread"; - LOGE("%s", err); - signal_error(state, err); - iperf_free_test(test); - g_test = NULL; - free(state); - return; - } + /* Notify Java that the test is starting */ + signal_output(state, "[iPerf JNI] Starting iPerf3 client...\n"); - /* Run the test */ - g_stop = 0; - int ret = iperf_run_client(test); + /* Spawn reader thread to forward iperf output to Java */ + if (pthread_create(&state->reader_thread, NULL, + reader_thread_func, state) != 0) { + free_argv(argv, total_argc); + close(pipefd[0]); + close(pipefd[1]); + const char *err = "Failed to start output reader thread"; + LOGE("%s", err); + signal_error(state, err); + iperf_free_test(test); + g_test = NULL; + free(state); + return; + } - /* Wait for reader thread to finish */ - pthread_join(state->reader_thread, NULL); + /* Run the test */ + g_stop = 0; + int ret = iperf_run_client(test); - /* Flush and close output */ - fflush(state->outfile); - fclose(state->outfile); - - /* Clean up iperf */ - iperf_free_test(test); - g_test = NULL; - - /* Free argv */ - free_argv(argv, argc); - - /* Signal result to Java */ - if (g_stop) { - signal_output(state, "[iPerf JNI] Test was stopped by user.\n"); - } else if (ret < 0) { - char errbuf[256]; - snprintf(errbuf, sizeof(errbuf), "[iPerf JNI] Test failed: %s\n", - iperf_strerror(i_errno)); - signal_error(state, errbuf); + /* + * We get here after iperf_run_client returns normally. + * If a signal arrives during iperf_run_client(), the + * sigend_handler fires longjmp() and we never reach here. + */ + iperf_cleanup_and_signal(env, state, argv, total_argc, ret, 0); } else { - signal_output(state, "[iPerf JNI] Test completed successfully.\n"); + /* + * We got here via longjmp() from sigend_handler. + * Clean up resources allocated before the setjmp. + * NOTE: state, argv, argc are still valid on the stack. + */ + LOGI("Test interrupted by signal %d", g_signed_sig); + + /* Stop flag for cleanup */ + g_stop = 1; + + /* Close the output file (the pipe write end) */ + if (state->outfile) { + fclose(state->outfile); + } + + /* Free argv (argv is still valid across longjmp since it's on stack) */ + free_argv(argv, total_argc); + + /* Run the cleanup with stopped=true. + * Note: argv is NULL, argc=0 so free_argv is a no-op. + * The state struct will be freed inside this function. */ + iperf_cleanup_and_signal(env, state, NULL, 0, 0, 1); } - - /* Signal completion to Java */ - signal_complete(state); - - /* Free global ref and state struct */ - (*env)->DeleteGlobalRef(env, state->callback_ref); - free(state); } /* ------------------------------------------------------------------ */ diff --git a/android/app/src/main/kotlin/com/novoyuuparosk/iperf3gui/IperfRunner.kt b/android/app/src/main/kotlin/com/novoyuuparosk/iperf3gui/IperfRunner.kt index 2da92f9..ffc72b8 100644 --- a/android/app/src/main/kotlin/com/novoyuuparosk/iperf3gui/IperfRunner.kt +++ b/android/app/src/main/kotlin/com/novoyuuparosk/iperf3gui/IperfRunner.kt @@ -19,7 +19,14 @@ object IperfRunner : FlutterPlugin { init { // Load native library at class initialization - System.loadLibrary("iperf3jni") + try { + android.util.Log.i("IperfRunner", "Loading native library...") + System.loadLibrary("iperf3jni") + android.util.Log.i("IperfRunner", "Native library loaded successfully") + } catch (e: UnsatisfiedLinkError) { + android.util.Log.e("IperfRunner", "Failed to load native library: ${e.message}") + e.printStackTrace() + } } override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {