diff --git a/android/app/src/main/cpp/iperf_jni.c b/android/app/src/main/cpp/iperf_jni.c index f6183aa..a19a7c7 100644 --- a/android/app/src/main/cpp/iperf_jni.c +++ b/android/app/src/main/cpp/iperf_jni.c @@ -14,8 +14,6 @@ #include #include #include -#include -#include #include "iperf.h" #include "iperf_api.h" @@ -30,24 +28,56 @@ static struct iperf_test *g_test = NULL; static volatile int g_stop = 0; -/* Callback interface in Java/Kotlin */ -typedef void (*output_callback_t)(JNIEnv *env, jobject callback, jstring line); -typedef void (*error_callback_t)(JNIEnv *env, jobject callback, jstring msg); -typedef void (*complete_callback_t)(JNIEnv *env, jobject callback); - -/* Per-call state to pass between JNI and the reader thread */ +/* Per-call state to pass between JNI and the reader thread. + * We store the callback object (global ref) and method IDs. + * Callbacks are invoked via CallVoidMethod, NOT as function pointers. */ struct iperf_run_state { JNIEnv *env; - jobject callback; + jobject callback_ref; /* Global ref to the callback object */ + jmethodID mid_output; /* Method ID for onOutput(String) */ + jmethodID mid_error; /* Method ID for onError(String) */ + jmethodID mid_complete; /* Method ID for onComplete() */ FILE *outfile; int pipe_fd[2]; JavaVM *jvm; pthread_t reader_thread; - output_callback_t on_output; - error_callback_t on_error; - complete_callback_t on_complete; }; +/* Helper: invoke the output callback via CallVoidMethod */ +static void signal_output(struct iperf_run_state *state, const char *msg) { + if (!state || !msg || !msg[0]) return; + JNIEnv *env = state->env; + if (!env || !state->mid_output) return; + + jstring jline = (*env)->NewStringUTF(env, msg); + if (jline) { + (*env)->CallVoidMethod(env, state->callback_ref, state->mid_output, jline); + (*env)->DeleteLocalRef(env, jline); + } +} + +/* Helper: invoke the error callback via CallVoidMethod */ +static void signal_error(struct iperf_run_state *state, const char *msg) { + if (!state || !msg) return; + JNIEnv *env = state->env; + if (!env || !state->mid_error) return; + + jstring jmsg = (*env)->NewStringUTF(env, msg); + if (jmsg) { + (*env)->CallVoidMethod(env, state->callback_ref, state->mid_error, jmsg); + (*env)->DeleteLocalRef(env, jmsg); + } +} + +/* Helper: invoke the complete callback via CallVoidMethod */ +static void signal_complete(struct iperf_run_state *state) { + if (!state || !state->mid_complete) return; + JNIEnv *env = state->env; + if (!env) return; + + (*env)->CallVoidMethod(env, state->callback_ref, state->mid_complete); +} + /* ------------------------------------------------------------------ */ /* Output reader thread — reads from iperf's output pipe and forwards */ /* each line to the Java callback via JNI. */ @@ -64,10 +94,6 @@ static void *reader_thread_func(void *arg) { return NULL; } - jclass callback_cls = (*env)->GetObjectClass(env, state->callback); - jmethodID mid_output = (*env)->GetMethodID( - env, callback_cls, "onOutput", "(Ljava/lang/String;)V"); - char buf[1024]; FILE *fp = fdopen(state->pipe_fd[0], "r"); if (fp) { @@ -77,8 +103,8 @@ static void *reader_thread_func(void *arg) { continue; jstring line = (*env)->NewStringUTF(env, buf); if (line != NULL) { - (*env)->CallVoidMethod(env, state->callback, - mid_output, line); + (*env)->CallVoidMethod(env, state->callback_ref, + state->mid_output, line); (*env)->DeleteLocalRef(env, line); } } @@ -133,40 +159,6 @@ static void free_argv(char **argv, int argc) { free(argv); } -/* ------------------------------------------------------------------ */ -/* Helper: signal an error through the Java callback. */ -/* ------------------------------------------------------------------ */ -static void signal_error(struct iperf_run_state *state, const char *msg) { - if (!state || !msg) return; - JNIEnv *env = state->env; - if (!env) return; - - if (state->on_error) { - jstring jmsg = (*env)->NewStringUTF(env, msg); - if (jmsg) { - state->on_error(env, state->callback, jmsg); - (*env)->DeleteLocalRef(env, jmsg); - } - } -} - -/* ------------------------------------------------------------------ */ -/* Helper: signal output through the Java callback. */ -/* ------------------------------------------------------------------ */ -static void signal_output(struct iperf_run_state *state, const char *msg) { - if (!state || !msg || !msg[0]) return; - JNIEnv *env = state->env; - if (!env) return; - - if (state->on_output) { - jstring jline = (*env)->NewStringUTF(env, msg); - if (jline) { - state->on_output(env, state->callback, jline); - (*env)->DeleteLocalRef(env, jline); - } - } -} - /* ------------------------------------------------------------------ */ /* JNI: Start an iperf3 test with the given arguments. */ /* Signature: void runIperfLive(String[] args, IperfCallback cb) */ @@ -176,18 +168,16 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive( JNIEnv *env, jobject thiz, jobjectArray arguments, jobject callback) { - /* Resolve callback methods */ - jclass callback_cls = (*env)->GetObjectClass(env, callback); - jmethodID mid_output = (*env)->GetMethodID( - env, callback_cls, "onOutput", "(Ljava/lang/String;)V"); - jmethodID mid_error = (*env)->GetMethodID( - env, callback_cls, "onError", "(Ljava/lang/String;)V"); - jmethodID mid_complete = (*env)->GetMethodID( - env, callback_cls, "onComplete", "()V"); + /* Create the iperf test object */ + struct iperf_test *test = iperf_new_test(); + if (!test) { + const char *err = "Failed to create iperf test"; + LOGE("%s", err); + return; + } - /* Get JavaVM for thread attachment */ - JavaVM *jvm; - (*env)->GetJavaVM(env, &jvm); + iperf_defaults(test); + g_test = test; /* Convert Java String[] -> char** argv */ int argc = 0; @@ -195,6 +185,22 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive( if (!argv) { const char *err = "Failed to parse arguments"; signal_error((struct iperf_run_state *) 0, err); + iperf_free_test(test); + g_test = NULL; + return; + } + + /* Parse arguments */ + if (iperf_parse_arguments(test, 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); + + iperf_free_test(test); + g_test = NULL; return; } @@ -204,11 +210,11 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive( free_argv(argv, argc); const char *err = "Failed to create output pipe"; LOGE("pipe() failed: %s", strerror(errno)); - if (mid_error) { - jstring jmsg = (*env)->NewStringUTF(env, err); - (*env)->CallVoidMethod(env, callback, mid_error, jmsg); - (*env)->DeleteLocalRef(env, jmsg); - } + + signal_error((struct iperf_run_state *) 0, err); + + iperf_free_test(test); + g_test = NULL; return; } @@ -219,63 +225,59 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive( free_argv(argv, argc); close(pipefd[0]); close(pipefd[1]); + iperf_free_test(test); + g_test = NULL; return; } + + /* Resolve callback method IDs (do this in the calling thread) */ + jclass callback_cls = (*env)->GetObjectClass(env, callback); + state->mid_output = (*env)->GetMethodID( + env, callback_cls, "onOutput", "(Ljava/lang/String;)V"); + state->mid_error = (*env)->GetMethodID( + env, callback_cls, "onError", "(Ljava/lang/String;)V"); + state->mid_complete = (*env)->GetMethodID( + env, callback_cls, "onComplete", "()V"); + + /* Get JavaVM for thread attachment */ + JavaVM *jvm; + (*env)->GetJavaVM(env, &jvm); + state->env = env; - state->callback = (*env)->NewGlobalRef(env, callback); + state->callback_ref = (*env)->NewGlobalRef(env, callback); state->outfile = fdopen(pipefd[1], "w"); state->pipe_fd[0] = pipefd[0]; state->jvm = jvm; - state->on_output = mid_output - ? (output_callback_t) mid_output : NULL; - state->on_error = mid_error - ? (error_callback_t) mid_error : NULL; - state->on_complete = mid_complete - ? (complete_callback_t) mid_complete : NULL; - - /* Create the iperf test object */ - struct iperf_test *test = iperf_new_test(); - if (!test) { - free_argv(argv, argc); - free(state); - close(pipefd[0]); - const char *err = "Failed to create iperf test"; - LOGE("iperf_new_test() returned NULL"); - return; - } - - iperf_defaults(test); - g_test = test; /* Redirect iperf's output to our pipe */ test->outfile = state->outfile; setvbuf(test->outfile, NULL, _IOLBF, 0); /* Line-buffered */ - /* Parse arguments */ - if (iperf_parse_arguments(test, argc, argv) < 0) { - char errbuf[256]; - snprintf(errbuf, sizeof(errbuf), "Argument parse error: %s", - iperf_strerror(i_errno)); + /* Notify Java that the test is starting */ + signal_output(state, "[iPerf JNI] Starting iPerf3 client...\n"); + + /* 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); - - signal_error(state, errbuf); - - fclose(test->outfile); + 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; - - /* Cleanup */ free(state); return; } - /* Notify Java that the test is starting */ - signal_output(state, "🚀 Initiating iPerf3 client request...\n"); - /* Run the test */ g_stop = 0; int ret = iperf_run_client(test); + /* Wait for reader thread to finish */ + pthread_join(state->reader_thread, NULL); + /* Flush and close output */ fflush(state->outfile); fclose(state->outfile); @@ -287,25 +289,23 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive( /* Free argv */ free_argv(argv, argc); - /* Signal completion */ + /* 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), "Test failed: %s", + 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_output(state, "[iPerf JNI] Test completed successfully.\n"); } /* Signal completion to Java */ - if (state->on_complete) { - state->on_complete(env, state->callback); - } + signal_complete(state); - /* Free the state struct */ + /* Free global ref and state struct */ + (*env)->DeleteGlobalRef(env, state->callback_ref); free(state); } @@ -340,21 +340,6 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_forceStopIperfTest( } } -/* ------------------------------------------------------------------ */ -/* JNI: Get elapsed milliseconds since test started. */ -/* Signature: int getTestProgress() */ -/* ------------------------------------------------------------------ */ -JNIEXPORT jint JNICALL -Java_com_novoyuuparosk_iperf3gui_IperfRunner_getTestProgress(JNIEnv *env, - jobject thiz) { - if (!g_test) return -1; - - /* Get elapsed time in usecs and convert to int ms */ - /* Note: iperf3 3.19 doesn't expose start_time on iperf_test struct. - * We track it locally via a static variable set at test start. */ - return (jint)(g_test->duration * 1000); /* fallback: return configured duration */ -} - /* ------------------------------------------------------------------ */ /* JNI: Get whether iperf is currently running. */ /* Signature: boolean isTestRunning() */ 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 771cfd9..2da92f9 100644 --- a/android/app/src/main/kotlin/com/novoyuuparosk/iperf3gui/IperfRunner.kt +++ b/android/app/src/main/kotlin/com/novoyuuparosk/iperf3gui/IperfRunner.kt @@ -27,7 +27,7 @@ object IperfRunner : FlutterPlugin { channel?.setMethodCallHandler { call, result -> when (call.method) { "runIperf" -> { - val args = call.argument>("args") + val args = call.argument>("args")?.toTypedArray() if (args != null) { runIperfLive(args, object : IperfCallback { override fun onOutput(line: String) { diff --git a/lib/ui/features/test_run/test_run_screen.dart b/lib/ui/features/test_run/test_run_screen.dart index 3fcc321..715ca5f 100644 --- a/lib/ui/features/test_run/test_run_screen.dart +++ b/lib/ui/features/test_run/test_run_screen.dart @@ -30,7 +30,7 @@ class _TestRunContent extends StatefulWidget { } class _TestRunContentState extends State<_TestRunContent> { - final _serverController = TextEditingController(text: 'iperf.es.net'); + final _serverController = TextEditingController(text: 'speedtest.uztelecom.uz'); final _portController = TextEditingController(text: '5201'); @override