Skip to content

Commit 3ca4243

Browse files
DanTupCommit Queue
authored and
Commit Queue
committed
[dds/dap] Handle errors during VM Service connection/initialization and ensure they're reported to the user
Sometimes we see unhandled exceptions during this setup work, see: - flutter/flutter#148346 - flutter/flutter#148348 Currently these are unhandled and bring the debug adapter down. In VS Code, the error (send to stderr) is not visible to the user so they just see a silent crash which makes it very difficult to report (and the Flutter crash reports don't have any context). This change will send the exception to the client and then cleanly terminate, which should give the user more information to open a good bug report (assuming the issue wasn't just something like them terminating the app as it was starting). Change-Id: I4aefbc278e6a0708924c6fa41c5179d581117689 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/366662 Reviewed-by: Ben Konyi <[email protected]> Reviewed-by: Helin Shiah <[email protected]> Commit-Queue: Ben Konyi <[email protected]>
1 parent 565bc48 commit 3ca4243

File tree

4 files changed

+155
-64
lines changed

4 files changed

+155
-64
lines changed

pkg/dds/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# 4.2.2
2+
- [DAP] Exceptions that occur while the debug adapter is connecting to the VM Service and configuring isolates will no longer cause the debug adapter to terminate. Instead, the errors are reporting via a `console` `OutputEvent` and the adapter will shut down gracefully.
3+
14
# 4.2.1
25
- [DAP]: Fixed an issue where breakpoint `changed` events might contain incorrect location information when new isolates are created, causing breakpoints to appear to move in the editor.
36
- [DAP]: For consistency with other values, automatic `toString()` invocations for debugger views no longer expand long strings and instead show truncated values. Full values continue to be returned for evaluation (`context=="repl"`) and when copying to the clipboard (`context=="clipboard"`).

pkg/dds/lib/src/dap/adapters/dart.dart

Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -623,25 +623,86 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
623623
/// The URI protocol will be changed to ws/wss but otherwise not normalized.
624624
/// The caller should handle any other normalisation (such as adding /ws to
625625
/// the end if required).
626+
///
627+
/// The implementation for this method is run in try/catch and any
628+
/// exceptions during initialization will result in the debug adapter
629+
/// reporting an error to the user and shutting down.
626630
Future<void> connectDebugger(Uri uri) async {
627-
// Start up a DDS instance for this VM.
628-
if (enableDds) {
629-
logger?.call('Starting a DDS instance for $uri');
630-
try {
631+
try {
632+
await _connectDebuggerImpl(uri);
633+
} catch (error, stack) {
634+
final message = 'Failed to connect/initialize debugger for $uri:\n'
635+
'$error\n$stack';
636+
logger?.call(message);
637+
sendConsoleOutput(message);
638+
shutdown();
639+
}
640+
}
641+
642+
/// Attempts to start a DDS instance to connect to the VM Service at [uri].
643+
///
644+
/// Returns the URI to connect the debugger to (whether it's a newly spawned
645+
/// DDS or there was an existing one).
646+
///
647+
/// If we failed to start DDS for a reason other than one already existed for
648+
/// that VM Service we will return `null` and initiate a shutdown with the
649+
/// exception printed to the user.
650+
///
651+
/// If a new DDS instance was started, it is assigned to [_dds].
652+
Future<Uri?> _startOrReuseDds(Uri uri) {
653+
// Use a completer to handle DDS because `startDds` (and therefore
654+
// `runZonedGuarded`) may never complete when there's an existing DDS
655+
// (see https://github.com/dart-lang/sdk/issues/55731#issuecomment-2114699898)
656+
final completer = Completer<Uri?>();
657+
runZonedGuarded(
658+
() async {
631659
final dds = await startDds(uri, uriConverter());
632660
_dds = dds;
633-
uri = dds.wsUri!;
634-
} on DartDevelopmentServiceException catch (e) {
635-
// If there's already a DDS instance, then just continue. This is common
636-
// when attaching, as the program may have already been run with a DDS
637-
// instance.
638-
if (e.errorCode ==
639-
DartDevelopmentServiceException.existingDdsInstanceError) {
640-
uri = vmServiceUriToWebSocket(uri);
661+
if (!completer.isCompleted) {
662+
completer.complete(dds.wsUri!);
663+
}
664+
},
665+
(error, stack) {
666+
if (error is DartDevelopmentServiceException &&
667+
error.errorCode ==
668+
DartDevelopmentServiceException.existingDdsInstanceError) {
669+
// If there's an existing DDS instance, we will just continue
670+
// but need to map the URI to the ws: version.
671+
if (!completer.isCompleted) {
672+
completer.complete(vmServiceUriToWebSocket(uri));
673+
}
641674
} else {
642-
rethrow;
675+
// Otherwise, we failed to start DDS for an unknown reason and
676+
// consider this a fatal error. Handle terminating here (so we can
677+
// print this error/stack)...
678+
_handleDebuggerInitializationError(
679+
'Failed to start DDS for $uri', error, stack);
680+
// ... and complete with no URI as a signal to the caller.
681+
if (!completer.isCompleted) {
682+
completer.complete(null);
683+
}
643684
}
685+
},
686+
);
687+
return completer.future;
688+
}
689+
690+
/// Connects to the VM Service at [uri] and initializes debugging.
691+
///
692+
/// This is the implementation for [connectDebugger] which is executed in a
693+
/// try/catch.
694+
Future<void> _connectDebuggerImpl(Uri uri) async {
695+
if (enableDds) {
696+
// Start up a DDS instance for this VM.
697+
logger?.call('Starting a DDS instance for $uri');
698+
final targetUri = await _startOrReuseDds(uri);
699+
if (targetUri == null) {
700+
// If we got no URI, this is a fatal error and we can skip everything
701+
// else. The detailed error would have been printed from
702+
// [_startOrReuseDds].
703+
return;
644704
}
705+
uri = targetUri;
645706
} else {
646707
uri = vmServiceUriToWebSocket(uri);
647708
}
@@ -714,6 +775,33 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
714775
_debuggerInitializedCompleter.complete();
715776
}
716777

778+
/// Handlers an error during debugger initialization (such as exceptions
779+
/// trying to call `getSupportedProtocols` or `getVM`) by sending it to the
780+
/// client and shutting down.
781+
///
782+
/// Without this, the exceptions may go unhandled and just terminate the debug
783+
/// adapter, which may not be visible to the user. For example VS Code does
784+
/// not expose stderr of a debug adapter process. With this change, the
785+
/// exception will show up in the Debug Console before the debug session
786+
/// terminates.
787+
void _handleDebuggerInitializationError(
788+
String reason, Object? error, StackTrace stack) {
789+
final message = '$reason\n$error\n$stack';
790+
logger?.call(message);
791+
isTerminating = true;
792+
sendConsoleOutput(message);
793+
shutdown();
794+
}
795+
796+
Future<DartDevelopmentService> startDds(Uri uri, UriConverter? uriConverter) {
797+
return DartDevelopmentService.startDartDevelopmentService(
798+
vmServiceUriToHttp(uri),
799+
enableAuthCodes: enableAuthCodes,
800+
ipv6: ipv6,
801+
uriConverter: uriConverter,
802+
);
803+
}
804+
717805
// This is intended for subclasses to override to provide a URI converter to
718806
// resolve package URIs to local paths.
719807
UriConverter? uriConverter() {
@@ -732,15 +820,6 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
732820
);
733821
}
734822

735-
Future<DartDevelopmentService> startDds(Uri uri, UriConverter? uriConverter) {
736-
return DartDevelopmentService.startDartDevelopmentService(
737-
vmServiceUriToHttp(uri),
738-
enableAuthCodes: enableAuthCodes,
739-
ipv6: ipv6,
740-
uriConverter: uriConverter,
741-
);
742-
}
743-
744823
/// Starts reporting progress to the client for a single operation.
745824
///
746825
/// The returned [DapProgressReporter] can be used to send updated messages

pkg/dds/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: dds
2-
version: 4.2.1
2+
version: 4.2.2
33
description: >-
44
A library used to spawn the Dart Developer Service, used to communicate with
55
a Dart VM Service instance.

pkg/dds/test/dap/integration/debug_attach_test.dart

Lines changed: 50 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -38,32 +38,28 @@ main() {
3838
),
3939
);
4040

41-
// Expect a "console" output event that prints the URI of the VM Service
42-
// the debugger connects to.
43-
final vmConnection = outputEvents.first;
44-
expect(vmConnection.output,
45-
startsWith('Connecting to VM Service at ws://127.0.0.1:'));
46-
expect(vmConnection.category, anyOf('console', isNull));
47-
48-
// Expect the normal applications output.
49-
final output = outputEvents
50-
.skip(2)
51-
.map((e) => e.output)
52-
// The stdout also contains the VM Service+DevTools banners.
53-
.where(
54-
(line) =>
55-
!line.startsWith('The Dart VM service is listening on') &&
56-
!line.startsWith(
57-
'The Dart DevTools debugger and profiler is available at'),
58-
)
59-
.join();
60-
expectLines(output, [
41+
expectLines(outputEvents.map((output) => output.output).join(), [
42+
startsWith('Connecting to VM Service at ws://127.0.0.1:'),
43+
'Connected to the VM Service.',
44+
startsWith('The Dart VM service is listening on'),
45+
startsWith('The Dart DevTools debugger and profiler is available at'),
6146
'Hello!',
6247
'World!',
6348
'args: [one, two]',
6449
'',
6550
'Exited.',
6651
]);
52+
53+
// Ensure the categories were set correctly.
54+
for (final output in outputEvents) {
55+
if (output.output.contains('VM Service') ||
56+
output.output.contains('Exited')) {
57+
expect(output.category, anyOf('console', isNull));
58+
} else {
59+
// User output.
60+
expect(output.category, 'stdout');
61+
}
62+
}
6763
});
6864

6965
test('can attach to a simple script using vmServiceInfoFile', () async {
@@ -91,34 +87,47 @@ main() {
9187
),
9288
);
9389

94-
// Expect a "console" output event that prints the URI of the VM Service
95-
// the debugger connects to.
96-
final vmConnection = outputEvents.first;
97-
expect(
98-
vmConnection.output,
90+
expectLines(outputEvents.map((output) => output.output).join(), [
9991
startsWith('Connecting to VM Service at ws://127.0.0.1:'),
100-
);
101-
expect(vmConnection.category, anyOf('console', isNull));
102-
103-
// Expect the normal applications output.
104-
final output = outputEvents
105-
.skip(2)
106-
.map((e) => e.output)
107-
// The stdout also contains the VM Service+DevTools banners.
108-
.where(
109-
(line) =>
110-
!line.startsWith('The Dart VM service is listening on') &&
111-
!line.startsWith(
112-
'The Dart DevTools debugger and profiler is available at'),
113-
)
114-
.join();
115-
expectLines(output, [
92+
'Connected to the VM Service.',
93+
startsWith('The Dart VM service is listening on'),
94+
startsWith('The Dart DevTools debugger and profiler is available at'),
11695
'Hello!',
11796
'World!',
11897
'args: [one, two]',
11998
'',
12099
'Exited.',
121100
]);
101+
102+
// Ensure the categories were set correctly.
103+
for (final output in outputEvents) {
104+
if (output.output.contains('VM Service') ||
105+
output.output.contains('Exited')) {
106+
expect(output.category, anyOf('console', isNull));
107+
} else {
108+
// User output.
109+
expect(output.category, 'stdout');
110+
}
111+
}
112+
});
113+
114+
test('reports initialization failures if can\t connect to the VM Service',
115+
() async {
116+
final outputEvents = await dap.client.collectOutput(
117+
launch: () => dap.client.attach(
118+
vmServiceUri: 'ws://bogus.local/',
119+
autoResumeOnEntry: false,
120+
autoResumeOnExit: false,
121+
),
122+
);
123+
124+
expect(
125+
outputEvents.map((e) => e.output).join(),
126+
allOf(
127+
contains('Failed to start DDS for ws://bogus.local/'),
128+
contains('Failed host lookup'),
129+
),
130+
);
122131
});
123132

124133
test('removes breakpoints/pause and resumes on detach', () async {

0 commit comments

Comments
 (0)