From e188da6399f24e5c90d8d77d4ac72d95397ccab1 Mon Sep 17 00:00:00 2001 From: Tuan Dinh Date: Fri, 18 Sep 2026 22:23:15 +0700 Subject: [PATCH] fix(dev): allow Ctrl+C to promptly kill dev server by closing active connections (#13020) * fix(dev): allow Ctrl+C to promptly kill dev server by closing active connections and adding force-exit timeout * test(dev): add regression assertions for prompt dev server exit on Ctrl+C * docs(changelog): add fragment for #13020 --- .../13020-dev-server-ctrl-c-prompt-exit.md | 1 + scripts/dev/run-next.mjs | 16 ++++++++++++++++ .../run-next-graceful-shutdown-12074.test.ts | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 changelog.d/fixes/13020-dev-server-ctrl-c-prompt-exit.md diff --git a/changelog.d/fixes/13020-dev-server-ctrl-c-prompt-exit.md b/changelog.d/fixes/13020-dev-server-ctrl-c-prompt-exit.md new file mode 100644 index 0000000000..2b69436656 --- /dev/null +++ b/changelog.d/fixes/13020-dev-server-ctrl-c-prompt-exit.md @@ -0,0 +1 @@ +- **fix(dev):** allow Ctrl+C to promptly kill dev server by closing active connections ([#13020](https://github.com/diegosouzapw/OmniRoute/pull/13020)) — thanks @tuandinh0801 diff --git a/scripts/dev/run-next.mjs b/scripts/dev/run-next.mjs index 80ca2b3d31..055e9f5d7d 100644 --- a/scripts/dev/run-next.mjs +++ b/scripts/dev/run-next.mjs @@ -235,15 +235,31 @@ async function start() { process.exit(1); }); + let isShuttingDown = false; const shutdown = async (signal) => { + if (isShuttingDown) { + // Second Ctrl+C / signal forces immediate exit + process.exit(1); + } + isShuttingDown = true; + + // Safety net: force exit after 2s if keep-alive sockets or Next.js app close hangs + const forceExitTimer = setTimeout(() => { + process.exit(0); + }, 2000); + forceExitTimer.unref?.(); + systemdNotifier.stopping(); try { + server.closeIdleConnections?.(); + server.closeAllConnections?.(); await new Promise((resolve) => server.close(resolve)); await globalThis.__omnirouteRequestShutdown?.(signal); await nextApp.close(); } catch (error) { console.error("[SHUTDOWN] Failed during signal:", signal, error); } finally { + clearTimeout(forceExitTimer); process.exit(0); } }; diff --git a/tests/unit/run-next-graceful-shutdown-12074.test.ts b/tests/unit/run-next-graceful-shutdown-12074.test.ts index c29c5b1f09..b7fd1571dc 100644 --- a/tests/unit/run-next-graceful-shutdown-12074.test.ts +++ b/tests/unit/run-next-graceful-shutdown-12074.test.ts @@ -27,3 +27,21 @@ test("the custom Next runner owns exit and awaits application cleanup before clo assert.ok(applicationCleanup < nextClose, "application cleanup must finish before Next closes"); assert.ok(nextClose < processExit, "process exit must remain the final shutdown action"); }); + +test("custom Next runner closes existing connections and supports force exit on Ctrl+C", () => { + const closeAll = runNextSource.indexOf("server.closeAllConnections?.()"); + const serverClose = runNextSource.indexOf("server.close(resolve)"); + assert.ok(closeAll >= 0, "must close active/keep-alive connections to prevent hanging on Ctrl+C"); + assert.ok(closeAll < serverClose, "connections must be terminated before server.close wait"); + + assert.match( + runNextSource, + /if\s*\(\s*isShuttingDown\s*\)\s*\{\s*\/\/[^\n]*\n\s*process\.exit\(1\);?\s*\}/, + "second signal / Ctrl+C must immediately exit" + ); + assert.match( + runNextSource, + /setTimeout\(\(\)\s*=>\s*\{\s*process\.exit\(0\);?\s*\},\s*\d+\)/, + "must have fallback force exit timer" + ); +});