import 'dart:async'; import 'dart:io'; 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 communicates with the native layer via a MethodChannel. /// Output is streamed live to the Flutter UI through callbacks. class Iperf3Service { static const MethodChannel _channel = MethodChannel('com.novoyuuparosk.iperf3gui/iperf'); /// Collect output lines during a test run. final List _outputBuffer = []; /// Completer that resolves when the native iperf3 test finishes. Completer? _runCompleter; /// Whether iperf3 is currently running. bool _isRunning = false; /// Whether iperf3 is currently running. bool get isRunning => _isRunning; /// Cancel any running test. Future stop() async { if (!_isRunning) return; try { await _channel.invokeMethod('stopIperf'); } catch (e) { debugPrint('Iperf3Service: error stopping: $e'); } } /// Run iperf3 with the given configuration. /// /// Returns an [Iperf3Result] with stdout/stderr and exit code. Future run(TestConfiguration config) async { if (_isRunning) { return Iperf3Result( rawOutput: 'Another test is already running.', exitCode: -2, ); } _isRunning = true; _outputBuffer.clear(); final args = [ '-c', config.server, '-p', config.port.toString(), '-t', config.duration.inSeconds.toString(), '-i', config.interval.toString(), '-P', config.parallelStreams.toString(), ]; debugPrint('Iperf3Service: running: ${args.join(" ")}'); // Set up the completer to await completion _runCompleter = Completer(); // Listen for events from native layer _channel.setMethodCallHandler((call) async { if (!_runCompleter!.isCompleted) { switch (call.method) { case 'onOutput': final output = call.arguments['output'] as String?; if (output != null) { _outputBuffer.add(output); debugPrint('Iperf3Service output: ${output.trim()}'); } break; case 'onError': final error = call.arguments['error'] as String?; if (error != null) { debugPrint('Iperf3Service error: $error'); _outputBuffer.add('[ERROR] $error'); // Don't complete yet — the test might still be running } break; case 'onComplete': debugPrint('Iperf3Service: test completed'); _isRunning = false; _runCompleter!.complete( Iperf3Result( rawOutput: _outputBuffer.join(''), exitCode: 0, ), ); break; } } }); // Start the test try { await _channel.invokeMethod('runIperf', {'args': args}); } catch (e, stackTrace) { debugPrint('Iperf3Service exception: $e'); debugPrint('Iperf3Service stack: $stackTrace'); _isRunning = false; if (!_runCompleter!.isCompleted) { _runCompleter!.completeError(e); } return Iperf3Result( rawOutput: 'Error starting iperf3: $e', exitCode: -1, ); } // Wait for the native layer to signal completion (with a timeout) try { final result = await _runCompleter!.future.timeout( config.duration + const Duration(seconds: 30), onTimeout: () { debugPrint('Iperf3Service: timeout, stopping test'); stop(); return Iperf3Result( rawOutput: 'Test timed out after ${config.duration.inSeconds}s', exitCode: -3, ); }, ); return result; } catch (e) { debugPrint('Iperf3Service: error waiting for completion: $e'); return Iperf3Result( rawOutput: 'Test failed: $e', exitCode: -1, ); } finally { _runCompleter = null; } } }