fix(app): stream native output and release run state on every exit path
_isRunning was never cleared on the timeout path, so a single timed-out run locked the app out of starting another until it was restarted. It is now cleared in a finally block covering all three exits. Output was buffered inside the service and only handed over once the run finished, so the UI showed nothing until the end despite the native layer streaming line by line. The service republishes each line on outputLines and the view model appends to a transcript as they arrive. The channel handler was re-registered on every run and dereferenced _runCompleter with `!`, which threw once a late callback arrived after the field had been nulled. It is registered once in the constructor and reads the completer through a local. Also drops the duplicated argument list in favour of TestConfiguration.toCliArgs(), so the flags the UI sends have one definition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,26 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:iperf3gui/data/services/iperf3_service.dart';
|
||||
import 'package:iperf3gui/domain/models/test_configuration.dart';
|
||||
import 'package:iperf3gui/domain/models/iperf3_result.dart';
|
||||
import 'package:iperf3gui/domain/models/test_configuration.dart';
|
||||
|
||||
/// State of the test run.
|
||||
enum TestRunState { idle, running, completed, error }
|
||||
|
||||
/// ViewModel for managing test run state.
|
||||
///
|
||||
/// Subscribes to [Iperf3Service.outputLines] so the transcript grows while the
|
||||
/// test runs rather than appearing all at once at the end.
|
||||
class TestRunViewModel extends ChangeNotifier {
|
||||
final Iperf3Service iperf3Service;
|
||||
|
||||
TestRunViewModel({required this.iperf3Service}) {
|
||||
_outputSubscription = iperf3Service.outputLines.listen(_onOutputLine);
|
||||
}
|
||||
|
||||
StreamSubscription<String>? _outputSubscription;
|
||||
|
||||
TestRunState _state = TestRunState.idle;
|
||||
TestRunState get state => _state;
|
||||
|
||||
@@ -19,12 +30,25 @@ class TestRunViewModel extends ChangeNotifier {
|
||||
TestConfiguration? _currentConfig;
|
||||
TestConfiguration? get currentConfig => _currentConfig;
|
||||
|
||||
TestRunViewModel({required this.iperf3Service});
|
||||
final StringBuffer _liveOutput = StringBuffer();
|
||||
|
||||
/// Output accumulated so far. Grows line by line during a run, and holds the
|
||||
/// finished transcript once the test completes.
|
||||
String get output => _liveOutput.toString();
|
||||
|
||||
/// Whether a test is currently in flight.
|
||||
bool get isRunning => _state == TestRunState.running;
|
||||
|
||||
/// Start a test with the given configuration.
|
||||
Future<void> runTest(TestConfiguration config) async {
|
||||
if (_state == TestRunState.running) return;
|
||||
|
||||
if (!config.isValid) {
|
||||
_liveOutput
|
||||
..clear()
|
||||
..writeln('Invalid configuration: check the server address and port.');
|
||||
_state = TestRunState.error;
|
||||
_result = null;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
@@ -32,26 +56,51 @@ class TestRunViewModel extends ChangeNotifier {
|
||||
_state = TestRunState.running;
|
||||
_currentConfig = config;
|
||||
_result = null;
|
||||
_liveOutput.clear();
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final result = await iperf3Service.run(config);
|
||||
_result = result;
|
||||
_state = result.exitCode == 0
|
||||
? TestRunState.completed
|
||||
: TestRunState.error;
|
||||
// Prefer the streamed transcript — it is what the user watched build up.
|
||||
// Fall back to the service's copy if nothing streamed through.
|
||||
if (_liveOutput.isEmpty && result.rawOutput.isNotEmpty) {
|
||||
_liveOutput.write(result.rawOutput);
|
||||
}
|
||||
_state =
|
||||
result.exitCode == 0 ? TestRunState.completed : TestRunState.error;
|
||||
} catch (e) {
|
||||
_liveOutput.writeln('Test failed: $e');
|
||||
_state = TestRunState.error;
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Request that a running test stop early.
|
||||
Future<void> stopTest() async {
|
||||
if (_state != TestRunState.running) return;
|
||||
await iperf3Service.stop();
|
||||
}
|
||||
|
||||
/// Reset to idle state.
|
||||
void reset() {
|
||||
_state = TestRunState.idle;
|
||||
_result = null;
|
||||
_currentConfig = null;
|
||||
_liveOutput.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _onOutputLine(String line) {
|
||||
_liveOutput.write(line.endsWith('\n') ? line : '$line\n');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_outputSubscription?.cancel();
|
||||
iperf3Service.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user