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:
n8n-cat-bot[bot] 2026-05-27 14:52:06 +01:00 committed by GitHub
parent c1856aff8d
commit d580d749f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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
}
}
});