Skip to content

Commit e285b26

Browse files
authored
Fix hot reload subscription problem (#291)
1 parent ec5d6aa commit e285b26

File tree

2 files changed

+85
-53
lines changed

2 files changed

+85
-53
lines changed

pkgs/dart_mcp_server/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* Add the abillity to limit the output of `analyze_files` to a set of paths.
1616
* Stop reporting non-zero exit codes from command line tools as tool errors.
1717
* Add descriptions for pub tools, add support for `pub deps` and `pub outdated`.
18+
* Fix a bug in hot_reload ([#290](https://github.com/dart-lang/ai/issues/290)).
1819

1920
# 0.1.0 (Dart SDK 3.9.0)
2021

pkgs/dart_mcp_server/lib/src/mixins/dtd.dart

Lines changed: 84 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,9 @@ base mixin DartToolingDaemonSupport
177177
return _callOnVmService(
178178
callback: (vmService) async {
179179
final appListener = await _AppListener.forVmService(vmService, this);
180-
if (!appListener.registeredServices.contains(_flutterDriverService)) {
180+
if (!appListener.registeredServices.containsKey(
181+
_flutterDriverService,
182+
)) {
181183
return _flutterDriverNotRegistered;
182184
}
183185
final vm = await vmService.getVM();
@@ -358,36 +360,17 @@ base mixin DartToolingDaemonSupport
358360
Future<CallToolResult> hotReload(CallToolRequest request) async {
359361
return _callOnVmService(
360362
callback: (vmService) async {
363+
final appListener = await _AppListener.forVmService(vmService, this);
361364
if (request.arguments?['clearRuntimeErrors'] == true) {
362-
(await _AppListener.forVmService(vmService, this)).errorLog.clear();
365+
appListener.errorLog.clear();
363366
}
364367

365368
final vm = await vmService.getVM();
366369
ReloadReport? report;
367-
StreamSubscription<Event>? serviceStreamSubscription;
368-
try {
369-
final hotReloadMethodNameCompleter = Completer<String?>();
370-
serviceStreamSubscription = vmService
371-
.onEvent(EventStreams.kService)
372-
.listen((Event e) {
373-
if (e.kind == EventKind.kServiceRegistered) {
374-
final serviceName = e.service!;
375-
if (serviceName == 'reloadSources') {
376-
// This may look something like 's0.reloadSources'.
377-
hotReloadMethodNameCompleter.complete(e.method);
378-
}
379-
}
380-
});
381370

382-
await vmService.streamListen(EventStreams.kService);
383-
384-
final hotReloadMethodName = await hotReloadMethodNameCompleter.future
385-
.timeout(
386-
const Duration(milliseconds: 1000),
387-
onTimeout: () async {
388-
return null;
389-
},
390-
);
371+
try {
372+
final hotReloadMethodName = await appListener
373+
.waitForServiceRegistration('reloadSources');
391374

392375
/// If we haven't seen a specific one, we just call the default one.
393376
if (hotReloadMethodName == null) {
@@ -406,9 +389,12 @@ base mixin DartToolingDaemonSupport
406389
report = ReloadReport(success: false);
407390
}
408391
}
409-
} finally {
410-
await serviceStreamSubscription?.cancel();
411-
await vmService.streamCancel(EventStreams.kService);
392+
} catch (e) {
393+
// Handle potential errors during the process
394+
return CallToolResult(
395+
isError: true,
396+
content: [TextContent(text: 'Hot reload failed: $e')],
397+
);
412398
}
413399
final success = report.success == true;
414400
return CallToolResult(
@@ -1066,7 +1052,12 @@ class _AppListener {
10661052
/// A broadcast stream of all errors that come in after you start listening.
10671053
Stream<String> get errorsStream => _errorsController.stream;
10681054

1069-
final Set<String> registeredServices;
1055+
/// A map of service names to the names of their methods.
1056+
final Map<String, String?> registeredServices;
1057+
1058+
/// A map of service names to completers that should be fired when the service
1059+
/// is registered.
1060+
final _pendingServiceRequests = <String, List<Completer<String?>>>{};
10701061

10711062
/// Controller for the [errorsStream].
10721063
final StreamController<String> _errorsController;
@@ -1105,9 +1096,36 @@ class _AppListener {
11051096
final errorLog = ErrorLog();
11061097
errorsController.stream.listen(errorLog.add);
11071098
final subscriptions = <StreamSubscription<void>>[];
1108-
final registeredServices = <String>{};
1099+
final registeredServices = <String, String?>{};
1100+
final pendingServiceRequests = <String, List<Completer<String?>>>{};
11091101

11101102
try {
1103+
subscriptions.addAll([
1104+
vmService.onServiceEvent.listen((Event e) {
1105+
switch (e.kind) {
1106+
case EventKind.kServiceRegistered:
1107+
final serviceName = e.service!;
1108+
registeredServices[serviceName] = e.method;
1109+
// If there are any pending requests for this service, complete
1110+
// them.
1111+
if (pendingServiceRequests.containsKey(serviceName)) {
1112+
for (final completer
1113+
in pendingServiceRequests[serviceName]!) {
1114+
completer.complete(e.method);
1115+
}
1116+
pendingServiceRequests.remove(serviceName);
1117+
}
1118+
case EventKind.kServiceUnregistered:
1119+
registeredServices.remove(e.service!);
1120+
}
1121+
}),
1122+
vmService.onIsolateEvent.listen((e) {
1123+
switch (e.kind) {
1124+
case EventKind.kServiceExtensionAdded:
1125+
registeredServices[e.extensionRPC!] = null;
1126+
}
1127+
}),
1128+
]);
11111129
subscriptions.add(
11121130
vmService.onExtensionEventWithHistory.listen((Event e) {
11131131
if (e.extensionKind == 'Flutter.Error') {
@@ -1135,23 +1153,6 @@ class _AppListener {
11351153
}),
11361154
);
11371155

1138-
subscriptions.addAll([
1139-
vmService.onServiceEvent.listen((Event e) {
1140-
switch (e.kind) {
1141-
case EventKind.kServiceRegistered:
1142-
registeredServices.add(e.service!);
1143-
case EventKind.kServiceUnregistered:
1144-
registeredServices.remove(e.service!);
1145-
}
1146-
}),
1147-
vmService.onIsolateEvent.listen((e) {
1148-
switch (e.kind) {
1149-
case EventKind.kServiceExtensionAdded:
1150-
registeredServices.add(e.extensionRPC!);
1151-
}
1152-
}),
1153-
]);
1154-
11551156
await [
11561157
vmService.streamListen(EventStreams.kExtension),
11571158
vmService.streamListen(EventStreams.kIsolate),
@@ -1161,7 +1162,9 @@ class _AppListener {
11611162

11621163
final vm = await vmService.getVM();
11631164
final isolate = await vmService.getIsolate(vm.isolates!.first.id!);
1164-
registeredServices.addAll(isolate.extensionRPCs ?? []);
1165+
for (final extension in isolate.extensionRPCs ?? <String>[]) {
1166+
registeredServices[extension] = null;
1167+
}
11651168
} catch (e) {
11661169
logger.log(LoggingLevel.error, 'Error subscribing to app errors: $e');
11671170
}
@@ -1175,18 +1178,46 @@ class _AppListener {
11751178
}();
11761179
}
11771180

1181+
/// Returns a future that completes with the registered method name for the
1182+
/// given [serviceName].
1183+
Future<String?> waitForServiceRegistration(
1184+
String serviceName, {
1185+
Duration timeout = const Duration(seconds: 1),
1186+
}) async {
1187+
if (registeredServices.containsKey(serviceName)) {
1188+
return registeredServices[serviceName];
1189+
}
1190+
final completer = Completer<String?>();
1191+
_pendingServiceRequests.putIfAbsent(serviceName, () => []).add(completer);
1192+
1193+
return completer.future.timeout(
1194+
timeout,
1195+
onTimeout: () {
1196+
// Important: Clean up the completer from the list on timeout.
1197+
_pendingServiceRequests[serviceName]?.remove(completer);
1198+
if (_pendingServiceRequests[serviceName]?.isEmpty ?? false) {
1199+
_pendingServiceRequests.remove(serviceName);
1200+
}
1201+
return null; // Return null on timeout
1202+
},
1203+
);
1204+
}
1205+
11781206
Future<void> shutdown() async {
11791207
errorLog.clear();
11801208
registeredServices.clear();
11811209
await _errorsController.close();
11821210
await Future.wait(_subscriptions.map((s) => s.cancel()));
11831211
try {
1184-
await _vmService.streamCancel(EventStreams.kExtension);
1185-
await _vmService.streamCancel(EventStreams.kIsolate);
1186-
await _vmService.streamCancel(EventStreams.kStderr);
1187-
await _vmService.streamCancel(EventStreams.kService);
1212+
await [
1213+
_vmService.streamCancel(EventStreams.kExtension),
1214+
_vmService.streamCancel(EventStreams.kIsolate),
1215+
_vmService.streamCancel(EventStreams.kStderr),
1216+
_vmService.streamCancel(EventStreams.kService),
1217+
].wait;
11881218
} on RPCError catch (_) {
1189-
// The vm service might already be disposed in which causes these to fail.
1219+
// The vm service might already be disposed which could cause these to
1220+
// fail.
11901221
}
11911222
}
11921223
}

0 commit comments

Comments
 (0)