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)
This commit is contained in:
@@ -1,63 +1,142 @@
|
||||
import 'dart:convert';
|
||||
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 [Process.start] and captures output.
|
||||
/// 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 {
|
||||
final String _binaryPath;
|
||||
static const MethodChannel _channel =
|
||||
MethodChannel('com.novoyuuparosk.iperf3gui/iperf');
|
||||
|
||||
// ignore: prefer_initializing_formals
|
||||
Iperf3Service({required String binaryPath}) : _binaryPath = binaryPath;
|
||||
/// 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 {
|
||||
final args = config.toCliArgs();
|
||||
if (_isRunning) {
|
||||
return Iperf3Result(
|
||||
rawOutput: 'Another test is already running.',
|
||||
exitCode: -2,
|
||||
);
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'Iperf3Service: running $_binaryPath ${args.join(" ")}',
|
||||
);
|
||||
_isRunning = true;
|
||||
_outputBuffer.clear();
|
||||
|
||||
final process = await Process.start(
|
||||
_binaryPath,
|
||||
args,
|
||||
runInShell: false, // must execve directly — Android sh can't exec ELF binaries
|
||||
);
|
||||
final args = <String>[
|
||||
'-c', config.server,
|
||||
'-p', config.port.toString(),
|
||||
'-t', config.duration.inSeconds.toString(),
|
||||
'-i', config.interval.toString(),
|
||||
'-P', config.parallelStreams.toString(),
|
||||
];
|
||||
|
||||
final stdoutBuffer = <int>[];
|
||||
final stderrBuffer = <int>[];
|
||||
debugPrint('Iperf3Service: running: ${args.join(" ")}');
|
||||
|
||||
final stdoutStream = process.stdout.transform(utf8.decoder).listen(
|
||||
(data) {
|
||||
stdoutBuffer.addAll(utf8.encode(data));
|
||||
debugPrint('Iperf3Service stdout: $data');
|
||||
},
|
||||
);
|
||||
final stderrStream = process.stderr.transform(utf8.decoder).listen(
|
||||
(data) {
|
||||
stderrBuffer.addAll(utf8.encode(data));
|
||||
debugPrint('Iperf3Service stderr: $data');
|
||||
},
|
||||
);
|
||||
// Set up the completer to await completion
|
||||
_runCompleter = Completer<Iperf3Result>();
|
||||
|
||||
final exitCode = await process.exitCode;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await stdoutStream.cancel();
|
||||
await stderrStream.cancel();
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
|
||||
final rawOutput = utf8.decode(stdoutBuffer) + utf8.decode(stderrBuffer);
|
||||
|
||||
debugPrint(
|
||||
'Iperf3Service: finished with exit code $exitCode, output length ${rawOutput.length}',
|
||||
);
|
||||
|
||||
return Iperf3Result(
|
||||
rawOutput: rawOutput,
|
||||
exitCode: exitCode,
|
||||
);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user