Repository navigation
Fix auto-attach failing when launching a new Edge instance #5081
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Vidal Ortega (vidorteg)
merged 4 commits into
main
from
user/vidorteg/fix-launch-scenario
Nov 3, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
e17038c
Fix auto-attach when Edge hands off to another process during launch
vidorteg ef36032
Merge branch 'main' into user/vidorteg/fix-launch-scenario
vidorteg 004b106
Addressing feedback
vidorteg 1d1feca
Merge branch 'main' into user/vidorteg/fix-launch-scenario
vidorteg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| import { spawn } from 'child_process'; | ||
| import puppeteer, { Browser, TargetType } from 'puppeteer-core'; | ||
|
|
||
| const RETRY_INTERVAL = 200; | ||
|
|
||
| export async function launchAndConnectToBrowser( | ||
| executablePath: string, port: number, args: string[], headless: boolean, timeout: number, | ||
| ): Promise<Browser> { | ||
| const deadline = Date.now() + timeout; | ||
| const browserProcess = spawn(executablePath, await puppeteer.defaultArgs({ args, headless }), { stdio: 'ignore' }); | ||
|
|
||
| let processExited = false; | ||
| let rejectOnProcessFailure: (error: Error) => void = () => undefined; | ||
| const processFailure = new Promise<never>((_, reject) => { | ||
| rejectOnProcessFailure = reject; | ||
| }); | ||
| // Prevents an unhandled rejection if the process fails after the launch has settled. | ||
| processFailure.catch(() => undefined); | ||
|
|
||
| browserProcess.once('error', error => { | ||
| processExited = true; | ||
| rejectOnProcessFailure(new Error(`Failed to start Microsoft Edge: ${error.message}`, { cause: error })); | ||
| }); | ||
| // Edge can hand off to another process and exit successfully before its debugging endpoint is ready. | ||
| browserProcess.once('exit', (code, signal) => { | ||
| processExited = true; | ||
| if (code !== 0) { | ||
| rejectOnProcessFailure(new Error(`Microsoft Edge exited before connecting (${signal ?? `code ${code}`}).`)); | ||
| } | ||
| }); | ||
|
|
||
| let browser: Browser | undefined; | ||
| try { | ||
| browser = await connectBeforeDeadline(port, deadline, processFailure); | ||
| await Promise.race([ | ||
| browser.waitForTarget(target => target.type() === TargetType.PAGE, { | ||
| timeout: Math.max(1, deadline - Date.now()), | ||
| }), | ||
| processFailure, | ||
| ]); | ||
| return browser; | ||
| } catch (error) { | ||
| await browser?.disconnect().catch(() => undefined); | ||
| // Only stop the process this launch still owns; after a successful handoff the | ||
| // browser belongs to another (possibly pre-existing) Edge process. | ||
| if (!processExited) { | ||
| browserProcess.kill(); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| async function connectBeforeDeadline(port: number, deadline: number, processFailure: Promise<never>): Promise<Browser> { | ||
| const timeoutMessage = `Timed out waiting for Microsoft Edge on port ${port}.`; | ||
| let lastError: unknown; | ||
| for (;;) { | ||
| const remaining = deadline - Date.now(); | ||
| if (remaining <= 0) { | ||
| throw new Error(timeoutMessage, { cause: lastError }); | ||
| } | ||
|
|
||
| let timer: NodeJS.Timeout | undefined; | ||
| const timedOut = new Promise<never>((_, reject) => { | ||
| timer = setTimeout(() => reject(new Error(timeoutMessage, { cause: lastError })), remaining); | ||
| }); | ||
| const attempt = puppeteer.connect({ browserURL: `http://127.0.0.1:${port}` }); | ||
| try { | ||
| const outcome = await Promise.race([ | ||
| attempt.then(browser => ({ browser }), (error: unknown) => ({ error })), | ||
| processFailure, | ||
| timedOut, | ||
| ]); | ||
| if ('browser' in outcome) { | ||
| return outcome.browser; | ||
| } | ||
| lastError = outcome.error; | ||
| } catch (error) { | ||
| // The launch was abandoned; don't leak a connection that completes afterwards. | ||
| void attempt.then(browser => browser.disconnect(), () => undefined); | ||
| throw error; | ||
| } finally { | ||
| clearTimeout(timer); | ||
| } | ||
|
|
||
| await Promise.race([ | ||
| new Promise(resolve => setTimeout(resolve, Math.min(RETRY_INTERVAL, Math.max(0, deadline - Date.now())))), | ||
| processFailure, | ||
| ]); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| import { ChildProcess, spawn } from 'child_process'; | ||
| import { EventEmitter } from 'events'; | ||
| import puppeteer, { Browser } from 'puppeteer-core'; | ||
| import { launchAndConnectToBrowser } from '../src/browserLauncher'; | ||
|
|
||
| jest.mock('child_process', () => ({ spawn: jest.fn() })); | ||
|
|
||
| describe('launchAndConnectToBrowser', () => { | ||
| let browserProcess: ChildProcess; | ||
| let browser: Browser; | ||
| let waitForTarget: jest.Mock; | ||
| let disconnect: jest.Mock; | ||
| const args = ['--remote-debugging-port=9222']; | ||
|
|
||
| beforeEach(() => { | ||
| jest.useFakeTimers(); | ||
| jest.clearAllMocks(); | ||
| browserProcess = Object.assign(new EventEmitter(), { kill: jest.fn() }) as unknown as ChildProcess; | ||
| jest.mocked(spawn).mockReturnValue(browserProcess); | ||
| jest.mocked(puppeteer.defaultArgs).mockImplementation(async options => options?.args ?? []); | ||
| waitForTarget = jest.fn().mockResolvedValue({}); | ||
| disconnect = jest.fn().mockResolvedValue(undefined); | ||
| const partialBrowser: Partial<Browser> = { waitForTarget, disconnect }; | ||
| browser = partialBrowser as Browser; | ||
| jest.mocked(puppeteer.connect).mockResolvedValue(browser); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| jest.useRealTimers(); | ||
| }); | ||
|
|
||
| function launch(timeout = 1000): Promise<Browser> { | ||
| return launchAndConnectToBrowser('edge.exe', 9222, args, false, timeout); | ||
| } | ||
|
|
||
| it('starts Edge with the standard arguments and connects to its debugging endpoint', async () => { | ||
| expect(await launch()).toBe(browser); | ||
| expect(puppeteer.defaultArgs).toHaveBeenCalledWith({ args, headless: false }); | ||
| expect(spawn).toHaveBeenCalledWith('edge.exe', args, { stdio: 'ignore' }); | ||
| expect(puppeteer.connect).toHaveBeenCalledWith({ browserURL: 'http://127.0.0.1:9222' }); | ||
| expect(waitForTarget).toHaveBeenCalledWith(expect.any(Function), { timeout: 1000 }); | ||
| }); | ||
|
|
||
| it('keeps waiting when the launcher process hands off and exits successfully', async () => { | ||
| jest.mocked(puppeteer.connect).mockRejectedValueOnce(new Error('ECONNREFUSED')); | ||
| const result = launch(); | ||
| await jest.advanceTimersByTimeAsync(0); | ||
| browserProcess.emit('exit', 0, null); | ||
| await jest.advanceTimersByTimeAsync(200); | ||
| expect(await result).toBe(browser); | ||
| expect(puppeteer.connect).toHaveBeenCalledTimes(2); | ||
| }); | ||
|
|
||
| it('reports startup errors', async () => { | ||
| jest.mocked(puppeteer.connect).mockRejectedValue(new Error('ECONNREFUSED')); | ||
| const result = launch(); | ||
| const rejection = expect(result).rejects.toThrow('Failed to start Microsoft Edge: not found'); | ||
| await jest.advanceTimersByTimeAsync(0); | ||
| browserProcess.emit('error', new Error('not found')); | ||
| await jest.advanceTimersByTimeAsync(200); | ||
| await rejection; | ||
| }); | ||
|
|
||
| it('reports unsuccessful exits', async () => { | ||
| jest.mocked(puppeteer.connect).mockRejectedValue(new Error('ECONNREFUSED')); | ||
| const result = launch(); | ||
| const rejection = expect(result).rejects.toThrow('Microsoft Edge exited before connecting (code 1).'); | ||
| await jest.advanceTimersByTimeAsync(0); | ||
| browserProcess.emit('exit', 1, null); | ||
| await jest.advanceTimersByTimeAsync(200); | ||
| await rejection; | ||
| }); | ||
|
|
||
| it('times out when the debugging endpoint never becomes available', async () => { | ||
| jest.mocked(puppeteer.connect).mockRejectedValue(new Error('ECONNREFUSED')); | ||
| const result = launch(500); | ||
| const rejection = expect(result).rejects.toThrow('Timed out waiting for Microsoft Edge on port 9222.'); | ||
| await jest.advanceTimersByTimeAsync(600); | ||
| await rejection; | ||
| }); | ||
|
|
||
| it('disconnects and stops the launched process when no page becomes available', async () => { | ||
| waitForTarget.mockRejectedValue(new Error('Waiting for target failed')); | ||
| await expect(launch()).rejects.toThrow('Waiting for target failed'); | ||
| expect(disconnect).toHaveBeenCalledTimes(1); | ||
| expect(browserProcess.kill).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('times out when a connection attempt stalls past the deadline', async () => { | ||
| jest.mocked(puppeteer.connect).mockReturnValue(new Promise<Browser>(() => undefined)); | ||
| const result = launch(500); | ||
| const rejection = expect(result).rejects.toThrow('Timed out waiting for Microsoft Edge on port 9222.'); | ||
| await jest.advanceTimersByTimeAsync(500); | ||
| await rejection; | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.