fix(jni): set app cache dir as temp dir and wrap callbacks in Handler
CI / Analyze & Test (push) Failing after 43s
CI / Analyze & Test (push) Failing after 43s
- 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.
This commit is contained in:
@@ -36,6 +36,7 @@
|
|||||||
/* Global state for controlling the iperf test */
|
/* Global state for controlling the iperf test */
|
||||||
static struct iperf_test *g_test = NULL;
|
static struct iperf_test *g_test = NULL;
|
||||||
static volatile int g_stop = 0;
|
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.
|
/* Per-call state to pass between JNI and the reader thread.
|
||||||
* We store the callback object (global ref) and method IDs.
|
* We store the callback object (global ref) and method IDs.
|
||||||
@@ -283,7 +284,7 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive(
|
|||||||
char errbuf[256];
|
char errbuf[256];
|
||||||
snprintf(errbuf, sizeof(errbuf),
|
snprintf(errbuf, sizeof(errbuf),
|
||||||
"Argument parse error: %s", iperf_strerror(i_errno));
|
"Argument parse error: %s", iperf_strerror(i_errno));
|
||||||
LOGE("%s", errbuf);
|
LOGE("Parse error: %s", errbuf);
|
||||||
free_argv(argv, total_argc);
|
free_argv(argv, total_argc);
|
||||||
iperf_free_test(test);
|
iperf_free_test(test);
|
||||||
g_test = NULL;
|
g_test = NULL;
|
||||||
@@ -339,6 +340,12 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive(
|
|||||||
test->outfile = state->outfile;
|
test->outfile = state->outfile;
|
||||||
setvbuf(test->outfile, NULL, _IOLBF, 0); /* Line-buffered */
|
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().
|
* Set up signal handling BEFORE calling iperf_run_client().
|
||||||
* iperf3's run() function uses setjmp/longjmp for signal handling.
|
* iperf3's run() function uses setjmp/longjmp for signal handling.
|
||||||
@@ -374,6 +381,7 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive(
|
|||||||
/* Run the test */
|
/* Run the test */
|
||||||
g_stop = 0;
|
g_stop = 0;
|
||||||
int ret = iperf_run_client(test);
|
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.
|
* We get here after iperf_run_client returns normally.
|
||||||
@@ -447,3 +455,20 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_isTestRunning(JNIEnv *env,
|
|||||||
jobject thiz) {
|
jobject thiz) {
|
||||||
return (g_test != NULL && !g_test->done);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
package com.novoyuuparosk.iperf3gui
|
package com.novoyuuparosk.iperf3gui
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kotlin binding layer for native iperf3 via JNI.
|
* 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,
|
* iperf3 runs natively inside the app process — no Process.start() needed,
|
||||||
* no SELinux restrictions. Output is streamed live to the Flutter UI via
|
* 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 {
|
object IperfRunner : FlutterPlugin {
|
||||||
|
|
||||||
private var channel: MethodChannel? = null
|
private var channel: MethodChannel? = null
|
||||||
|
private var appContext: android.content.Context? = null
|
||||||
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Load native library at class initialization
|
|
||||||
try {
|
try {
|
||||||
android.util.Log.i("IperfRunner", "Loading native library...")
|
android.util.Log.i("IperfRunner", "Loading native library...")
|
||||||
System.loadLibrary("iperf3jni")
|
System.loadLibrary("iperf3jni")
|
||||||
@@ -30,48 +30,55 @@ object IperfRunner : FlutterPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
appContext = binding.applicationContext
|
||||||
channel = MethodChannel(binding.binaryMessenger, "com.novoyuuparosk.iperf3gui/iperf")
|
channel = MethodChannel(binding.binaryMessenger, "com.novoyuuparosk.iperf3gui/iperf")
|
||||||
channel?.setMethodCallHandler { call, result ->
|
channel?.setMethodCallHandler { call, result ->
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"runIperf" -> {
|
"runIperf" -> {
|
||||||
val args = call.argument<List<String>>("args")?.toTypedArray()
|
val args = call.argument<List<String>>("args")?.toTypedArray()
|
||||||
if (args != null) {
|
if (args != null) {
|
||||||
runIperfLive(args, object : IperfCallback {
|
// Set temp dir to app's cache dir — /data/local/tmp is NOT writable
|
||||||
override fun onOutput(line: String) {
|
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))
|
channel?.invokeMethod("onOutput", mapOf("output" to line))
|
||||||
}
|
},
|
||||||
override fun onError(error: String) {
|
onError = { error ->
|
||||||
channel?.invokeMethod("onError", mapOf("error" to error))
|
channel?.invokeMethod("onError", mapOf("error" to error))
|
||||||
}
|
},
|
||||||
override fun onComplete() {
|
onComplete = {
|
||||||
channel?.invokeMethod("onComplete", emptyMap<String, Any>())
|
channel?.invokeMethod("onComplete", emptyMap<String, Any>())
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
runIperfLive(args, safeCallback)
|
||||||
result.success(null)
|
result.success(null)
|
||||||
} else {
|
} else {
|
||||||
result.error("INVALID_ARGS", "args must be a String array", null)
|
result.error("INVALID_ARGS", "args must be a String array", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"stopIperf" -> {
|
"stopIperf" -> {
|
||||||
forceStopIperfTest(object : IperfCallback {
|
val safeCallback = MainThreadCallback(
|
||||||
override fun onOutput(line: String) {
|
onOutput = { line ->
|
||||||
channel?.invokeMethod("onOutput", mapOf("output" to line))
|
channel?.invokeMethod("onOutput", mapOf("output" to line))
|
||||||
}
|
},
|
||||||
override fun onError(error: String) {
|
onError = { error ->
|
||||||
channel?.invokeMethod("onError", mapOf("error" to error))
|
channel?.invokeMethod("onError", mapOf("error" to error))
|
||||||
}
|
},
|
||||||
override fun onComplete() {
|
onComplete = {
|
||||||
channel?.invokeMethod("onComplete", emptyMap<String, Any>())
|
channel?.invokeMethod("onComplete", emptyMap<String, Any>())
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
forceStopIperfTest(safeCallback)
|
||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
"isTestRunning" -> {
|
"isTestRunning" -> {
|
||||||
result.success(isTestRunning())
|
result.success(isTestRunning())
|
||||||
}
|
}
|
||||||
else -> {
|
else -> result.notImplemented()
|
||||||
result.notImplemented()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,37 +91,39 @@ object IperfRunner : FlutterPlugin {
|
|||||||
// Native method bindings
|
// Native method bindings
|
||||||
// ─────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* Start an iperf3 test with the given arguments.
|
|
||||||
* This calls the native C function via JNI.
|
|
||||||
*/
|
|
||||||
external fun runIperfLive(arguments: Array<String>, callback: IperfCallback)
|
external fun runIperfLive(arguments: Array<String>, callback: IperfCallback)
|
||||||
|
|
||||||
/**
|
|
||||||
* Gracefully stop a running iperf3 test.
|
|
||||||
*/
|
|
||||||
external fun forceStopIperfTest(callback: IperfCallback)
|
external fun forceStopIperfTest(callback: IperfCallback)
|
||||||
|
external fun setTempDir(tmpDir: String)
|
||||||
/**
|
|
||||||
* Check if an iperf3 test is currently running.
|
|
||||||
* @return true if a test is in progress, false otherwise
|
|
||||||
*/
|
|
||||||
external fun isTestRunning(): Boolean
|
external fun isTestRunning(): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback interface for iperf3 test output.
|
* Callback interface for iperf3 test output.
|
||||||
*
|
* Used by the native layer to stream results back to Flutter in real-time.
|
||||||
* Used by the native layer to stream test results back to Flutter
|
|
||||||
* in real-time.
|
|
||||||
*/
|
*/
|
||||||
interface IperfCallback {
|
interface IperfCallback {
|
||||||
/** Called with each line of iperf3 output (stdout/stderr) */
|
|
||||||
fun onOutput(line: String)
|
fun onOutput(line: String)
|
||||||
|
|
||||||
/** Called when an error occurs */
|
|
||||||
fun onError(error: String)
|
fun onError(error: String)
|
||||||
|
|
||||||
/** Called when the test completes (success or failure) */
|
|
||||||
fun onComplete()
|
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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user