Skip to content

Commit c16f02b

Browse files
committed
fix(dev): reap every pooled fork on shutdown
1 parent 747c898 commit c16f02b

4 files changed

Lines changed: 61 additions & 3 deletions

File tree

packages/nuxt-cli/src/dev/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ class IPC {
2727
constructor() {
2828
// only kill process if it is a fork
2929
if (this.enabled) {
30+
// Without a parent there is nobody to reap this process, and it may be
31+
// holding the dev server port or the inspector port.
32+
process.once('disconnect', () => {
33+
process.exit(0)
34+
})
3035
process.once('unhandledRejection', (reason) => {
3136
this.send({ type: 'nuxt:internal:dev:rejection', message: reason instanceof Error ? reason.toString() : 'Unhandled Rejection' })
3237
process.exit()

packages/nuxt-cli/src/dev/pool.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { NuxtDevContext, NuxtDevIPCMessage } from './utils'
55

66
import { fork } from 'node:child_process'
77
import process from 'node:process'
8-
import { isDeno } from 'std-env'
98
import { debug } from '../utils/logger'
109

1110
interface ForkPoolOptions {
@@ -195,7 +194,8 @@ export class ForkPool {
195194
const wasAlive = fork.state !== 'dead' && !!fork.process && fork.process.exitCode === null
196195
fork.state = 'dead'
197196
if (fork.process) {
198-
fork.process.kill(signal === 0 && isDeno ? 'SIGTERM' : signal)
197+
// signal 0 only probes for liveness, so map the `exit` case onto a real signal
198+
fork.process.kill(signal === 0 ? 'SIGTERM' : signal)
199199
}
200200
this.removeFork(fork)
201201

@@ -223,7 +223,8 @@ export class ForkPool {
223223
}
224224

225225
private killAll(signal: NodeJS.Signals | number): void {
226-
for (const fork of this.pool) {
226+
// `killFork` mutates the pool, so iterate over a snapshot
227+
for (const fork of [...this.pool]) {
227228
this.killFork(fork, signal)
228229
}
229230
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import process from 'node:process'
2+
3+
// Node refs the IPC channel while a `message` listener is attached, which is
4+
// what keeps this stand-in for a warm fork alive after it reports readiness.
5+
process.on('message', () => {})
6+
process.send({ type: 'nuxt:internal:dev:fork-ready' })
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { ChildProcess } from 'node:child_process'
2+
import { fileURLToPath } from 'node:url'
3+
import { afterEach, describe, expect, it, vi } from 'vitest'
4+
5+
import { ForkPool } from '../../src/dev/pool'
6+
7+
const devEntry = fileURLToPath(new URL('../fixtures/fork-pool-entry.mjs', import.meta.url))
8+
9+
const pools: ForkPool[] = []
10+
11+
function createPool(poolSize?: number) {
12+
globalThis.__nuxt_cli__ = { ...globalThis.__nuxt_cli__, devEntry } as typeof globalThis.__nuxt_cli__
13+
const pool = new ForkPool({ rawArgs: [], poolSize, listenOverrides: {} })
14+
pools.push(pool)
15+
return pool
16+
}
17+
18+
async function waitForReady(pool: ForkPool, count: number) {
19+
await vi.waitFor(() => {
20+
expect(pool.getStats().ready).toBe(count)
21+
}, { timeout: 20_000, interval: 25 })
22+
}
23+
24+
afterEach(async () => {
25+
for (const pool of pools.splice(0)) {
26+
await (pool as unknown as { killAll: (signal: number) => void }).killAll(0)
27+
}
28+
})
29+
30+
describe('forkPool', () => {
31+
it('should kill every fork on shutdown', { timeout: 30_000 }, async () => {
32+
const pool = createPool(3)
33+
pool.startWarming()
34+
await waitForReady(pool, 3)
35+
36+
const processes = (pool as unknown as { pool: Array<{ process: ChildProcess }> }).pool.map(f => f.process)
37+
;(pool as unknown as { killAll: (signal: number) => void }).killAll(0)
38+
39+
await vi.waitFor(() => {
40+
for (const child of processes) {
41+
expect(child.killed).toBe(true)
42+
}
43+
}, { timeout: 10_000, interval: 25 })
44+
expect(pool.getStats().total).toBe(0)
45+
})
46+
})

0 commit comments

Comments
 (0)