mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
fix(render): runProc waits for the exit code until the kill deadline; newtab retries once on a cold daemon
A process whose pipes have reached EOF is exiting, but runProc gave the exit code only five seconds to arrive and then returned null, which run() reports as a failed command. Under CI's six-shard load one such render failed with the artifact already written. The SIGTERM/SIGKILL timers already bound the wait, so the exit race now runs to the kill deadline. The first CLI call auto-starts the browse daemon; on a cold start it can answer 'Unable to connect' once while the server is still coming up. That single case is retried after 1.5s; every other newtab failure is not. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
287b976d9f
commit
7724c46ddc
+18
-2
@@ -315,7 +315,11 @@ async function runProc(cmd: string, args: string[], timeoutMs: number): Promise<
|
||||
const read = Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text()]);
|
||||
const giveUp = new Promise<[string, string]>((resolve) => after(timeoutMs + 10_000, () => resolve(['', ''])));
|
||||
const [stdout, stderr] = await Promise.race([read, giveUp]);
|
||||
const code = await Promise.race([child.exited, new Promise<null>((resolve) => after(5_000, () => resolve(null)))]);
|
||||
// Pipes at EOF means the child is exiting; wait for the exit code until the
|
||||
// SIGKILL above has had its turn. A flat 5s bound here once failed a CI render
|
||||
// whose fake had already written its artifact — under a 6-shard load the
|
||||
// reaper needed longer than that, and a null code reads as a failed command.
|
||||
const code = await Promise.race([child.exited, new Promise<null>((resolve) => after(timeoutMs + 6_000, () => resolve(null)))]);
|
||||
for (const t of timers) clearTimeout(t);
|
||||
return { code, stdout, stderr, error: timedOut ? `timed out after ${timeoutMs}ms` : undefined };
|
||||
}
|
||||
@@ -512,7 +516,19 @@ export async function renderWithBrowse(spec: RenderSpec, bin: string | null = re
|
||||
try {
|
||||
work = fs.mkdtempSync(path.join(SAFE_TMP_DIR, 'gstack-render-browse-'));
|
||||
srv = serveDir(root);
|
||||
const opened = (await run(['newtab', '--json'])).match(/\{[^\n]*"tabId"[^\n]*\}/)?.[0];
|
||||
// The first CLI call auto-starts the daemon; on a cold start it can answer
|
||||
// "Unable to connect" once while the server is still coming up. One retry
|
||||
// after a short pause turns that into the wait it really is.
|
||||
const openTab = async () => (await run(['newtab', '--json'])).match(/\{[^\n]*"tabId"[^\n]*\}/)?.[0];
|
||||
let opened: string | undefined;
|
||||
try {
|
||||
opened = await openTab();
|
||||
} catch (e) {
|
||||
if (!/Unable to connect/.test((e as Error).message)) throw e;
|
||||
log.push('newtab: daemon not up yet — retrying once');
|
||||
await Bun.sleep(1_500);
|
||||
opened = await openTab();
|
||||
}
|
||||
tab = opened ? JSON.parse(opened).tabId : undefined;
|
||||
if (typeof tab !== 'number') throw new Error('browse newtab --json returned no tabId');
|
||||
const T = ['--tab-id', String(tab)];
|
||||
|
||||
Reference in New Issue
Block a user