tear down the previous workspace's services on project switch (T-367)
buildDispatcher composed a fresh PaneRegistry, FilesService, SearchService, and EditorRegistry per workspace, but their shutdown() methods had zero callers — every project switch left the old set's file watcher emitting into the new workspace's bus and its PTYs alive. The dispatcher now pairs with a teardown closure that the serialized swap invokes after the old server stops; the same-path reuse fast-path drops the unused new set without teardown since its services are inert until a command starts them. SearchService gains the shutdown() it was missing (cancels in-flight searches). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+37
-9
@@ -136,7 +136,13 @@ Future<void> main() async {
|
||||
// below doSwapIpcServer for why. (T-352)
|
||||
Future<void> swapChain = Future<void>.value();
|
||||
|
||||
Future<void> doSwapIpcServer(DaemonDispatcher dispatcher, Directory workRoot) async {
|
||||
// Teardown of the service set behind the currently-served dispatcher
|
||||
// (pane PTYs, file watcher, in-flight searches, editor buffers). Swapped
|
||||
// alongside the IPC server so a project switch can't leak the previous
|
||||
// workspace's watchers into the new one's bus (T-367).
|
||||
Future<void> Function()? activeSubsystemTeardown;
|
||||
|
||||
Future<void> doSwapIpcServer(DaemonDispatcher dispatcher, Future<void> Function() teardown, Directory workRoot) async {
|
||||
if (kIsWeb) return;
|
||||
// Already serving this exact workspace? Reuse the live server.
|
||||
// The startup factory binds the launch CWD, then the project-open
|
||||
@@ -148,6 +154,9 @@ Future<void> main() async {
|
||||
final live = ipcServer;
|
||||
if (live != null && live.isRunning && live.workspaceRoot == workRoot.path) {
|
||||
ipcLog.info('ipc', 'already serving ${workRoot.path}; reusing the live server');
|
||||
// The freshly built dispatcher is dropped unused — its services are
|
||||
// inert (watchers/PTYs only start via dispatched commands), so there
|
||||
// is nothing to tear down. The live server keeps its own set.
|
||||
// Idempotent — a no-op when the client is already connected here.
|
||||
await ipcClient?.reconnectAt(live.socketPath);
|
||||
return;
|
||||
@@ -163,6 +172,16 @@ Future<void> main() async {
|
||||
} catch (e) {
|
||||
ipcLog.warn('mcp', 'stop failed during swap: $e');
|
||||
}
|
||||
// The old server is down — release the previous workspace's services
|
||||
// before the new set takes over (T-367). The shutdown() methods are
|
||||
// idempotent, so a failed swap retried later is safe.
|
||||
try {
|
||||
await activeSubsystemTeardown?.call();
|
||||
} catch (e, st) {
|
||||
ipcLog.warn('ipc', 'subsystem teardown failed during swap: $e');
|
||||
ipcLog.debug('ipc', '$st');
|
||||
}
|
||||
activeSubsystemTeardown = teardown;
|
||||
final server = IpcServer(dispatcher: dispatcher, workspaceRoot: workRoot.path, log: ipcLog, events: daemonBus);
|
||||
ipcServer = server;
|
||||
try {
|
||||
@@ -197,14 +216,14 @@ Future<void> main() async {
|
||||
// load (stale/global pql.db) yet working after a manual refresh. Chaining
|
||||
// every swap makes them apply in call order; the repo swap is issued last
|
||||
// and therefore wins. (T-352)
|
||||
Future<void> swapIpcServer(DaemonDispatcher dispatcher, Directory workRoot) {
|
||||
final next = swapChain.then((_) => doSwapIpcServer(dispatcher, workRoot));
|
||||
Future<void> swapIpcServer(DaemonDispatcher dispatcher, Future<void> Function() teardown, Directory workRoot) {
|
||||
final next = swapChain.then((_) => doSwapIpcServer(dispatcher, teardown, workRoot));
|
||||
// A failed swap must not break the chain for the next one.
|
||||
swapChain = next.catchError((Object _) {});
|
||||
return next;
|
||||
}
|
||||
|
||||
DaemonDispatcher buildDispatcher(DaemonBus events, Toolchain tc, Directory workRoot, LayoutArrangement arrangement, PanelRegistry panels) {
|
||||
(DaemonDispatcher, Future<void> Function()) buildDispatcher(DaemonBus events, Toolchain tc, Directory workRoot, LayoutArrangement arrangement, PanelRegistry panels) {
|
||||
final dispatcher = DaemonDispatcher();
|
||||
final eventSink = _BusEventSink(events);
|
||||
final paneRegistry = PaneRegistry(events: eventSink);
|
||||
@@ -297,7 +316,16 @@ Future<void> main() async {
|
||||
};
|
||||
});
|
||||
registerArgvUnwrap(dispatcher);
|
||||
return dispatcher;
|
||||
// Paired teardown for this workspace's stateful services — the swap
|
||||
// calls it when this dispatcher stops being served (T-367).
|
||||
Future<void> teardown() async {
|
||||
await paneRegistry.shutdown();
|
||||
await filesService.shutdown();
|
||||
await searchService.shutdown();
|
||||
await editorRegistry.shutdown();
|
||||
}
|
||||
|
||||
return (dispatcher, teardown);
|
||||
}
|
||||
|
||||
final services = await KernelServices.boot(
|
||||
@@ -314,7 +342,7 @@ Future<void> main() async {
|
||||
kernelArrangement = arrangement;
|
||||
kernelPanels = panels;
|
||||
final workRoot = startupWorkRoot;
|
||||
final dispatcher = buildDispatcher(events, toolchain, workRoot, arrangement, panels);
|
||||
final (dispatcher, teardown) = buildDispatcher(events, toolchain, workRoot, arrangement, panels);
|
||||
// Build the client at the workspace's socket path. The
|
||||
// server is started below (swapIpcServer) which the
|
||||
// client will then auto-connect to via its reconnect
|
||||
@@ -329,7 +357,7 @@ Future<void> main() async {
|
||||
// the connect immediate. _connect's already-connected guard
|
||||
// keeps these two paths from opening a second socket.
|
||||
unawaited(client.start());
|
||||
unawaited(swapIpcServer(dispatcher, workRoot));
|
||||
unawaited(swapIpcServer(dispatcher, teardown, workRoot));
|
||||
return client;
|
||||
},
|
||||
onProjectOpen: kIsWeb
|
||||
@@ -339,8 +367,8 @@ Future<void> main() async {
|
||||
final arrangement = kernelArrangement;
|
||||
final panels = kernelPanels;
|
||||
if (bus == null || arrangement == null || panels == null) return;
|
||||
final dispatcher = buildDispatcher(bus, toolchain, Directory(path), arrangement, panels);
|
||||
await swapIpcServer(dispatcher, Directory(path));
|
||||
final (dispatcher, teardown) = buildDispatcher(bus, toolchain, Directory(path), arrangement, panels);
|
||||
await swapIpcServer(dispatcher, teardown, Directory(path));
|
||||
},
|
||||
);
|
||||
// Expose the reader nav to the `clide status` snapshot (T-221). Boot
|
||||
|
||||
@@ -52,6 +52,15 @@ class SearchService {
|
||||
_active.remove(id)?.cancel();
|
||||
}
|
||||
|
||||
/// Cancel every in-flight search. Called when the workspace service
|
||||
/// set is torn down on project switch (T-367).
|
||||
Future<void> shutdown() async {
|
||||
for (final c in _active.values) {
|
||||
c.cancel();
|
||||
}
|
||||
_active.clear();
|
||||
}
|
||||
|
||||
/// Compute (preview) or perform (apply) a search-and-replace.
|
||||
///
|
||||
/// Preview returns per-file before/after edits without touching disk.
|
||||
|
||||
Reference in New Issue
Block a user