6262 * now runs the child's walk before it compiles and exports the port it lands
6363 * on (`portFlagEnv`).
6464 *
65- * So the third boot here holds a port for its whole life (`holdPort`), asks
65+ * So the third boot here holds a port for its whole life (an HTTP neighbour
66+ * answering 503, see {@link holdPortWithNeighbour}), asks
6667 * `os dev -p` for it, and reads the shifted port out of the child's own
6768 * banner. The served and the materialized `baseUrl` must both name that port,
6869 * and a flow pinging the app through the connector (`fxp_ping`, the fixture's
7374 * `bin/run.js` with `NODE_ENV` unset, the shipped entry resolving commands from
7475 * `dist/` (`requireBuiltCli`), in its own process group (`dev` supervises a
7576 * `serve` grandchild). Each boot asks for a NON-ZERO port drawn with
76- * `reservePort()` (the busy-port boot holds its own with `holdPort()`), because that number is the subject: the config must read
77+ * `reservePort()` (the busy-port boot holds its own instead), because that
78+ * number is the subject: the config must read
7779 * the port the flag names, before the boot, and a kernel-assigned `-p 0` read
7880 * back afterwards cannot be known to the config (the draw is ledgered in
7981 * `released-port-production-spawn.pin.test.ts`). A lost race on that draw is
8486
8587import { describe , it , expect , beforeAll , afterAll } from 'vitest' ;
8688import { spawn , type ChildProcess } from 'node:child_process' ;
89+ import { createServer as createHttpServer } from 'node:http' ;
8790import { existsSync , mkdirSync , mkdtempSync , readFileSync , rmSync , writeFileSync } from 'node:fs' ;
8891import { tmpdir } from 'node:os' ;
8992import { join , resolve } from 'node:path' ;
@@ -93,7 +96,6 @@ import {
9396 childEnv ,
9497 E2E_AUTH_SECRET ,
9598 E2E_SECRET_KEY ,
96- holdPort ,
9799 portContentionError ,
98100 portDriftError ,
99101 probeThroughChild ,
@@ -302,6 +304,38 @@ function boot(
302304 } ) ;
303305}
304306
307+ /**
308+ * Hold a port for a whole boot with a neighbour that ANSWERS: an HTTP server
309+ * replying 503 to everything, as another app on that port would.
310+ *
311+ * ⛔ Not `holdPort()` from the shared helper. Its bare socket never answers, so
312+ * a ping dialing it waits out the handler's timeout, and its `release()` then
313+ * waits forever on the connection the ping left behind (measured: the socket
314+ * sat in CLOSE_WAIT and the hook timed out at 600 s).
315+ */
316+ function holdPortWithNeighbour ( ) : Promise < { port : number ; release : ( ) => Promise < void > } > {
317+ return new Promise ( ( resolveHold , rejectHold ) => {
318+ const server = createHttpServer ( ( _req , res ) => {
319+ res . writeHead ( 503 , { connection : 'close' } ) . end ( 'not this app' ) ;
320+ } ) ;
321+ server . on ( 'error' , rejectHold ) ;
322+ server . listen ( 0 , '0.0.0.0' , ( ) => {
323+ const address = server . address ( ) ;
324+ if ( address === null || typeof address === 'string' ) {
325+ rejectHold ( new Error ( `listen(0) produced no numeric address: ${ String ( address ) } ` ) ) ;
326+ return ;
327+ }
328+ resolveHold ( {
329+ port : address . port ,
330+ release : ( ) => new Promise < void > ( ( done ) => {
331+ server . closeAllConnections ( ) ;
332+ server . close ( ( ) => done ( ) ) ;
333+ } ) ,
334+ } ) ;
335+ } ) ;
336+ } ) ;
337+ }
338+
305339async function stopGroup ( child : ChildProcess ) : Promise < void > {
306340 if ( child . pid === undefined || child . exitCode !== null || child . signalCode !== null ) return ;
307341 await new Promise < void > ( ( done ) => {
@@ -415,7 +449,7 @@ beforeAll(async () => {
415449 }
416450
417451 // `os dev -p N` with N held for the whole boot: the server shifts off it.
418- const held = await holdPort ( ) ;
452+ const held = await holdPortWithNeighbour ( ) ;
419453 try {
420454 live = await boot (
421455 'os dev -p (held)' ,
0 commit comments