Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 73 additions & 44 deletions src/browser/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,72 +15,101 @@ let _config: BrowserConfig;
const MAX_CONSOLE = 500;
const MAX_NETWORK = 500;

export async function launchBrowser(config: BrowserConfig): Promise<void> {
_config = config;

/**
* Close whatever browser is currently referenced, swallowing any error.
* Used to guarantee we never abandon a live browser process before
* (re)launching — otherwise the old `headless_shell` leaks. Best-effort:
* a close failure must not block the relaunch.
*/
async function discardBrowser(b: Browser | null): Promise<void> {
if (!b) return;
try {
const launchOptions: Parameters<typeof chromium.launch>[0] = {
headless: config.headless,
args: [
'--no-first-run',
'--no-default-browser-check',
`--window-size=${config.viewport.width},${config.viewport.height + 100}`,
],
};

if (config.channel) {
launchOptions.channel = config.channel as any;
}
await b.close();
} catch (err) {
console.error('[glance] Error closing previous browser (ignored):', err);
}
}

_browser = await chromium.launch(launchOptions);
export async function launchBrowser(config: BrowserConfig): Promise<void> {
_config = config;

_browser.on('disconnected', () => {
// Never abandon an existing browser process: close it before launching a
// new one. ensureBrowser() relaunches when `_browser` is null/disconnected,
// and the `disconnected` handler nulls `_browser` while the OS process may
// still be alive — so without this guard each relaunch orphaned a live
// headless_shell, accumulating until OOM.
await discardBrowser(_browser);
_browser = null;
_context = null;
_pages.clear();
_activePageId = null;

const attach = async (browser: Browser, label: string): Promise<void> => {
_browser = browser;
browser.on('disconnected', () => {
console.error('[glance] Browser disconnected, will relaunch on next tool call');
_browser = null;
_context = null;
_pages.clear();
_activePageId = null;
// Fully release the (now-dead) browser; close() is a no-op once the
// process is gone but cleans up Playwright-side handles deterministically.
void discardBrowser(browser);
if (_browser === browser) {
_browser = null;
_context = null;
_pages.clear();
_activePageId = null;
}
});

_context = await _browser.newContext({
_context = await browser.newContext({
viewport: config.viewport,
ignoreHTTPSErrors: config.securityProfile === 'local-dev',
});

// Create initial page
const page = await _context.newPage();
const pageId = randomUUID();
_pages.set(pageId, page);
_activePageId = pageId;
_setupPageListeners(page, pageId);

_readyResolve?.();
console.error('[glance] Browser launched and ready');
console.error(`[glance] Browser launched${label} and ready`);
};

try {
const launchOptions: Parameters<typeof chromium.launch>[0] = {
headless: config.headless,
args: [
'--no-first-run',
'--no-default-browser-check',
`--window-size=${config.viewport.width},${config.viewport.height + 100}`,
],
};

if (config.channel) {
launchOptions.channel = config.channel as any;
}

await attach(await chromium.launch(launchOptions), '');
} catch (err) {
console.error('[glance] Failed to launch browser:', err);
// Try with bundled chromium path detection disabled, fallback to system chrome
// A browser may have launched on the first attempt before a later step
// (newContext/newPage) threw — discard it so the fallback launch can't
// orphan it.
await discardBrowser(_browser);
_browser = null;
// Try the system Chrome channel as a fallback.
try {
_browser = await chromium.launch({
headless: config.headless,
channel: 'chrome',
args: ['--no-first-run', '--no-default-browser-check'],
});

_context = await _browser.newContext({
viewport: config.viewport,
ignoreHTTPSErrors: config.securityProfile === 'local-dev',
});

const page = await _context.newPage();
const pageId = randomUUID();
_pages.set(pageId, page);
_activePageId = pageId;
_setupPageListeners(page, pageId);

_readyResolve?.();
console.error('[glance] Browser launched (system Chrome) and ready');
await attach(
await chromium.launch({
headless: config.headless,
channel: 'chrome',
args: ['--no-first-run', '--no-default-browser-check'],
}),
' (system Chrome)',
);
} catch (err2) {
console.error('[glance] Failed to launch system Chrome:', err2);
await discardBrowser(_browser);
_browser = null;
_readyResolve?.();
}
}
Expand Down