iperf3 exposes a hook for exactly this use — iperf.h describes json_callback
as "allow user apps to receive the JSON strings, instead of writing them to
the output file". Setting it alongside --json-stream's two flags makes iperf3
hand each report ("start", "interval", "end", "error") straight to the bridge
as a JSON string, and test->outfile is never touched.
That removes the pipe, the reader thread, the fdopen/fgets loop, and
AttachCurrentThread from the output path — roughly 90 lines. It also removes
the two constraints that came with them and were both gotten wrong: that the
write end must be closed before joining a reader blocked in fgets(), and that
exactly one side may own the shared state struct. Neither can be violated now
because neither exists.
json_callback carries no user-data pointer, so the run state is reached
through a global published before iperf_run_client() and cleared after it.
Only one test runs at a time, which runIperfLive already enforced. The
callback fires on iperf3's reporting thread, so it resolves a JNIEnv for the
calling thread rather than trusting a stored one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stopping a test shuts down the control socket, so iperf_run_client() returns
-1 with "unable to send control message ... Broken pipe". That is the
expected consequence of the stop, not a fault, but the bridge read the
negative return as a failure — so pressing STOP left the UI showing
"Error (exit 1)" above a broken-pipe message.
The bridge now consults its own g_stop flag before classifying the outcome,
and the distinction travels up as Iperf3Result.stopped so the status line can
say "Stopped". A stop is reported with exit code 0: the run ended the way the
user asked it to.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
runIperfLive() blocks for the full duration of a test, but the MethodChannel
handler called it directly on the platform thread. That froze the UI for the
whole run, and because output reaches Dart by posting to the main looper, it
also meant no line could be delivered until the test was already over — the
live output the bridge streams had nowhere to go. Tests now run on a
dedicated single-thread executor and the channel call returns immediately.
MainThreadCallback's constructor properties were named onOutput/onError/
onComplete, the same names as the interface methods overriding them, so
`mainHandler.post { onOutput(line) }` resolved to the member function rather
than the lambda and re-posted itself instead of reaching the channel. Renamed
to emit* so the call cannot bind to the wrong declaration.
A failed System.loadLibrary now surfaces as a NATIVE_LIB_MISSING channel
error rather than leaving every later call to hang, and detaching from the
engine stops an in-flight test whose callbacks would fire into a dead
channel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every test hung on the thread that started it. iperf_cleanup_and_signal()
called pthread_join() on the reader thread before closing the pipe's write
end, but the reader blocks in fgets() until EOF and EOF only arrives once
that write end is closed — two lines further down, after the join. Neither
side could ever make progress.
The reader also freed the shared state struct on exit while the caller went
on to read state->outfile and state->callback_ref and free it a second time,
so any path that got past the join corrupted the heap. Ownership now sits
solely with the JNI caller, which frees the struct after the join.
On the longjmp path outfile was fclose'd twice, once inline and once inside
the cleanup helper, so close_outfile() is idempotent and the hand-rolled
unwinding is gone. Signal handlers are restored to SIG_DFL once a test ends,
because leaving them armed pointed a later SIGTERM at a jmp_buf whose stack
frame no longer existed.
g_test is now guarded by a mutex and a second concurrent run is refused: a
stop request arrives on the platform thread while the test runs elsewhere,
which would otherwise race the worker retiring the pointer. Startup failures
report through onError/onComplete instead of returning silently, so Dart
resolves its completer rather than waiting out a timeout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 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.
Two root causes prevented iperf3 tests from running:
1. argv[0] bug: iperf_parse_arguments() expects argv[0] to be the
program name. We passed Dart args directly, so argv[0] was '-c',
which iperf3 misinterpreted as the program name → 'Argument parse
error: must either be a client (-c) or server (-s)'. Fixed by
prepending "iperf3" as argv[0] and shifting user args to argv[1+].
2. Signal handling: iperf_catch_sigend() installs handlers that use
longjmp() to a jmp_buf, but we never initialized the buffer with
setjmp(). Any signal during iperf_run_client() caused SIGSEGV or
hang. Added setjmp/longjmp wrapper matching main.c's pattern,
SIGPIPE=SIG_IGN, and a 30s connect_timeout to avoid DNS hangs.
Also extracted iperf_cleanup_and_signal() to deduplicate the
normal-return and longjmp paths. Added diagnostic logging to
IperfRunner.kt's library loading for faster future debugging.
The root cause of the SIGBUS (alignment fault) crash was casting jmethodID
values to function pointers and invoking them directly. jmethodID is an
opaque JNI type that must only be used via CallVoidMethod/CallObjectMethod
etc. Calling it as a function pointer dereferences garbage memory, causing
SIGBUS at address 0xb1.
Changes:
- Replace function pointer typedefs (output_callback_t etc.) with a struct
storing JNIEnv + callback global ref + jmethodIDs
- All callbacks now go through CallVoidMethod(env, ref, mid, ...)
- Remove getTestProgress() (never used, unused start_time workaround)
- Fix Kotlin arg cast: List<String> -> toTypedArray() (ClassCastException)
- Update UI default server to speedtest.uztelecom.uz
- Switch from bundled iperf3 binary (Process.start) to JNI compilation
- iperf3 3.21 source API was incompatible (start_time, iperf_strerror, etc.)
- iperf3 3.19 source is proven to work (used by CellularLab)
- CMakeLists.txt updated: config.h generated into cpp/iperf/ for correct include paths
- iperf_jni.c fixed for 3.19 API (no start_time on iperf_test)
- getTestProgress() returns g_test->duration as fallback (no real-time elapsed tracking yet)
- NDK CMake pinned to 3.22.1
- UI layer updated: removed Iperf3BinaryPathProvider references
- Tests updated: TestRunViewModelTest matches new service interface
- pubspec.yaml: removed iperf3 assets bundle, added http dependency
- AndroidManifest: INTERNET permission (needed for hostname resolution)
- Move MainActivity.kt from novoyuuparosk.iperf3gui to
com.novoyuuparosk.iperf3gui package directory
- Update package declaration to match namespace
- This fixes the ClassNotFoundException crash on launch