d551f41b41
- Switch from bundled iperf3 binary (Process.start) to JNI compilation - iperf3 3.21 source API was incompatible (start_time, iperf_strerror, etc.) - iperf3 3.19 source is proven to work (used by CellularLab) - CMakeLists.txt updated: config.h generated into cpp/iperf/ for correct include paths - iperf_jni.c fixed for 3.19 API (no start_time on iperf_test) - getTestProgress() returns g_test->duration as fallback (no real-time elapsed tracking yet) - NDK CMake pinned to 3.22.1 - UI layer updated: removed Iperf3BinaryPathProvider references - Tests updated: TestRunViewModelTest matches new service interface - pubspec.yaml: removed iperf3 assets bundle, added http dependency - AndroidManifest: INTERNET permission (needed for hostname resolution)
143 lines
4.3 KiB
Dart
143 lines
4.3 KiB
Dart
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<String> _outputBuffer = [];
|
|
|
|
/// Completer that resolves when the native iperf3 test finishes.
|
|
Completer<Iperf3Result>? _runCompleter;
|
|
|
|
/// Whether iperf3 is currently running.
|
|
bool _isRunning = false;
|
|
|
|
/// Whether iperf3 is currently running.
|
|
bool get isRunning => _isRunning;
|
|
|
|
/// Cancel any running test.
|
|
Future<void> 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<Iperf3Result> run(TestConfiguration config) async {
|
|
if (_isRunning) {
|
|
return Iperf3Result(
|
|
rawOutput: 'Another test is already running.',
|
|
exitCode: -2,
|
|
);
|
|
}
|
|
|
|
_isRunning = true;
|
|
_outputBuffer.clear();
|
|
|
|
final args = <String>[
|
|
'-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<Iperf3Result>();
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|