Co-Authored-By: Codex <noreply@anthropic.com>master
| @@ -12,6 +12,10 @@ export function startProcess(command, args, options = {}) { | |||||
| child.stdout.on('data', (chunk) => output.push(chunk.toString())); | child.stdout.on('data', (chunk) => output.push(chunk.toString())); | ||||
| child.stderr.on('data', (chunk) => output.push(chunk.toString())); | child.stderr.on('data', (chunk) => output.push(chunk.toString())); | ||||
| child.once('error', (error) => { | |||||
| output.push(`\nprocess failed to start: ${error.message}\n`); | |||||
| }); | |||||
| child.once('exit', (code) => { | child.once('exit', (code) => { | ||||
| if (code !== null && code !== 0) { | if (code !== null && code !== 0) { | ||||
| output.push(`\nprocess exited with code ${code}\n`); | output.push(`\nprocess exited with code ${code}\n`); | ||||
| @@ -25,6 +29,17 @@ function hasExited(child) { | |||||
| return child.exitCode !== null || child.signalCode !== null; | return child.exitCode !== null || child.signalCode !== null; | ||||
| } | } | ||||
| function sendSignal(child, signal) { | |||||
| if (!child || hasExited(child)) return false; | |||||
| try { | |||||
| return child.kill(signal); | |||||
| } catch (error) { | |||||
| if (error?.code === 'ESRCH' || hasExited(child)) return false; | |||||
| throw error; | |||||
| } | |||||
| } | |||||
| function waitForExit(child, timeoutMs) { | function waitForExit(child, timeoutMs) { | ||||
| if (!child || hasExited(child)) return Promise.resolve(true); | if (!child || hasExited(child)) return Promise.resolve(true); | ||||
| @@ -72,10 +87,10 @@ export async function stopProcess(child) { | |||||
| return; | return; | ||||
| } | } | ||||
| child.kill('SIGTERM'); | |||||
| sendSignal(child, 'SIGTERM'); | |||||
| const exited = await waitForExit(child, 5000); | const exited = await waitForExit(child, 5000); | ||||
| if (exited) return; | if (exited) return; | ||||
| child.kill('SIGKILL'); | |||||
| sendSignal(child, 'SIGKILL'); | |||||
| await waitForExit(child, 5000); | await waitForExit(child, 5000); | ||||
| } | } | ||||
| @@ -1,18 +1,31 @@ | |||||
| export async function waitForHttp(url, options = {}) { | export async function waitForHttp(url, options = {}) { | ||||
| const timeoutMs = options.timeoutMs || 60000; | const timeoutMs = options.timeoutMs || 60000; | ||||
| const intervalMs = options.intervalMs || 500; | const intervalMs = options.intervalMs || 500; | ||||
| const attemptTimeoutMs = options.attemptTimeoutMs || 5000; | |||||
| const start = Date.now(); | const start = Date.now(); | ||||
| let lastError; | let lastError; | ||||
| while (Date.now() - start < timeoutMs) { | while (Date.now() - start < timeoutMs) { | ||||
| const remainingMs = timeoutMs - (Date.now() - start); | |||||
| if (remainingMs <= 0) break; | |||||
| const controller = new AbortController(); | |||||
| const timeout = setTimeout(() => controller.abort(), Math.min(attemptTimeoutMs, remainingMs)); | |||||
| timeout.unref?.(); | |||||
| try { | try { | ||||
| const response = await fetch(url, { cache: 'no-store' }); | |||||
| const response = await fetch(url, { cache: 'no-store', signal: controller.signal }); | |||||
| if (response.ok) return response; | if (response.ok) return response; | ||||
| lastError = new Error(`HTTP ${response.status}`); | lastError = new Error(`HTTP ${response.status}`); | ||||
| } catch (error) { | } catch (error) { | ||||
| lastError = error; | lastError = error; | ||||
| } finally { | |||||
| clearTimeout(timeout); | |||||
| } | } | ||||
| await new Promise((resolve) => setTimeout(resolve, intervalMs)); | |||||
| const remainingAfterAttemptMs = timeoutMs - (Date.now() - start); | |||||
| if (remainingAfterAttemptMs <= 0) break; | |||||
| await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, remainingAfterAttemptMs))); | |||||
| } | } | ||||
| throw new Error(`Timed out waiting for ${url}: ${lastError?.message || 'no response'}`); | throw new Error(`Timed out waiting for ${url}: ${lastError?.message || 'no response'}`); | ||||