11import { createServer as createHttpServer } from "node:http" ;
2+ import { execSync } from "node:child_process" ;
23import { existsSync , readFileSync } from "node:fs" ;
34import { readFile } from "node:fs/promises" ;
45import { resolve , dirname , extname } from "node:path" ;
56import { fileURLToPath } from "node:url" ;
7+ import * as p from "@clack/prompts" ;
68import { createWSServer , type WSClientMessage } from "./ws.js" ;
79import { createWatcher } from "./watcher.js" ;
810import type { PtyManager } from "./pty.js" ;
@@ -12,6 +14,22 @@ import { getSchemaName } from "../dbos.js";
1214import { getAppSchema } from "../cli/app.js" ;
1315import 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+
1533const 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