Skip to content

Commit 8201fe3

Browse files
backnotpropclaude
andauthored
fix(vscode): reuse IPC server port across restarts (#252) (#256)
Persist the IPC port in workspaceState and try to rebind to it on activation so restored terminals still have a valid PLANNOTATOR_VSCODE_PORT. Falls back to a random port if the preferred one is taken. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 190f7ec commit 8201fe3

2 files changed

Lines changed: 35 additions & 24 deletions

File tree

apps/vscode-extension/src/extension.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,16 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
4848
vscode.window.showInformationMessage("Plannotator panel opened");
4949
};
5050

51-
// Start local IPC server to receive URLs from the router script
51+
// Start local IPC server to receive URLs from the router script.
52+
// Reuse the last port so restored terminals still have a valid PLANNOTATOR_VSCODE_PORT.
53+
const lastPort = context.workspaceState.get<number>("ipcPort");
5254
const { server, port } = await createIpcServer((url) => {
5355
openInPanel(url).catch((err) => {
5456
log.error(`[open] failed: ${err}`);
5557
vscode.window.showErrorMessage(`Plannotator: ${err}`);
5658
});
57-
});
59+
}, lastPort);
60+
context.workspaceState.update("ipcPort", port);
5861
context.subscriptions.push({ dispose: () => server.close() });
5962

6063
// Inject env vars into integrated terminals

apps/vscode-extension/src/ipc-server.ts

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,39 @@ import * as http from "http";
66
*/
77
export function createIpcServer(
88
onUrl: (url: string) => void,
9+
preferredPort?: number,
910
): Promise<{ server: http.Server; port: number }> {
10-
return new Promise((resolve, reject) => {
11-
const server = http.createServer((req, res) => {
12-
const parsed = new globalThis.URL(req.url!, "http://localhost");
13-
const targetUrl = parsed.searchParams.get("url");
11+
const handler = (req: http.IncomingMessage, res: http.ServerResponse) => {
12+
const parsed = new globalThis.URL(req.url!, "http://localhost");
13+
const targetUrl = parsed.searchParams.get("url");
1414

15-
if (req.method === "GET" && parsed.pathname === "/open" && targetUrl) {
16-
onUrl(targetUrl);
17-
res.writeHead(200);
18-
res.end("ok");
19-
} else {
20-
res.writeHead(404);
21-
res.end("not found");
22-
}
23-
});
15+
if (req.method === "GET" && parsed.pathname === "/open" && targetUrl) {
16+
onUrl(targetUrl);
17+
res.writeHead(200);
18+
res.end("ok");
19+
} else {
20+
res.writeHead(404);
21+
res.end("not found");
22+
}
23+
};
2424

25-
server.listen(0, "127.0.0.1", () => {
26-
const addr = server.address();
27-
if (addr && typeof addr === "object") {
28-
resolve({ server, port: addr.port });
29-
} else {
30-
reject(new Error("Failed to get server address"));
31-
}
25+
function listen(port: number): Promise<{ server: http.Server; port: number }> {
26+
return new Promise((resolve, reject) => {
27+
const server = http.createServer(handler);
28+
server.listen(port, "127.0.0.1", () => {
29+
const addr = server.address();
30+
if (addr && typeof addr === "object") {
31+
resolve({ server, port: addr.port });
32+
} else {
33+
reject(new Error("Failed to get server address"));
34+
}
35+
});
36+
server.on("error", reject);
3237
});
38+
}
3339

34-
server.on("error", reject);
35-
});
40+
if (preferredPort) {
41+
return listen(preferredPort).catch(() => listen(0));
42+
}
43+
return listen(0);
3644
}

0 commit comments

Comments
 (0)