2bba647a7b
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>