fix(dev): stop stale frontend when portal exits

This commit is contained in:
john
2026-07-27 10:28:12 +08:00
parent 6b479c16f9
commit c88623855f
5 changed files with 85 additions and 6 deletions
+8 -5
View File
@@ -12,6 +12,7 @@ import {
deepseekDisableThinkingEnabled,
resolveDeepseekNoThinkListenPort,
} from '../deepseek-no-think-proxy.mjs';
import { handleUnexpectedChildExit } from './dev-process-lifecycle.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const opsDir = path.join(root, 'ops');
@@ -52,11 +53,13 @@ function spawnChild(command, args, label, cwd = root, extraEnv = {}) {
stdio: 'inherit',
});
child.on('exit', (code, signal) => {
if (signal) return;
if (code && code !== 0) {
console.error(`[${label}] exited with code ${code}`);
shutdown(code ?? 1);
}
handleUnexpectedChildExit({
label,
code,
signal,
stopping,
shutdown,
});
});
return child;
}
+16
View File
@@ -0,0 +1,16 @@
export function handleUnexpectedChildExit({
label,
code,
signal,
stopping,
shutdown,
log = console.error,
}) {
if (stopping) return false;
const detail = signal ? `signal ${signal}` : `code ${code ?? 'unknown'}`;
const shutdownCode = Number.isInteger(code) && code > 0 ? code : 1;
log(`[${label}] exited unexpectedly (${detail}); shutting down dev stack`);
shutdown(shutdownCode);
return true;
}
+47
View File
@@ -0,0 +1,47 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { handleUnexpectedChildExit } from './dev-process-lifecycle.mjs';
function invokeExit({ code, signal, stopping = false }) {
const shutdownCodes = [];
const messages = [];
const handled = handleUnexpectedChildExit({
label: 'portal',
code,
signal,
stopping,
shutdown: (shutdownCode) => shutdownCodes.push(shutdownCode),
log: (message) => messages.push(message),
});
return { handled, shutdownCodes, messages };
}
test('clean child exit shuts down the remaining dev stack as a failure', () => {
const result = invokeExit({ code: 0, signal: null });
assert.equal(result.handled, true);
assert.deepEqual(result.shutdownCodes, [1]);
assert.match(result.messages[0], /code 0/);
});
test('signal exit shuts down the remaining dev stack as a failure', () => {
const result = invokeExit({ code: null, signal: 'SIGKILL' });
assert.equal(result.handled, true);
assert.deepEqual(result.shutdownCodes, [1]);
assert.match(result.messages[0], /signal SIGKILL/);
});
test('non-zero child exit preserves its failure code', () => {
const result = invokeExit({ code: 7, signal: null });
assert.deepEqual(result.shutdownCodes, [7]);
});
test('child exits during an intentional shutdown are ignored', () => {
const result = invokeExit({ code: 0, signal: null, stopping: true });
assert.equal(result.handled, false);
assert.deepEqual(result.shutdownCodes, []);
assert.deepEqual(result.messages, []);
});