Skip to content

Commit 35d8056

Browse files
committed
feat(lifecycle): enhance CDP disconnect handling during change checks and extension reloads
1 parent 517a3cb commit 35d8056

4 files changed

Lines changed: 79 additions & 2 deletions

File tree

src/host-pipe.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ export interface CheckForChangesResult {
256256
extRebuilt: boolean;
257257
extBuildError: string | null;
258258
extClientReloaded: boolean;
259+
newCdpPort?: number;
260+
newClientStartedAt?: number;
259261
}
260262

261263
/**

src/main.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,18 @@ const pipeline = new RequestPipeline({
271271
mcpServerRoot: mcpServerDir,
272272
extensionPath: config.extensionBridgePath,
273273
hotReloadEnabled: config.hotReload.enabled && config.explicitExtensionDevelopmentPath,
274+
onBeforeChangeCheck: () => {
275+
lifecycleService.suppressCdpDisconnectDuringChangeCheck();
276+
},
277+
onAfterChangeCheck: async (result) => {
278+
lifecycleService.resumeCdpDisconnectHandling();
279+
if (result.extClientReloaded && result.newCdpPort) {
280+
await lifecycleService.reconnectAfterExtensionReload(
281+
result.newCdpPort,
282+
result.newClientStartedAt,
283+
);
284+
}
285+
},
274286
});
275287

276288
function registerTool(targetServer: McpServer, tool: ToolDefinition): void {

src/services/LifecycleService.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ class LifecycleService {
3636
private exitCleanupDone = false;
3737
private shutdownHandlersRegistered = false;
3838

39+
/**
40+
* When true, CDP disconnect events are suppressed (not treated as crashes).
41+
* Set during checkForChanges RPC which may kill the Client window.
42+
*/
43+
private _changeCheckInProgress = false;
44+
3945
/** Client workspace folder the MCP server controls */
4046
private _clientWorkspace: string | undefined;
4147
/** Extension development path for the Client */
@@ -254,7 +260,8 @@ class LifecycleService {
254260

255261
// Unexpected CDP close → user closed the debug window → exit
256262
cdpService.setDisconnectHandler((intentional, lastPort) => {
257-
if (intentional) {
263+
if (intentional || this._changeCheckInProgress) {
264+
logger(`[Lifecycle] CDP closed (suppressed — intentional=${intentional}, changeCheck=${this._changeCheckInProgress})`);
258265
return;
259266
}
260267

@@ -285,6 +292,45 @@ class LifecycleService {
285292

286293
// ── State Getters ──────────────────────────────────────
287294

295+
// ── Change Check CDP Suppression ────────────────────────
296+
297+
/**
298+
* Suppress CDP disconnect handling during checkForChanges RPC.
299+
* The extension may kill the Client window during this RPC,
300+
* which would otherwise be treated as "user closed debug window".
301+
*/
302+
suppressCdpDisconnectDuringChangeCheck(): void {
303+
this._changeCheckInProgress = true;
304+
logger('[Lifecycle] CDP disconnect suppressed for change check');
305+
}
306+
307+
/**
308+
* Resume normal CDP disconnect handling after checkForChanges.
309+
*/
310+
resumeCdpDisconnectHandling(): void {
311+
this._changeCheckInProgress = false;
312+
logger('[Lifecycle] CDP disconnect handling resumed');
313+
}
314+
315+
/**
316+
* Update internal state after the extension reloaded the Client.
317+
* Reconnects CDP to the new port and re-inits event subscriptions.
318+
*/
319+
async reconnectAfterExtensionReload(newCdpPort: number, clientStartedAt?: number): Promise<void> {
320+
logger(`[Lifecycle] Reconnecting CDP after extension reload — new port: ${newCdpPort}`);
321+
322+
// Disconnect old CDP (if still lingering) — intentional
323+
cdpService.disconnect();
324+
clearCdpEventData();
325+
326+
this._cdpPort = newCdpPort;
327+
await cdpService.connect(newCdpPort);
328+
await initCdpEventSubscriptions();
329+
330+
this._debugWindowStartedAt = clientStartedAt ?? Date.now();
331+
logger(`[Lifecycle] Extension reload reconnect complete — sessionTs=${new Date(this._debugWindowStartedAt).toISOString()}`);
332+
}
333+
288334
get isConnected(): boolean {
289335
return cdpService.isConnected;
290336
}

src/services/requestPipeline.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ export interface ChangeCheckResult {
7171
extRebuilt: boolean;
7272
extBuildError: string | null;
7373
extClientReloaded: boolean;
74+
newCdpPort?: number;
75+
newClientStartedAt?: number;
7476
}
7577

7678
interface PipelineEntry {
@@ -100,6 +102,10 @@ export interface PipelineDeps {
100102
extensionPath: string;
101103
/** Master switch — when false, all hot-reload checks are skipped. */
102104
hotReloadEnabled: boolean;
105+
/** Suppress CDP disconnect handling before checkForChanges (extension may kill Client). */
106+
onBeforeChangeCheck?: () => void;
107+
/** Restore CDP disconnect handling + reconnect CDP if extension reloaded Client. */
108+
onAfterChangeCheck?: (result: ChangeCheckResult) => Promise<void>;
103109
}
104110

105111
// ── RequestPipeline ──────────────────────────────────────
@@ -206,6 +212,9 @@ export class RequestPipeline {
206212
): Promise<ChangeCheckResult | null> {
207213
let check: ChangeCheckResult;
208214

215+
// Suppress CDP disconnect handling — extension may kill Client during rebuild
216+
this.deps.onBeforeChangeCheck?.();
217+
209218
try {
210219
check = await this.deps.checkForChanges(
211220
this.deps.mcpServerRoot,
@@ -215,7 +224,7 @@ export class RequestPipeline {
215224
// checkForChanges RPC failed — proceed in degraded mode
216225
const message = err instanceof Error ? err.message : String(err);
217226
logger(`[pipeline] checkForChanges RPC failed: ${message} — proceeding without hot-reload check`);
218-
return {
227+
check = {
219228
mcpChanged: false,
220229
mcpRebuilt: false,
221230
mcpBuildError: null,
@@ -226,6 +235,14 @@ export class RequestPipeline {
226235
};
227236
}
228237

238+
// Restore CDP disconnect handling + reconnect if extension reloaded Client
239+
try {
240+
await this.deps.onAfterChangeCheck?.(check);
241+
} catch (afterErr) {
242+
const msg = afterErr instanceof Error ? afterErr.message : String(afterErr);
243+
logger(`[pipeline] onAfterChangeCheck failed: ${msg}`);
244+
}
245+
229246
// Extension build failure → return error, skip tool execution
230247
if (check.extBuildError) {
231248
entry.resolve({

0 commit comments

Comments
 (0)