mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-30 16:26:59 +02:00
fix: Free port 5678 when quitting n8n-node dev (no-changelog) (#30786)
Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Declan Carroll <declan@n8n.io> Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com> Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
parent
c1856aff8d
commit
d580d749f4
|
|
@ -30,6 +30,7 @@ const CONFIG = {
|
|||
KILL_TIMEOUT_MS: 1000,
|
||||
PROCESS_KILL_DELAY_MS: 100,
|
||||
EXIT_KILL_TIMEOUT_MS: 500,
|
||||
GROUP_POLL_INTERVAL_MS: 50,
|
||||
};
|
||||
|
||||
function calculatePanelHeight(numPanels: number, headerLines: number): number {
|
||||
|
|
@ -336,54 +337,61 @@ function printAllCommandOutputs(outputs: CommandOutput[], headerText?: string):
|
|||
);
|
||||
}
|
||||
|
||||
function isProcessGroupAlive(pgid: number): boolean {
|
||||
if (process.platform === 'win32') return false;
|
||||
try {
|
||||
process.kill(-pgid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function sendKillSignal(proc: ChildProcess, pid: number, signal: 'SIGTERM' | 'SIGKILL'): void {
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
execSync(`taskkill /PID ${pid} /T /F`, { timeout: CONFIG.KILL_TIMEOUT_MS });
|
||||
} else {
|
||||
process.kill(-pid, signal);
|
||||
}
|
||||
} catch {
|
||||
try {
|
||||
proc.kill(signal);
|
||||
} catch {
|
||||
// Process already gone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function killProcess(proc: ChildProcess, graceful: boolean): Promise<void> {
|
||||
if (!proc.pid || proc.exitCode !== null) return;
|
||||
if (!proc.pid || proc.exitCode !== null || proc.signalCode !== null) return;
|
||||
|
||||
const pid = proc.pid;
|
||||
const isWindows = process.platform === 'win32';
|
||||
const hasExited = (): boolean => proc.exitCode !== null || proc.signalCode !== null;
|
||||
|
||||
return await new Promise<void>((resolve) => {
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
sendKillSignal(proc, pid, graceful ? 'SIGTERM' : 'SIGKILL');
|
||||
|
||||
if (graceful) {
|
||||
timeoutId = setTimeout(() => {
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
execSync(`taskkill /PID ${pid} /T /F`, { timeout: CONFIG.KILL_TIMEOUT_MS });
|
||||
} else {
|
||||
process.kill(-pid, 'SIGKILL');
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during force kill
|
||||
}
|
||||
resolve();
|
||||
}, CONFIG.GRACEFUL_SHUTDOWN_TIMEOUT);
|
||||
}
|
||||
if (!graceful) {
|
||||
await sleep(CONFIG.PROCESS_KILL_DELAY_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
proc.once('exit', () => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
resolve();
|
||||
});
|
||||
// Wait for the whole process group to drain rather than only the direct
|
||||
// child. The direct child (shell wrapper or `npx`) typically exits much
|
||||
// faster than the n8n server it spawns, which needs time to release its
|
||||
// listening port. Exiting before the descendants finish would leak the
|
||||
// port until the user manually killed the process.
|
||||
const deadline = Date.now() + CONFIG.GRACEFUL_SHUTDOWN_TIMEOUT;
|
||||
while (Date.now() < deadline) {
|
||||
const groupDrained = isWindows || !isProcessGroupAlive(pid);
|
||||
if (hasExited() && groupDrained) return;
|
||||
await sleep(CONFIG.GROUP_POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
execSync(`taskkill /PID ${pid} /T /F`, { timeout: CONFIG.KILL_TIMEOUT_MS });
|
||||
} else {
|
||||
process.kill(-pid, graceful ? 'SIGTERM' : 'SIGKILL');
|
||||
}
|
||||
} catch {
|
||||
try {
|
||||
proc.kill(graceful ? 'SIGTERM' : 'SIGKILL');
|
||||
} catch {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
if (!graceful) {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
setTimeout(resolve, CONFIG.PROCESS_KILL_DELAY_MS);
|
||||
}
|
||||
});
|
||||
// Graceful window elapsed — force-kill anything still alive in the group.
|
||||
sendKillSignal(proc, pid, 'SIGKILL');
|
||||
await sleep(CONFIG.PROCESS_KILL_DELAY_MS);
|
||||
}
|
||||
|
||||
export function runCommands(config: CommandsConfig): void {
|
||||
|
|
@ -445,18 +453,20 @@ export function runCommands(config: CommandsConfig): void {
|
|||
process.on('SIGTERM', handleSignal);
|
||||
|
||||
process.on('exit', () => {
|
||||
if (!cleanupPerformed && childProcesses.length > 0) {
|
||||
for (const proc of childProcesses) {
|
||||
if (!proc.pid) continue;
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
execSync(`taskkill /PID ${proc.pid} /T /F`, { timeout: CONFIG.EXIT_KILL_TIMEOUT_MS });
|
||||
} else {
|
||||
process.kill(-proc.pid, 'SIGKILL');
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during exit cleanup
|
||||
// Always fire a final SIGKILL to each spawned process group. If the
|
||||
// graceful cleanup already drained the group this is a no-op; if it
|
||||
// didn't (e.g. unexpected exit, race), this prevents the n8n server
|
||||
// or other descendants from outliving the CLI and holding their port.
|
||||
for (const proc of childProcesses) {
|
||||
if (!proc.pid) continue;
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
execSync(`taskkill /PID ${proc.pid} /T /F`, { timeout: CONFIG.EXIT_KILL_TIMEOUT_MS });
|
||||
} else {
|
||||
process.kill(-proc.pid, 'SIGKILL');
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during exit cleanup
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user