248 lines
12 KiB
JavaScript
248 lines
12 KiB
JavaScript
import { LogType } from '../../../utils/log.js';
|
|
import { BrowsingContextImpl, serializeOrigin, } from '../context/BrowsingContextImpl.js';
|
|
import { WorkerRealm } from '../script/WorkerRealm.js';
|
|
import { CdpTarget } from './CdpTarget.js';
|
|
const cdpToBidiTargetTypes = {
|
|
service_worker: 'service-worker',
|
|
shared_worker: 'shared-worker',
|
|
worker: 'dedicated-worker',
|
|
};
|
|
export class CdpTargetManager {
|
|
#browserCdpClient;
|
|
#cdpConnection;
|
|
#targetKeysToBeIgnoredByAutoAttach = new Set();
|
|
#selfTargetId;
|
|
#eventManager;
|
|
#browsingContextStorage;
|
|
#networkStorage;
|
|
#bluetoothProcessor;
|
|
#preloadScriptStorage;
|
|
#realmStorage;
|
|
#defaultUserContextId;
|
|
#logger;
|
|
#unhandledPromptBehavior;
|
|
#prerenderingDisabled;
|
|
constructor(cdpConnection, browserCdpClient, selfTargetId, eventManager, browsingContextStorage, realmStorage, networkStorage, bluetoothProcessor, preloadScriptStorage, defaultUserContextId, prerenderingDisabled, unhandledPromptBehavior, logger) {
|
|
this.#cdpConnection = cdpConnection;
|
|
this.#browserCdpClient = browserCdpClient;
|
|
this.#targetKeysToBeIgnoredByAutoAttach.add(selfTargetId);
|
|
this.#selfTargetId = selfTargetId;
|
|
this.#eventManager = eventManager;
|
|
this.#browsingContextStorage = browsingContextStorage;
|
|
this.#preloadScriptStorage = preloadScriptStorage;
|
|
this.#networkStorage = networkStorage;
|
|
this.#bluetoothProcessor = bluetoothProcessor;
|
|
this.#realmStorage = realmStorage;
|
|
this.#defaultUserContextId = defaultUserContextId;
|
|
this.#prerenderingDisabled = prerenderingDisabled;
|
|
this.#unhandledPromptBehavior = unhandledPromptBehavior;
|
|
this.#logger = logger;
|
|
this.#setEventListeners(browserCdpClient);
|
|
}
|
|
/**
|
|
* This method is called for each CDP session, since this class is responsible
|
|
* for creating and destroying all targets and browsing contexts.
|
|
*/
|
|
#setEventListeners(cdpClient) {
|
|
cdpClient.on('Target.attachedToTarget', (params) => {
|
|
this.#handleAttachedToTargetEvent(params, cdpClient);
|
|
});
|
|
cdpClient.on('Target.detachedFromTarget', this.#handleDetachedFromTargetEvent.bind(this));
|
|
cdpClient.on('Target.targetInfoChanged', this.#handleTargetInfoChangedEvent.bind(this));
|
|
cdpClient.on('Inspector.targetCrashed', () => {
|
|
this.#handleTargetCrashedEvent(cdpClient);
|
|
});
|
|
cdpClient.on('Page.frameAttached', this.#handleFrameAttachedEvent.bind(this));
|
|
cdpClient.on('Page.frameDetached', this.#handleFrameDetachedEvent.bind(this));
|
|
cdpClient.on('Page.frameSubtreeWillBeDetached', this.#handleFrameSubtreeWillBeDetached.bind(this));
|
|
}
|
|
#handleFrameAttachedEvent(params) {
|
|
const parentBrowsingContext = this.#browsingContextStorage.findContext(params.parentFrameId);
|
|
if (parentBrowsingContext !== undefined) {
|
|
BrowsingContextImpl.create(params.frameId, params.parentFrameId, parentBrowsingContext.userContext, parentBrowsingContext.cdpTarget, this.#eventManager, this.#browsingContextStorage, this.#realmStorage,
|
|
// At this point, we don't know the URL of the frame yet, so it will be updated
|
|
// later.
|
|
'about:blank', undefined, this.#unhandledPromptBehavior, this.#logger);
|
|
}
|
|
}
|
|
#handleFrameDetachedEvent(params) {
|
|
// In case of OOPiF no need in deleting BrowsingContext.
|
|
if (params.reason === 'swap') {
|
|
return;
|
|
}
|
|
this.#browsingContextStorage.findContext(params.frameId)?.dispose(true);
|
|
}
|
|
#handleFrameSubtreeWillBeDetached(params) {
|
|
this.#browsingContextStorage.findContext(params.frameId)?.dispose(true);
|
|
}
|
|
#handleAttachedToTargetEvent(params, parentSessionCdpClient) {
|
|
const { sessionId, targetInfo } = params;
|
|
const targetCdpClient = this.#cdpConnection.getCdpClient(sessionId);
|
|
const detach = async () => {
|
|
// Detaches and resumes the target suppressing errors.
|
|
await targetCdpClient
|
|
.sendCommand('Runtime.runIfWaitingForDebugger')
|
|
.then(() => parentSessionCdpClient.sendCommand('Target.detachFromTarget', params))
|
|
.catch((error) => this.#logger?.(LogType.debugError, error));
|
|
};
|
|
// Do not attach to the Mapper target.
|
|
if (this.#selfTargetId === targetInfo.targetId) {
|
|
void detach();
|
|
return;
|
|
}
|
|
// Service workers are special case because they attach to the
|
|
// browser target and the page target (so twice per worker) during
|
|
// the regular auto-attach and might hang if the CDP session on
|
|
// the browser level is not detached. The logic to detach the
|
|
// right session is handled in the switch below.
|
|
const targetKey = targetInfo.type === 'service_worker'
|
|
? `${parentSessionCdpClient.sessionId}_${targetInfo.targetId}`
|
|
: targetInfo.targetId;
|
|
// Mapper generally only needs one session per target. If we
|
|
// receive additional auto-attached sessions, that is very likely
|
|
// coming from custom CDP sessions.
|
|
if (this.#targetKeysToBeIgnoredByAutoAttach.has(targetKey)) {
|
|
// Return to leave the session untouched.
|
|
return;
|
|
}
|
|
this.#targetKeysToBeIgnoredByAutoAttach.add(targetKey);
|
|
switch (targetInfo.type) {
|
|
case 'tab':
|
|
// Tab targets are required only to handle page targets beneath them.
|
|
this.#setEventListeners(targetCdpClient);
|
|
// Auto-attach to the page target. No need in resuming tab target debugger, as it
|
|
// should preserve the page target debugger state, and will be resumed by the page
|
|
// target.
|
|
void (async () => {
|
|
await targetCdpClient.sendCommand('Target.setAutoAttach', {
|
|
autoAttach: true,
|
|
waitForDebuggerOnStart: true,
|
|
flatten: true,
|
|
});
|
|
})();
|
|
return;
|
|
case 'page':
|
|
case 'iframe': {
|
|
const cdpTarget = this.#createCdpTarget(targetCdpClient, parentSessionCdpClient, targetInfo);
|
|
const maybeContext = this.#browsingContextStorage.findContext(targetInfo.targetId);
|
|
if (maybeContext && targetInfo.type === 'iframe') {
|
|
// OOPiF.
|
|
maybeContext.updateCdpTarget(cdpTarget);
|
|
}
|
|
else {
|
|
// If attaching to existing browser instance, there could be OOPiF targets. This
|
|
// case is handled by the `findFrameParentId` method.
|
|
const parentId = this.#findFrameParentId(targetInfo, parentSessionCdpClient.sessionId);
|
|
const userContext = targetInfo.browserContextId &&
|
|
targetInfo.browserContextId !== this.#defaultUserContextId
|
|
? targetInfo.browserContextId
|
|
: 'default';
|
|
// New context.
|
|
BrowsingContextImpl.create(targetInfo.targetId, parentId, userContext, cdpTarget, this.#eventManager, this.#browsingContextStorage, this.#realmStorage,
|
|
// Hack: when a new target created, CDP emits targetInfoChanged with an empty
|
|
// url, and navigates it to about:blank later. When the event is emitted for
|
|
// an existing target (reconnect), the url is already known, and navigation
|
|
// events will not be emitted anymore. Replacing empty url with `about:blank`
|
|
// allows to handle both cases in the same way.
|
|
// "7.3.2.1 Creating browsing contexts".
|
|
// https://html.spec.whatwg.org/multipage/document-sequences.html#creating-browsing-contexts
|
|
// TODO: check who to deal with non-null creator and its `creatorOrigin`.
|
|
targetInfo.url === '' ? 'about:blank' : targetInfo.url, targetInfo.openerFrameId ?? targetInfo.openerId, this.#unhandledPromptBehavior, this.#logger);
|
|
}
|
|
return;
|
|
}
|
|
case 'service_worker':
|
|
case 'worker': {
|
|
const realm = this.#realmStorage.findRealm({
|
|
cdpSessionId: parentSessionCdpClient.sessionId,
|
|
});
|
|
// If there is no browsing context, this worker is already terminated.
|
|
if (!realm) {
|
|
void detach();
|
|
return;
|
|
}
|
|
const cdpTarget = this.#createCdpTarget(targetCdpClient, parentSessionCdpClient, targetInfo);
|
|
this.#handleWorkerTarget(cdpToBidiTargetTypes[targetInfo.type], cdpTarget, realm);
|
|
return;
|
|
}
|
|
// In CDP, we only emit shared workers on the browser and not the set of
|
|
// frames that use the shared worker. If we change this in the future to
|
|
// behave like service workers (emits on both browser and frame targets),
|
|
// we can remove this block and merge service workers with the above one.
|
|
case 'shared_worker': {
|
|
const cdpTarget = this.#createCdpTarget(targetCdpClient, parentSessionCdpClient, targetInfo);
|
|
this.#handleWorkerTarget(cdpToBidiTargetTypes[targetInfo.type], cdpTarget);
|
|
return;
|
|
}
|
|
}
|
|
// DevTools or some other not supported by BiDi target. Just release
|
|
// debugger and ignore them.
|
|
void detach();
|
|
}
|
|
/** Try to find the parent browsing context ID for the given attached target. */
|
|
#findFrameParentId(targetInfo, parentSessionId) {
|
|
if (targetInfo.type !== 'iframe') {
|
|
return null;
|
|
}
|
|
const parentId = targetInfo.openerFrameId ?? targetInfo.openerId;
|
|
if (parentId !== undefined) {
|
|
return parentId;
|
|
}
|
|
if (parentSessionId !== undefined) {
|
|
return (this.#browsingContextStorage.findContextBySession(parentSessionId)
|
|
?.id ?? null);
|
|
}
|
|
return null;
|
|
}
|
|
#createCdpTarget(targetCdpClient, parentCdpClient, targetInfo) {
|
|
this.#setEventListeners(targetCdpClient);
|
|
const target = CdpTarget.create(targetInfo.targetId, targetCdpClient, this.#browserCdpClient, parentCdpClient, this.#realmStorage, this.#eventManager, this.#preloadScriptStorage, this.#browsingContextStorage, this.#networkStorage, this.#prerenderingDisabled, this.#unhandledPromptBehavior, this.#logger);
|
|
this.#networkStorage.onCdpTargetCreated(target);
|
|
this.#bluetoothProcessor.onCdpTargetCreated(target);
|
|
return target;
|
|
}
|
|
#workers = new Map();
|
|
#handleWorkerTarget(realmType, cdpTarget, ownerRealm) {
|
|
cdpTarget.cdpClient.on('Runtime.executionContextCreated', (params) => {
|
|
const { uniqueId, id, origin } = params.context;
|
|
const workerRealm = new WorkerRealm(cdpTarget.cdpClient, this.#eventManager, id, this.#logger, serializeOrigin(origin), ownerRealm ? [ownerRealm] : [], uniqueId, this.#realmStorage, realmType);
|
|
this.#workers.set(cdpTarget.cdpSessionId, workerRealm);
|
|
});
|
|
}
|
|
#handleDetachedFromTargetEvent({ sessionId, targetId, }) {
|
|
if (targetId) {
|
|
this.#preloadScriptStorage.find({ targetId }).map((preloadScript) => {
|
|
preloadScript.dispose(targetId);
|
|
});
|
|
}
|
|
const context = this.#browsingContextStorage.findContextBySession(sessionId);
|
|
if (context) {
|
|
context.dispose(true);
|
|
return;
|
|
}
|
|
const worker = this.#workers.get(sessionId);
|
|
if (worker) {
|
|
this.#realmStorage.deleteRealms({
|
|
cdpSessionId: worker.cdpClient.sessionId,
|
|
});
|
|
}
|
|
}
|
|
#handleTargetInfoChangedEvent(params) {
|
|
const context = this.#browsingContextStorage.findContext(params.targetInfo.targetId);
|
|
if (context) {
|
|
context.onTargetInfoChanged(params);
|
|
}
|
|
}
|
|
#handleTargetCrashedEvent(cdpClient) {
|
|
// This is primarily used for service and shared workers. CDP tends to not
|
|
// signal they closed gracefully and instead says they crashed to signal
|
|
// they are closed.
|
|
const realms = this.#realmStorage.findRealms({
|
|
cdpSessionId: cdpClient.sessionId,
|
|
});
|
|
for (const realm of realms) {
|
|
realm.dispose();
|
|
}
|
|
}
|
|
}
|
|
//# sourceMappingURL=CdpTargetManager.js.map
|