Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

feat(agent): enable out of process iframes #50

Merged
merged 1 commit into from
Mar 2, 2023
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
21 changes: 19 additions & 2 deletions agent/main/lib/Browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import env from '../env';
import DevtoolsPreferences from './DevtoolsPreferences';
import Page, { IPageCreateOptions } from './Page';
import IConnectionTransport from '../interfaces/IConnectionTransport';
import { IBrowserContextHooks } from '@ulixee/unblocked-specification/agent/hooks/IBrowserHooks';
import GetVersionResponse = Protocol.Browser.GetVersionResponse;
import TargetInfo = Protocol.Target.TargetInfo;
import { IBrowserContextHooks } from '@ulixee/unblocked-specification/agent/hooks/IBrowserHooks';

const { log } = Log(module);

Expand Down Expand Up @@ -293,6 +293,7 @@ export default class Browser extends TypedEventEmitter<IBrowserEvents> implement
this.devtoolsSession.on('Target.attachedToTarget', this.onAttachedToTarget.bind(this));
this.devtoolsSession.on('Target.detachedFromTarget', this.onDetachedFromTarget.bind(this));
this.devtoolsSession.on('Target.targetCreated', this.onTargetCreated.bind(this));
this.devtoolsSession.on('Target.targetInfoChanged', this.onTargetInfoChanged.bind(this));
this.devtoolsSession.on('Target.targetDestroyed', this.onTargetDestroyed.bind(this));
this.devtoolsSession.on('Target.targetCrashed', this.onTargetCrashed.bind(this));
}
Expand Down Expand Up @@ -358,6 +359,10 @@ export default class Browser extends TypedEventEmitter<IBrowserEvents> implement

assert(targetInfo.browserContextId, `targetInfo: ${JSON.stringify(targetInfo, null, 2)}`);

this.browserContextsById
.get(targetInfo.browserContextId)
?.targetsById.set(targetInfo.targetId, targetInfo);

const isDevtoolsPanel = targetInfo.url.startsWith('devtools://devtools');
if (
event.targetInfo.type === 'page' &&
Expand Down Expand Up @@ -400,6 +405,8 @@ export default class Browser extends TypedEventEmitter<IBrowserEvents> implement
const context = new BrowserContext(this, false);
context.hooks = this.browserContextCreationHooks ?? {};
context.id = targetInfo.browserContextId;
context.targetsById.set(targetInfo.targetId, targetInfo);

if (this.connectOnlyToPageTargets) {
context.addPageInitializationOptions(this.connectOnlyToPageTargets);
}
Expand All @@ -409,7 +416,7 @@ export default class Browser extends TypedEventEmitter<IBrowserEvents> implement
if (targetInfo.type === 'page' && !isDevtoolsPanel) {
const devtoolsSession = this.connection.getSession(sessionId);
const context = this.getBrowserContext(targetInfo.browserContextId);
context?.onPageAttached(devtoolsSession, targetInfo).catch(() => null);
context?.onPageAttached(devtoolsSession, targetInfo).catch(console.error);
return;
}

Expand Down Expand Up @@ -456,11 +463,21 @@ export default class Browser extends TypedEventEmitter<IBrowserEvents> implement
}
}

private onTargetInfoChanged(event: Protocol.Target.TargetInfoChangedEvent): void {
const { targetInfo } = event;
this.browserContextsById
.get(targetInfo.browserContextId)
?.targetsById.set(targetInfo.targetId, targetInfo);
}

private async onTargetCreated(event: Protocol.Target.TargetCreatedEvent): Promise<void> {
const { targetInfo } = event;
if (this.debugLog) {
log.stats('onTargetCreated', { targetInfo, sessionId: null });
}
this.browserContextsById
.get(targetInfo.browserContextId)
?.targetsById.set(targetInfo.targetId, targetInfo);

if (targetInfo.type === 'page' && !targetInfo.attached) {
const context = this.getBrowserContext(targetInfo.browserContextId);
Expand Down
28 changes: 23 additions & 5 deletions agent/main/lib/BrowserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { TypedEventEmitter } from '@ulixee/commons/lib/eventUtils';
import { IBoundLog } from '@ulixee/commons/interfaces/ILog';
import { CanceledPromiseError } from '@ulixee/commons/interfaces/IPendingWaitEvent';
import ProtocolMapping from 'devtools-protocol/types/protocol-mapping';
import { IPage } from '@ulixee/unblocked-specification/agent/browser/IPage';
import {
IBrowserContextHooks,
IInteractHooks,
Expand All @@ -28,6 +27,7 @@ import Resources from './Resources';
import WebsocketMessages from './WebsocketMessages';
import { DefaultCommandMarker } from './DefaultCommandMarker';
import DevtoolsSessionLogger from './DevtoolsSessionLogger';
import FrameOutOfProcess from './FrameOutOfProcess';
import CookieParam = Protocol.Network.CookieParam;
import TargetInfo = Protocol.Target.TargetInfo;
import CreateBrowserContextRequest = Protocol.Target.CreateBrowserContextRequest;
Expand All @@ -54,6 +54,7 @@ export default class BrowserContext
public workersById = new Map<string, Worker>();
public pagesById = new Map<string, Page>();
public pagesByTabId = new Map<number, Page>();
public targetsById = new Map<string, TargetInfo>();
public devtoolsSessionsById = new Map<string, DevtoolsSession>();
public devtoolsSessionLogger: DevtoolsSessionLogger;
public proxy: IProxyConnectionOptions;
Expand Down Expand Up @@ -126,8 +127,6 @@ export default class BrowserContext
});
}

public defaultPageInitializationFn: (page: IPage) => Promise<any> = () => Promise.resolve();

async newPage(options?: IPageCreateOptions): Promise<Page> {
const createTargetPromise = new Resolvable<void>();
this.creatingTargetPromises.push(createTargetPromise.promise);
Expand Down Expand Up @@ -178,11 +177,29 @@ export default class BrowserContext
initializePage(page: Page): Promise<any> {
if (page.runPageScripts === false) return Promise.resolve();

return Promise.all([this.defaultPageInitializationFn(page), this.hooks.onNewPage?.(page)]);
return this.hooks.onNewPage?.(page) ?? Promise.resolve();
}

initializeOutOfProcessIframe(frame: FrameOutOfProcess): Promise<any> {
if (frame.page.runPageScripts === false) return Promise.resolve();

return this.hooks.onNewFrameProcess?.(frame.frame) ?? Promise.resolve();
}

onIframeAttached(devtoolsSession: DevtoolsSession, targetInfo: TargetInfo, pageId: string): void {
const page = this.pagesById.get(pageId);
if (!page) return;

this.devtoolsSessionLogger.subscribeToDevtoolsMessages(devtoolsSession, {
sessionType: 'iframe',
pageTargetId: pageId,
iframeTargetId: targetInfo.targetId,
});
}

async onPageAttached(devtoolsSession: DevtoolsSession, targetInfo: TargetInfo): Promise<Page> {
this.attachedTargetIds.add(targetInfo.targetId);
this.targetsById.set(targetInfo.targetId, targetInfo);
await Promise.all(this.creatingTargetPromises);
if (this.pagesById.has(targetInfo.targetId)) return;

Expand Down Expand Up @@ -219,6 +236,7 @@ export default class BrowserContext

onTargetDetached(targetId: string): void {
this.attachedTargetIds.delete(targetId);
this.targetsById.delete(targetId);
const page = this.pagesById.get(targetId);
if (page) {
this.pagesById.delete(targetId);
Expand All @@ -237,6 +255,7 @@ export default class BrowserContext
}

onDevtoolsPanelAttached(devtoolsSession: DevtoolsSession, targetInfo: TargetInfo): void {
this.targetsById.set(targetInfo.targetId, targetInfo);
this.devtoolsSessionsById.set(targetInfo.targetId, devtoolsSession);
this.hooks.onDevtoolsPanelAttached?.(devtoolsSession, targetInfo).catch(() => null);
}
Expand Down Expand Up @@ -434,7 +453,6 @@ export default class BrowserContext
this.pagesById.clear();
this.pagesByTabId.clear();
this.devtoolsSessionsById.clear();
this.defaultPageInitializationFn = null;
this.waitForPageAttachedById = null;
this.creatingTargetPromises.length = null;
this.commandMarker = null;
Expand Down
7 changes: 6 additions & 1 deletion agent/main/lib/DevtoolsSessionLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface IMessageDetails {
sessionType: IDevtoolsLogEvents['devtools-message']['sessionType'];
workerTargetId?: string;
pageTargetId?: string;
iframeTargetId?: string;
}

export default class DevtoolsSessionLogger extends TypedEventEmitter<IDevtoolsLogEvents> {
Expand Down Expand Up @@ -146,6 +147,10 @@ export default class DevtoolsSessionLogger extends TypedEventEmitter<IDevtoolsLo
pageId = params.targetInfo.targetId;
event.pageTargetId = pageId;
}

if (!frameId && params.targetInfo && params.targetInfo?.type === 'iframe') {
frameId = params.targetInfo.targetId;
}
}

if (event.direction === 'send') {
Expand Down Expand Up @@ -231,7 +236,7 @@ interface IDevtoolsLogEvents {
workerTargetId?: string;
frameId?: string;
requestId?: string;
sessionType: 'page' | 'worker' | 'browser';
sessionType: 'page' | 'worker' | 'browser' | 'iframe';
sessionId: string;
method?: string;
id?: number;
Expand Down
3 changes: 2 additions & 1 deletion agent/main/lib/DomStorageTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@ export default class DomStorageTracker extends TypedEventEmitter<IDomStorageEven
networkManager: NetworkManager,
logger: IBoundLog,
isEnabled: boolean,
session?: DevtoolsSession
) {
super();
this.isEnabled = isEnabled;
this.page = page;
const session = page.devtoolsSession;
session ??= page.devtoolsSession
this.devtoolsSession = session;
this.networkManager = networkManager;
this.storageByOrigin = storageByOrigin ?? {};
Expand Down
70 changes: 60 additions & 10 deletions agent/main/lib/Frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import { IBoundLog } from '@ulixee/commons/interfaces/ILog';
import Resolvable from '@ulixee/commons/lib/Resolvable';
import IPoint from '@ulixee/unblocked-specification/agent/browser/IPoint';
import IWindowOffset from '@ulixee/unblocked-specification/agent/browser/IWindowOffset';
import { IInteractionGroups } from '@ulixee/unblocked-specification/agent/interact/IInteractions';
import {
IElementInteractVerification,
IInteractionGroups,
} from '@ulixee/unblocked-specification/agent/interact/IInteractions';
import IRegisteredEventListener from '@ulixee/commons/interfaces/IRegisteredEventListener';
import { IInteractHooks } from '@ulixee/unblocked-specification/agent/hooks/IHooks';
import EventSubscriber from '@ulixee/commons/lib/EventSubscriber';
Expand All @@ -35,6 +38,8 @@ import Interactor from './Interactor';
import FrameNavigations from './FrameNavigations';
import FrameNavigationsObserver from './FrameNavigationsObserver';
import IWaitForOptions from '../interfaces/IWaitForOptions';
import IJsPath from '@ulixee/js-path/interfaces/IJsPath';
import FrameOutOfProcess from './FrameOutOfProcess';
import PageFrame = Protocol.Page.Frame;

const ContextNotFoundCode = -32000;
Expand All @@ -43,7 +48,7 @@ const InPageNavigationLoaderPrefix = 'inpage';
export default class Frame extends TypedEventEmitter<IFrameEvents> implements IFrame {
// TODO: switch this to "id" and migrate "id" to "devtoolsId"
public readonly frameId: number;

public didSwapOutOfProcess: boolean = false;
public get id(): string {
return this.internalFrame.id;
}
Expand Down Expand Up @@ -85,32 +90,42 @@ export default class Frame extends TypedEventEmitter<IFrameEvents> implements IF
return this.navigationLoadersById[this.activeLoaderId];
}

public get childFrames(): Frame[] {
const list: Frame[] = [];
for (const value of this.#framesManager.activeFrames) {
if (value.parentId === this.id) list.push(value);
}
return list;
}

public get page(): Page {
return this.#framesManager.page;
}

public interactor: Interactor;

public jsPath: JsPath;
public activeLoaderId: string;
public navigationLoadersById: { [loaderId: string]: NavigationLoader } = {};
public readonly logger: IBoundLog;
public get hooks(): IInteractHooks {
return this.page.browserContext.hooks;
}

public navigations: FrameNavigations;

public navigationsObserver: FrameNavigationsObserver;
public devtoolsSession: DevtoolsSession;

private outOfProcess: FrameOutOfProcess;

#framesManager: FramesManager;

private waitTimeouts: { timeout: NodeJS.Timeout; reject: (reason?: any) => void }[] = [];
private frameElementDevtoolsNodeId?: Promise<string>;
private readonly parentFrame: Frame | null;
private readonly devtoolsSession: DevtoolsSession;

private defaultLoaderId: string;
private startedLoaderId: string;

public readonly pendingNewDocumentScripts: { script: string; isolated: boolean }[] = [];
private defaultContextId: number;
private isolatedContextId: number;
private readonly activeContextIds: Set<number>;
Expand Down Expand Up @@ -151,6 +166,25 @@ export default class Frame extends TypedEventEmitter<IFrameEvents> implements IF
this.onAttached(internalFrame);
}

public async updateDevtoolsSession(devtoolsSession: DevtoolsSession): Promise<void> {
if (this.devtoolsSession === devtoolsSession) return;
this.devtoolsSession = devtoolsSession;
if (
devtoolsSession === this.#framesManager.devtoolsSession ||
devtoolsSession === this.parentFrame?.devtoolsSession
) {
this.outOfProcess = null;
return;
}

this.outOfProcess = new FrameOutOfProcess(this.page, this);
await this.outOfProcess.initialize();
}

public isOopif(): boolean {
return !!this.outOfProcess;
}

public close(error?: Error): void {
this.isClosing = true;
const cancelMessage = 'Cancel Pending Promise. Frame closed.';
Expand All @@ -171,7 +205,10 @@ export default class Frame extends TypedEventEmitter<IFrameEvents> implements IF
if (this.activeLoaderId !== this.defaultLoaderId) return;
if (this.parentId) return;

const newDocumentScripts = this.#framesManager.pendingNewDocumentScripts;
const newDocumentScripts = [
...this.#framesManager.pendingNewDocumentScripts,
...this.pendingNewDocumentScripts,
];
if (newDocumentScripts.length) {
const scripts = [...newDocumentScripts];
this.#framesManager.pendingNewDocumentScripts.length = 0;
Expand Down Expand Up @@ -309,6 +346,15 @@ export default class Frame extends TypedEventEmitter<IFrameEvents> implements IF
return navigation;
}

public click(
jsPathOrSelector: IJsPath | string,
verification?: IElementInteractVerification,
): Promise<void> {
let jsPath = jsPathOrSelector;
if (typeof jsPath === 'string') jsPath = ['document', ['querySelector', jsPathOrSelector]];
return this.interact([{ command: 'click', mousePosition: jsPath, verification }]);
}

public async interact(...interactionGroups: IInteractionGroups): Promise<void> {
const timeoutMs = 120e3;
const interactionResolvable = new Resolvable<void>(timeoutMs);
Expand Down Expand Up @@ -363,7 +409,7 @@ export default class Frame extends TypedEventEmitter<IFrameEvents> implements IF
if (!this.parentId) return { x: 0, y: 0 };
const parentOffset = await this.parentFrame.getContainerOffset();
const frameElementNodeId = await this.getFrameElementDevtoolsNodeId();
const thisOffset = await this.evaluateOnNode<IPoint>(
const thisOffset = await this.parentFrame.evaluateOnNode<IPoint>(
frameElementNodeId,
`(() => {
const rect = this.getBoundingClientRect();
Expand Down Expand Up @@ -456,7 +502,7 @@ export default class Frame extends TypedEventEmitter<IFrameEvents> implements IF
if (!this.parentFrame || this.frameElementDevtoolsNodeId)
return this.frameElementDevtoolsNodeId;

this.frameElementDevtoolsNodeId = this.devtoolsSession
this.frameElementDevtoolsNodeId = this.parentFrame.devtoolsSession
.send('DOM.getFrameOwner', { frameId: this.id }, this)
.then(owner => this.parentFrame.resolveDevtoolsNodeId(owner.backendNodeId, true));

Expand Down Expand Up @@ -658,12 +704,16 @@ export default class Frame extends TypedEventEmitter<IFrameEvents> implements IF
}

public removeContextId(executionContextId: number): void {
if (this.defaultContextId === executionContextId) this.defaultContextId = null;
if (this.defaultContextId === executionContextId) {
this.defaultContextId = null;
this.defaultContextCreated = null;
}
if (this.isolatedContextId === executionContextId) this.isolatedContextId = null;
}

public clearContextIds(): void {
this.defaultContextId = null;
this.defaultContextCreated = null;
this.isolatedContextId = null;
}

Expand Down
Loading