Skip to content
Merged
Show file tree
Hide file tree
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
18 changes: 4 additions & 14 deletions apps/desktop/src/desktop-browser-broker-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function createDesktopBrowserBrokerClient(args: {
let retry: ReturnType<typeof setTimeout> | null = null;
let stopped = false;
let generation = 0;
let registryServerOrigin: string | null = null;
let serverUrl = args.getServerUrl();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-read the target URL on each broker retry

When applyServerTarget() starts on the builtin target and then activateLocalServerMoveIfLocked() changes the target to the committed move destination, reconnect() has already captured the builtin URL. Descriptor lookup for that origin normally fails after the move, but every scheduled attempt now retries the same cached URL instead of consulting getServerUrl() as before, so native Browser automation remains disconnected from the moved server until another explicit reconnect occurs.

Useful? React with 👍 / 👎.


function disconnect(): void {
generation += 1;
Expand All @@ -114,24 +114,14 @@ export function createDesktopBrowserBrokerClient(args: {
if (stopped || socket !== null) return;
const currentGeneration = generation;
try {
const serverUrl = args.getServerUrl();
const serverOrigin = new URL(serverUrl).origin;
if (
registryServerOrigin !== null &&
registryServerOrigin !== serverOrigin
)
args.broker.resetServer();
registryServerOrigin = serverOrigin;
args.broker.setServerOrigin(serverOrigin);
const descriptor = await readServerBrokerDescriptor({
dataDir: args.dataDir,
homeDir: args.homeDir,
serverOrigin,
});
if (stopped || generation !== currentGeneration) return;
if (new URL(args.getServerUrl()).origin !== serverOrigin) {
schedule();
return;
}
const connection = new WebSocket(descriptor.url, {
headers: { authorization: `Bearer ${descriptor.token}` },
maxPayload: 1024 * 1024,
Expand Down Expand Up @@ -159,8 +149,7 @@ export function createDesktopBrowserBrokerClient(args: {
connection.on("open", () => {
if (
stopped ||
generation !== currentGeneration ||
new URL(args.getServerUrl()).origin !== serverOrigin
generation !== currentGeneration
) {
connection.terminate();
return;
Expand Down Expand Up @@ -232,6 +221,7 @@ export function createDesktopBrowserBrokerClient(args: {
void connect();
return {
reconnect() {
serverUrl = args.getServerUrl();
if (retry !== null) clearTimeout(retry);
retry = null;
disconnect();
Expand Down
7 changes: 5 additions & 2 deletions apps/desktop/src/desktop-browser-broker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export function createDesktopBrowserBroker(args: {
const registryListeners = new Set<() => void>();
const snapshots = new Map<string, string>();
let hostId: string | null = null;
let serverOrigin: string | null = null;

function instanceForWindow(webContentsId: number): InstanceEntry | undefined {
return [...instances.values()].find(
Expand Down Expand Up @@ -316,15 +317,17 @@ export function createDesktopBrowserBroker(args: {
instance.descriptor.generation = randomUUID();
} else changed();
},
resetServer() {
setServerOrigin(value: string) {
if (serverOrigin === value) return;
serverOrigin = value;
hostId = null;
for (const lease of [...leases.values()]) revoke(lease);
args.manager.destroyAll();
for (const instance of instances.values()) {
instance.threads.clear();
instance.descriptor.generation = randomUUID();
}
snapshots.clear();
args.manager.setServerOrigin(value);
},
getTarget(webContentsId: number): BbDesktopBrowserTarget | null {
const instance = instanceForWindow(webContentsId);
Expand Down
97 changes: 72 additions & 25 deletions apps/desktop/src/desktop-browser-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ interface NativeTabRef extends NativeTabScope {
}

interface BrowserViewEntry {
registry: Map<string, BrowserViewEntry>;
webContents: WebContents;
view: WebContentsView;
hostWindow: DesktopBrowserHostWindow;
Expand Down Expand Up @@ -266,6 +267,7 @@ interface SetEntryDesiredBoundsArgs {
}

export interface DesktopBrowserViewManager {
setServerOrigin(serverOrigin: string): void;
createTab(args: {
hostWindow: DesktopBrowserHostWindow;
tabId: string;
Expand Down Expand Up @@ -426,7 +428,9 @@ export function createDesktopBrowserViewManager(
): DesktopBrowserViewManager {
const partition = args.partition ?? BB_BROWSER_PARTITION;
const pagePreloadPath = args.pagePreloadPath;
const entries = new Map<string, BrowserViewEntry>();
let entries = new Map<string, BrowserViewEntry>();
const serverEntries = new Map<string, Map<string, BrowserViewEntry>>();
let serverOrigin: string | null = null;
const automationTabListeners = new Set<() => void>();
const popupWindows = new Set<BrowserWindow>();
const resizingHostIds = new Set<number>();
Expand Down Expand Up @@ -514,7 +518,7 @@ export function createDesktopBrowserViewManager(
if (entry.rendererRecoveryAttempts >= RENDERER_RECOVERY_MAX_ATTEMPTS) {
entry.rendererRecoveryState = "blocked";
entry.lastErrorText = "The page renderer stopped repeatedly";
pushState(hostWindow, tabId);
pushState(hostWindow, tabId, entry);
return;
}
entry.rendererRecoveryTimer = setTimeout(() => {
Expand Down Expand Up @@ -546,7 +550,11 @@ export function createDesktopBrowserViewManager(
entry.webContents
.capturePage()
.then((image) => {
if (!isHostResizing(hostWindow) || image.isEmpty()) {
if (
entry.registry !== entries ||
!isHostResizing(hostWindow) ||
image.isEmpty()
) {
return;
}
const dataUrl = `data:image/jpeg;base64,${image
Expand Down Expand Up @@ -582,9 +590,9 @@ export function createDesktopBrowserViewManager(
function pushState(
hostWindow: DesktopBrowserHostWindow,
tabId: string,
entry: BrowserViewEntry,
): void {
const entry = entries.get(browserViewKey(hostWindow, tabId));
if (!entry || entry.webContents.isDestroyed()) {
if (entry.registry !== entries || entry.webContents.isDestroyed()) {
return;
}
send(
Expand Down Expand Up @@ -668,18 +676,22 @@ export function createDesktopBrowserViewManager(
const key = browserViewKey(hostWindow, tabId);

webContents.on("destroyed", () => {
if (entries.get(key) === entry) {
destroyEntry(hostWindow, key);
if (entry.registry.get(key) === entry) {
destroyEntry(hostWindow, key, entry);
}
});
webContents.on("did-navigate", notifyAutomationTabs);
webContents.on("did-navigate-in-page", notifyAutomationTabs);
webContents.on("page-title-updated", notifyAutomationTabs);
const notify = () => {
if (entry.registry === entries) notifyAutomationTabs();
};
webContents.on("did-navigate", notify);
webContents.on("did-navigate-in-page", notify);
webContents.on("page-title-updated", notify);

if (pagePreloadPath !== null) {
webContents.ipc.on(
BB_DESKTOP_BROWSER_GUEST_MESSAGE_CHANNEL,
(_event, payload: unknown) => {
if (entry.registry !== entries) return;
const parsed = guestPageMessageSchema.safeParse(payload);
if (
!parsed.success ||
Expand Down Expand Up @@ -710,6 +722,10 @@ export function createDesktopBrowserViewManager(
});

webContents.on("before-input-event", (event, input) => {
if (entry.registry !== entries) {
event.preventDefault();
return;
}
if (input.type !== "keyDown" || input.isAutoRepeat || input.isComposing) {
return;
}
Expand Down Expand Up @@ -746,6 +762,7 @@ export function createDesktopBrowserViewManager(
guardMainFrameNavigation(webContents, isAllowedBrowserUrl);

webContents.setWindowOpenHandler((details) => {
if (entry.registry !== entries) return { action: "deny" };
const opensPopup = details.disposition === "new-window";
const allowedUrl = opensPopup
? isAllowedPopupNavigationUrl(details.url)
Expand Down Expand Up @@ -785,7 +802,7 @@ export function createDesktopBrowserViewManager(
});

webContents.on("context-menu", (_event, params) => {
if (webContents.isDestroyed()) {
if (entry.registry !== entries || webContents.isDestroyed()) {
return;
}
const { editFlags } = params;
Expand All @@ -812,7 +829,10 @@ export function createDesktopBrowserViewManager(
});

webContents.on("found-in-page", (_event, result) => {
if (result.requestId !== entry.activeFindRequestId) {
if (
entry.registry !== entries ||
result.requestId !== entry.activeFindRequestId
) {
return;
}
send(hostWindow, BB_DESKTOP_BROWSER_FIND_RESULT_CHANNEL, {
Expand All @@ -836,7 +856,7 @@ export function createDesktopBrowserViewManager(
) {
entry.lastErrorText = "The page renderer could not start";
applyEntryVisibility(entry, hostWindow);
pushState(hostWindow, tabId);
pushState(hostWindow, tabId, entry);
return;
}
entry.rendererRecoveryState = "pending";
Expand All @@ -847,7 +867,7 @@ export function createDesktopBrowserViewManager(

webContents.on("media-started-playing", () => pauseHiddenMedia(entry));

const refresh = () => pushState(hostWindow, tabId);
const refresh = () => pushState(hostWindow, tabId, entry);
webContents.on("did-finish-load", () => {
resetEntryRendererRecovery(entry);
applyEntryVisibility(entry, hostWindow);
Expand Down Expand Up @@ -892,6 +912,7 @@ export function createDesktopBrowserViewManager(
},
});
const entry: BrowserViewEntry = {
registry: entries,
view,
webContents: view.webContents,
hostWindow: args.hostWindow,
Expand Down Expand Up @@ -929,7 +950,7 @@ export function createDesktopBrowserViewManager(
}

function disposeEntry(key: string, entry: BrowserViewEntry): void {
entries.delete(key);
entry.registry.delete(key);
clearEntryRendererRecoveryTimer(entry);
for (const popupWindow of [...entry.popupWindows]) {
if (!popupWindow.isDestroyed()) popupWindow.destroy();
Expand All @@ -938,14 +959,14 @@ export function createDesktopBrowserViewManager(
if (!entry.webContents.isDestroyed()) {
entry.webContents.close();
}
notifyAutomationTabs();
if (entry.registry === entries) notifyAutomationTabs();
}

function destroyEntry(
hostWindow: DesktopBrowserHostWindow,
key: string,
entry = entries.get(key),
): void {
const entry = entries.get(key);
if (!entry) {
return;
}
Expand Down Expand Up @@ -1073,6 +1094,31 @@ export function createDesktopBrowserViewManager(
}

return {
setServerOrigin(value) {
if (serverOrigin === value) return;
if (serverOrigin === null) {
serverOrigin = value;
serverEntries.set(value, entries);
return;
}
resizingHostIds.clear();
for (const entry of entries.values()) {
entry.visible = false;
applyEntryVisibility(entry, entry.hostWindow);
Comment on lines +1105 to +1107

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Throttle automation views while their server is inactive

Automation-created views are initialized with backgroundThrottling: false, and parking a server only marks them invisible and mutes media. Consequently, every retained automation page from previously visited servers can continue running timers and scripts at full foreground frequency, causing CPU and battery usage to accumulate across server switches; restore throttling while these entries are parked and reinstate their automation setting when activated.

Useful? React with 👍 / 👎.

for (const popup of entry.popupWindows) {
if (!popup.isDestroyed()) popup.hide();
Comment on lines +1108 to +1109

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pause or mute popups when parking their server

If a native page has a popup playing audio when the user switches servers, popup.hide() only removes the window from view; it does not mute or pause the popup's separate WebContents. The parent entry is muted through applyEntryVisibility(), but its popups are not, so content from the inactive server can remain audible until the popup closes or the user switches back.

Useful? React with 👍 / 👎.

}
}
serverOrigin = value;
entries = serverEntries.get(value) ?? new Map();
serverEntries.set(value, entries);
for (const entry of entries.values()) {
for (const popup of entry.popupWindows) {
if (!popup.isDestroyed()) popup.showInactive();
}
}
notifyAutomationTabs();
},
createTab(request) {
if (!isAllowedBrowserUrl(request.url))
throw new Error("Unsupported browser URL");
Expand All @@ -1094,7 +1140,7 @@ export function createDesktopBrowserViewManager(
applyEntryVisibility(entry, request.hostWindow);
loadIfNeeded(entry, request.url);
notifyAutomationTabs();
pushState(request.hostWindow, request.tabId);
pushState(request.hostWindow, request.tabId, entry);
return nativeTab(request.tabId, entry);
},
listTabs({ hostWebContentsId, threadId }) {
Expand Down Expand Up @@ -1213,7 +1259,7 @@ export function createDesktopBrowserViewManager(
loadIfNeeded(entry, request.url);
notifyAutomationTabs();
}
pushState(hostWindow, request.tabId);
pushState(hostWindow, request.tabId, entry);
},
detach({ hostWindow, tabId }) {
destroyEntry(hostWindow, browserViewKey(hostWindow, tabId));
Expand Down Expand Up @@ -1360,11 +1406,11 @@ export function createDesktopBrowserViewManager(
releaseWindow(hostWebContentsId) {
resizingHostIds.delete(hostWebContentsId);
const prefix = `${hostWebContentsId}:`;
for (const [key, entry] of [...entries.entries()]) {
if (!key.startsWith(prefix)) {
continue;
for (const registry of new Set([entries, ...serverEntries.values()])) {
for (const [key, entry] of [...registry]) {
if (!key.startsWith(prefix)) continue;
disposeEntry(key, entry);
}
disposeEntry(key, entry);
}
},
destroyAll() {
Expand All @@ -1375,9 +1421,10 @@ export function createDesktopBrowserViewManager(
}
}
popupWindows.clear();
for (const [key, entry] of [...entries.entries()]) {
disposeEntry(key, entry);
for (const registry of new Set([entries, ...serverEntries.values()])) {
for (const [key, entry] of [...registry]) disposeEntry(key, entry);
}
serverEntries.clear();
},
};
}
8 changes: 7 additions & 1 deletion apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1828,14 +1828,20 @@ async function loadServerMovedView(move: DesktopServerMove): Promise<void> {

async function applyServerTarget(): Promise<void> {
startupErrorPage = null;
desktopBrowserBrokerClient?.reconnect();
if (serverTargetStore === null) {
return;
}
connectSessionRenewal?.stop();
serverTargetGeneration += 1;
const generation = serverTargetGeneration;
const isCurrent = (): boolean => serverTargetGeneration === generation;
for (const window of BrowserWindow.getAllWindows()) {
if (applicationWindowWebContentsIds.has(window.webContents.id))
desktopBrowserViewManager?.prepareWindowReload(window);
}
await loadLoadingView();
if (!isCurrent()) return;
desktopBrowserBrokerClient?.reconnect();
if (serverTargetStore.getTarget().kind === "builtin") {
await activateLocalServerMoveIfLocked();
if (!isCurrent()) {
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/test/desktop-browser-main-ipc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ interface SendBrowserIpcArgs {
}

class RecordingDesktopBrowserViewManager implements DesktopBrowserViewManager {
setServerOrigin(): void {}

createTab(): never {
throw new Error("Not used by renderer IPC");
}
Expand Down
Loading
Loading