Skip to content

Commit 66d8384

Browse files
committed
Fix Ctrl+C exit and stabilize e2e ports
1 parent 74070c1 commit 66d8384

File tree

2 files changed

+47
-15
lines changed

2 files changed

+47
-15
lines changed

cli/src/__tests__/e2e/test-server-utils.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { spawn, execSync } from 'child_process'
2+
import { createServer } from 'net'
23
import path from 'path'
34
import http from 'http'
45

56
import type { ChildProcess } from 'child_process'
7+
import type { AddressInfo } from 'net'
68

79
const WEB_DIR = path.join(__dirname, '../../../../web')
810

@@ -14,26 +16,41 @@ export interface E2EServer {
1416
}
1517

1618
/**
17-
* Find an available port for the web server
19+
* Find an available port for the web server.
20+
* Uses an ephemeral OS-assigned port to avoid EADDRINUSE races between parallel tests.
1821
*/
19-
export function findAvailableServerPort(basePort: number = 3100): number {
20-
for (let port = basePort; port < basePort + 100; port++) {
21-
try {
22-
execSync(`lsof -i:${port}`, { stdio: 'pipe' })
23-
// Port is in use, try next
24-
} catch {
25-
// Port is available
26-
return port
27-
}
28-
}
29-
throw new Error(`Could not find available port starting from ${basePort}`)
22+
export async function findAvailableServerPort(_basePort: number = 3100): Promise<number> {
23+
return await new Promise((resolve, reject) => {
24+
const server = createServer()
25+
server.unref()
26+
27+
server.on('error', (error) => {
28+
server.close()
29+
reject(error)
30+
})
31+
32+
server.listen(0, () => {
33+
const address = server.address()
34+
server.close((closeErr) => {
35+
if (closeErr) {
36+
reject(closeErr)
37+
return
38+
}
39+
if (address && typeof address === 'object') {
40+
resolve((address as AddressInfo).port)
41+
return
42+
}
43+
reject(new Error('Could not determine an available port'))
44+
})
45+
})
46+
})
3047
}
3148

3249
/**
3350
* Start the web server for e2e tests
3451
*/
3552
export async function startE2EServer(databaseUrl: string): Promise<E2EServer> {
36-
const port = findAvailableServerPort(3100)
53+
const port = await findAvailableServerPort(3100)
3754
const url = `http://localhost:${port}`
3855
const backendUrl = url
3956

cli/src/hooks/use-exit-handler.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,33 @@ export const useExitHandler = ({
5353

5454
if (!nextCtrlCWillExit) {
5555
setNextCtrlCWillExit(true)
56-
setTimeout(() => {
56+
exitWarningTimeoutRef.current = setTimeout(() => {
5757
setNextCtrlCWillExit(false)
58+
exitWarningTimeoutRef.current = null
5859
}, 2000)
5960
return true
6061
}
6162

63+
const exitNow = () => {
64+
try {
65+
process.stdout.write('\nGoodbye! Exiting...\n')
66+
} catch {
67+
// Ignore stdout write errors during shutdown
68+
}
69+
process.exit(0)
70+
}
71+
6272
if (exitWarningTimeoutRef.current) {
6373
clearTimeout(exitWarningTimeoutRef.current)
6474
exitWarningTimeoutRef.current = null
6575
}
6676

67-
flushAnalytics().then(() => process.exit(0))
77+
const flushed = flushAnalytics()
78+
if (flushed && typeof (flushed as Promise<void>).finally === 'function') {
79+
;(flushed as Promise<void>).finally(exitNow)
80+
} else {
81+
exitNow()
82+
}
6883
return true
6984
}, [inputValue, setInputValue, nextCtrlCWillExit])
7085

0 commit comments

Comments
 (0)