Skip to content

Commit 7ceec68

Browse files
Askirclaude
andcommitted
fix(dev-ui): handle EADDRINUSE by prompting to kill existing instance
When the dev server port is already in use, prompt the user to kill the existing process instead of crashing with an unhandled error. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0de2bc6 commit 7ceec68

File tree

1 file changed

+32
-2
lines changed

1 file changed

+32
-2
lines changed

packages/core/src/dev-ui/dev-server.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { createServer as createHttpServer } from "node:http";
2+
import { execSync } from "node:child_process";
23
import { existsSync, readFileSync } from "node:fs";
34
import { readFile } from "node:fs/promises";
45
import { resolve, dirname, extname } from "node:path";
56
import { fileURLToPath } from "node:url";
7+
import * as p from "@clack/prompts";
68
import { createWSServer, type WSClientMessage } from "./ws.js";
79
import { createWatcher } from "./watcher.js";
810
import type { PtyManager } from "./pty.js";
@@ -12,6 +14,22 @@ import { getSchemaName } from "../dbos.js";
1214
import { getAppSchema } from "../cli/app.js";
1315
import pg from "pg";
1416

17+
async function killPortHolder(port: number): Promise<boolean> {
18+
try {
19+
const pids = execSync(`lsof -ti :${port}`, { encoding: "utf-8" }).trim();
20+
if (pids) {
21+
for (const pid of pids.split("\n")) {
22+
process.kill(parseInt(pid, 10), "SIGTERM");
23+
}
24+
await new Promise((r) => setTimeout(r, 500));
25+
return true;
26+
}
27+
} catch {
28+
// no process on port or kill failed
29+
}
30+
return false;
31+
}
32+
1533
const MIME: Record<string, string> = {
1634
".html": "text/html; charset=utf-8",
1735
".js": "text/javascript; charset=utf-8",
@@ -136,6 +154,7 @@ export async function startDevServer(options: DevServerOptions) {
136154
};
137155

138156
const { broadcast, sendTo, wss } = createWSServer(httpServer, onClientMessage);
157+
wss.on("error", () => {}); // errors handled on httpServer
139158

140159
try {
141160
const { createPtyManager } = await import("./pty.js");
@@ -193,10 +212,21 @@ export async function startDevServer(options: DevServerOptions) {
193212

194213
const hostname = host ? "0.0.0.0" : "localhost";
195214

196-
// Try the requested port; fall back to OS-assigned port if taken
215+
// Try the requested port; offer to kill existing instance if taken
197216
const actualPort = await new Promise<number>((resolvePromise, rejectPromise) => {
198-
httpServer.once("error", (err: NodeJS.ErrnoException) => {
217+
httpServer.once("error", async (err: NodeJS.ErrnoException) => {
199218
if (err.code === "EADDRINUSE") {
219+
const shouldKill = await p.confirm({
220+
message: "0pflow is likely already running. Kill it and restart?",
221+
});
222+
if (!p.isCancel(shouldKill) && shouldKill) {
223+
const killed = await killPortHolder(port);
224+
if (killed) {
225+
httpServer.listen(port, hostname, () => resolvePromise(port));
226+
return;
227+
}
228+
}
229+
// Fall back to OS-assigned port
200230
httpServer.listen(0, hostname, () => {
201231
const addr = httpServer.address();
202232
resolvePromise(typeof addr === "object" && addr ? addr.port : 0);

0 commit comments

Comments
 (0)