diff --git a/gui/packages/ubuntupro/lib/l10n/app_en.arb b/gui/packages/ubuntupro/lib/l10n/app_en.arb index 2dbd6441b..9fd8d0581 100644 --- a/gui/packages/ubuntupro/lib/l10n/app_en.arb +++ b/gui/packages/ubuntupro/lib/l10n/app_en.arb @@ -24,8 +24,7 @@ "agentStateCannotStart": "Ubuntu Pro background agent cannot be started.", "agentStateUnknownEnv": "Ubuntu Pro background agent state cannot be verified. Check your environment settings.", "agentStateQuerying": "Checking Ubuntu Pro background agent's state.", - "agentStateUnreachable": "Ubuntu Pro background agent is unreachable.", - "agentRetryButton": "Click to restart it", + "agentStateUnreachable": "Attempting to recover Ubuntu Pro background agent.", "proHeading": "The most comprehensive subscription\nfor open-source software security\nnow available on WSL", "subscribeNow": "Subscribe Now", diff --git a/gui/packages/ubuntupro/lib/launch_agent.dart b/gui/packages/ubuntupro/lib/launch_agent.dart index aa39197f5..cb2d088db 100644 --- a/gui/packages/ubuntupro/lib/launch_agent.dart +++ b/gui/packages/ubuntupro/lib/launch_agent.dart @@ -15,6 +15,8 @@ import 'core/environment.dart'; Future launchAgent(String agentRelativePath) async { final agentPath = p.join(msixRootDir().path, agentRelativePath); try { + // Attempts to kill a possibly stuck agent. Failure is desirable in this case. + await Process.run('taskkill.exe', ['/f', '/im', p.basename(agentPath)]); await Process.start( agentPath, ['-vv'], diff --git a/gui/packages/ubuntupro/lib/pages/startup/agent_monitor.dart b/gui/packages/ubuntupro/lib/pages/startup/agent_monitor.dart index 962557e6f..7d2b2ee68 100644 --- a/gui/packages/ubuntupro/lib/pages/startup/agent_monitor.dart +++ b/gui/packages/ubuntupro/lib/pages/startup/agent_monitor.dart @@ -161,7 +161,15 @@ class AgentStartupMonitor { /// Thus, we delete the existing `addr` file and retry launching the agent. Future reset() async { if (_addrFilePath != null) { - await File(_addrFilePath!).delete(); + try { + await File(_addrFilePath!).delete(); + } on PathNotFoundException { + // TODO: Log + // ignore: avoid_print + print( + 'Port file expected but not found. Likely a race with the agent at this point, not an issue.', + ); + } } } } diff --git a/gui/packages/ubuntupro/lib/pages/startup/startup_model.dart b/gui/packages/ubuntupro/lib/pages/startup/startup_model.dart index 55183e499..8a68a6395 100644 --- a/gui/packages/ubuntupro/lib/pages/startup/startup_model.dart +++ b/gui/packages/ubuntupro/lib/pages/startup/startup_model.dart @@ -43,6 +43,8 @@ class StartupModel extends ChangeNotifier { ViewState get view => _view; StreamSubscription? _subs; + static const _retryLimit = 5; + int _retries = 0; /// Starts the monitor and subscribes to its events. Returns a future that /// completes when the agent monitor startup routine completes. @@ -67,6 +69,12 @@ class StartupModel extends ChangeNotifier { _view == ViewState.retry, "resetAgent only if it's possible to retry", ); + if (_retries >= _retryLimit) { + _view = ViewState.crash; + notifyListeners(); + return; + } + ++_retries; await monitor.reset(); await _subs?.cancel(); return init(); diff --git a/gui/packages/ubuntupro/lib/pages/startup/startup_page.dart b/gui/packages/ubuntupro/lib/pages/startup/startup_page.dart index b5ede7079..e04f2fffb 100644 --- a/gui/packages/ubuntupro/lib/pages/startup/startup_page.dart +++ b/gui/packages/ubuntupro/lib/pages/startup/startup_page.dart @@ -50,9 +50,12 @@ class _StartupAnimatedChildState extends State { super.initState(); final model = context.read(); model.init(); - model.addListener(() { + model.addListener(() async { if (model.view == ViewState.ok) { - Navigator.of(context).pushReplacementNamed(widget.nextRoute); + await Navigator.of(context).pushReplacementNamed(widget.nextRoute); + } + if (model.view == ViewState.retry) { + await model.resetAgent(); } }); } @@ -60,20 +63,12 @@ class _StartupAnimatedChildState extends State { Widget buildChild(ViewState view, String message) { switch (view) { case ViewState.inProgress: + case ViewState.retry: return StartupInProgressWidget(message); case ViewState.ok: return const SizedBox.shrink(); - case ViewState.retry: - return StartupRetryWidget( - message: message, - retry: OutlinedButton( - onPressed: context.read().resetAgent, - child: Text(AppLocalizations.of(context).agentRetryButton), - ), - ); - case ViewState.crash: return StartupErrorWidget(message); } diff --git a/gui/packages/ubuntupro/lib/pages/startup/startup_widgets.dart b/gui/packages/ubuntupro/lib/pages/startup/startup_widgets.dart index 88c0e0e3f..866591f5b 100644 --- a/gui/packages/ubuntupro/lib/pages/startup/startup_widgets.dart +++ b/gui/packages/ubuntupro/lib/pages/startup/startup_widgets.dart @@ -57,7 +57,6 @@ class StartupInProgressWidget extends StatelessWidget { message: message, bottom: const LinearProgressIndicator(), ), - showTitleBar: false, ); } } @@ -78,26 +77,3 @@ class StartupErrorWidget extends StatelessWidget { ); } } - -/// Displays an error icon followed by the [errorMessage] and a button allowing -/// users to manually request a reset/retry operation. -class StartupRetryWidget extends StatelessWidget { - const StartupRetryWidget({ - super.key, - required this.message, - required this.retry, - }); - final String message; - final Widget retry; - - @override - Widget build(BuildContext context) { - return Pro4WindowsPage( - body: StatusColumn( - top: const Icon(Icons.error_outline, size: 64), - message: message, - bottom: retry, - ), - ); - } -} diff --git a/gui/packages/ubuntupro/test/startup/startup_page_test.dart b/gui/packages/ubuntupro/test/startup/startup_page_test.dart index 98f009744..20e27b874 100644 --- a/gui/packages/ubuntupro/test/startup/startup_page_test.dart +++ b/gui/packages/ubuntupro/test/startup/startup_page_test.dart @@ -58,32 +58,6 @@ void main() { expect(find.text(lastText), findsOneWidget); }); - testWidgets('button for retry', (tester) async { - final monitor = MockAgentStartupMonitor(); - when(monitor.start()).thenAnswer( - (_) => Stream.fromIterable( - [ - AgentState.querying, - AgentState.starting, - AgentState.invalid, - AgentState.unreachable, - ], - ), - ); - final model = StartupModel(monitor); - await tester.pumpWidget(buildApp(model)); - - await model.init(); - await tester.pumpAndSettle(); - - // no success - expect(find.text(lastText), findsNothing); - // show error icon - expect(find.byIcon(Icons.error_outline), findsOneWidget); - // and a retry button - expect(find.byType(OutlinedButton), findsOneWidget); - }); - testWidgets('terminal error no button', (tester) async { final monitor = MockAgentStartupMonitor(); when(monitor.start()).thenAnswer( @@ -134,7 +108,6 @@ void main() { ); await tester.pumpWidget(app); - await tester.pumpAndSettle(); final context = tester.element(find.byType(StartupAnimatedChild)); final model = Provider.of(context, listen: false); diff --git a/gui/packages/ubuntupro/test/startup/startup_widgets_test.dart b/gui/packages/ubuntupro/test/startup/startup_widgets_test.dart deleted file mode 100644 index 9c2d49dd2..000000000 --- a/gui/packages/ubuntupro/test/startup/startup_widgets_test.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:ubuntupro/pages/startup/startup_widgets.dart'; - -void main() { - const message = 'Hello'; - MaterialApp buildApp(Widget home) => MaterialApp( - home: home, - localizationsDelegates: AppLocalizations.localizationsDelegates, - ); - testWidgets('inprogress no appbar', (tester) async { - await tester.pumpWidget(buildApp(const StartupInProgressWidget(message))); - expect(find.byType(AppBar), findsNothing); - }); - testWidgets('retry shows appbar', (tester) async { - await tester.pumpWidget( - buildApp( - const StartupRetryWidget( - message: message, - retry: Icon(Icons.check), - ), - ), - ); - expect(find.byType(AppBar), findsOneWidget); - }); - - testWidgets('error also shows appbar', (tester) async { - await tester.pumpWidget(buildApp(const StartupErrorWidget(message))); - expect(find.byType(AppBar), findsOneWidget); - }); -}