Skip to content

Commit

Permalink
feat(gui): Smooth startup Part II (#373)
Browse files Browse the repository at this point in the history
This completes UDENG-1595, by building on top of #355 with the visible
aspects of the change.
Now the GUI automatically attempts to delete the `addr` file and restart
the agent.
Here's how it looks like when a port file is found but the agent is not
running, thus not responsive.


https://github.com/canonical/ubuntu-pro-for-windows/assets/11138291/870b32e3-17f6-4890-8376-0473b0cb5b23
  • Loading branch information
CarlosNihelton authored Oct 31, 2023
2 parents 6699249 + 797cb02 commit ff0fccd
Show file tree
Hide file tree
Showing 8 changed files with 26 additions and 97 deletions.
3 changes: 1 addition & 2 deletions gui/packages/ubuntupro/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions gui/packages/ubuntupro/lib/launch_agent.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import 'core/environment.dart';
Future<bool> 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'],
Expand Down
10 changes: 9 additions & 1 deletion gui/packages/ubuntupro/lib/pages/startup/agent_monitor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,15 @@ class AgentStartupMonitor {
/// Thus, we delete the existing `addr` file and retry launching the agent.
Future<void> 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.',
);
}
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions gui/packages/ubuntupro/lib/pages/startup/startup_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class StartupModel extends ChangeNotifier {
ViewState get view => _view;

StreamSubscription<AgentState>? _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.
Expand All @@ -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();
Expand Down
17 changes: 6 additions & 11 deletions gui/packages/ubuntupro/lib/pages/startup/startup_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,30 +50,25 @@ class _StartupAnimatedChildState extends State<StartupAnimatedChild> {
super.initState();
final model = context.read<StartupModel>();
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();
}
});
}

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<StartupModel>().resetAgent,
child: Text(AppLocalizations.of(context).agentRetryButton),
),
);

case ViewState.crash:
return StartupErrorWidget(message);
}
Expand Down
24 changes: 0 additions & 24 deletions gui/packages/ubuntupro/lib/pages/startup/startup_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ class StartupInProgressWidget extends StatelessWidget {
message: message,
bottom: const LinearProgressIndicator(),
),
showTitleBar: false,
);
}
}
Expand All @@ -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,
),
);
}
}
27 changes: 0 additions & 27 deletions gui/packages/ubuntupro/test/startup/startup_page_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -134,7 +108,6 @@ void main() {
);

await tester.pumpWidget(app);
await tester.pumpAndSettle();

final context = tester.element(find.byType(StartupAnimatedChild));
final model = Provider.of<StartupModel>(context, listen: false);
Expand Down
32 changes: 0 additions & 32 deletions gui/packages/ubuntupro/test/startup/startup_widgets_test.dart

This file was deleted.

0 comments on commit ff0fccd

Please sign in to comment.