Skip to content

Commit 8809779

Browse files
DanTupcommit-bot@chromium.org
authored andcommitted
[dds] Handle "Service Disappeared" errors from DDS/VM Service during shutdown
Change-Id: I1ddc59c56778461d5f42b210422eb24ba2f6da7b Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/220005 Reviewed-by: Ben Konyi <[email protected]> Commit-Queue: Ben Konyi <[email protected]>
1 parent 1f3e5ed commit 8809779

File tree

3 files changed

+95
-12
lines changed

3 files changed

+95
-12
lines changed

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

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:path/path.dart' as path;
1313
import 'package:vm_service/vm_service.dart' as vm;
1414

1515
import '../../../dds.dart';
16+
import '../../rpc_error_codes.dart';
1617
import '../base_debug_adapter.dart';
1718
import '../exceptions.dart';
1819
import '../isolate_manager.dart';
@@ -49,6 +50,9 @@ const maxToStringsPerEvaluation = 10;
4950
/// will work.
5051
const threadExceptionExpression = r'$_threadException';
5152

53+
/// Typedef for handlers of VM Service stream events.
54+
typedef _StreamEventHandler<T> = FutureOr<void> Function(T data);
55+
5256
/// Pattern for extracting useful error messages from an evaluation exception.
5357
final _evalErrorMessagePattern = RegExp('Error: (.*)');
5458

@@ -354,6 +358,17 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
354358

355359
late final sendLogsToClient = args.sendLogsToClient ?? false;
356360

361+
/// Whether or not the DAP is terminating.
362+
///
363+
/// When set to `true`, some requests that return "Service Disappeared" errors
364+
/// will be caught and dropped as these are expected if the process is
365+
/// terminating.
366+
///
367+
/// This flag may be set by incoming requests from the client
368+
/// (terminateRequest/disconnectRequest) or when a process terminates, or the
369+
/// VM Service disconnects.
370+
bool isTerminating = false;
371+
357372
DartDebugAdapter(
358373
ByteStreamServerChannel channel, {
359374
this.ipv6 = false,
@@ -531,16 +546,20 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
531546
this.vmService = vmService;
532547

533548
unawaited(vmService.onDone.then((_) => _handleVmServiceClosed()));
549+
550+
// Handlers must be wrapped to handle Service Disappeared errors if async
551+
// code tries to call the VM Service after termination begins.
552+
final wrap = _wrapHandlerWithErrorHandling;
534553
_subscriptions.addAll([
535-
vmService.onIsolateEvent.listen(handleIsolateEvent),
536-
vmService.onDebugEvent.listen(handleDebugEvent),
537-
vmService.onLoggingEvent.listen(handleLoggingEvent),
538-
vmService.onExtensionEvent.listen(handleExtensionEvent),
539-
vmService.onServiceEvent.listen(handleServiceEvent),
554+
vmService.onIsolateEvent.listen(wrap(handleIsolateEvent)),
555+
vmService.onDebugEvent.listen(wrap(handleDebugEvent)),
556+
vmService.onLoggingEvent.listen(wrap(handleLoggingEvent)),
557+
vmService.onExtensionEvent.listen(wrap(handleExtensionEvent)),
558+
vmService.onServiceEvent.listen(wrap(handleServiceEvent)),
540559
if (_subscribeToOutputStreams)
541-
vmService.onStdoutEvent.listen(_handleStdoutEvent),
560+
vmService.onStdoutEvent.listen(wrap(_handleStdoutEvent)),
542561
if (_subscribeToOutputStreams)
543-
vmService.onStderrEvent.listen(_handleStderrEvent),
562+
vmService.onStderrEvent.listen(wrap(_handleStderrEvent)),
544563
]);
545564
await Future.wait([
546565
vmService.streamListen(vm.EventStreams.kIsolate),
@@ -558,8 +577,20 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
558577
// Let the subclass do any existing setup once we have a connection.
559578
await debuggerConnected(vmInfo);
560579

561-
// Process any existing isolates that may have been created before the
562-
// streams above were set up.
580+
await _withErrorHandling(
581+
() => _configureExistingIsolates(vmService, vmInfo, resumeIfStarting),
582+
);
583+
584+
_debuggerInitializedCompleter.complete();
585+
}
586+
587+
/// Process any existing isolates that may have been created before the
588+
/// streams above were set up.
589+
Future<void> _configureExistingIsolates(
590+
vm.VmService vmService,
591+
vm.VM vmInfo,
592+
bool resumeIfStarting,
593+
) async {
563594
final existingIsolateRefs = vmInfo.isolates;
564595
final existingIsolates = existingIsolateRefs != null
565596
? await Future.wait(existingIsolateRefs
@@ -596,8 +627,6 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
596627
}
597628
}
598629
}));
599-
600-
_debuggerInitializedCompleter.complete();
601630
}
602631

603632
/// Handles the clients "continue" ("resume") request for the thread in
@@ -695,6 +724,8 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
695724
DisconnectArguments? args,
696725
void Function() sendResponse,
697726
) async {
727+
isTerminating = true;
728+
698729
await disconnectImpl();
699730
await shutdown();
700731
sendResponse();
@@ -839,6 +870,7 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
839870
return;
840871
}
841872

873+
isTerminating = true;
842874
_hasSentTerminatedEvent = true;
843875
// Always add a leading newline since the last written text might not have
844876
// had one.
@@ -1318,6 +1350,8 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
13181350
TerminateArguments? args,
13191351
void Function() sendResponse,
13201352
) async {
1353+
isTerminating = true;
1354+
13211355
await terminateImpl();
13221356
await shutdown();
13231357
sendResponse();
@@ -1661,6 +1695,7 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
16611695
}
16621696

16631697
Future<void> _handleVmServiceClosed() async {
1698+
isTerminating = true;
16641699
if (terminateOnVmServiceClose) {
16651700
handleSessionTerminate();
16661701
}
@@ -1767,6 +1802,40 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
17671802
streamClosed: streamClosedCompleter.future,
17681803
);
17691804
}
1805+
1806+
/// Wraps a function with an error handler that handles errors that occur when
1807+
/// the VM Service/DDS shuts down.
1808+
///
1809+
/// When the debug adapter is terminating, it's possible in-flight requests
1810+
/// triggered by handlers will fail with "Service Disappeared". This is
1811+
/// normal and such errors can be ignored, rather than allowed to pass
1812+
/// uncaught.
1813+
_StreamEventHandler<T> _wrapHandlerWithErrorHandling<T>(
1814+
_StreamEventHandler<T> handler,
1815+
) {
1816+
return (data) => _withErrorHandling(() => handler(data));
1817+
}
1818+
1819+
/// Calls a function with an error handler that handles errors that occur when
1820+
/// the VM Service/DDS shuts down.
1821+
///
1822+
/// When the debug adapter is terminating, it's possible in-flight requests
1823+
/// will fail with "Service Disappeared". This is normal and such errors can
1824+
/// be ignored, rather than allowed to pass uncaught.
1825+
FutureOr<T?> _withErrorHandling<T>(FutureOr<T> Function() func) async {
1826+
try {
1827+
return await func();
1828+
} on vm.RPCError catch (e) {
1829+
// If we're been asked to shut down while this request was occurring,
1830+
// it's normal to get kServiceDisappeared so we should handle this
1831+
// silently.
1832+
if (isTerminating && e.code == RpcErrorCodes.kServiceDisappeared) {
1833+
return null;
1834+
}
1835+
1836+
rethrow;
1837+
}
1838+
}
17701839
}
17711840

17721841
/// An implementation of [LaunchRequestArguments] that includes all fields used

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:async';
56
import 'dart:io';
67

78
import 'package:dds/src/dap/protocol_generated.dart';
@@ -159,6 +160,19 @@ main() {
159160
final source = await client.getValidSource(topFrame.source!);
160161
expect(source.content, contains('void print(Object? object) {'));
161162
});
163+
164+
test('can shutdown during startup', () async {
165+
final testFile = dap.createTestFile(simpleArgPrintingProgram);
166+
167+
// Terminate the app immediately upon recieving the first Thread event.
168+
// The DAP is also responding to this event to configure the isolate (eg.
169+
// set breakpoints and exception pause behaviour) and will cause it to
170+
// receive "Service has disappeared" responses if these are in-flight as
171+
// the process terminates. These should not go unhandled since they are
172+
// normal during shutdown.
173+
unawaited(dap.client.event('thread').then((_) => dap.client.terminate()));
174+
await dap.client.start(file: testFile);
175+
});
162176
// These tests can be slow due to starting up the external server process.
163177
}, timeout: Timeout.none);
164178

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ class DapTestClient {
399399
} else {
400400
completer.completeError(message);
401401
}
402-
} else if (message is Event) {
402+
} else if (message is Event && !_eventController.isClosed) {
403403
_eventController.add(message);
404404

405405
// When we see a terminated event, close the event stream so if any

0 commit comments

Comments
 (0)