Files
iperf3-gui/lib/ui/features/test_run/test_run_view_model.dart
T
mikkeli 3b01b0c4a6 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>
2026-08-26 19:03:22 +09:00

107 lines
3.0 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:iperf3gui/data/services/iperf3_service.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;
Iperf3Result? _result;
Iperf3Result? get result => _result;
TestConfiguration? _currentConfig;
TestConfiguration? get currentConfig => _currentConfig;
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;
}
_state = TestRunState.running;
_currentConfig = config;
_result = null;
_liveOutput.clear();
notifyListeners();
try {
final result = await iperf3Service.run(config);
_result = result;
// 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();
}
}