fix(browser): dedupe concurrent lazy start (#61772) (#61772)

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Sukhdeep
2026-04-25 13:39:14 +08:00
committed by GitHub
parent d79b9e0af4
commit 6c1d4414d9
3 changed files with 50 additions and 1 deletions

View File

@@ -200,7 +200,9 @@ export function createProfileAvailability({
throw new BrowserProfileUnavailableError(formatChromeMcpAttachFailure(lastError));
};
const ensureBrowserAvailable = async (): Promise<void> => {
let inflightEnsureBrowserAvailable: Promise<void> | null = null;
const ensureBrowserAvailableOnce = async (): Promise<void> => {
await reconcileProfileRuntime();
if (capabilities.usesChromeMcp) {
if (profile.userDataDir && !fs.existsSync(profile.userDataDir)) {
@@ -305,6 +307,16 @@ export function createProfileAvailability({
}
};
const ensureBrowserAvailable = async (): Promise<void> => {
if (inflightEnsureBrowserAvailable) {
return inflightEnsureBrowserAvailable;
}
inflightEnsureBrowserAvailable = ensureBrowserAvailableOnce().finally(() => {
inflightEnsureBrowserAvailable = null;
});
return inflightEnsureBrowserAvailable;
};
const stopRunningBrowser = async (): Promise<{ stopped: boolean }> => {
await reconcileProfileRuntime();
if (capabilities.usesChromeMcp) {

View File

@@ -88,6 +88,42 @@ describe("browser server-context ensureBrowserAvailable", () => {
expect(stopOpenClawChrome).toHaveBeenCalledTimes(1);
});
it("deduplicates concurrent lazy-start calls to prevent PortInUseError", async () => {
const { launchOpenClawChrome, stopOpenClawChrome, isChromeCdpReady, profile } =
setupEnsureBrowserAvailableHarness();
isChromeCdpReady.mockResolvedValue(true);
mockLaunchedChrome(launchOpenClawChrome, 456);
const first = profile.ensureBrowserAvailable();
const second = profile.ensureBrowserAvailable();
await vi.advanceTimersByTimeAsync(100);
await expect(Promise.all([first, second])).resolves.toEqual([undefined, undefined]);
expect(launchOpenClawChrome).toHaveBeenCalledTimes(1);
expect(stopOpenClawChrome).not.toHaveBeenCalled();
});
it("clears the concurrent lazy-start guard after launch failure", async () => {
const { launchOpenClawChrome, stopOpenClawChrome, isChromeCdpReady, profile } =
setupEnsureBrowserAvailableHarness();
isChromeCdpReady.mockResolvedValue(true);
launchOpenClawChrome.mockRejectedValueOnce(
new Error("PortInUseError: listen EADDRINUSE 127.0.0.1:18800"),
);
const first = profile.ensureBrowserAvailable();
const second = profile.ensureBrowserAvailable();
await expect(Promise.all([first, second])).rejects.toThrow("PortInUseError");
mockLaunchedChrome(launchOpenClawChrome, 789);
const retry = profile.ensureBrowserAvailable();
await vi.advanceTimersByTimeAsync(100);
await expect(retry).resolves.toBeUndefined();
expect(launchOpenClawChrome).toHaveBeenCalledTimes(2);
expect(stopOpenClawChrome).not.toHaveBeenCalled();
});
it("reuses a pre-existing loopback browser after an initial short probe miss", async () => {
const { launchOpenClawChrome, stopOpenClawChrome, isChromeCdpReady, profile, state } =
setupEnsureBrowserAvailableHarness();