From af26a1c38564c28c221e8daedb0bc33785ee7318 Mon Sep 17 00:00:00 2001 From: Irene Date: Wed, 26 Aug 2026 18:30:20 +0900 Subject: [PATCH] fix(jni): set app cache dir as temp dir and wrap callbacks in Handler - Set app cache dir as temp directory for mkstemp() to avoid EACCES on Android sandbox (iperf_set_test_template). - Wrap all MethodChannel callbacks in MainThreadCallback with Handler(Looper.getMainLooper()) to prevent SIGABRT from calling invokeMethod from native reader thread. - Add setTempDir JNI function and logging for test return value. --- android/app/src/main/cpp/iperf_jni.c | 27 +++++- .../novoyuuparosk/iperf3gui/IperfRunner.kt | 97 ++++++++++--------- 2 files changed, 79 insertions(+), 45 deletions(-) diff --git a/android/app/src/main/cpp/iperf_jni.c b/android/app/src/main/cpp/iperf_jni.c index 2a8dc03..a8f874b 100644 --- a/android/app/src/main/cpp/iperf_jni.c +++ b/android/app/src/main/cpp/iperf_jni.c @@ -36,6 +36,7 @@ /* Global state for controlling the iperf test */ static struct iperf_test *g_test = NULL; static volatile int g_stop = 0; +static char g_tmp_template[1024] = "/data/local/tmp/iperf3.XXXXXX"; /* Per-call state to pass between JNI and the reader thread. * We store the callback object (global ref) and method IDs. @@ -283,7 +284,7 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive( char errbuf[256]; snprintf(errbuf, sizeof(errbuf), "Argument parse error: %s", iperf_strerror(i_errno)); - LOGE("%s", errbuf); + LOGE("Parse error: %s", errbuf); free_argv(argv, total_argc); iperf_free_test(test); g_test = NULL; @@ -339,6 +340,12 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive( test->outfile = state->outfile; setvbuf(test->outfile, NULL, _IOLBF, 0); /* Line-buffered */ + /* Set temp directory for stream buffers (mkstemp in iperf_new_stream). + * On Android, /data/local/tmp is NOT writable from the app sandbox — + * use the app's cache dir instead. iperf_set_test_template() handles + * the strdup internally. */ + iperf_set_test_template(test, g_tmp_template); + /* * Set up signal handling BEFORE calling iperf_run_client(). * iperf3's run() function uses setjmp/longjmp for signal handling. @@ -374,6 +381,7 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive( /* Run the test */ g_stop = 0; int ret = iperf_run_client(test); + LOGI("iperf_run_client returned: %d i_errno=%d strerror=%s ctrl_sck=%d", ret, i_errno, iperf_strerror(i_errno), test->ctrl_sck); /* * We get here after iperf_run_client returns normally. @@ -447,3 +455,20 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_isTestRunning(JNIEnv *env, jobject thiz) { return (g_test != NULL && !g_test->done); } + +/* ------------------------------------------------------------------ */ +/* JNI: Set the temp directory for stream buffers. */ +/* Signature: void setTempDir(String tmpDir) */ +/* ------------------------------------------------------------------ */ +JNIEXPORT void JNICALL +Java_com_novoyuuparosk_iperf3gui_IperfRunner_setTempDir(JNIEnv *env, + jobject thiz, + jstring tmpDir) { + if (!tmpDir) return; + const char *cstr = (*env)->GetStringUTFChars(env, tmpDir, NULL); + if (cstr) { + snprintf(g_tmp_template, sizeof(g_tmp_template), "%s/iperf3.XXXXXX", cstr); + (*env)->ReleaseStringUTFChars(env, tmpDir, cstr); + LOGI("Temp dir set to: %s", g_tmp_template); + } +} 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 ffc72b8..2e57025 100644 --- a/android/app/src/main/kotlin/com/novoyuuparosk/iperf3gui/IperfRunner.kt +++ b/android/app/src/main/kotlin/com/novoyuuparosk/iperf3gui/IperfRunner.kt @@ -1,24 +1,24 @@ package com.novoyuuparosk.iperf3gui +import android.os.Handler +import android.os.Looper import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodChannel /** * Kotlin binding layer for native iperf3 via JNI. * - * This class loads the native library (libiperf3jni.so) and exposes - * iperf3 functionality to Flutter via MethodChannel callbacks. - * * iperf3 runs natively inside the app process — no Process.start() needed, * no SELinux restrictions. Output is streamed live to the Flutter UI via - * the IperfCallback interface. + * the IperfCallback interface, with all callbacks posted to the main thread. */ object IperfRunner : FlutterPlugin { private var channel: MethodChannel? = null + private var appContext: android.content.Context? = null + private val mainHandler = Handler(Looper.getMainLooper()) init { - // Load native library at class initialization try { android.util.Log.i("IperfRunner", "Loading native library...") System.loadLibrary("iperf3jni") @@ -30,48 +30,55 @@ object IperfRunner : FlutterPlugin { } override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + appContext = binding.applicationContext channel = MethodChannel(binding.binaryMessenger, "com.novoyuuparosk.iperf3gui/iperf") channel?.setMethodCallHandler { call, result -> when (call.method) { "runIperf" -> { val args = call.argument>("args")?.toTypedArray() if (args != null) { - runIperfLive(args, object : IperfCallback { - override fun onOutput(line: String) { + // Set temp dir to app's cache dir — /data/local/tmp is NOT writable + appContext?.cacheDir?.absolutePath?.let { cacheDir -> + setTempDir(cacheDir) + } + // Wrap callbacks so they always run on the main thread. + // The native reader thread cannot call invokeMethod directly. + val safeCallback = MainThreadCallback( + onOutput = { line -> channel?.invokeMethod("onOutput", mapOf("output" to line)) - } - override fun onError(error: String) { + }, + onError = { error -> channel?.invokeMethod("onError", mapOf("error" to error)) - } - override fun onComplete() { + }, + onComplete = { channel?.invokeMethod("onComplete", emptyMap()) } - }) + ) + runIperfLive(args, safeCallback) result.success(null) } else { result.error("INVALID_ARGS", "args must be a String array", null) } } "stopIperf" -> { - forceStopIperfTest(object : IperfCallback { - override fun onOutput(line: String) { + val safeCallback = MainThreadCallback( + onOutput = { line -> channel?.invokeMethod("onOutput", mapOf("output" to line)) - } - override fun onError(error: String) { + }, + onError = { error -> channel?.invokeMethod("onError", mapOf("error" to error)) - } - override fun onComplete() { + }, + onComplete = { channel?.invokeMethod("onComplete", emptyMap()) } - }) + ) + forceStopIperfTest(safeCallback) result.success(null) } "isTestRunning" -> { result.success(isTestRunning()) } - else -> { - result.notImplemented() - } + else -> result.notImplemented() } } } @@ -84,37 +91,39 @@ object IperfRunner : FlutterPlugin { // Native method bindings // ───────────────────────────────────────────────────────────────── - /** - * Start an iperf3 test with the given arguments. - * This calls the native C function via JNI. - */ external fun runIperfLive(arguments: Array, callback: IperfCallback) - - /** - * Gracefully stop a running iperf3 test. - */ external fun forceStopIperfTest(callback: IperfCallback) - - /** - * Check if an iperf3 test is currently running. - * @return true if a test is in progress, false otherwise - */ + external fun setTempDir(tmpDir: String) external fun isTestRunning(): Boolean /** * Callback interface for iperf3 test output. - * - * Used by the native layer to stream test results back to Flutter - * in real-time. + * Used by the native layer to stream results back to Flutter in real-time. */ interface IperfCallback { - /** Called with each line of iperf3 output (stdout/stderr) */ fun onOutput(line: String) - - /** Called when an error occurs */ fun onError(error: String) - - /** Called when the test completes (success or failure) */ fun onComplete() } -} + + /** + * Thread-safe wrapper: posts all callbacks to the main thread. + * This is required because the native reader thread cannot safely + * call MethodChannel.invokeMethod from a background thread. + */ + private class MainThreadCallback( + private val onOutput: (String) -> Unit, + private val onError: (String) -> Unit, + private val onComplete: () -> Unit + ) : IperfCallback { + override fun onOutput(line: String) { + mainHandler.post { onOutput(line) } + } + override fun onError(error: String) { + mainHandler.post { onError(error) } + } + override fun onComplete() { + mainHandler.post { onComplete() } + } + } +} \ No newline at end of file