Skip to content

Commit 3abd0f3

Browse files
committed
[dwds] Fix bug where no-op hot restart doesn't cancel a subscription
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 3abd0f3

File tree

7 files changed

+104
-20
lines changed

7 files changed

+104
-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: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,17 @@ void main() {
3333

3434
tearDownAll(provider.dispose);
3535

36+
Future<void> recompile() async {
37+
await context.recompile(fullRestart: false);
38+
}
39+
3640
Future<void> makeEditAndRecompile() async {
3741
context.makeEditToDartLibFile(
3842
libFileName: 'library1.dart',
3943
toReplace: originalString,
4044
replaceWith: newString,
4145
);
42-
await context.recompile(fullRestart: false);
46+
await recompile();
4347
}
4448

4549
group('Injected client', () {
@@ -64,11 +68,10 @@ void main() {
6468

6569
test('can hot reload', () async {
6670
final client = context.debugConnection.vmService;
67-
await makeEditAndRecompile();
6871

72+
await makeEditAndRecompile();
6973
final vm = await client.getVM();
7074
final isolate = await client.getIsolate(vm.isolates!.first.id!);
71-
7275
final report = await fakeClient.reloadSources(isolate.id!);
7376
expect(report.success, true);
7477

@@ -84,5 +87,43 @@ void main() {
8487
expect(source, contains(newString));
8588
expect(source.contains(originalString), false);
8689
});
90+
91+
test('can hot reload with no changes, hot reload with changes, and '
92+
'hot reload again with no changes', () async {
93+
final client = context.debugConnection.vmService;
94+
95+
// Empty hot reload,
96+
await recompile();
97+
final vm = await client.getVM();
98+
final isolate = await client.getIsolate(vm.isolates!.first.id!);
99+
var report = await fakeClient.reloadSources(isolate.id!);
100+
expect(report.success, true);
101+
102+
final rootLib = isolate.rootLib;
103+
await client.evaluate(isolate.id!, rootLib!.id!, 'evaluate()');
104+
var source = await context.webDriver.pageSource;
105+
expect(source, contains(originalString));
106+
expect(source.contains(newString), false);
107+
108+
// Hot reload.
109+
await makeEditAndRecompile();
110+
report = await fakeClient.reloadSources(isolate.id!);
111+
expect(report.success, true);
112+
113+
await client.evaluate(isolate.id!, rootLib.id!, 'evaluate()');
114+
source = await context.webDriver.pageSource;
115+
expect(source, contains(newString));
116+
expect(source.contains(originalString), false);
117+
118+
// Empty hot reload.
119+
await recompile();
120+
report = await fakeClient.reloadSources(isolate.id!);
121+
expect(report.success, true);
122+
123+
await client.evaluate(isolate.id!, rootLib.id!, 'evaluate()');
124+
source = await context.webDriver.pageSource;
125+
expect(source, contains(newString));
126+
expect(source.contains(originalString), false);
127+
});
87128
}, timeout: Timeout.factor(2));
88129
}

0 commit comments

Comments
 (0)