[dds] Add DAP support for 'hotReload' custom request that calls reloadSources
Change-Id: I413cd9ce8f72e97d14a3fb7762f98833a5991a1d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/219793
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart
index 655d646..5f70e3b 100644
--- a/pkg/dds/lib/src/dap/adapters/dart.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart.dart
@@ -53,6 +53,13 @@
/// Typedef for handlers of VM Service stream events.
typedef _StreamEventHandler<T> = FutureOr<void> Function(T data);
+/// A null result passed to `sendResponse` functions when there is no result.
+///
+/// Because the signature of `sendResponse` is generic, an argument must be
+/// provided even when the generic type is `void`. This value is used to make
+/// it clearer in calling code that the result is unused.
+const _noResult = null;
+
/// Pattern for extracting useful error messages from an evaluation exception.
final _evalErrorMessagePattern = RegExp('Error: (.*)');
@@ -661,26 +668,26 @@
) async {
switch (request.command) {
- /// Used by tests to validate available protocols (e.g. DDS). There may be
- /// value in making this available to clients in future, but for now it's
- /// internal.
+ // Used by tests to validate available protocols (e.g. DDS). There may be
+ // value in making this available to clients in future, but for now it's
+ // internal.
case '_getSupportedProtocols':
final protocols = await vmService?.getSupportedProtocols();
sendResponse(protocols?.toJson());
break;
- /// Used to toggle debug settings such as whether SDK/Packages are
- /// debuggable while the session is in progress.
+ // Used to toggle debug settings such as whether SDK/Packages are
+ // debuggable while the session is in progress.
case 'updateDebugOptions':
if (args != null) {
await _updateDebugOptions(args.args);
}
- sendResponse(null);
+ sendResponse(_noResult);
break;
- /// Allows an editor to call a service/service extension that it was told
- /// about via a custom 'dart.serviceRegistered' or
- /// 'dart.serviceExtensionAdded' event.
+ // Allows an editor to call a service/service extension that it was told
+ // about via a custom 'dart.serviceRegistered' or
+ // 'dart.serviceExtensionAdded' event.
case 'callService':
final method = args?.args['method'] as String?;
if (method == null) {
@@ -696,6 +703,15 @@
sendResponse(response?.json);
break;
+ // Used to reload sources for all isolates. This supports Hot Reload for
+ // Dart apps. Flutter's DAP handles this command itself (and sends it
+ // through the run daemon) as it needs to perform additional work to
+ // rebuild widgets afterwards.
+ case 'hotReload':
+ await _isolateManager.reloadSources();
+ sendResponse(_noResult);
+ break;
+
default:
await super.customRequest(request, args, sendResponse);
}
diff --git a/pkg/dds/lib/src/dap/base_debug_adapter.dart b/pkg/dds/lib/src/dap/base_debug_adapter.dart
index 2d82bbd..215a975 100644
--- a/pkg/dds/lib/src/dap/base_debug_adapter.dart
+++ b/pkg/dds/lib/src/dap/base_debug_adapter.dart
@@ -376,7 +376,7 @@
/// passes an unused arg so that `Function()` can be passed to a function
/// accepting `Function<T>(T x)` where `T` happens to be `void`.
///
- /// This allows handlers to simple call sendResponse() where they have no
+ /// This allows handlers to simply call sendResponse() where they have no
/// return value but need to send a valid response.
_VoidArgRequestHandler<TArg> _withVoidResponse<TArg>(
_VoidNoArgRequestHandler<TArg> handler,
diff --git a/pkg/dds/lib/src/dap/isolate_manager.dart b/pkg/dds/lib/src/dap/isolate_manager.dart
index fd2d342..47a8dde 100644
--- a/pkg/dds/lib/src/dap/isolate_manager.dart
+++ b/pkg/dds/lib/src/dap/isolate_manager.dart
@@ -136,10 +136,7 @@
vm.Event event, {
bool resumeIfStarting = true,
}) async {
- final isolateId = event.isolate?.id;
- if (isolateId == null) {
- return;
- }
+ final isolateId = event.isolate?.id!;
final eventKind = event.kind;
if (eventKind == vm.EventKind.kIsolateStart ||
@@ -203,12 +200,16 @@
return info;
}
+ /// Calls reloadSources for all isolates.
+ Future<void> reloadSources() async {
+ await Future.wait(_threadsByThreadId.values.map(
+ (isolate) => _reloadSources(isolate.isolate),
+ ));
+ }
+
Future<void> resumeIsolate(vm.IsolateRef isolateRef,
[String? resumeType]) async {
- final isolateId = isolateRef.id;
- if (isolateId == null) {
- return;
- }
+ final isolateId = isolateRef.id!;
final thread = _threadsByIsolateId[isolateId];
if (thread == null) {
@@ -527,6 +528,18 @@
}
}
+ /// Calls reloadSources for the given isolate.
+ Future<void> _reloadSources(vm.IsolateRef isolateRef) async {
+ final service = _adapter.vmService;
+ if (!debug || service == null) {
+ return;
+ }
+
+ final isolateId = isolateRef.id!;
+
+ await service.reloadSources(isolateId);
+ }
+
/// Sets breakpoints for an individual isolate.
///
/// If [uri] is provided, only breakpoints for that URI will be sent (used
@@ -592,10 +605,7 @@
return;
}
- final isolateId = isolateRef.id;
- if (isolateId == null) {
- return;
- }
+ final isolateId = isolateRef.id!;
final isolate = await service.getIsolate(isolateId);
final libraries = isolate.libraries;
diff --git a/pkg/dds/test/dap/integration/debug_test.dart b/pkg/dds/test/dap/integration/debug_test.dart
index 12f17b4..d6df8e6 100644
--- a/pkg/dds/test/dap/integration/debug_test.dart
+++ b/pkg/dds/test/dap/integration/debug_test.dart
@@ -173,6 +173,34 @@
unawaited(dap.client.event('thread').then((_) => dap.client.terminate()));
await dap.client.start(file: testFile);
});
+
+ test('can hot reload', () async {
+ const originalText = 'ORIGINAL TEXT';
+ const newText = 'NEW TEXT';
+
+ // Create a script that prints 'ORIGINAL TEXT'.
+ final testFile = dap.createTestFile(stringPrintingProgram(originalText));
+
+ // Start the program and wait for 'ORIGINAL TEXT' to be printed.
+ await Future.wait([
+ dap.client.initialize(),
+ dap.client.launch(testFile.path),
+ ], eagerError: true);
+
+ // Expect the original text.
+ await dap.client.outputEvents
+ .firstWhere((event) => event.output.trim() == originalText);
+
+ // Update the file and hot reload.
+ testFile.writeAsStringSync(stringPrintingProgram(newText));
+ await dap.client.hotReload();
+
+ // Expect the new text.
+ await dap.client.outputEvents
+ .firstWhere((event) => event.output.trim() == newText);
+
+ await dap.client.terminate();
+ });
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
diff --git a/pkg/dds/test/dap/integration/test_client.dart b/pkg/dds/test/dap/integration/test_client.dart
index af40d1c..55f21c0 100644
--- a/pkg/dds/test/dap/integration/test_client.dart
+++ b/pkg/dds/test/dap/integration/test_client.dart
@@ -148,7 +148,7 @@
sendRequest(ContinueArguments(threadId: threadId));
/// Sends a custom request to the server and waits for a response.
- Future<Response> custom(String name, Object? args) async {
+ Future<Response> custom(String name, [Object? args]) async {
return sendRequest(args, overrideCommand: name);
}
@@ -191,6 +191,11 @@
_serverRequestHandlers[request] = handler;
}
+ /// Send a custom 'hotReload' request to the server.
+ Future<Response> hotReload() async {
+ return custom('hotReload');
+ }
+
/// Send an initialize request to the server.
///
/// This occurs before the request to start running/debugging a script and is
diff --git a/pkg/dds/test/dap/integration/test_scripts.dart b/pkg/dds/test/dap/integration/test_scripts.dart
index 2b24727..59bc718 100644
--- a/pkg/dds/test/dap/integration/test_scripts.dart
+++ b/pkg/dds/test/dap/integration/test_scripts.dart
@@ -2,6 +2,8 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
+import 'dart:convert';
+
import 'package:test/test.dart';
/// A marker used in some test scripts/tests for where to set breakpoints.
@@ -53,6 +55,24 @@
}
''';
+/// Returns a simple Dart script that prints the provided string repeatedly.
+String stringPrintingProgram(String text) {
+ // jsonEncode the string to get it into a quoted/escaped form that can be
+ // embedded in the string.
+ final encodedTextString = jsonEncode(text);
+ return '''
+ import 'dart:async';
+
+ main() async {
+ Timer.periodic(Duration(milliseconds: 10), (_) => printSomething());
+ }
+
+ void printSomething() {
+ print($encodedTextString);
+ }
+''';
+}
+
/// A simple async Dart script that when stopped at the line of '// BREAKPOINT'
/// will contain multiple stack frames across some async boundaries.
const simpleAsyncProgram = '''
diff --git a/pkg/dds/tool/dap/README.md b/pkg/dds/tool/dap/README.md
index a65f707..ac86324 100644
--- a/pkg/dds/tool/dap/README.md
+++ b/pkg/dds/tool/dap/README.md
@@ -74,6 +74,17 @@
}
```
+### `hotReload`
+
+`hotReload` calls the VM's `reloadSources` service for each active isolate, reloading all modified source files.
+
+```
+{
+ "method": "hotReload",
+ "params": null
+}
+```
+
## Custom Events
The debug adapter may emit several custom events that are useful to clients.