Skip to content

Commit b947357

Browse files
authored
[dwds] Fix bug where no-op hot restart doesn't cancel a subscription and publish 25.0.1 (#2668)
In hot restart, we wait for all sources to be parsed before continuing to create the isolate. In order to do so, we listen on parsed sources by registering a subscription. In the case where we have no sources to restart, we don't cancel the subscription. This results in an issue where on the next hot restart that contains changes, the old subscription is triggered, potentially completing an already completed completer. Instead, we should always cancel the subscription. Hot reload code is changed as well to do the same. Additional checks are added to check if the completer is completed before we complete again. While this is not needed for this issue, it is possible other sources can be downloaded by the app, which may trigger the function in the listener, which could potentially try and complete the completed completer. Tests are added for both hot restart and hot reload to check that alternating empty hot restarts and non-empty hot restarts work.
1 parent c0492f1 commit b947357

File tree

8 files changed

+134
-20
lines changed

8 files changed

+134
-20
lines changed

dwds/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 25.0.1
2+
3+
### Bug Fixes:
4+
5+
- Fix issue in hot restart where a hot restart with no changes followed by one
6+
with changes, a `Future` was completed again, resulting in a crash.
7+
18
## 25.0.0
29

310
- Implemented hot restart over websockets with multi window support.

dwds/lib/src/dwds_vm_client.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -611,20 +611,20 @@ Future<Map<String, dynamic>> _hotRestart(
611611
globalToolConfiguration.loadStrategy is DdcLibraryBundleStrategy;
612612
final computedReloadedSrcs = Completer<void>();
613613
final reloadedSrcs = <String>{};
614+
late StreamSubscription<String> parsedScriptsSubscription;
614615
if (isDdcLibraryBundle) {
615616
// Injected client should send a request to recreate the isolate after the
616617
// hot restart. The creation of the isolate should in turn wait until all
617618
// scripts are parsed.
618619
chromeProxyService.allowedToCreateIsolate = Completer<void>();
619620
final debugger = await chromeProxyService.debuggerFuture;
620-
late StreamSubscription<String> parsedScriptsSubscription;
621621
parsedScriptsSubscription = debugger.parsedScriptsController.stream
622622
.listen((url) {
623623
computedReloadedSrcs.future.then((_) async {
624624
reloadedSrcs.remove(Uri.parse(url).normalizePath().path);
625-
if (reloadedSrcs.isEmpty) {
625+
if (reloadedSrcs.isEmpty &&
626+
!chromeProxyService.allowedToCreateIsolate.isCompleted) {
626627
chromeProxyService.allowedToCreateIsolate.complete();
627-
await parsedScriptsSubscription.cancel();
628628
}
629629
});
630630
});
@@ -648,6 +648,8 @@ Future<Map<String, dynamic>> _hotRestart(
648648
chromeProxyService.allowedToCreateIsolate.complete();
649649
}
650650
computedReloadedSrcs.complete();
651+
await chromeProxyService.allowedToCreateIsolate.future;
652+
await parsedScriptsSubscription.cancel();
651653
} else {
652654
assert(remoteObject.value == null);
653655
}

dwds/lib/src/services/chrome_proxy_service.dart

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,18 +1089,15 @@ class ChromeProxyService extends ProxyService {
10891089
final parsedAllReloadedSrcs = Completer<void>();
10901090
// Wait until all the reloaded scripts are parsed before we reinitialize
10911091
// metadata below.
1092-
late StreamSubscription<String> parsedScriptsSubscription;
1093-
parsedScriptsSubscription = debugger.parsedScriptsController.stream.listen((
1094-
url,
1095-
) {
1096-
computedReloadedSrcs.future.then((_) async {
1097-
reloadedSrcs.remove(Uri.parse(url).normalizePath().path);
1098-
if (reloadedSrcs.isEmpty) {
1099-
parsedAllReloadedSrcs.complete();
1100-
await parsedScriptsSubscription.cancel();
1101-
}
1102-
});
1103-
});
1092+
final parsedScriptsSubscription = debugger.parsedScriptsController.stream
1093+
.listen((url) {
1094+
computedReloadedSrcs.future.then((_) {
1095+
reloadedSrcs.remove(Uri.parse(url).normalizePath().path);
1096+
if (reloadedSrcs.isEmpty && !parsedAllReloadedSrcs.isCompleted) {
1097+
parsedAllReloadedSrcs.complete();
1098+
}
1099+
});
1100+
});
11041101

11051102
// Initiate a hot reload.
11061103
_logger.info('Issuing \$dartHotReloadStartDwds request');
@@ -1121,6 +1118,7 @@ class ChromeProxyService extends ProxyService {
11211118
}
11221119
computedReloadedSrcs.complete();
11231120
if (reloadedSrcs.isNotEmpty) await parsedAllReloadedSrcs.future;
1121+
await parsedScriptsSubscription.cancel();
11241122

11251123
if (!pauseIsolatesOnStart) {
11261124
// Finish hot reload immediately.

dwds/lib/src/version.dart

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dwds/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: dwds
22
# Every time this changes you need to run `dart run build_runner build`.
3-
version: 25.0.0
3+
version: 25.0.1
44

55
description: >-
66
A service that proxies between the Chrome debug protocol and the Dart VM

dwds/test/common/hot_restart_common.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,42 @@ void runTests({
470470
),
471471
);
472472
});
473+
474+
test('can hot restart with no changes, hot restart with changes, and '
475+
'hot restart again with no changes', () async {
476+
// Empty hot restart.
477+
var mainDone = waitForMainToExecute();
478+
await recompile();
479+
final hotRestart = context.getRegisteredServiceExtension('hotRestart');
480+
await fakeClient.callServiceExtension(hotRestart!);
481+
482+
await mainDone;
483+
var source = await context.webDriver.pageSource;
484+
expect(source.contains(originalString), isTrue);
485+
expect(source.contains(newString), isFalse);
486+
487+
// Hot restart.
488+
mainDone = waitForMainToExecute();
489+
await makeEditAndRecompile();
490+
await fakeClient.callServiceExtension(hotRestart);
491+
492+
await mainDone;
493+
source = await context.webDriver.pageSource;
494+
// Main is re-invoked which shouldn't clear the state.
495+
expect(source.contains(originalString), isTrue);
496+
expect(source.contains(newString), isTrue);
497+
498+
// Empty hot restart.
499+
mainDone = waitForMainToExecute();
500+
await recompile();
501+
await fakeClient.callServiceExtension(hotRestart);
502+
503+
await mainDone;
504+
source = await context.webDriver.pageSource;
505+
expect(source.contains(originalString), isTrue);
506+
// `newString` should now exist twice in the source.
507+
expect(source.contains(RegExp('$newString.*$newString')), isTrue);
508+
});
473509
}, timeout: Timeout.factor(2));
474510

475511
group(

dwds/test/hot_reload_test.dart

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
@Timeout(Duration(minutes: 5))
88
library;
99

10+
import 'dart:async';
11+
1012
import 'package:dwds/expression_compiler.dart';
1113
import 'package:test/test.dart';
1214
import 'package:test_common/logging.dart';
@@ -33,13 +35,31 @@ void main() {
3335

3436
tearDownAll(provider.dispose);
3537

38+
Future<void> recompile() async {
39+
await context.recompile(fullRestart: false);
40+
}
41+
3642
Future<void> makeEditAndRecompile() async {
3743
context.makeEditToDartLibFile(
3844
libFileName: 'library1.dart',
3945
toReplace: originalString,
4046
replaceWith: newString,
4147
);
42-
await context.recompile(fullRestart: false);
48+
await recompile();
49+
}
50+
51+
/// Wait for `evaluate` to finish executing before checking expectations by
52+
/// checking for a log output.
53+
Future<void> waitForEvaluateToExecute() async {
54+
final completer = Completer<void>();
55+
final expectedString = 'evaluate executed';
56+
final subscription = context.webkitDebugger.onConsoleAPICalled.listen((e) {
57+
if (e.args.first.value == expectedString) {
58+
completer.complete();
59+
}
60+
});
61+
await completer.future;
62+
await subscription.cancel();
4363
}
4464

4565
group('Injected client', () {
@@ -64,11 +84,10 @@ void main() {
6484

6585
test('can hot reload', () async {
6686
final client = context.debugConnection.vmService;
67-
await makeEditAndRecompile();
6887

88+
await makeEditAndRecompile();
6989
final vm = await client.getVM();
7090
final isolate = await client.getIsolate(vm.isolates!.first.id!);
71-
7291
final report = await fakeClient.reloadSources(isolate.id!);
7392
expect(report.success, true);
7493

@@ -78,8 +97,54 @@ void main() {
7897
expect(source, contains(originalString));
7998
expect(source.contains(newString), false);
8099

100+
final evaluateDone = waitForEvaluateToExecute();
81101
final rootLib = isolate.rootLib;
82102
await client.evaluate(isolate.id!, rootLib!.id!, 'evaluate()');
103+
await evaluateDone;
104+
source = await context.webDriver.pageSource;
105+
expect(source, contains(newString));
106+
expect(source.contains(originalString), false);
107+
});
108+
109+
test('can hot reload with no changes, hot reload with changes, and '
110+
'hot reload again with no changes', () async {
111+
final client = context.debugConnection.vmService;
112+
113+
// Empty hot reload,
114+
await recompile();
115+
final vm = await client.getVM();
116+
final isolate = await client.getIsolate(vm.isolates!.first.id!);
117+
var report = await fakeClient.reloadSources(isolate.id!);
118+
expect(report.success, true);
119+
120+
var evaluateDone = waitForEvaluateToExecute();
121+
final rootLib = isolate.rootLib;
122+
await client.evaluate(isolate.id!, rootLib!.id!, 'evaluate()');
123+
await evaluateDone;
124+
var source = await context.webDriver.pageSource;
125+
expect(source, contains(originalString));
126+
expect(source.contains(newString), false);
127+
128+
// Hot reload.
129+
await makeEditAndRecompile();
130+
report = await fakeClient.reloadSources(isolate.id!);
131+
expect(report.success, true);
132+
133+
evaluateDone = waitForEvaluateToExecute();
134+
await client.evaluate(isolate.id!, rootLib.id!, 'evaluate()');
135+
await evaluateDone;
136+
source = await context.webDriver.pageSource;
137+
expect(source, contains(newString));
138+
expect(source.contains(originalString), false);
139+
140+
// Empty hot reload.
141+
await recompile();
142+
report = await fakeClient.reloadSources(isolate.id!);
143+
expect(report.success, true);
144+
145+
evaluateDone = waitForEvaluateToExecute();
146+
await client.evaluate(isolate.id!, rootLib.id!, 'evaluate()');
147+
await evaluateDone;
83148
source = await context.webDriver.pageSource;
84149
expect(source, contains(newString));
85150
expect(source.contains(originalString), false);

fixtures/_test_hot_reload/web/main.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ import 'dart:js_interop';
77

88
import 'package:_test_hot_reload/library1.dart';
99

10+
@JS('console.log')
11+
external void log(String _);
12+
1013
@JS('document.body.innerHTML')
1114
external set innerHtml(String html);
1215

1316
void evaluate() {
1417
innerHtml = 'Program is running!\n $reloadValue}\n';
18+
19+
// Wait for this print statement so that we know evaluate is done executing.
20+
log('evaluate executed');
1521
}
1622

1723
void main() {

0 commit comments

Comments
 (0)