@@ -8,7 +8,7 @@ import colors from 'picocolors'
88import { isCI } from 'std-env'
99
1010import { restoreRawMode } from '../utils/console'
11- import { clearTakeover , isInteractiveSession , isLockEnabled , isProcessAlive , markTakenOver , readLock } from '../utils/lockfile'
11+ import { clearStaleLock , clearTakeover , isInteractiveSession , isLockEnabled , isProcessAlive , markTakenOver , readLock } from '../utils/lockfile'
1212import { logger } from '../utils/logger'
1313
1414/** How long the outgoing dev server has to exit and release its port. */
@@ -30,7 +30,7 @@ export type TakeoverRefusalReason
3030export type TakeoverResult
3131 /** Nothing to take over, or nothing we are willing to touch. */
3232 = | { action : 'none' }
33- /** A lock was found but its owner is gone; it will be cleaned up on acquire . */
33+ /** A lock was found but its owner is gone, so it has been removed . */
3434 | { action : 'stale' }
3535 /** The previous server is gone and its port is ours. */
3636 | { action : 'taken' , port : number , pid : number }
@@ -71,28 +71,33 @@ export async function takeOverDevServer(buildDir: string, options: TakeoverOptio
7171 return { action : 'none' }
7272 }
7373
74- // Signalling a `build` is never on the table, and neither is a server on a
75- // port we were not asked for: an explicit `--port` that differs skips the
76- // takeover, and the ordinary lock check decides whether starting is allowed.
74+ // Signalling a `build` is never on the table, and a dev server that has not
75+ // bound a port yet cannot be identified well enough to touch.
7776 if ( existing . command !== 'dev' || ! existing . port ) {
7877 return { action : 'none' }
7978 }
80- if ( options . requestedPort !== undefined && options . requestedPort !== existing . port ) {
81- return { action : 'none' }
82- }
8379
84- if ( ! isProcessAlive ( existing . pid ) ) {
85- if ( ! await isPortFree ( existing . port , existing . hostname ) ) {
80+ const alive = isProcessAlive ( existing . pid )
81+ const portFree = await isPortFree ( existing . port , existing . hostname )
82+
83+ // Either signal alone means the recorded server is gone: an exited process
84+ // cannot come back, and a free port means the PID was recycled inside the
85+ // lock's trust window, so it now belongs to a stranger we must not signal.
86+ // The lock is dropped here because `acquireLock` only judges liveness by PID
87+ // and would otherwise refuse to start on behalf of a server that is not there.
88+ if ( ! alive || portFree ) {
89+ if ( ! alive && ! portFree ) {
8690 logger . warn ( `The dev server that was using port ${ existing . port } is gone, but something is still listening there.` )
8791 }
92+ clearStaleLock ( buildDir , existing )
8893 return { action : 'stale' }
8994 }
9095
91- // An alive PID with a free port means the PID was recycled inside the lock's
92- // trust window and belongs to an unrelated process. Signalling it would kill
93- // a stranger .
94- if ( await isPortFree ( existing . port , existing . hostname ) ) {
95- return { action : 'stale ' }
96+ // A live server on a port we were not asked for is left alone: an explicit
97+ // `--port` that differs skips the takeover, and the ordinary lock check
98+ // decides whether starting is allowed .
99+ if ( options . requestedPort !== undefined && options . requestedPort !== existing . port ) {
100+ return { action : 'none ' }
96101 }
97102
98103 if ( options . takeover === false ) {
0 commit comments