Files
iperf3-gui/lib/data/services/iperf3_service.dart
T

213 lines
7.1 KiB
Dart
Raw Normal View History

2026-08-25 13:46:33 +09:00
import 'dart:async';
import 'dart:developer' as developer;
2026-08-25 13:46:33 +09:00
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.
2026-08-25 13:46:33 +09:00
///
/// 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<Iperf3Event> _eventController =
StreamController<Iperf3Event>.broadcast();
/// Live reports, one event per native report. Use this to render progress.
@override
Stream<Iperf3Event> get events => _eventController.stream;
/// Raw JSON lines received during the current (or most recent) run.
final List<String> _rawLines = [];
2026-08-25 13:46:33 +09:00
/// Completer that resolves when the native iperf3 test finishes.
Completer<Iperf3Result>? _runCompleter;
/// Whether iperf3 is currently running.
bool _isRunning = false;
@override
2026-08-25 13:46:33 +09:00
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;
2026-08-25 13:46:33 +09:00
/// Cancel any running test.
@override
2026-08-25 13:46:33 +09:00
Future<void> stop() async {
if (!_isRunning) return;
_stopRequested = true;
2026-08-25 13:46:33 +09:00
try {
await _channel.invokeMethod('stopIperf');
} catch (e) {
_log('Iperf3Service: error stopping: $e');
2026-08-25 13:46:33 +09:00
}
}
/// Release the stream controller. Call from the owner's dispose().
@override
Future<void> 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<Iperf3Result> run(TestConfiguration config) async {
2026-08-25 13:46:33 +09:00
if (_isRunning) {
return const Iperf3Result(
2026-08-25 13:46:33 +09:00
rawOutput: 'Another test is already running.',
exitCode: exitCodeBusy,
2026-08-25 13:46:33 +09:00
);
}
_isRunning = true;
_sawError = false;
_stopRequested = false;
_rawLines.clear();
2026-08-25 13:46:33 +09:00
final args = config.toCliArgs();
_log('Iperf3Service: running: ${args.join(" ")}');
2026-08-25 13:46:33 +09:00
final completer = Completer<Iperf3Result>();
_runCompleter = completer;
2026-08-25 13:46:33 +09:00
try {
await _channel.invokeMethod('runIperf', {'args': args});
} catch (e, stackTrace) {
_log('Iperf3Service exception: $e');
_log('Iperf3Service stack: $stackTrace');
_finishRun();
2026-08-25 13:46:33 +09:00
return Iperf3Result(
rawOutput: 'Error starting iperf3: $e',
exitCode: exitCodeStartFailed,
2026-08-25 13:46:33 +09:00
);
}
// Wait for the native layer to signal completion. The grace period covers
// DNS resolution and the 30s native connect timeout.
2026-08-25 13:46:33 +09:00
try {
return await completer.future.timeout(
config.duration + const Duration(seconds: 45),
2026-08-25 13:46:33 +09:00
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.'));
2026-08-25 13:46:33 +09:00
return Iperf3Result(
rawOutput: _rawOutput(),
exitCode: exitCodeTimeout,
2026-08-25 13:46:33 +09:00
);
},
);
} catch (e) {
_log('Iperf3Service: error waiting for completion: $e');
2026-08-25 13:46:33 +09:00
return Iperf3Result(
rawOutput: 'Test failed: $e',
exitCode: exitCodeStartFailed,
2026-08-25 13:46:33 +09:00
);
} finally {
_finishRun();
}
}
/// Handle a callback pushed up from the Kotlin/JNI layer.
Future<void> _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);
2026-08-25 13:46:33 +09:00
}
}
/// 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');