Update dashboard, kb, memory +4 more (+28 ~18 -1)
This commit is contained in:
582
node_modules/puppeteer-core/src/cdp/FrameManager.ts
generated
vendored
Normal file
582
node_modules/puppeteer-core/src/cdp/FrameManager.ts
generated
vendored
Normal file
@@ -0,0 +1,582 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import {type CDPSession, CDPSessionEvent} from '../api/CDPSession.js';
|
||||
import {FrameEvent} from '../api/Frame.js';
|
||||
import type {NewDocumentScriptEvaluation} from '../api/Page.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
|
||||
import {debugError, PuppeteerURL, UTILITY_WORLD_NAME} from '../common/util.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {Deferred} from '../util/Deferred.js';
|
||||
import {disposeSymbol} from '../util/disposable.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
import type {Binding} from './Binding.js';
|
||||
import {CdpPreloadScript} from './CdpPreloadScript.js';
|
||||
import type {CdpCDPSession} from './CdpSession.js';
|
||||
import {isTargetClosedError} from './Connection.js';
|
||||
import {CdpDeviceRequestPromptManager} from './DeviceRequestPrompt.js';
|
||||
import {ExecutionContext} from './ExecutionContext.js';
|
||||
import {CdpFrame} from './Frame.js';
|
||||
import type {FrameManagerEvents} from './FrameManagerEvents.js';
|
||||
import {FrameManagerEvent} from './FrameManagerEvents.js';
|
||||
import {FrameTree} from './FrameTree.js';
|
||||
import type {IsolatedWorld} from './IsolatedWorld.js';
|
||||
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
|
||||
import {NetworkManager} from './NetworkManager.js';
|
||||
import type {CdpPage} from './Page.js';
|
||||
import type {CdpTarget} from './Target.js';
|
||||
|
||||
const TIME_FOR_WAITING_FOR_SWAP = 100; // ms.
|
||||
|
||||
/**
|
||||
* A frame manager manages the frames for a given {@link Page | page}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class FrameManager extends EventEmitter<FrameManagerEvents> {
|
||||
#page: CdpPage;
|
||||
#networkManager: NetworkManager;
|
||||
#timeoutSettings: TimeoutSettings;
|
||||
#isolatedWorlds = new Set<string>();
|
||||
#client: CdpCDPSession;
|
||||
#scriptsToEvaluateOnNewDocument = new Map<string, CdpPreloadScript>();
|
||||
#bindings = new Set<Binding>();
|
||||
|
||||
_frameTree = new FrameTree<CdpFrame>();
|
||||
|
||||
/**
|
||||
* Set of frame IDs stored to indicate if a frame has received a
|
||||
* frameNavigated event so that frame tree responses could be ignored as the
|
||||
* frameNavigated event usually contains the latest information.
|
||||
*/
|
||||
#frameNavigatedReceived = new Set<string>();
|
||||
|
||||
#deviceRequestPromptManagerMap = new WeakMap<
|
||||
CDPSession,
|
||||
CdpDeviceRequestPromptManager
|
||||
>();
|
||||
|
||||
#frameTreeHandled?: Deferred<void>;
|
||||
|
||||
get timeoutSettings(): TimeoutSettings {
|
||||
return this.#timeoutSettings;
|
||||
}
|
||||
|
||||
get networkManager(): NetworkManager {
|
||||
return this.#networkManager;
|
||||
}
|
||||
|
||||
get client(): CdpCDPSession {
|
||||
return this.#client;
|
||||
}
|
||||
|
||||
constructor(
|
||||
client: CdpCDPSession,
|
||||
page: CdpPage,
|
||||
timeoutSettings: TimeoutSettings,
|
||||
) {
|
||||
super();
|
||||
this.#client = client;
|
||||
this.#page = page;
|
||||
this.#networkManager = new NetworkManager(
|
||||
this,
|
||||
page.browser().isNetworkEnabled(),
|
||||
);
|
||||
this.#timeoutSettings = timeoutSettings;
|
||||
this.setupEventListeners(this.#client);
|
||||
client.once(CDPSessionEvent.Disconnected, () => {
|
||||
this.#onClientDisconnect().catch(debugError);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the frame's client is disconnected. We don't know if the
|
||||
* disconnect means that the frame is removed or if it will be replaced by a
|
||||
* new frame. Therefore, we wait for a swap event.
|
||||
*/
|
||||
async #onClientDisconnect() {
|
||||
const mainFrame = this._frameTree.getMainFrame();
|
||||
if (!mainFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.#page.browser().connected) {
|
||||
// If the browser is not connected we know
|
||||
// that activation will not happen
|
||||
this.#removeFramesRecursively(mainFrame);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const child of mainFrame.childFrames()) {
|
||||
this.#removeFramesRecursively(child);
|
||||
}
|
||||
const swapped = Deferred.create<void>({
|
||||
timeout: TIME_FOR_WAITING_FOR_SWAP,
|
||||
message: 'Frame was not swapped',
|
||||
});
|
||||
mainFrame.once(FrameEvent.FrameSwappedByActivation, () => {
|
||||
swapped.resolve();
|
||||
});
|
||||
try {
|
||||
await swapped.valueOrThrow();
|
||||
} catch {
|
||||
this.#removeFramesRecursively(mainFrame);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the main frame is replaced by another main frame,
|
||||
* we maintain the main frame object identity while updating
|
||||
* its frame tree and ID.
|
||||
*/
|
||||
async swapFrameTree(client: CdpCDPSession): Promise<void> {
|
||||
this.#client = client;
|
||||
const frame = this._frameTree.getMainFrame();
|
||||
if (frame) {
|
||||
this.#frameNavigatedReceived.add(this.#client.target()._targetId);
|
||||
this._frameTree.removeFrame(frame);
|
||||
frame.updateId(this.#client.target()._targetId);
|
||||
this._frameTree.addFrame(frame);
|
||||
frame.updateClient(client);
|
||||
}
|
||||
this.setupEventListeners(client);
|
||||
client.once(CDPSessionEvent.Disconnected, () => {
|
||||
this.#onClientDisconnect().catch(debugError);
|
||||
});
|
||||
await this.initialize(client, frame);
|
||||
await this.#networkManager.addClient(client);
|
||||
if (frame) {
|
||||
frame.emit(FrameEvent.FrameSwappedByActivation, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async registerSpeculativeSession(client: CdpCDPSession): Promise<void> {
|
||||
await this.#networkManager.addClient(client);
|
||||
}
|
||||
|
||||
private setupEventListeners(session: CDPSession) {
|
||||
session.on('Page.frameAttached', async event => {
|
||||
await this.#frameTreeHandled?.valueOrThrow();
|
||||
this.#onFrameAttached(session, event.frameId, event.parentFrameId);
|
||||
});
|
||||
session.on('Page.frameNavigated', async event => {
|
||||
this.#frameNavigatedReceived.add(event.frame.id);
|
||||
await this.#frameTreeHandled?.valueOrThrow();
|
||||
void this.#onFrameNavigated(event.frame, event.type);
|
||||
});
|
||||
session.on('Page.navigatedWithinDocument', async event => {
|
||||
await this.#frameTreeHandled?.valueOrThrow();
|
||||
this.#onFrameNavigatedWithinDocument(event.frameId, event.url);
|
||||
});
|
||||
session.on(
|
||||
'Page.frameDetached',
|
||||
async (event: Protocol.Page.FrameDetachedEvent) => {
|
||||
await this.#frameTreeHandled?.valueOrThrow();
|
||||
this.#onFrameDetached(
|
||||
event.frameId,
|
||||
event.reason as Protocol.Page.FrameDetachedEventReason,
|
||||
);
|
||||
},
|
||||
);
|
||||
session.on('Page.frameStartedLoading', async event => {
|
||||
await this.#frameTreeHandled?.valueOrThrow();
|
||||
this.#onFrameStartedLoading(event.frameId);
|
||||
});
|
||||
session.on('Page.frameStoppedLoading', async event => {
|
||||
await this.#frameTreeHandled?.valueOrThrow();
|
||||
this.#onFrameStoppedLoading(event.frameId);
|
||||
});
|
||||
session.on('Runtime.executionContextCreated', async event => {
|
||||
await this.#frameTreeHandled?.valueOrThrow();
|
||||
this.#onExecutionContextCreated(event.context, session);
|
||||
});
|
||||
session.on('Page.lifecycleEvent', async event => {
|
||||
await this.#frameTreeHandled?.valueOrThrow();
|
||||
this.#onLifecycleEvent(event);
|
||||
});
|
||||
}
|
||||
|
||||
async initialize(client: CDPSession, frame?: CdpFrame | null): Promise<void> {
|
||||
try {
|
||||
this.#frameTreeHandled?.resolve();
|
||||
this.#frameTreeHandled = Deferred.create();
|
||||
// We need to schedule all these commands while the target is paused,
|
||||
// therefore, it needs to happen synchronously. At the same time we
|
||||
// should not start processing execution context and frame events before
|
||||
// we received the initial information about the frame tree.
|
||||
await Promise.all([
|
||||
this.#networkManager.addClient(client),
|
||||
client.send('Page.enable'),
|
||||
client.send('Page.getFrameTree').then(({frameTree}) => {
|
||||
this.#handleFrameTree(client, frameTree);
|
||||
this.#frameTreeHandled?.resolve();
|
||||
}),
|
||||
client.send('Page.setLifecycleEventsEnabled', {enabled: true}),
|
||||
client.send('Runtime.enable').then(() => {
|
||||
return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME);
|
||||
}),
|
||||
...(frame
|
||||
? Array.from(this.#scriptsToEvaluateOnNewDocument.values())
|
||||
: []
|
||||
).map(script => {
|
||||
return frame?.addPreloadScript(script);
|
||||
}),
|
||||
...(frame ? Array.from(this.#bindings.values()) : []).map(binding => {
|
||||
return frame?.addExposedFunctionBinding(binding);
|
||||
}),
|
||||
]);
|
||||
} catch (error) {
|
||||
this.#frameTreeHandled?.resolve();
|
||||
// The target might have been closed before the initialization finished.
|
||||
if (isErrorLike(error) && isTargetClosedError(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
page(): CdpPage {
|
||||
return this.#page;
|
||||
}
|
||||
|
||||
mainFrame(): CdpFrame {
|
||||
const mainFrame = this._frameTree.getMainFrame();
|
||||
assert(mainFrame, 'Requesting main frame too early!');
|
||||
return mainFrame;
|
||||
}
|
||||
|
||||
frames(): CdpFrame[] {
|
||||
return Array.from(this._frameTree.frames());
|
||||
}
|
||||
|
||||
frame(frameId: string): CdpFrame | null {
|
||||
return this._frameTree.getById(frameId) || null;
|
||||
}
|
||||
|
||||
async addExposedFunctionBinding(binding: Binding): Promise<void> {
|
||||
this.#bindings.add(binding);
|
||||
await Promise.all(
|
||||
this.frames().map(async frame => {
|
||||
return await frame.addExposedFunctionBinding(binding);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async removeExposedFunctionBinding(binding: Binding): Promise<void> {
|
||||
this.#bindings.delete(binding);
|
||||
await Promise.all(
|
||||
this.frames().map(async frame => {
|
||||
return await frame.removeExposedFunctionBinding(binding);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async evaluateOnNewDocument(
|
||||
source: string,
|
||||
): Promise<NewDocumentScriptEvaluation> {
|
||||
const {identifier} = await this.mainFrame()
|
||||
._client()
|
||||
.send('Page.addScriptToEvaluateOnNewDocument', {
|
||||
source,
|
||||
});
|
||||
|
||||
const preloadScript = new CdpPreloadScript(
|
||||
this.mainFrame(),
|
||||
identifier,
|
||||
source,
|
||||
);
|
||||
|
||||
this.#scriptsToEvaluateOnNewDocument.set(identifier, preloadScript);
|
||||
|
||||
await Promise.all(
|
||||
this.frames().map(async frame => {
|
||||
return await frame.addPreloadScript(preloadScript);
|
||||
}),
|
||||
);
|
||||
|
||||
return {identifier};
|
||||
}
|
||||
|
||||
async removeScriptToEvaluateOnNewDocument(identifier: string): Promise<void> {
|
||||
const preloadScript = this.#scriptsToEvaluateOnNewDocument.get(identifier);
|
||||
if (!preloadScript) {
|
||||
throw new Error(
|
||||
`Script to evaluate on new document with id ${identifier} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
this.#scriptsToEvaluateOnNewDocument.delete(identifier);
|
||||
|
||||
await Promise.all(
|
||||
this.frames().map(frame => {
|
||||
const identifier = preloadScript.getIdForFrame(frame);
|
||||
if (!identifier) {
|
||||
return;
|
||||
}
|
||||
return frame
|
||||
._client()
|
||||
.send('Page.removeScriptToEvaluateOnNewDocument', {
|
||||
identifier,
|
||||
})
|
||||
.catch(debugError);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
onAttachedToTarget(target: CdpTarget): void {
|
||||
if (target._getTargetInfo().type !== 'iframe') {
|
||||
return;
|
||||
}
|
||||
|
||||
const frame = this.frame(target._getTargetInfo().targetId);
|
||||
if (frame) {
|
||||
frame.updateClient(target._session()!);
|
||||
}
|
||||
this.setupEventListeners(target._session()!);
|
||||
void this.initialize(target._session()!, frame);
|
||||
}
|
||||
|
||||
_deviceRequestPromptManager(
|
||||
client: CDPSession,
|
||||
): CdpDeviceRequestPromptManager {
|
||||
let manager = this.#deviceRequestPromptManagerMap.get(client);
|
||||
if (manager === undefined) {
|
||||
manager = new CdpDeviceRequestPromptManager(
|
||||
client,
|
||||
this.#timeoutSettings,
|
||||
);
|
||||
this.#deviceRequestPromptManagerMap.set(client, manager);
|
||||
}
|
||||
return manager;
|
||||
}
|
||||
|
||||
#onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
|
||||
const frame = this.frame(event.frameId);
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
frame._onLifecycleEvent(event.loaderId, event.name);
|
||||
this.emit(FrameManagerEvent.LifecycleEvent, frame);
|
||||
frame.emit(FrameEvent.LifecycleEvent, undefined);
|
||||
}
|
||||
|
||||
#onFrameStartedLoading(frameId: string): void {
|
||||
const frame = this.frame(frameId);
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
frame._onLoadingStarted();
|
||||
}
|
||||
|
||||
#onFrameStoppedLoading(frameId: string): void {
|
||||
const frame = this.frame(frameId);
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
frame._onLoadingStopped();
|
||||
this.emit(FrameManagerEvent.LifecycleEvent, frame);
|
||||
frame.emit(FrameEvent.LifecycleEvent, undefined);
|
||||
}
|
||||
|
||||
#handleFrameTree(
|
||||
session: CDPSession,
|
||||
frameTree: Protocol.Page.FrameTree,
|
||||
): void {
|
||||
if (frameTree.frame.parentId) {
|
||||
this.#onFrameAttached(
|
||||
session,
|
||||
frameTree.frame.id,
|
||||
frameTree.frame.parentId,
|
||||
);
|
||||
}
|
||||
if (!this.#frameNavigatedReceived.has(frameTree.frame.id)) {
|
||||
void this.#onFrameNavigated(frameTree.frame, 'Navigation');
|
||||
} else {
|
||||
this.#frameNavigatedReceived.delete(frameTree.frame.id);
|
||||
}
|
||||
|
||||
if (!frameTree.childFrames) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const child of frameTree.childFrames) {
|
||||
this.#handleFrameTree(session, child);
|
||||
}
|
||||
}
|
||||
|
||||
#onFrameAttached(
|
||||
session: CDPSession,
|
||||
frameId: string,
|
||||
parentFrameId: string,
|
||||
): void {
|
||||
let frame = this.frame(frameId);
|
||||
if (frame) {
|
||||
const parentFrame = this.frame(parentFrameId);
|
||||
if (session && parentFrame && frame.client !== parentFrame?.client) {
|
||||
// If an OOP iframes becomes a normal iframe
|
||||
// again it is first attached to the parent frame before the
|
||||
// target is removed.
|
||||
frame.updateClient(session);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
frame = new CdpFrame(this, frameId, parentFrameId, session);
|
||||
this._frameTree.addFrame(frame);
|
||||
this.emit(FrameManagerEvent.FrameAttached, frame);
|
||||
}
|
||||
|
||||
async #onFrameNavigated(
|
||||
framePayload: Protocol.Page.Frame,
|
||||
navigationType: Protocol.Page.NavigationType,
|
||||
): Promise<void> {
|
||||
const frameId = framePayload.id;
|
||||
const isMainFrame = !framePayload.parentId;
|
||||
|
||||
let frame = this._frameTree.getById(frameId);
|
||||
|
||||
// Detach all child frames first.
|
||||
if (frame) {
|
||||
for (const child of frame.childFrames()) {
|
||||
this.#removeFramesRecursively(child);
|
||||
}
|
||||
}
|
||||
|
||||
// Update or create main frame.
|
||||
if (isMainFrame) {
|
||||
if (frame) {
|
||||
// Update frame id to retain frame identity on cross-process navigation.
|
||||
this._frameTree.removeFrame(frame);
|
||||
frame._id = frameId;
|
||||
} else {
|
||||
// Initial main frame navigation.
|
||||
frame = new CdpFrame(this, frameId, undefined, this.#client);
|
||||
}
|
||||
this._frameTree.addFrame(frame);
|
||||
}
|
||||
|
||||
frame = await this._frameTree.waitForFrame(frameId);
|
||||
frame._navigated(framePayload);
|
||||
this.emit(FrameManagerEvent.FrameNavigated, frame);
|
||||
frame.emit(FrameEvent.FrameNavigated, navigationType);
|
||||
}
|
||||
|
||||
async #createIsolatedWorld(session: CDPSession, name: string): Promise<void> {
|
||||
const key = `${session.id()}:${name}`;
|
||||
|
||||
if (this.#isolatedWorlds.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await session.send('Page.addScriptToEvaluateOnNewDocument', {
|
||||
source: `//# sourceURL=${PuppeteerURL.INTERNAL_URL}`,
|
||||
worldName: name,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
this.frames()
|
||||
.filter(frame => {
|
||||
return frame.client === session;
|
||||
})
|
||||
.map(frame => {
|
||||
// Frames might be removed before we send this, so we don't want to
|
||||
// throw an error.
|
||||
return session
|
||||
.send('Page.createIsolatedWorld', {
|
||||
frameId: frame._id,
|
||||
worldName: name,
|
||||
grantUniveralAccess: true,
|
||||
})
|
||||
.catch(debugError);
|
||||
}),
|
||||
);
|
||||
|
||||
this.#isolatedWorlds.add(key);
|
||||
}
|
||||
|
||||
#onFrameNavigatedWithinDocument(frameId: string, url: string): void {
|
||||
const frame = this.frame(frameId);
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
frame._navigatedWithinDocument(url);
|
||||
this.emit(FrameManagerEvent.FrameNavigatedWithinDocument, frame);
|
||||
frame.emit(FrameEvent.FrameNavigatedWithinDocument, undefined);
|
||||
this.emit(FrameManagerEvent.FrameNavigated, frame);
|
||||
frame.emit(FrameEvent.FrameNavigated, 'Navigation');
|
||||
}
|
||||
|
||||
#onFrameDetached(
|
||||
frameId: string,
|
||||
reason: Protocol.Page.FrameDetachedEventReason,
|
||||
): void {
|
||||
const frame = this.frame(frameId);
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
switch (reason) {
|
||||
case 'remove':
|
||||
// Only remove the frame if the reason for the detached event is
|
||||
// an actual removement of the frame.
|
||||
// For frames that become OOP iframes, the reason would be 'swap'.
|
||||
this.#removeFramesRecursively(frame);
|
||||
break;
|
||||
case 'swap':
|
||||
this.emit(FrameManagerEvent.FrameSwapped, frame);
|
||||
frame.emit(FrameEvent.FrameSwapped, undefined);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#onExecutionContextCreated(
|
||||
contextPayload: Protocol.Runtime.ExecutionContextDescription,
|
||||
session: CDPSession,
|
||||
): void {
|
||||
const auxData = contextPayload.auxData as {frameId?: string} | undefined;
|
||||
const frameId = auxData && auxData.frameId;
|
||||
const frame = typeof frameId === 'string' ? this.frame(frameId) : undefined;
|
||||
let world: IsolatedWorld | undefined;
|
||||
if (frame) {
|
||||
// Only care about execution contexts created for the current session.
|
||||
if (frame.client !== session) {
|
||||
return;
|
||||
}
|
||||
if (contextPayload.auxData && contextPayload.auxData['isDefault']) {
|
||||
world = frame.worlds[MAIN_WORLD];
|
||||
} else if (contextPayload.name === UTILITY_WORLD_NAME) {
|
||||
// In case of multiple sessions to the same target, there's a race between
|
||||
// connections so we might end up creating multiple isolated worlds.
|
||||
// We can use either.
|
||||
world = frame.worlds[PUPPETEER_WORLD];
|
||||
}
|
||||
}
|
||||
// If there is no world, the context is not meant to be handled by us.
|
||||
if (!world) {
|
||||
return;
|
||||
}
|
||||
const context = new ExecutionContext(
|
||||
frame?.client || this.#client,
|
||||
contextPayload,
|
||||
world,
|
||||
);
|
||||
world.setContext(context);
|
||||
}
|
||||
|
||||
#removeFramesRecursively(frame: CdpFrame): void {
|
||||
for (const child of frame.childFrames()) {
|
||||
this.#removeFramesRecursively(child);
|
||||
}
|
||||
frame[disposeSymbol]();
|
||||
this._frameTree.removeFrame(frame);
|
||||
this.emit(FrameManagerEvent.FrameDetached, frame);
|
||||
frame.emit(FrameEvent.FrameDetached, frame);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user