"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CdpTargetManager = void 0;
const log_js_1 = require("../../../utils/log.js");
const BrowsingContextImpl_js_1 = require("../context/BrowsingContextImpl.js");
const WorkerRealm_js_1 = require("../script/WorkerRealm.js");
const CdpTarget_js_1 = require("./CdpTarget.js");
const cdpToBidiTargetTypes = {
service_worker: 'service-worker',
shared_worker: 'shared-worker',
worker: 'dedicated-worker',
};
class CdpTargetManager {
#browserCdpClient;
#cdpConnection;
#targetKeysToBeIgnoredByAutoAttach = new Set();
#selfTargetId;
#eventManager;
#browsingContextStorage;
#networkStorage;
#preloadScriptStorage;
#realmStorage;
#defaultUserContextId;
#logger;
#unhandledPromptBehavior;
constructor(cdpConnection, browserCdpClient, selfTargetId, eventManager, browsingContextStorage, realmStorage, networkStorage, preloadScriptStorage, defaultUserContextId, 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.#realmStorage = realmStorage;
this.#defaultUserContextId = defaultUserContextId;
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));
}
#handleFrameAttachedEvent(params) {
const parentBrowsingContext = this.#browsingContextStorage.findContext(params.parentFrameId);
if (parentBrowsingContext !== undefined) {
BrowsingContextImpl_js_1.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();
}
#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?.(log_js_1.LogType.debugError, error));
};
if (this.#selfTargetId !== targetInfo.targetId) {
// 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 'page':
case 'iframe': {
if (this.#selfTargetId === targetInfo.targetId) {
void detach();
return;
}
const cdpTarget = this.#createCdpTarget(targetCdpClient, targetInfo);
const maybeContext = this.#browsingContextStorage.findContext(targetInfo.targetId);
if (maybeContext && targetInfo.type === 'iframe') {
// OOPiF.
maybeContext.updateCdpTarget(cdpTarget);
}
else {
const userContext = targetInfo.browserContextId &&
targetInfo.browserContextId !== this.#defaultUserContextId
? targetInfo.browserContextId
: 'default';
// New context.
BrowsingContextImpl_js_1.BrowsingContextImpl.create(targetInfo.targetId, null, 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, 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, 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();
}
#createCdpTarget(targetCdpClient, targetInfo) {
this.#setEventListeners(targetCdpClient);
const target = CdpTarget_js_1.CdpTarget.create(targetInfo.targetId, targetCdpClient, this.#browserCdpClient, this.#realmStorage, this.#eventManager, this.#preloadScriptStorage, this.#browsingContextStorage, this.#networkStorage, this.#unhandledPromptBehavior, this.#logger);
this.#networkStorage.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_js_1.WorkerRealm(cdpTarget.cdpClient, this.#eventManager, id, this.#logger, (0, BrowsingContextImpl_js_1.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();
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();
}
}
}
exports.CdpTargetManager = CdpTargetManager;
//# sourceMappingURL=CdpTargetManager.js.map |