import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:iperf3gui/domain/models/iperf3_result.dart'; import 'package:iperf3gui/domain/models/test_configuration.dart'; /// Service that runs iperf3 via JNI (native library). /// /// On Android, iperf3 is compiled into the app as a shared library (.so). /// This avoids SELinux restrictions on executing ELF binaries from assets. /// /// The service talks to the native layer over a MethodChannel. Native output /// arrives line by line on [outputLines] while the test runs; [run] resolves /// with the accumulated transcript once the native layer reports completion. class Iperf3Service { Iperf3Service() { _channel.setMethodCallHandler(_handleNativeCall); } static const MethodChannel _channel = MethodChannel('com.novoyuuparosk.iperf3gui/iperf'); /// Exit code reported when the native layer signalled an error. static const int exitCodeNativeError = 1; /// Exit code reported when another test is already in flight. static const int exitCodeBusy = -2; /// Exit code reported when the test could not be started at all. static const int exitCodeStartFailed = -1; /// Exit code reported when the native layer never signalled completion. static const int exitCodeTimeout = -3; /// Lines emitted by the native layer during the current run. final StreamController _outputController = StreamController.broadcast(); /// Live output, one line per event. Use this to render progress; [run]'s /// result carries the same text joined together. Stream get outputLines => _outputController.stream; /// Transcript of the current (or most recent) run. final List _outputBuffer = []; /// Completer that resolves when the native iperf3 test finishes. Completer? _runCompleter; /// Whether iperf3 is currently running. bool _isRunning = false; bool get isRunning => _isRunning; /// Whether the native layer reported an error during this run. bool _sawError = false; /// Whether [stop] was called for the current run. bool _stopRequested = false; /// Cancel any running test. Future stop() async { if (!_isRunning) return; _stopRequested = true; try { await _channel.invokeMethod('stopIperf'); } catch (e) { debugPrint('Iperf3Service: error stopping: $e'); } } /// Release the stream controller. Call from the owner's dispose(). Future dispose() async { _channel.setMethodCallHandler(null); await _outputController.close(); } /// Run iperf3 with the given configuration. /// /// Returns an [Iperf3Result] with the full output and an exit code. Future run(TestConfiguration config) async { if (_isRunning) { return const Iperf3Result( rawOutput: 'Another test is already running.', exitCode: exitCodeBusy, ); } _isRunning = true; _sawError = false; _stopRequested = false; _outputBuffer.clear(); final args = config.toCliArgs(); debugPrint('Iperf3Service: running: ${args.join(" ")}'); final completer = Completer(); _runCompleter = completer; try { await _channel.invokeMethod('runIperf', {'args': args}); } catch (e, stackTrace) { debugPrint('Iperf3Service exception: $e'); debugPrint('Iperf3Service stack: $stackTrace'); _finishRun(); return Iperf3Result( rawOutput: 'Error starting iperf3: $e', exitCode: exitCodeStartFailed, ); } // Wait for the native layer to signal completion. The grace period covers // DNS resolution and the 30s native connect timeout. try { return await completer.future.timeout( config.duration + const Duration(seconds: 45), onTimeout: () { debugPrint('Iperf3Service: timeout, stopping test'); // Ask the native layer to wind down, then release local state so a // stuck run can never lock the UI out of starting another test. _channel.invokeMethod('stopIperf').catchError((Object e) { debugPrint('Iperf3Service: error stopping after timeout: $e'); return null; }); _emit('[ERROR] Timed out waiting for iperf3 to finish.'); return Iperf3Result( rawOutput: _transcript(), exitCode: exitCodeTimeout, ); }, ); } catch (e) { debugPrint('Iperf3Service: error waiting for completion: $e'); return Iperf3Result( rawOutput: 'Test failed: $e', exitCode: exitCodeStartFailed, ); } finally { _finishRun(); } } /// Handle a callback pushed up from the Kotlin/JNI layer. Future _handleNativeCall(MethodCall call) async { switch (call.method) { case 'onOutput': final output = call.arguments['output'] as String?; if (output != null) { _emit(output); } case 'onError': final error = call.arguments['error'] as String?; if (error != null) { // Not terminal on its own — iperf3 reports the failure reason and // then completes, so wait for onComplete before resolving. _sawError = true; _emit('[ERROR] $error'); } case 'onComplete': debugPrint('Iperf3Service: test completed'); final completer = _runCompleter; if (completer != null && !completer.isCompleted) { completer.complete( Iperf3Result( rawOutput: _transcript(), // A user-requested stop is a normal outcome, not a failure. exitCode: (_sawError && !_stopRequested) ? exitCodeNativeError : 0, stopped: _stopRequested, ), ); } default: debugPrint('Iperf3Service: unhandled native call ${call.method}'); } } void _emit(String line) { _outputBuffer.add(line); if (!_outputController.isClosed) { _outputController.add(line); } debugPrint('Iperf3Service output: ${line.trimRight()}'); } /// Native output already carries its own newlines; keep it verbatim except /// for lines we synthesised ourselves. String _transcript() => _outputBuffer .map((line) => line.endsWith('\n') ? line : '$line\n') .join(); void _finishRun() { _isRunning = false; _runCompleter = null; } }