Files
iperf3-gui/lib/data/services/iperf3_service.dart
T
irene d551f41b41 feat: switch iperf3 to JNI with 3.19 source
- 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)
2026-08-25 13:46:33 +09:00

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;
}
}
}