Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/devtools_app/lib/src/framework/framework_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ extension FrameworkCore on Never {
final uri = normalizeVmServiceUri(serviceUriAsString);
if (uri != null) {
vmServiceInitializationInProgress = true;
_lastServiceUriAsString = serviceUriAsString;
final finishedCompleter = Completer<void>();

try {
Expand Down Expand Up @@ -187,6 +188,34 @@ extension FrameworkCore on Never {
static void _defaultErrorReporter(String title, Object error) {
notificationService.pushError('$title, $error', isReportable: false);
}

/// The URI of the last VM service connection, used for reconnection.
static String? _lastServiceUriAsString;

/// Attempts to reconnect to the last known VM service.
///
/// Returns true if reconnection was successful.
static Future<bool> reconnectVmService() async {
if (vmServiceInitializationInProgress) {
_log.warning(
'Reconnection attempt ignored: initialization already in progress.',
);
return false;
}
final lastUri = _lastServiceUriAsString;
if (lastUri == null) return false;

_log.info('Attempting to reconnect to VM service at: $lastUri');
final success = await initVmService(
serviceUriAsString: lastUri,
);
if (success) {
_log.info('VM service reconnection successful');
} else {
_log.warning('VM service reconnection failed');
}
return success;
}
Comment thread
khanak0509 marked this conversation as resolved.
}

Future<void> _initDTDConnection() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import '../../shared/config_specific/import_export/import_export.dart';
import '../../shared/framework/routing.dart';
import '../../shared/globals.dart';
import '../../shared/primitives/query_parameters.dart';
import '../framework_core.dart';

class DisconnectObserver extends StatefulWidget {
const DisconnectObserver({
Expand All @@ -34,9 +35,10 @@ class DisconnectObserver extends StatefulWidget {
class DisconnectObserverState extends State<DisconnectObserver>
with AutoDisposeMixin {
OverlayEntry? currentDisconnectedOverlay;

late ConnectedState currentConnectionState;

bool _isReconnecting = false;

@override
void initState() {
super.initState();
Expand All @@ -52,6 +54,7 @@ class DisconnectObserverState extends State<DisconnectObserver>
if (currentConnectionState.connected &&
currentDisconnectedOverlay != null) {
setState(() {
_isReconnecting = false;
hideDisconnectedOverlay();
});
} else if (!currentConnectionState.connected) {
Expand Down Expand Up @@ -95,6 +98,38 @@ class DisconnectObserverState extends State<DisconnectObserver>
currentDisconnectedOverlay = null;
}

Future<void> _attemptReconnect() async {
if (_isReconnecting) return;
_isReconnecting = true;
currentDisconnectedOverlay?.markNeedsBuild();

bool success = false;
try {
success = await FrameworkCore.reconnectVmService().timeout(
const Duration(seconds: 5),
);
} catch (_) {
success = false;
}

if (mounted) {
_isReconnecting = false;
if (success) {
final uri = serviceConnection.serviceManager.serviceUri;
if (uri != null) {
unawaited(
widget.routerDelegate.updateArgsIfChanged({
DevToolsQueryParams.vmServiceUriKey: uri,
}),
);
}
hideDisconnectedOverlay();
} else {
currentDisconnectedOverlay?.markNeedsBuild();
}
}
}

Future<void> _reviewHistory() async {
assert(offlineDataController.offlineDataJson.isNotEmpty);

Expand Down Expand Up @@ -124,16 +159,29 @@ class DisconnectObserverState extends State<DisconnectObserver>
child: Column(
children: [
const Spacer(),
Text('Disconnected', style: theme.textTheme.headlineMedium),
const SizedBox(height: defaultSpacing),
if (!isEmbedded())
ConnectToNewAppButton(
routerDelegate: widget.routerDelegate,
onPressed: hideDisconnectedOverlay,
gaScreen: gac.devToolsMain,
)
else
const Text('Run a new debug session to reconnect.'),
if (_isReconnecting) ...[
const CircularProgressIndicator(),
const SizedBox(height: defaultSpacing),
Text(
'Reconnecting...',
style: theme.textTheme.headlineMedium,
),
] else ...[
Text('Disconnected', style: theme.textTheme.headlineMedium),
const SizedBox(height: defaultSpacing),
ElevatedButton(
onPressed: _attemptReconnect,
child: const Text('Reconnect'),
),
if (!isEmbedded()) ...[
const SizedBox(height: denseSpacing),
ConnectToNewAppButton(
routerDelegate: widget.routerDelegate,
onPressed: hideDisconnectedOverlay,
gaScreen: gac.devToolsMain,
),
],
],
const Spacer(),
if (offlineDataController.offlineDataJson.isNotEmpty) ...[
ElevatedButton(
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ To learn more about DevTools, check out the

## General updates

* Fixed an issue where DevTools could get stuck in a disconnected state (e.g., after a Mac goes to sleep) by adding a manual "Reconnect" button to the disconnected screen. -
[#9838](https://github.com/flutter/devtools/issues/9838)
* Resolve several memory leaks. - [#9857](https://github.com/flutter/devtools/pull/9857)
* Fixed a bug where highlighted search matches in tables were unreadable in dark
mode because the highlight color had become fully opaque. -
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@ void main() {
find.byType(ConnectToNewAppButton),
showingOverlay && !isEmbedded() ? findsOneWidget : findsNothing,
);
// The Reconnect button should be shown in all modes when disconnected.
expect(
find.text('Run a new debug session to reconnect.'),
showingOverlay && isEmbedded() ? findsOneWidget : findsNothing,
find.text('Reconnect'),
showingOverlay ? findsOneWidget : findsNothing,
);
expect(
find.text('Review recent data (offline)'),
Expand Down Expand Up @@ -170,5 +171,58 @@ void main() {
// TODO(kenz): test navigation that occurs by clicking on buttons. This will
// require either modifying the test wrappers to take a set of routes or
// writing an integration test for this user journey.

group('reconnect button', () {
testWidgets('shows Reconnect button in embedded mode', (
WidgetTester tester,
) async {
setGlobal(IdeTheme, IdeTheme(embedMode: EmbedMode.embedOne));
await pumpDisconnectObserver(tester);
verifyObserverState(tester, connected: true, showingOverlay: false);

// Trigger a disconnect.
fakeServiceConnectionManager.serviceManager.setConnectedState(false);
await tester.pumpAndSettle();
verifyObserverState(tester, connected: false, showingOverlay: true);

// Verify the Reconnect button is present in embedded mode.
expect(find.text('Reconnect'), findsOneWidget);
// ConnectToNewAppButton should NOT be shown in embedded mode.
expect(find.byType(ConnectToNewAppButton), findsNothing);
});

testWidgets('shows Reconnect button in non-embedded mode', (
WidgetTester tester,
) async {
await pumpDisconnectObserver(tester);
verifyObserverState(tester, connected: true, showingOverlay: false);

// Trigger a disconnect.
fakeServiceConnectionManager.serviceManager.setConnectedState(false);
await tester.pumpAndSettle();
verifyObserverState(tester, connected: false, showingOverlay: true);

// Both Reconnect and ConnectToNewAppButton should be shown.
expect(find.text('Reconnect'), findsOneWidget);
expect(find.byType(ConnectToNewAppButton), findsOneWidget);
});

testWidgets('hides overlay when reconnection succeeds', (
WidgetTester tester,
) async {
await pumpDisconnectObserver(tester);
verifyObserverState(tester, connected: true, showingOverlay: false);

// Trigger a disconnect.
fakeServiceConnectionManager.serviceManager.setConnectedState(false);
await tester.pumpAndSettle();
verifyObserverState(tester, connected: false, showingOverlay: true);

// Simulate a successful reconnection by setting connected state.
fakeServiceConnectionManager.serviceManager.setConnectedState(true);
await tester.pumpAndSettle();
verifyObserverState(tester, connected: true, showingOverlay: false);
});
});
});
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading