From 3af8c8e385939b8b2bb70c0d2422d71b7386745b Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:42:17 +0530 Subject: [PATCH] fix: teardown UI sibling gracefully on ok start exit --- .changeset/stale-ui-shutdown.md | Bin 0 -> 202 bytes packages/cli/src/commands/start.ts | 104 ++++++++++++++++++----------- 2 files changed, 64 insertions(+), 40 deletions(-) create mode 100644 .changeset/stale-ui-shutdown.md diff --git a/.changeset/stale-ui-shutdown.md b/.changeset/stale-ui-shutdown.md new file mode 100644 index 0000000000000000000000000000000000000000..9d9a3595d19ef53714a87f99a2b1535bc4cbb3fa GIT binary patch literal 202 zcmXwzK?=e!6hz;;;2pYiRouIB>j6AKt*Hh}Dm9{BUVRD3=MS0rGef?gjxr5vc0>|a zf^%cTEC!WMEyx- literal 0 HcmV?d00001 diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 368db5f98..abf8bca43 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -541,54 +541,61 @@ export function withIdleShutdownProcessExit( }; } -export function buildIdleShutdownHandler( - input: BuildIdleShutdownHandlerInput, -): () => Promise { +export async function teardownUiSibling( + input: Omit & { reason?: string }, +): Promise { const graceMs = input.sigtermGraceMs ?? DEFAULT_SIGTERM_GRACE_MS; const pollMs = input.sigtermPollIntervalMs ?? DEFAULT_SIGTERM_POLL_MS; const sleep = input.sleep ?? ((ms: number) => wait(ms)); + const reason = input.reason ?? 'teardown'; - return async () => { - try { - const lock = input.readUiLock(); - if (lock && input.isAlive(lock.pid)) { - try { - input.killPid(lock.pid, 'SIGTERM'); - input.log?.info({ pid: lock.pid, port: lock.port }, 'idle-shutdown: SIGTERM UI sibling'); - // Wait up to graceMs for the UI process to exit under SIGTERM. - const deadline = Date.now() + graceMs; - while (Date.now() < deadline) { - if (!input.isAlive(lock.pid)) break; - await sleep(pollMs); - } - if (input.isAlive(lock.pid)) { - // Grace expired — escalate to SIGKILL. Operators see this at WARN. - try { - input.killPid(lock.pid, 'SIGKILL'); - input.log?.warn( - { pid: lock.pid, graceMs }, - 'idle-shutdown: SIGTERM grace expired — escalated to SIGKILL', - ); - } catch (err) { - input.log?.error( - { pid: lock.pid, err: err instanceof Error ? err.message : String(err) }, - 'idle-shutdown: SIGKILL failed', - ); - } + try { + const lock = input.readUiLock(); + if (lock && input.isAlive(lock.pid)) { + try { + input.killPid(lock.pid, 'SIGTERM'); + input.log?.info({ pid: lock.pid, port: lock.port }, `${reason}: SIGTERM UI sibling`); + // Wait up to graceMs for the UI process to exit under SIGTERM. + const deadline = Date.now() + graceMs; + while (Date.now() < deadline) { + if (!input.isAlive(lock.pid)) break; + await sleep(pollMs); + } + if (input.isAlive(lock.pid)) { + // Grace expired — escalate to SIGKILL. Operators see this at WARN. + try { + input.killPid(lock.pid, 'SIGKILL'); + input.log?.warn( + { pid: lock.pid, graceMs }, + `${reason}: SIGTERM grace expired — escalated to SIGKILL`, + ); + } catch (err) { + input.log?.error( + { pid: lock.pid, err: err instanceof Error ? err.message : String(err) }, + `${reason}: SIGKILL failed`, + ); } - } catch (err) { - input.log?.warn( - { pid: lock.pid, err: err instanceof Error ? err.message : String(err) }, - 'idle-shutdown: failed to SIGTERM UI sibling', - ); } + } catch (err) { + input.log?.warn( + { pid: lock.pid, err: err instanceof Error ? err.message : String(err) }, + `${reason}: failed to SIGTERM UI sibling`, + ); } - } catch (err) { - input.log?.warn( - { err: err instanceof Error ? err.message : String(err) }, - 'idle-shutdown: UI lookup failed; proceeding with destroy', - ); } + } catch (err) { + input.log?.warn( + { err: err instanceof Error ? err.message : String(err) }, + `${reason}: UI lookup failed; proceeding`, + ); + } +} + +export function buildIdleShutdownHandler( + input: BuildIdleShutdownHandlerInput, +): () => Promise { + return async () => { + await teardownUiSibling({ ...input, reason: 'idle-shutdown' }); await input.destroy(); }; } @@ -1349,6 +1356,23 @@ export async function runStartCommand(config: Config, opts: StartCommandOptions) for (const line of details) { console.log(dim(` ${line}`)); } + + if (booted.uiSpawnDecision?.action === 'spawn') { + try { + const { readUiLock, isProcessAlive } = await import('@inkeep/open-knowledge-server'); + const { wait } = await import('@inkeep/open-knowledge-core'); + await teardownUiSibling({ + readUiLock: () => readUiLock(booted.lockDir), + isAlive: isProcessAlive, + killPid: (pid, sig) => process.kill(pid, sig), + sleep: (ms) => wait(ms), + reason: 'shutdown', + }); + } catch (err) { + // Best-effort teardown + } + } + try { await booted.destroy(); } catch (err) {