diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 118deed11..d705dc2ea 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -500,6 +500,15 @@ async function sendCommand(state: ServerState, command: string, args: string[], await writeStdout('Server stopped'); return; } + if (command === 'restart' && !(await isServerHealthy(state.port))) { + const restartEnv: Record = {}; + if (_globalFlags?.proxyUrl) restartEnv.BROWSE_PROXY_URL = _globalFlags.proxyUrl; + if (_globalFlags?.headed) restartEnv.BROWSE_HEADED = '1'; + if (_globalFlags?.configHash) restartEnv.BROWSE_CONFIG_HASH = _globalFlags.configHash; + await startServer(Object.keys(restartEnv).length ? restartEnv : undefined); + await writeStdout('Server restarted'); + return; + } if (retries >= 1) throw new Error('[browse] Server crashed twice in a row — aborting'); console.error('[browse] Server connection lost. Restarting...'); // Kill the old server to avoid orphaned chromium processes diff --git a/browse/src/write-commands.ts b/browse/src/write-commands.ts index daebd18a0..ab43c80f4 100644 --- a/browse/src/write-commands.ts +++ b/browse/src/write-commands.ts @@ -128,6 +128,24 @@ const CLEANUP_SELECTORS = { ], }; +async function withManualTimeout( + promise: Promise, + timeoutMs: number +): Promise<{ timedOut: false; value: T } | { timedOut: true }> { + promise.catch(() => {}); + let timeout: ReturnType | undefined; + try { + return await Promise.race([ + promise.then((value) => ({ timedOut: false as const, value })), + new Promise<{ timedOut: true }>((resolve) => { + timeout = setTimeout(() => resolve({ timedOut: true }), timeoutMs); + }), + ]); + } finally { + if (timeout) clearTimeout(timeout); + } +} + export async function handleWriteCommand( command: string, args: string[], @@ -148,9 +166,17 @@ export async function handleWriteCommand( // must not leave stale content that could resurrect on a later context recreation. session.clearLoadedHtml(); const normalizedUrl = await validateNavigationUrl(url); - const response = await page.goto(normalizedUrl, { waitUntil: 'domcontentloaded', timeout: 15000 }); + const response = await page.goto(normalizedUrl, { waitUntil: 'commit', timeout: 15000 }); + const domReady = await withManualTimeout( + page.waitForLoadState('domcontentloaded', { timeout: 15000 }), + 16000 + ); + if (domReady.timedOut) { + await page.evaluate(() => window.stop()).catch(() => {}); + } const status = response?.status() || 'unknown'; - return `Navigated to ${normalizedUrl} (${status})`; + const suffix = domReady.timedOut ? '; domcontentloaded timed out' : ''; + return `Navigated to ${normalizedUrl} (${status}${suffix})`; } case 'back': { diff --git a/browse/test/config.test.ts b/browse/test/config.test.ts index 7e6b8108c..98ac29571 100644 --- a/browse/test/config.test.ts +++ b/browse/test/config.test.ts @@ -332,6 +332,11 @@ describe('cli command dispatch', () => { expect(cliSource).not.toContain('IS_WINDOWS ? 2 : 1, result.stdout'); }); + test('restart connection loss starts once instead of resending restart', () => { + expect(cliSource).toContain("if (command === 'restart' && !(await isServerHealthy(state.port)))"); + expect(cliSource).toContain("await writeStdout('Server restarted')"); + }); + test('default headless cold-start does not print a delayed startup banner', () => { expect(cliSource).not.toContain("console.error('[browse] Starting server...')"); expect(cliSource).toContain('Starting server in headed mode'); @@ -339,6 +344,16 @@ describe('cli command dispatch', () => { }); }); +describe('write command dispatch', () => { + const writeSource = fs.readFileSync(path.resolve(__dirname, '../src/write-commands.ts'), 'utf-8'); + + test('goto commits first and bounds domcontentloaded wait', () => { + expect(writeSource).toContain("page.goto(normalizedUrl, { waitUntil: 'commit', timeout: 15000 })"); + expect(writeSource).toContain("page.waitForLoadState('domcontentloaded', { timeout: 15000 })"); + expect(writeSource).toContain("await page.evaluate(() => window.stop()).catch(() => {})"); + }); +}); + describe('resolveGstackHome', () => { test('honors GSTACK_HOME env var when set', () => { const orig = process.env.GSTACK_HOME;