import 'dart:async'; import 'dart:developer' as developer; import 'package:flutter/services.dart'; import 'package:iperf3gui/domain/models/iperf3_event.dart'; import 'package:iperf3gui/domain/models/iperf3_result.dart'; import 'package:iperf3gui/domain/models/test_configuration.dart'; import 'package:iperf3gui/domain/services/iperf3_runner.dart'; /// [Iperf3Runner] backed by the native iperf3 library over a MethodChannel. /// /// 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. The test runs /// in `--json-stream` mode, so each native report arrives as one JSON object; /// they are parsed and published on [events] while the test runs. [run] /// resolves once the native layer reports completion, carrying the raw JSON /// lines as the result's output. class Iperf3Service implements Iperf3Runner { Iperf3Service() { _channel.setMethodCallHandler(_handleNativeCall); } /// A channel's handler is keyed by channel name in the binary messenger, so /// it is process-global: constructing a second instance would take the /// handler away from the first. The app composes exactly one, in main(). final MethodChannel _channel = const MethodChannel(_channelName); static const String _channelName = '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; /// Events parsed from the native layer during the current run. final StreamController _eventController = StreamController.broadcast(); /// Live reports, one event per native report. Use this to render progress. @override Stream get events => _eventController.stream; /// Raw JSON lines received during the current (or most recent) run. final List _rawLines = []; /// Completer that resolves when the native iperf3 test finishes. Completer? _runCompleter; /// Whether iperf3 is currently running. bool _isRunning = false; @override 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. @override Future stop() async { if (!_isRunning) return; _stopRequested = true; try { await _channel.invokeMethod('stopIperf'); } catch (e) { _log('Iperf3Service: error stopping: $e'); } } /// Release the stream controller. Call from the owner's dispose(). @override Future dispose() async { _channel.setMethodCallHandler(null); await _eventController.close(); } /// Run iperf3 with the given configuration. /// /// Returns an [Iperf3Result] with the full output and an exit code. @override Future run(TestConfiguration config) async { if (_isRunning) { return const Iperf3Result( rawOutput: 'Another test is already running.', exitCode: exitCodeBusy, ); } _isRunning = true; _sawError = false; _stopRequested = false; _rawLines.clear(); final args = config.toCliArgs(); _log('Iperf3Service: running: ${args.join(" ")}'); final completer = Completer(); _runCompleter = completer; try { await _channel.invokeMethod('runIperf', {'args': args}); } catch (e, stackTrace) { _log('Iperf3Service exception: $e'); _log('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: () { _log('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) { _log('Iperf3Service: error stopping after timeout: $e'); return null; }); _publish(const Iperf3Error('Timed out waiting for iperf3 to finish.')); return Iperf3Result( rawOutput: _rawOutput(), exitCode: exitCodeTimeout, ); }, ); } catch (e) { _log('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) { _rawLines.add(output); final event = Iperf3Event.parse(output); // iperf3 reports a failed test as an `error` event rather than a // non-zero return, so this is where most failures surface. if (event is Iperf3Error) _sawError = true; _publish(event); } 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; _publish(Iperf3Error(error)); } case 'onComplete': _log('Iperf3Service: test completed'); final completer = _runCompleter; if (completer != null && !completer.isCompleted) { completer.complete( Iperf3Result( rawOutput: _rawOutput(), // A user-requested stop is a normal outcome, not a failure. exitCode: (_sawError && !_stopRequested) ? exitCodeNativeError : 0, stopped: _stopRequested, ), ); } default: _log('Iperf3Service: unhandled native call ${call.method}'); } } void _publish(Iperf3Event event) { if (!_eventController.isClosed) { _eventController.add(event); } } /// The raw JSON stream as received, one event per line. String _rawOutput() => _rawLines .map((line) => line.endsWith('\n') ? line : '$line\n') .join(); void _finishRun() { _isRunning = false; _runCompleter = null; } } void _log(String message) => developer.log(message, name: 'Iperf3Service');