"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