Skip to content

Commit 4c3fa22

Browse files
[dart_tooling_mcp_server] Persist VmService objects (#62)
1 parent e239767 commit 4c3fa22

File tree

6 files changed

+277
-110
lines changed

6 files changed

+277
-110
lines changed

pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart

+9-3
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,15 @@ base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
115115
final watcher = DirectoryWatcher(rootPath);
116116
_watchSubscriptions.add(
117117
watcher.events.listen((event) {
118-
_analysisContexts
119-
?.contextFor(event.path)
120-
.changeFile(p.normalize(event.path));
118+
try {
119+
_analysisContexts
120+
?.contextFor(event.path)
121+
.changeFile(p.normalize(event.path));
122+
} catch (_) {
123+
// Fail gracefully.
124+
// TODO(https://github.com/dart-lang/ai/issues/65): remove this
125+
// catch if possible.
126+
}
121127
}),
122128
);
123129
}

pkgs/dart_tooling_mcp_server/lib/src/mixins/dart_cli.dart

+3
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ base mixin DartCliSupport on ToolsSupport, LoggingSupport {
4444
}
4545

4646
/// Helper to run a dart command in multiple project roots.
47+
///
48+
/// [defaultPaths] may be specified if one or more path arguments are required
49+
/// for the dart command (e.g. `dart format <default paths>`).
4750
Future<CallToolResult> _runDartCommandInRoots(
4851
CallToolRequest request,
4952
String commandName,

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

+122-67
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,62 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
2424
/// ready to be invoked.
2525
bool _getDebugSessionsReady = false;
2626

27+
/// A Map of [VmService] objects by their associated VM Service URI
28+
/// (represented as a String).
29+
///
30+
/// [VmService] objects are automatically removed from the Map when the
31+
/// [VmService] shuts down.
32+
@visibleForTesting
33+
final activeVmServices = <String, VmService>{};
34+
35+
/// Whether to await the disposal of all [VmService] objects in
36+
/// [activeVmServices] upon server shutdown or loss of DTD connection.
37+
///
38+
/// Defaults to false but can be flipped to true for testing purposes.
39+
@visibleForTesting
40+
static bool debugAwaitVmServiceDisposal = false;
41+
2742
/// Called when the DTD connection is lost, resets all associated state.
28-
void _resetDtd() {
43+
Future<void> _resetDtd() async {
2944
_dtd = null;
3045
_getDebugSessionsReady = false;
46+
47+
final future = Future.wait(
48+
activeVmServices.values.map((vmService) => vmService.dispose()),
49+
);
50+
debugAwaitVmServiceDisposal ? await future : unawaited(future);
51+
52+
activeVmServices.clear();
53+
}
54+
55+
@visibleForTesting
56+
Future<void> updateActiveVmServices() async {
57+
final dtd = _dtd;
58+
if (dtd == null) return;
59+
60+
// TODO: in the future, get the active VM service URIs from DTD directly
61+
// instead of from the `Editor.getDebugSessions` service method.
62+
if (!_getDebugSessionsReady) {
63+
// Give it a chance to get ready.
64+
await Future<void>.delayed(const Duration(seconds: 1));
65+
if (!_getDebugSessionsReady) return;
66+
}
67+
68+
final response = await dtd.getDebugSessions();
69+
final debugSessions = response.debugSessions;
70+
for (final debugSession in debugSessions) {
71+
if (activeVmServices.containsKey(debugSession.vmServiceUri)) {
72+
continue;
73+
}
74+
final vmService = await vmServiceConnectUri(debugSession.vmServiceUri);
75+
activeVmServices[debugSession.vmServiceUri] = vmService;
76+
unawaited(
77+
vmService.onDone.then((_) {
78+
activeVmServices.remove(debugSession.vmServiceUri);
79+
vmService.dispose();
80+
}),
81+
);
82+
}
3183
}
3284

3385
@override
@@ -46,6 +98,12 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
4698
return super.initialize(request);
4799
}
48100

101+
@override
102+
Future<void> shutdown() async {
103+
await _resetDtd();
104+
await super.shutdown();
105+
}
106+
49107
/// Connects to the Dart Tooling Daemon.
50108
FutureOr<CallToolResult> _connect(CallToolRequest request) async {
51109
if (_dtd != null) {
@@ -65,7 +123,7 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
65123
_dtd = await DartToolingDaemon.connect(
66124
Uri.parse(request.arguments!['uri'] as String),
67125
);
68-
unawaited(_dtd!.done.then((_) => _resetDtd()));
126+
unawaited(_dtd!.done.then((_) async => await _resetDtd()));
69127

70128
_listenForServices();
71129
return CallToolResult(
@@ -151,62 +209,66 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
151209
Future<CallToolResult> hotReload(CallToolRequest request) async {
152210
return _callOnVmService(
153211
callback: (vmService) async {
154-
final vm = await vmService.getVM();
155-
156-
final hotReloadMethodNameCompleter = Completer<String?>();
157-
vmService.onEvent(EventStreams.kService).listen((Event e) {
158-
if (e.kind == EventKind.kServiceRegistered) {
159-
final serviceName = e.service!;
160-
if (serviceName == 'reloadSources') {
161-
// This may look something like 's0.reloadSources'.
162-
hotReloadMethodNameCompleter.complete(e.method);
163-
}
164-
}
165-
});
166-
await vmService.streamListen(EventStreams.kService);
167-
final hotReloadMethodName = await hotReloadMethodNameCompleter.future
168-
.timeout(
169-
const Duration(milliseconds: 1000),
170-
onTimeout: () async {
171-
return null;
172-
},
212+
StreamSubscription<Event>? serviceStreamSubscription;
213+
try {
214+
final hotReloadMethodNameCompleter = Completer<String?>();
215+
serviceStreamSubscription = vmService
216+
.onEvent(EventStreams.kService)
217+
.listen((Event e) {
218+
if (e.kind == EventKind.kServiceRegistered) {
219+
final serviceName = e.service!;
220+
if (serviceName == 'reloadSources') {
221+
// This may look something like 's0.reloadSources'.
222+
hotReloadMethodNameCompleter.complete(e.method);
223+
}
224+
}
225+
});
226+
227+
await vmService.streamListen(EventStreams.kService);
228+
229+
final hotReloadMethodName = await hotReloadMethodNameCompleter.future
230+
.timeout(
231+
const Duration(milliseconds: 1000),
232+
onTimeout: () async {
233+
return null;
234+
},
235+
);
236+
if (hotReloadMethodName == null) {
237+
return CallToolResult(
238+
isError: true,
239+
content: [
240+
TextContent(
241+
text:
242+
'The hot reload service has not been registered yet. '
243+
'Please wait a few seconds and try again.',
244+
),
245+
],
173246
);
174-
await vmService.streamCancel(EventStreams.kService);
175-
176-
if (hotReloadMethodName == null) {
177-
return CallToolResult(
178-
isError: true,
179-
content: [
180-
TextContent(
181-
text:
182-
'The hot reload service has not been registered yet, '
183-
'please wait a few seconds and try again.',
184-
),
185-
],
186-
);
187-
}
247+
}
188248

189-
final result = await vmService.callMethod(
190-
hotReloadMethodName,
191-
isolateId: vm.isolates!.first.id,
192-
);
193-
final resultType = result.json?['type'];
194-
if (resultType == 'Success' ||
195-
(resultType == 'ReloadReport' && result.json?['success'] == true)) {
196-
return CallToolResult(
197-
content: [TextContent(text: 'Hot reload succeeded.')],
198-
);
199-
} else {
200-
return CallToolResult(
201-
isError: true,
202-
content: [
203-
TextContent(
204-
text:
205-
'Hot reload failed:\n'
206-
'${result.json}',
207-
),
208-
],
249+
final vm = await vmService.getVM();
250+
final result = await vmService.callMethod(
251+
hotReloadMethodName,
252+
isolateId: vm.isolates!.first.id,
209253
);
254+
final resultType = result.json?['type'];
255+
if (resultType == 'Success' ||
256+
(resultType == 'ReloadReport' &&
257+
result.json?['success'] == true)) {
258+
return CallToolResult(
259+
content: [TextContent(text: 'Hot reload succeeded.')],
260+
);
261+
} else {
262+
return CallToolResult(
263+
isError: true,
264+
content: [
265+
TextContent(text: 'Hot reload failed:\n${result.json}'),
266+
],
267+
);
268+
}
269+
} finally {
270+
await serviceStreamSubscription?.cancel();
271+
await vmService.streamCancel(EventStreams.kService);
210272
}
211273
},
212274
);
@@ -355,19 +417,12 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
355417
if (!_getDebugSessionsReady) return _dtdNotReady;
356418
}
357419

358-
final response = await dtd.getDebugSessions();
359-
final debugSessions = response.debugSessions;
360-
if (debugSessions.isEmpty) return _noActiveDebugSession;
420+
await updateActiveVmServices();
421+
if (activeVmServices.isEmpty) return _noActiveDebugSession;
361422

362-
// TODO: Consider holding on to this connection.
363-
final vmService = await vmServiceConnectUri(
364-
debugSessions.first.vmServiceUri,
365-
);
366-
try {
367-
return await callback(vmService);
368-
} finally {
369-
unawaited(vmService.dispose());
370-
}
423+
// TODO: support selecting a VM Service if more than one are available.
424+
final vmService = activeVmServices.values.first;
425+
return await callback(vmService);
371426
}
372427

373428
@visibleForTesting

0 commit comments

Comments
 (0)