Fix browse restart and resilient goto

This commit is contained in:
Catfish-75 2026-05-29 10:52:46 +03:00
parent eca0610e44
commit 7d48890e9b
3 changed files with 52 additions and 2 deletions

View File

@ -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<string, string> = {};
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

View File

@ -128,6 +128,24 @@ const CLEANUP_SELECTORS = {
],
};
async function withManualTimeout<T>(
promise: Promise<T>,
timeoutMs: number
): Promise<{ timedOut: false; value: T } | { timedOut: true }> {
promise.catch(() => {});
let timeout: ReturnType<typeof setTimeout> | 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': {

View File

@ -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;