Skip to content

Commit 75fa859

Browse files
Fix race condition when running preload script for inline browser (#619)
Our preload script for the inline browser is built as an ESM module. There is a known issue when loading ESM preload scripts: electron/electron#40777. This causes the preload script to sometimes run after the hosted page has been rendered. When this happens, the hosted page cannot access the APIs exposed by the preload script, leading to unexpected behavior. The fix is to update the build to generate this preload script as a commonJS module.
1 parent 4ad4fac commit 75fa859

File tree

8 files changed

+87
-82
lines changed

8 files changed

+87
-82
lines changed

ts/packages/agents/browser/src/electron/agentActivation.ts

+13-9
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ declare global {
1010
}
1111

1212
let siteAgent: string = "";
13+
let siteAgentInitialized = false;
1314
function setupSiteAgent() {
1415
if (window.browserConnect) {
16+
if (siteAgentInitialized) {
17+
return;
18+
}
19+
1520
const pageUrl = window.location.href;
1621
const host = new URL(pageUrl).host;
1722

@@ -57,20 +62,19 @@ function setupSiteAgent() {
5762
siteAgent = "browser.instacart";
5863
window.browserConnect.enableSiteAgent("browser.instacart");
5964
}
65+
66+
siteAgentInitialized = true;
6067
} else {
6168
console.log("browserconnect not found by UI events script");
6269
}
6370
}
6471

65-
window.addEventListener("message", (event) => {
66-
if (event.data === "setupSiteAgent") {
67-
setupSiteAgent();
68-
}
69-
if (
70-
event.data === "disableSiteAgent" &&
71-
siteAgent &&
72-
window.browserConnect
73-
) {
72+
document.addEventListener("DOMContentLoaded", () => {
73+
setupSiteAgent();
74+
});
75+
76+
window.addEventListener("beforeunload", (event) => {
77+
if (siteAgent !== undefined && window.browserConnect !== undefined) {
7478
window.browserConnect.disableSiteAgent(siteAgent);
7579
}
7680
});

ts/packages/agents/browser/src/extension/contentScript.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -530,11 +530,9 @@ async function awaitPageIncrementalUpdates() {
530530
detector
531531
.detect()
532532
.then(() => {
533-
console.log("Page incremental load completed.");
534533
resolve("true");
535534
})
536-
.catch((error: Error) => {
537-
console.error("Failed to detect page load completion:", error);
535+
.catch((_error: Error) => {
538536
resolve("false");
539537
});
540538
});
@@ -730,9 +728,10 @@ window.addEventListener(
730728
"message",
731729
async (event) => {
732730
if (
733-
event.data.source == "preload" &&
734-
event.data.target == "contentScript" &&
735-
event.data.messageType == "scriptActionRequest"
731+
event.data !== undefined &&
732+
event.data.source === "preload" &&
733+
event.data.target === "contentScript" &&
734+
event.data.messageType === "scriptActionRequest"
736735
) {
737736
await handleScriptAction(event.data.body, (response) => {
738737
window.top?.postMessage(

ts/packages/agents/browser/src/extension/webTypeAgentMain.ts

+15-24
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { createRpc } from "agent-rpc/rpc";
1010
import { createAgentRpcServer } from "agent-rpc/server";
1111
import { isWebAgentMessageFromDispatcher } from "../../dist/common/webAgentMessageTypes.mjs";
1212
import {
13+
WebAgentDisconnectMessageFromDispatcher,
1314
WebAgentRegisterMessage,
1415
WebAgentRpcMessage,
1516
} from "../common/webAgentMessageTypes.mjs";
@@ -51,32 +52,20 @@ function ensureDynamicTypeAgentManager(): DynamicTypeAgentManager {
5152
}
5253
const messageChannelProvider = createGenericChannelProvider(
5354
(message: any) => {
54-
const rpcMessage = {
55+
window.postMessage({
5556
source: "webAgent",
5657
method: "webAgent/message",
5758
params: message,
58-
} as WebAgentRpcMessage;
59-
60-
if (window.webAgentApi) {
61-
window.webAgentApi.sendWebAgentMessage(rpcMessage);
62-
} else {
63-
window.postMessage(rpcMessage);
64-
}
59+
} as WebAgentRpcMessage);
6560
},
6661
);
6762

6863
const registerChannel = createGenericChannel((message: any) => {
69-
const rpcMessage = {
64+
window.postMessage({
7065
source: "webAgent",
7166
method: "webAgent/register",
7267
params: message,
73-
} as WebAgentRegisterMessage;
74-
75-
if (window.webAgentApi) {
76-
window.webAgentApi.sendWebAgentMessage(rpcMessage);
77-
} else {
78-
window.postMessage(rpcMessage);
79-
}
68+
} as WebAgentRegisterMessage);
8069
});
8170

8271
const rpc = createRpc<DynamicTypeAgentManagerInvokeFunctions>(
@@ -123,13 +112,7 @@ function ensureDynamicTypeAgentManager(): DynamicTypeAgentManager {
123112
}
124113
};
125114

126-
if (window.webAgentApi) {
127-
window.webAgentApi.onWebAgentMessage((event) => {
128-
messageHandler(event);
129-
});
130-
} else {
131-
window.addEventListener("message", messageHandler);
132-
}
115+
window.addEventListener("message", messageHandler);
133116

134117
return manager;
135118
}
@@ -140,5 +123,13 @@ global.registerTypeAgent = async (
140123
agent: AppAgent,
141124
): Promise<void> => {
142125
const manager = ensureDynamicTypeAgentManager();
143-
return manager.addTypeAgent(name, manifest, agent);
126+
await manager.addTypeAgent(name, manifest, agent);
127+
128+
window.addEventListener("beforeunload", (event) => {
129+
window.postMessage({
130+
source: "webAgent",
131+
method: "webAgent/disconnect",
132+
params: name,
133+
});
134+
});
144135
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { defineConfig, externalizeDepsPlugin } from "electron-vite";
5+
import { resolve } from "path";
6+
7+
export default defineConfig({
8+
preload: {
9+
plugins: [externalizeDepsPlugin()],
10+
build: {
11+
sourcemap: true,
12+
rollupOptions: {
13+
input: {
14+
webview: resolve(__dirname, "src/preload/webView.ts"),
15+
},
16+
output: {
17+
// For the CJS preload
18+
format: "cjs",
19+
entryFileNames: "[name].cjs",
20+
dir: "out/preload-cjs",
21+
preserveModules: true, // Ensure each module is preserved in the output
22+
},
23+
},
24+
},
25+
},
26+
});

ts/packages/shell/electron.vite.config.mts

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export default defineConfig({
1818
rollupOptions: {
1919
input: {
2020
index: resolve(__dirname, "src/preload/index.ts"),
21-
webview: resolve(__dirname, "src/preload/webView.ts"),
2221
},
2322
},
2423
},

ts/packages/shell/package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,19 @@
1313
"type": "module",
1414
"main": "./out/main/index.js",
1515
"scripts": {
16-
"build": "concurrently npm:typecheck npm:build:electron",
17-
"build:electron": "electron-vite build",
16+
"build": "concurrently npm:typecheck npm:build:electron npm:build:electron:cjs",
17+
"build:electron": "electron-vite build --config electron.vite.config.mts",
18+
"build:electron:cjs": "electron-vite build --config electron.vite-cjs.config.mts --logLevel error",
1819
"build:linux": "npm run build && electron-builder --linux --config",
1920
"build:mac": "npm run build && electron-builder --mac --config",
2021
"build:win": "npm run build && electron-builder --win --config",
2122
"clean": "rimraf --glob out *.tsbuildinfo *.done.build.log",
22-
"dev": "electron-vite dev",
23+
"dev": "npm run build:electron:cjs && electron-vite dev",
2324
"postinstall": "cross-env \"npm_execpath=\" electron-builder install-app-deps",
2425
"prettier": "prettier --check . --ignore-path ../../.prettierignore",
2526
"prettier:fix": "prettier --write . --ignore-path ../../.prettierignore",
2627
"shell:test": "npx playwright test",
27-
"start": "electron-vite preview",
28+
"start": "npm run build:electron:cjs && electron-vite preview",
2829
"typecheck": "concurrently npm:typecheck:node npm:typecheck:web",
2930
"typecheck:node": "tsc -p tsconfig.node.json",
3031
"typecheck:web": "tsc -p tsconfig.web.json"

ts/packages/shell/src/main/index.ts

+14-15
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
DevicePermissionHandlerHandlerDetails,
1313
WebContents,
1414
BrowserView,
15+
session,
1516
} from "electron";
1617
import path, { join } from "node:path";
1718
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
@@ -103,7 +104,7 @@ function setContentSize() {
103104
const time = performance.now();
104105
debugShell("Starting...");
105106

106-
function createWindow() {
107+
async function createWindow() {
107108
debugShell("Creating window", performance.now() - time);
108109

109110
// Create the browser window.
@@ -131,6 +132,14 @@ function createWindow() {
131132

132133
mainWindow.webContents.setUserAgent(userAgent);
133134

135+
const browserExtensionPath = join(
136+
app.getAppPath(),
137+
"../agents/browser/dist/electron",
138+
);
139+
await session.defaultSession.loadExtension(browserExtensionPath, {
140+
allowFileAccess: true,
141+
});
142+
134143
chatView = new BrowserView({
135144
webPreferences: {
136145
preload: join(__dirname, "../preload/index.mjs"),
@@ -280,7 +289,7 @@ function createWindow() {
280289
if (!inlineBrowserView && mainWindowSize) {
281290
inlineBrowserView = new BrowserView({
282291
webPreferences: {
283-
preload: join(__dirname, "../preload/webview.mjs"),
292+
preload: join(__dirname, "../preload-cjs/webview.cjs"),
284293
sandbox: false,
285294
},
286295
});
@@ -299,9 +308,6 @@ function createWindow() {
299308
}
300309

301310
inlineBrowserView?.webContents.loadURL(targetUrl.toString());
302-
inlineBrowserView?.webContents.on("did-finish-load", () => {
303-
inlineBrowserView?.webContents.send("init-site-agent");
304-
});
305311
};
306312

307313
ShellSettings.getinstance().onCloseInlineBrowser = (): void => {
@@ -570,7 +576,7 @@ async function initialize() {
570576

571577
await initializeSpeech();
572578

573-
const { mainWindow, chatView } = createWindow();
579+
const { mainWindow, chatView } = await createWindow();
574580

575581
let settingSummary: string = "";
576582
function updateSummary(dispatcher: Dispatcher) {
@@ -646,19 +652,12 @@ async function initialize() {
646652
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
647653
app.on("browser-window-created", async (_, window) => {
648654
optimizer.watchWindowShortcuts(window);
649-
const browserExtensionPath = join(
650-
app.getAppPath(),
651-
"../agents/browser/dist/electron",
652-
);
653-
await window.webContents.session.loadExtension(browserExtensionPath, {
654-
allowFileAccess: true,
655-
});
656655
});
657656

658-
app.on("activate", function () {
657+
app.on("activate", async function () {
659658
// On macOS it's common to re-create a window in the app when the
660659
// dock icon is clicked and there are no other windows open.
661-
if (BrowserWindow.getAllWindows().length === 0) createWindow();
660+
if (BrowserWindow.getAllWindows().length === 0) await createWindow();
662661
});
663662

664663
// On windows, we will spin up a local end point that listens

ts/packages/shell/src/preload/webView.ts

+9-23
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,17 @@ const { webFrame } = require("electron");
66

77
import DOMPurify from "dompurify";
88
import { ipcRenderer } from "electron";
9-
import EventEmitter from "events";
10-
11-
const internalEventEmitter = new EventEmitter();
129

1310
ipcRenderer.on("received-from-browser-ipc", async (_, data) => {
1411
if (data.error) {
1512
console.error(data.error);
1613
return;
1714
}
1815

19-
if (data.method && data.method.indexOf("/") > 0) {
16+
if (data.method !== undefined && data.method.indexOf("/") > 0) {
2017
const [schema, actionName] = data.method?.split("/");
2118

22-
if (schema == "browser") {
19+
if (schema === "browser") {
2320
if (actionName == "siteTranslatorStatus") {
2421
if (data.body.status == "initializing") {
2522
console.log(`Initializing ${data.body.translator}`);
@@ -49,8 +46,8 @@ ipcRenderer.on("received-from-browser-ipc", async (_, data) => {
4946
id: data.id,
5047
result: message,
5148
});
52-
} else if (schema == "webAgent") {
53-
internalEventEmitter.emit("web-agent-message", data);
49+
} else if (schema === "webAgent") {
50+
window.postMessage(data);
5451
}
5552

5653
console.log(
@@ -59,10 +56,6 @@ ipcRenderer.on("received-from-browser-ipc", async (_, data) => {
5956
}
6057
});
6158

62-
ipcRenderer.on("init-site-agent", () => {
63-
window.postMessage("setupSiteAgent");
64-
});
65-
6659
function sendToBrowserAgent(message: any) {
6760
ipcRenderer.send("send-to-browser-ipc", message);
6861
}
@@ -436,7 +429,7 @@ async function runSiteAction(schemaName: string, action: any) {
436429
return confirmationMessage;
437430
}
438431

439-
await ipcRenderer.invoke("init-browser-ipc");
432+
ipcRenderer.invoke("init-browser-ipc");
440433

441434
contextBridge.exposeInMainWorld("browserConnect", {
442435
enableSiteAgent: (translatorName) => {
@@ -457,15 +450,8 @@ contextBridge.exposeInMainWorld("browserConnect", {
457450
},
458451
});
459452

460-
contextBridge.exposeInMainWorld("webAgentApi", {
461-
onWebAgentMessage: (callback: (message: any) => void) => {
462-
internalEventEmitter.on("web-agent-message", callback);
463-
},
464-
sendWebAgentMessage: (message: any) => {
465-
sendToBrowserAgent(message);
466-
},
453+
window.addEventListener("message", (event) => {
454+
if (event.data !== undefined && event.data.source === "webAgent") {
455+
sendToBrowserAgent(event.data);
456+
}
467457
});
468-
469-
window.onbeforeunload = () => {
470-
window.postMessage("disableSiteAgent");
471-
};

0 commit comments

Comments
 (0)