diff --git a/additions/juggler/Helper.js b/additions/juggler/Helper.js deleted file mode 100644 index f5a64d6..0000000 --- a/additions/juggler/Helper.js +++ /dev/null @@ -1,239 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); - -class Helper { - decorateAsEventEmitter(objectToDecorate) { - const { EventEmitter } = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); - const emitter = new EventEmitter(); - objectToDecorate.on = emitter.on.bind(emitter); - objectToDecorate.addEventListener = emitter.on.bind(emitter); - objectToDecorate.off = emitter.off.bind(emitter); - objectToDecorate.removeEventListener = emitter.off.bind(emitter); - objectToDecorate.once = emitter.once.bind(emitter); - objectToDecorate.emit = emitter.emit.bind(emitter); - } - - collectAllBrowsingContexts(rootBrowsingContext, allBrowsingContexts = []) { - allBrowsingContexts.push(rootBrowsingContext); - for (const child of rootBrowsingContext.children) - this.collectAllBrowsingContexts(child, allBrowsingContexts); - return allBrowsingContexts; - } - - awaitTopic(topic) { - return new Promise(resolve => { - const listener = () => { - Services.obs.removeObserver(listener, topic); - resolve(); - } - Services.obs.addObserver(listener, topic); - }); - } - - toProtocolNavigationId(loadIdentifier) { - return `nav-${loadIdentifier}`; - } - - addObserver(handler, topic) { - Services.obs.addObserver(handler, topic); - return () => Services.obs.removeObserver(handler, topic); - } - - addMessageListener(receiver, eventName, handler) { - receiver.addMessageListener(eventName, handler); - return () => receiver.removeMessageListener(eventName, handler); - } - - addEventListener(receiver, eventName, handler, options) { - receiver.addEventListener(eventName, handler, options); - return () => { - try { - receiver.removeEventListener(eventName, handler, options); - } catch (e) { - // This could fail when window has navigated cross-process - // and we remove the listener from WindowProxy. - // Nothing we can do here - so ignore the error. - } - }; - } - - awaitEvent(receiver, eventName) { - return new Promise(resolve => { - receiver.addEventListener(eventName, function listener() { - receiver.removeEventListener(eventName, listener); - resolve(); - }); - }); - } - - on(receiver, eventName, handler, options) { - // The toolkit/modules/EventEmitter.jsm dispatches event name as a first argument. - // Fire event listeners without it for convenience. - const handlerWrapper = (_, ...args) => handler(...args); - receiver.on(eventName, handlerWrapper, options); - return () => receiver.off(eventName, handlerWrapper); - } - - addProgressListener(progress, listener, flags) { - progress.addProgressListener(listener, flags); - return () => progress.removeProgressListener(listener); - } - - removeListeners(listeners) { - for (const tearDown of listeners) - tearDown.call(null); - listeners.splice(0, listeners.length); - } - - generateId() { - const string = uuidGen.generateUUID().toString(); - return string.substring(1, string.length - 1); - } - - getLoadContext(channel) { - let loadContext = null; - try { - if (channel.notificationCallbacks) - loadContext = channel.notificationCallbacks.getInterface(Ci.nsILoadContext); - } catch (e) {} - try { - if (!loadContext && channel.loadGroup) - loadContext = channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext); - } catch (e) { } - return loadContext; - } - - getNetworkErrorStatusText(status) { - if (!status) - return null; - for (const key of Object.keys(Cr)) { - if (Cr[key] === status) - return key; - } - // Security module. The following is taken from - // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/How_to_check_the_secruity_state_of_an_XMLHTTPRequest_over_SSL - if ((status & 0xff0000) === 0x5a0000) { - // NSS_SEC errors (happen below the base value because of negative vals) - if ((status & 0xffff) < Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE)) { - // The bases are actually negative, so in our positive numeric space, we - // need to subtract the base off our value. - const nssErr = Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff); - switch (nssErr) { - case 11: - return 'SEC_ERROR_EXPIRED_CERTIFICATE'; - case 12: - return 'SEC_ERROR_REVOKED_CERTIFICATE'; - case 13: - return 'SEC_ERROR_UNKNOWN_ISSUER'; - case 20: - return 'SEC_ERROR_UNTRUSTED_ISSUER'; - case 21: - return 'SEC_ERROR_UNTRUSTED_CERT'; - case 36: - return 'SEC_ERROR_CA_CERT_INVALID'; - case 90: - return 'SEC_ERROR_INADEQUATE_KEY_USAGE'; - case 176: - return 'SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED'; - default: - return 'SEC_ERROR_UNKNOWN'; - } - } - const sslErr = Math.abs(Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff); - switch (sslErr) { - case 3: - return 'SSL_ERROR_NO_CERTIFICATE'; - case 4: - return 'SSL_ERROR_BAD_CERTIFICATE'; - case 8: - return 'SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE'; - case 9: - return 'SSL_ERROR_UNSUPPORTED_VERSION'; - case 12: - return 'SSL_ERROR_BAD_CERT_DOMAIN'; - default: - return 'SSL_ERROR_UNKNOWN'; - } - } - return ''; - } - - browsingContextToFrameId(browsingContext) { - if (!browsingContext) - return undefined; - if (!browsingContext.parent) - return 'mainframe-' + browsingContext.browserId; - return 'subframe-' + browsingContext.id; - } -} - -const helper = new Helper(); - -class EventWatcher { - constructor(receiver, eventNames, pendingEventWatchers = new Set()) { - this._pendingEventWatchers = pendingEventWatchers; - this._pendingEventWatchers.add(this); - - this._events = []; - this._pendingPromises = []; - this._eventListeners = eventNames.map(eventName => - helper.on(receiver, eventName, this._onEvent.bind(this, eventName)), - ); - } - - _onEvent(eventName, eventObject) { - this._events.push({eventName, eventObject}); - for (const promise of this._pendingPromises) - promise.resolve(); - this._pendingPromises = []; - } - - async ensureEvent(aEventName, predicate) { - if (typeof aEventName !== 'string') - throw new Error('ERROR: ensureEvent expects a "string" as its first argument'); - while (true) { - const result = this.getEvent(aEventName, predicate); - if (result) - return result; - await new Promise((resolve, reject) => this._pendingPromises.push({resolve, reject})); - } - } - - async ensureEvents(eventNames, predicate) { - if (!Array.isArray(eventNames)) - throw new Error('ERROR: ensureEvents expects an array of event names as its first argument'); - return await Promise.all(eventNames.map(eventName => this.ensureEvent(eventName, predicate))); - } - - async ensureEventsAndDispose(eventNames, predicate) { - if (!Array.isArray(eventNames)) - throw new Error('ERROR: ensureEventsAndDispose expects an array of event names as its first argument'); - const result = await this.ensureEvents(eventNames, predicate); - this.dispose(); - return result; - } - - getEvent(aEventName, predicate = (eventObject) => true) { - return this._events.find(({eventName, eventObject}) => eventName === aEventName && predicate(eventObject))?.eventObject; - } - - hasEvent(aEventName, predicate) { - return !!this.getEvent(aEventName, predicate); - } - - dispose() { - this._pendingEventWatchers.delete(this); - for (const promise of this._pendingPromises) - promise.reject(new Error('EventWatcher is being disposed')); - this._pendingPromises = []; - helper.removeListeners(this._eventListeners); - } -} - -var EXPORTED_SYMBOLS = [ "Helper", "EventWatcher" ]; -this.Helper = Helper; -this.EventWatcher = EventWatcher; - diff --git a/additions/juggler/JugglerFrameParent.jsm b/additions/juggler/JugglerFrameParent.jsm deleted file mode 100644 index e621fab..0000000 --- a/additions/juggler/JugglerFrameParent.jsm +++ /dev/null @@ -1,42 +0,0 @@ -"use strict"; - -const { TargetRegistry } = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js'); -const { Helper } = ChromeUtils.import('chrome://juggler/content/Helper.js'); - -const helper = new Helper(); - -var EXPORTED_SYMBOLS = ['JugglerFrameParent']; - -class JugglerFrameParent extends JSWindowActorParent { - constructor() { - super(); - } - - receiveMessage() { } - - async actorCreated() { - // Actors are registered per the WindowGlobalParent / WindowGlobalChild pair. We are only - // interested in those WindowGlobalParent actors that are matching current browsingContext - // window global. - // See https://github.com/mozilla/gecko-dev/blob/cd2121e7d83af1b421c95e8c923db70e692dab5f/testing/mochitest/BrowserTestUtils/BrowserTestUtilsParent.sys.mjs#L15 - if (!this.manager?.isCurrentGlobal) - return; - - // Only interested in main frames for now. - if (this.browsingContext.parent) - return; - - this._target = TargetRegistry.instance()?.targetForBrowserId(this.browsingContext.browserId); - if (!this._target) - return; - - this.actorName = `browser::page[${this._target.id()}]/${this.browsingContext.browserId}/${this.browsingContext.id}/${this._target.nextActorSequenceNumber()}`; - this._target.setActor(this); - } - - didDestroy() { - if (!this._target) - return; - this._target.removeActor(this); - } -} diff --git a/additions/juggler/NetworkObserver.js b/additions/juggler/NetworkObserver.js deleted file mode 100644 index ef269b0..0000000 --- a/additions/juggler/NetworkObserver.js +++ /dev/null @@ -1,967 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); -const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm'); -const { ChannelEventSinkFactory } = ChromeUtils.import("chrome://remote/content/cdp/observers/ChannelEventSink.jsm"); - - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; -const Cr = Components.results; -const Cm = Components.manager; -const CC = Components.Constructor; -const helper = new Helper(); - -const UINT32_MAX = Math.pow(2, 32)-1; - -const BinaryInputStream = CC('@mozilla.org/binaryinputstream;1', 'nsIBinaryInputStream', 'setInputStream'); -const BinaryOutputStream = CC('@mozilla.org/binaryoutputstream;1', 'nsIBinaryOutputStream', 'setOutputStream'); -const StorageStream = CC('@mozilla.org/storagestream;1', 'nsIStorageStream', 'init'); - -// Cap response storage with 100Mb per tracked tab. -const MAX_RESPONSE_STORAGE_SIZE = 100 * 1024 * 1024; - -const pageNetworkSymbol = Symbol('PageNetwork'); - -class PageNetwork { - static forPageTarget(target) { - if (!target) - return undefined; - let result = target[pageNetworkSymbol]; - if (!result) { - result = new PageNetwork(target); - target[pageNetworkSymbol] = result; - } - return result; - } - - constructor(target) { - helper.decorateAsEventEmitter(this); - this._target = target; - this._extraHTTPHeaders = null; - this._responseStorage = new ResponseStorage(MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10); - this._requestInterceptionEnabled = false; - // This is requestId => NetworkRequest map, only contains requests that are - // awaiting interception action (abort, resume, fulfill) over the protocol. - this._interceptedRequests = new Map(); - } - - setExtraHTTPHeaders(headers) { - this._extraHTTPHeaders = headers; - } - - combinedExtraHTTPHeaders() { - return [ - ...(this._target.browserContext().extraHTTPHeaders || []), - ...(this._extraHTTPHeaders || []), - ]; - } - - enableRequestInterception() { - this._requestInterceptionEnabled = true; - } - - disableRequestInterception() { - this._requestInterceptionEnabled = false; - for (const intercepted of this._interceptedRequests.values()) - intercepted.resume(); - this._interceptedRequests.clear(); - } - - resumeInterceptedRequest(requestId, url, method, headers, postData) { - this._takeIntercepted(requestId).resume(url, method, headers, postData); - } - - fulfillInterceptedRequest(requestId, status, statusText, headers, base64body) { - this._takeIntercepted(requestId).fulfill(status, statusText, headers, base64body); - } - - abortInterceptedRequest(requestId, errorCode) { - this._takeIntercepted(requestId).abort(errorCode); - } - - getResponseBody(requestId) { - if (!this._responseStorage) - throw new Error('Responses are not tracked for the given browser'); - return this._responseStorage.getBase64EncodedResponse(requestId); - } - - _takeIntercepted(requestId) { - const intercepted = this._interceptedRequests.get(requestId); - if (!intercepted) - throw new Error(`Cannot find request "${requestId}"`); - this._interceptedRequests.delete(requestId); - return intercepted; - } -} - -class NetworkRequest { - constructor(networkObserver, httpChannel, redirectedFrom) { - this._networkObserver = networkObserver; - this.httpChannel = httpChannel; - - const loadInfo = this.httpChannel.loadInfo; - const browsingContext = loadInfo?.frameBrowsingContext || loadInfo?.workerAssociatedBrowsingContext || loadInfo?.browsingContext; - - this._frameId = helper.browsingContextToFrameId(browsingContext); - - this.requestId = httpChannel.channelId + ''; - this.navigationId = httpChannel.isMainDocumentChannel && loadInfo ? helper.toProtocolNavigationId(loadInfo.jugglerLoadIdentifier) : undefined; - - this._redirectedIndex = 0; - if (redirectedFrom) { - this.redirectedFromId = redirectedFrom.requestId; - this._redirectedIndex = redirectedFrom._redirectedIndex + 1; - this.requestId = this.requestId + '-redirect' + this._redirectedIndex; - this.navigationId = redirectedFrom.navigationId; - // Finish previous request now. Since we inherit the listener, we could in theory - // use onStopRequest, but that will only happen after the last redirect has finished. - redirectedFrom._sendOnRequestFinished(); - } - // In case of proxy auth, we get two requests with the same channel: - // - one is pre-auth - // - second is with auth header. - // - // In this case, we create this NetworkRequest object with a `redirectedFrom` - // object, and they both share the same httpChannel. - // - // Since we want to maintain _channelToRequest map without clashes, - // we must call `_sendOnRequestFinished` **before** we update it with a new object - // here. - if (this._networkObserver._channelToRequest.has(this.httpChannel)) - throw new Error(`Internal Error: invariant is broken for _channelToRequest map`); - this._networkObserver._channelToRequest.set(this.httpChannel, this); - - if (redirectedFrom) { - this._pageNetwork = redirectedFrom._pageNetwork; - } else if (browsingContext) { - const target = this._networkObserver._targetRegistry.targetForBrowserId(browsingContext.browserId); - this._pageNetwork = PageNetwork.forPageTarget(target); - } - this._expectingInterception = false; - this._expectingResumedRequest = undefined; // { method, headers, postData } - this._overriddenHeadersForRedirect = redirectedFrom?._overriddenHeadersForRedirect; - this._sentOnResponse = false; - this._fulfilled = false; - - if (this._overriddenHeadersForRedirect) - overrideRequestHeaders(httpChannel, this._overriddenHeadersForRedirect); - else if (this._pageNetwork) - appendExtraHTTPHeaders(httpChannel, this._pageNetwork.combinedExtraHTTPHeaders()); - - this._responseBodyChunks = []; - - httpChannel.QueryInterface(Ci.nsITraceableChannel); - this._originalListener = httpChannel.setNewListener(this); - if (redirectedFrom) { - // Listener is inherited for regular redirects, so we'd like to avoid - // calling into previous NetworkRequest. - this._originalListener = redirectedFrom._originalListener; - } - - this._previousCallbacks = httpChannel.notificationCallbacks; - httpChannel.notificationCallbacks = this; - - this.QueryInterface = ChromeUtils.generateQI([ - Ci.nsIAuthPrompt2, - Ci.nsIAuthPromptProvider, - Ci.nsIInterfaceRequestor, - Ci.nsINetworkInterceptController, - Ci.nsIStreamListener, - ]); - - if (this.redirectedFromId) { - // Redirects are not interceptable. - this._sendOnRequest(false); - } - } - - // Public interception API. - resume(url, method, headers, postData) { - this._expectingResumedRequest = { method, headers, postData }; - const newUri = url ? Services.io.newURI(url) : null; - this._interceptedChannel.resetInterceptionWithURI(newUri); - this._interceptedChannel = undefined; - } - - // Public interception API. - abort(errorCode) { - const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE; - this._interceptedChannel.cancelInterception(error); - this._interceptedChannel = undefined; - } - - // Public interception API. - fulfill(status, statusText, headers, base64body) { - this._fulfilled = true; - this._interceptedChannel.synthesizeStatus(status, statusText); - for (const header of headers) { - this._interceptedChannel.synthesizeHeader(header.name, header.value); - if (header.name.toLowerCase() === 'set-cookie') { - Services.cookies.QueryInterface(Ci.nsICookieService); - Services.cookies.setCookieStringFromHttp(this.httpChannel.URI, header.value, this.httpChannel); - } - } - const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); - synthesized.data = base64body ? atob(base64body) : ''; - this._interceptedChannel.startSynthesizedResponse(synthesized, null, null, '', false); - this._interceptedChannel.finishSynthesizedResponse(); - this._interceptedChannel = undefined; - } - - // Instrumentation called by NetworkObserver. - _onInternalRedirect(newChannel) { - // Intercepted requests produce "internal redirects" - this is both for our own - // interception and service workers. - // An internal redirect does not necessarily have the same channelId, - // but inherits notificationCallbacks and the listener, - // and should be used instead of an old channel. - this._networkObserver._channelToRequest.delete(this.httpChannel); - this.httpChannel = newChannel; - this._networkObserver._channelToRequest.set(this.httpChannel, this); - } - - // Instrumentation called by NetworkObserver. - _onInternalRedirectReady() { - // Resumed request is first internally redirected to a new request, - // and then the new request is ready to be updated. - if (!this._expectingResumedRequest) - return; - const { method, headers, postData } = this._expectingResumedRequest; - this._overriddenHeadersForRedirect = headers; - this._expectingResumedRequest = undefined; - - if (headers) - overrideRequestHeaders(this.httpChannel, headers); - else if (this._pageNetwork) - appendExtraHTTPHeaders(this.httpChannel, this._pageNetwork.combinedExtraHTTPHeaders()); - if (method) - this.httpChannel.requestMethod = method; - if (postData !== undefined) - setPostData(this.httpChannel, postData, headers); - } - - // nsIInterfaceRequestor - getInterface(iid) { - if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPromptProvider) || iid.equals(Ci.nsINetworkInterceptController)) - return this; - if (iid.equals(Ci.nsIAuthPrompt)) // Block nsIAuthPrompt - we want nsIAuthPrompt2 to be used instead. - throw Cr.NS_ERROR_NO_INTERFACE; - if (this._previousCallbacks) - return this._previousCallbacks.getInterface(iid); - throw Cr.NS_ERROR_NO_INTERFACE; - } - - // nsIAuthPromptProvider - getAuthPrompt(aPromptReason, iid) { - return this; - } - - // nsIAuthPrompt2 - asyncPromptAuth(aChannel, aCallback, aContext, level, authInfo) { - let canceled = false; - Promise.resolve().then(() => { - if (canceled) - return; - const hasAuth = this.promptAuth(aChannel, level, authInfo); - if (hasAuth) - aCallback.onAuthAvailable(aContext, authInfo); - else - aCallback.onAuthCancelled(aContext, true); - }); - return { - QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]), - cancel: () => { - aCallback.onAuthCancelled(aContext, false); - canceled = true; - } - }; - } - - // nsIAuthPrompt2 - promptAuth(aChannel, level, authInfo) { - if (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) - return false; - const pageNetwork = this._pageNetwork; - if (!pageNetwork) - return false; - let credentials = null; - if (authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) { - const proxy = this._networkObserver._targetRegistry.getProxyInfo(aChannel); - credentials = proxy ? {username: proxy.username, password: proxy.password} : null; - } else { - credentials = pageNetwork._target.browserContext().httpCredentials; - } - if (!credentials) - return false; - const origin = aChannel.URI.scheme + '://' + aChannel.URI.hostPort; - if (credentials.origin && origin.toLowerCase() !== credentials.origin.toLowerCase()) - return false; - authInfo.username = credentials.username; - authInfo.password = credentials.password; - // This will produce a new request with respective auth header set. - // It will have the same id as ours. We expect it to arrive as new request and - // will treat it as our own redirect. - this._networkObserver._expectRedirect(this.httpChannel.channelId + '', this); - return true; - } - - // nsINetworkInterceptController - shouldPrepareForIntercept(aURI, channel) { - const interceptController = this._fallThroughInterceptController(); - if (interceptController && interceptController.shouldPrepareForIntercept(aURI, channel)) { - // We assume that interceptController is a service worker if there is one, - // and yield interception to it. We are not going to intercept ourselves, - // so we send onRequest now. - this._sendOnRequest(false); - return true; - } - - if (channel !== this.httpChannel) { - // Not our channel? Just in case this happens, don't do anything. - return false; - } - - // We do not want to intercept any redirects, because we are not able - // to intercept subresource redirects, and it's unreliable for main requests. - // We do not sendOnRequest here, because redirects do that in constructor. - if (this.redirectedFromId) - return false; - - const shouldIntercept = this._shouldIntercept(); - if (!shouldIntercept) { - // We are not intercepting - ready to issue onRequest. - this._sendOnRequest(false); - return false; - } - - this._expectingInterception = true; - return true; - } - - // nsINetworkInterceptController - channelIntercepted(intercepted) { - if (!this._expectingInterception) { - // We are not intercepting, fall-through. - const interceptController = this._fallThroughInterceptController(); - if (interceptController) - interceptController.channelIntercepted(intercepted); - return; - } - - this._expectingInterception = false; - this._interceptedChannel = intercepted.QueryInterface(Ci.nsIInterceptedChannel); - - const pageNetwork = this._pageNetwork; - if (!pageNetwork) { - // Just in case we disabled instrumentation while intercepting, resume and forget. - this.resume(); - return; - } - - // Ok, so now we have intercepted the request, let's issue onRequest. - // If interception has been disabled while we were intercepting, resume and forget. - const interceptionEnabled = this._shouldIntercept(); - this._sendOnRequest(!!interceptionEnabled); - if (interceptionEnabled) - pageNetwork._interceptedRequests.set(this.requestId, this); - else - this.resume(); - } - - // nsIStreamListener - onDataAvailable(aRequest, aInputStream, aOffset, aCount) { - // Turns out webcompat shims might redirect to - // SimpleChannel, so we get requests from a different channel. - // See https://github.com/microsoft/playwright/issues/9418#issuecomment-944836244 - if (aRequest !== this.httpChannel) - return; - // For requests with internal redirect (e.g. intercepted by Service Worker), - // we do not get onResponse normally, but we do get nsIStreamListener notifications. - this._sendOnResponse(false); - - const iStream = new BinaryInputStream(aInputStream); - const sStream = new StorageStream(8192, aCount, null); - const oStream = new BinaryOutputStream(sStream.getOutputStream(0)); - - // Copy received data as they come. - const data = iStream.readBytes(aCount); - this._responseBodyChunks.push(data); - - oStream.writeBytes(data, aCount); - try { - this._originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount); - } catch (e) { - // Be ready to original listener exceptions. - } - } - - // nsIStreamListener - onStartRequest(aRequest) { - // Turns out webcompat shims might redirect to - // SimpleChannel, so we get requests from a different channel. - // See https://github.com/microsoft/playwright/issues/9418#issuecomment-944836244 - if (aRequest !== this.httpChannel) - return; - try { - this._originalListener.onStartRequest(aRequest); - } catch (e) { - // Be ready to original listener exceptions. - } - } - - // nsIStreamListener - onStopRequest(aRequest, aStatusCode) { - // Turns out webcompat shims might redirect to - // SimpleChannel, so we get requests from a different channel. - // See https://github.com/microsoft/playwright/issues/9418#issuecomment-944836244 - if (aRequest !== this.httpChannel) - return; - try { - this._originalListener.onStopRequest(aRequest, aStatusCode); - } catch (e) { - // Be ready to original listener exceptions. - } - - if (aStatusCode === 0) { - // For requests with internal redirect (e.g. intercepted by Service Worker), - // we do not get onResponse normally, but we do get nsIRequestObserver notifications. - this._sendOnResponse(false); - const body = this._responseBodyChunks.join(''); - const pageNetwork = this._pageNetwork; - if (pageNetwork) - pageNetwork._responseStorage.addResponseBody(this, body); - this._sendOnRequestFinished(); - } else { - this._sendOnRequestFailed(aStatusCode); - } - - delete this._responseBodyChunks; - } - - _shouldIntercept() { - const pageNetwork = this._pageNetwork; - if (!pageNetwork) - return false; - if (pageNetwork._requestInterceptionEnabled) - return true; - const browserContext = pageNetwork._target.browserContext(); - if (browserContext.requestInterceptionEnabled) - return true; - return false; - } - - _fallThroughInterceptController() { - try { - return this._previousCallbacks?.getInterface(Ci.nsINetworkInterceptController); - } catch (e) { - return undefined; - } - } - - _sendOnRequest(isIntercepted) { - // Note: we call _sendOnRequest either after we intercepted the request, - // or at the first moment we know that we are not going to intercept. - const pageNetwork = this._pageNetwork; - if (!pageNetwork) - return; - const loadInfo = this.httpChannel.loadInfo; - const causeType = loadInfo?.externalContentPolicyType || Ci.nsIContentPolicy.TYPE_OTHER; - const internalCauseType = loadInfo?.internalContentPolicyType || Ci.nsIContentPolicy.TYPE_OTHER; - pageNetwork.emit(PageNetwork.Events.Request, { - url: this.httpChannel.URI.spec, - frameId: this._frameId, - isIntercepted, - requestId: this.requestId, - redirectedFrom: this.redirectedFromId, - postData: readRequestPostData(this.httpChannel), - headers: requestHeaders(this.httpChannel), - method: this.httpChannel.requestMethod, - navigationId: this.navigationId, - cause: causeTypeToString(causeType), - internalCause: causeTypeToString(internalCauseType), - }, this._frameId); - } - - _sendOnResponse(fromCache, opt_statusCode, opt_statusText) { - if (this._sentOnResponse) { - // We can come here twice because of internal redirects, e.g. service workers. - return; - } - this._sentOnResponse = true; - const pageNetwork = this._pageNetwork; - if (!pageNetwork) - return; - - this.httpChannel.QueryInterface(Ci.nsIHttpChannelInternal); - this.httpChannel.QueryInterface(Ci.nsITimedChannel); - const timing = { - startTime: this.httpChannel.channelCreationTime, - domainLookupStart: this.httpChannel.domainLookupStartTime, - domainLookupEnd: this.httpChannel.domainLookupEndTime, - connectStart: this.httpChannel.connectStartTime, - secureConnectionStart: this.httpChannel.secureConnectionStartTime, - connectEnd: this.httpChannel.connectEndTime, - requestStart: this.httpChannel.requestStartTime, - responseStart: this.httpChannel.responseStartTime, - }; - - const { status, statusText, headers } = responseHead(this.httpChannel, opt_statusCode, opt_statusText); - let remoteIPAddress = undefined; - let remotePort = undefined; - try { - remoteIPAddress = this.httpChannel.remoteAddress; - remotePort = this.httpChannel.remotePort; - } catch (e) { - // remoteAddress is not defined for cached requests. - } - - const fromServiceWorker = this._networkObserver._channelIdsFulfilledByServiceWorker.has(this.requestId); - this._networkObserver._channelIdsFulfilledByServiceWorker.delete(this.requestId); - - pageNetwork.emit(PageNetwork.Events.Response, { - requestId: this.requestId, - securityDetails: getSecurityDetails(this.httpChannel), - fromCache, - headers, - remoteIPAddress, - remotePort, - status, - statusText, - timing, - fromServiceWorker, - }, this._frameId); - } - - _sendOnRequestFailed(error) { - const pageNetwork = this._pageNetwork; - if (pageNetwork) { - pageNetwork.emit(PageNetwork.Events.RequestFailed, { - requestId: this.requestId, - errorCode: helper.getNetworkErrorStatusText(error), - }, this._frameId); - } - this._networkObserver._channelToRequest.delete(this.httpChannel); - } - - _sendOnRequestFinished() { - const pageNetwork = this._pageNetwork; - // Undefined |responseEndTime| means there has been no response yet. - // This happens when request interception API is used to redirect - // the request to a different URL. - // In this case, we should not emit "requestFinished" event. - if (pageNetwork && this.httpChannel.responseEndTime !== undefined) { - let protocolVersion = undefined; - try { - protocolVersion = this.httpChannel.protocolVersion; - } catch (e) { - // protocolVersion is unavailable in certain cases. - }; - pageNetwork.emit(PageNetwork.Events.RequestFinished, { - requestId: this.requestId, - responseEndTime: this.httpChannel.responseEndTime, - transferSize: this.httpChannel.transferSize, - encodedBodySize: this.httpChannel.encodedBodySize, - protocolVersion, - }, this._frameId); - } - this._networkObserver._channelToRequest.delete(this.httpChannel); - } -} - -class NetworkObserver { - static instance() { - return NetworkObserver._instance || null; - } - - constructor(targetRegistry) { - helper.decorateAsEventEmitter(this); - NetworkObserver._instance = this; - - this._targetRegistry = targetRegistry; - - this._channelToRequest = new Map(); // http channel -> network request - this._expectedRedirect = new Map(); // expected redirect channel id (string) -> network request - this._channelIdsFulfilledByServiceWorker = new Set(); // http channel ids that were fulfilled by service worker - - const protocolProxyService = Cc['@mozilla.org/network/protocol-proxy-service;1'].getService(); - this._channelProxyFilter = { - QueryInterface: ChromeUtils.generateQI([Ci.nsIProtocolProxyChannelFilter]), - applyFilter: (channel, defaultProxyInfo, proxyFilter) => { - const proxy = this._targetRegistry.getProxyInfo(channel); - if (!proxy) { - proxyFilter.onProxyFilterResult(defaultProxyInfo); - return; - } - if (this._targetRegistry.shouldBustHTTPAuthCacheForProxy(proxy)) - Services.obs.notifyObservers(null, "net:clear-active-logins"); - proxyFilter.onProxyFilterResult(protocolProxyService.newProxyInfo( - proxy.type, - proxy.host, - proxy.port, - '', /* aProxyAuthorizationHeader */ - '', /* aConnectionIsolationKey */ - Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST, /* aFlags */ - UINT32_MAX, /* aFailoverTimeout */ - null, /* failover proxy */ - )); - }, - }; - protocolProxyService.registerChannelFilter(this._channelProxyFilter, 0 /* position */); - - // Register self as ChannelEventSink to track redirects. - ChannelEventSinkFactory.getService().registerCollector({ - _onChannelRedirect: this._onRedirect.bind(this), - }); - - this._eventListeners = [ - helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request'), - helper.addObserver(this._onResponse.bind(this, false /* fromCache */), 'http-on-examine-response'), - helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-cached-response'), - helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-merged-response'), - helper.addObserver(this._onServiceWorkerResponse.bind(this), 'service-worker-synthesized-response'), - ]; - } - - _expectRedirect(channelId, previous) { - this._expectedRedirect.set(channelId, previous); - } - - _onRedirect(oldChannel, newChannel, flags) { - if (!(oldChannel instanceof Ci.nsIHttpChannel) || !(newChannel instanceof Ci.nsIHttpChannel)) - return; - const oldHttpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel); - const newHttpChannel = newChannel.QueryInterface(Ci.nsIHttpChannel); - const request = this._channelToRequest.get(oldHttpChannel); - if (flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL) { - if (request) - request._onInternalRedirect(newHttpChannel); - } else if (flags & Ci.nsIChannelEventSink.REDIRECT_STS_UPGRADE) { - if (request) { - // This is an internal HSTS upgrade. The original http request is canceled, and a new - // equivalent https request is sent. We forge 307 redirect to follow Chromium here: - // https://source.chromium.org/chromium/chromium/src/+/main:net/url_request/url_request_http_job.cc;l=211 - request._sendOnResponse(false, 307, 'Temporary Redirect'); - this._expectRedirect(newHttpChannel.channelId + '', request); - } - } else { - if (request) - this._expectRedirect(newHttpChannel.channelId + '', request); - } - } - - _onRequest(channel, topic) { - if (!(channel instanceof Ci.nsIHttpChannel)) - return; - const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); - const channelId = httpChannel.channelId + ''; - const redirectedFrom = this._expectedRedirect.get(channelId); - if (redirectedFrom) { - this._expectedRedirect.delete(channelId); - new NetworkRequest(this, httpChannel, redirectedFrom); - } else { - const redirectedRequest = this._channelToRequest.get(httpChannel); - if (redirectedRequest) - redirectedRequest._onInternalRedirectReady(); - else - new NetworkRequest(this, httpChannel); - } - } - - _onResponse(fromCache, httpChannel, topic) { - const request = this._channelToRequest.get(httpChannel); - if (request) - request._sendOnResponse(fromCache); - } - - _onServiceWorkerResponse(channel, topic) { - if (!(channel instanceof Ci.nsIHttpChannel)) - return; - const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); - const channelId = httpChannel.channelId + ''; - this._channelIdsFulfilledByServiceWorker.add(channelId); - } - - dispose() { - this._activityDistributor.removeObserver(this); - ChannelEventSinkFactory.unregister(); - helper.removeListeners(this._eventListeners); - } -} - -const protocolVersionNames = { - [Ci.nsITransportSecurityInfo.TLS_VERSION_1]: 'TLS 1', - [Ci.nsITransportSecurityInfo.TLS_VERSION_1_1]: 'TLS 1.1', - [Ci.nsITransportSecurityInfo.TLS_VERSION_1_2]: 'TLS 1.2', - [Ci.nsITransportSecurityInfo.TLS_VERSION_1_3]: 'TLS 1.3', -}; - -function getSecurityDetails(httpChannel) { - const securityInfo = httpChannel.securityInfo; - if (!securityInfo) - return null; - securityInfo.QueryInterface(Ci.nsITransportSecurityInfo); - if (!securityInfo.serverCert) - return null; - return { - protocol: protocolVersionNames[securityInfo.protocolVersion] || '', - subjectName: securityInfo.serverCert.commonName, - issuer: securityInfo.serverCert.issuerCommonName, - // Convert to seconds. - validFrom: securityInfo.serverCert.validity.notBefore / 1000 / 1000, - validTo: securityInfo.serverCert.validity.notAfter / 1000 / 1000, - }; -} - -function readRequestPostData(httpChannel) { - if (!(httpChannel instanceof Ci.nsIUploadChannel)) - return undefined; - let iStream = httpChannel.uploadStream; - if (!iStream) - return undefined; - const isSeekableStream = iStream instanceof Ci.nsISeekableStream; - const isTellableStream = iStream instanceof Ci.nsITellableStream; - - // For some reason, we cannot rewind back big streams, - // so instead we should clone them. - const isCloneable = iStream instanceof Ci.nsICloneableInputStream; - if (isCloneable) - iStream = iStream.clone(); - - let prevOffset; - // Surprisingly, stream might implement `nsITellableStream` without - // implementing the `tell` method. - if (isSeekableStream && isTellableStream && iStream.tell) { - prevOffset = iStream.tell(); - iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); - } - - // Read data from the stream. - let result = undefined; - try { - const maxLen = iStream.available(); - // Cap at 10Mb. - if (maxLen <= 10 * 1024 * 1024) { - const buffer = NetUtil.readInputStreamToString(iStream, maxLen); - result = btoa(buffer); - } - } catch (err) { - } - - // Seek locks the file, so seek to the beginning only if necko hasn't - // read it yet, since necko doesn't seek to 0 before reading (at lest - // not till 459384 is fixed). - if (isSeekableStream && prevOffset == 0 && !isCloneable) - iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); - return result; -} - -function requestHeaders(httpChannel) { - const headers = []; - httpChannel.visitRequestHeaders({ - visitHeader: (name, value) => headers.push({name, value}), - }); - return headers; -} - -function clearRequestHeaders(httpChannel) { - for (const header of requestHeaders(httpChannel)) { - // We cannot remove the "host" header. - if (header.name.toLowerCase() === 'host') - continue; - httpChannel.setRequestHeader(header.name, '', false /* merge */); - } -} - -function overrideRequestHeaders(httpChannel, headers) { - clearRequestHeaders(httpChannel); - appendExtraHTTPHeaders(httpChannel, headers); -} - -function causeTypeToString(causeType) { - for (let key in Ci.nsIContentPolicy) { - if (Ci.nsIContentPolicy[key] === causeType) - return key; - } - return 'TYPE_OTHER'; -} - -function appendExtraHTTPHeaders(httpChannel, headers) { - if (!headers) - return; - for (const header of headers) - httpChannel.setRequestHeader(header.name, header.value, false /* merge */); -} - -class ResponseStorage { - constructor(maxTotalSize, maxResponseSize) { - this._totalSize = 0; - this._maxResponseSize = maxResponseSize; - this._maxTotalSize = maxTotalSize; - this._responses = new Map(); - } - - addResponseBody(request, body) { - if (body.length > this._maxResponseSize) { - this._responses.set(request.requestId, { - evicted: true, - body: '', - }); - return; - } - let encodings = []; - // Note: fulfilled request comes with decoded body right away. - if ((request.httpChannel instanceof Ci.nsIEncodedChannel) && request.httpChannel.contentEncodings && !request.httpChannel.applyConversion && !request._fulfilled) { - const encodingHeader = request.httpChannel.getResponseHeader("Content-Encoding"); - encodings = encodingHeader.split(/\s*\t*,\s*\t*/); - } - this._responses.set(request.requestId, {body, encodings}); - this._totalSize += body.length; - if (this._totalSize > this._maxTotalSize) { - for (let [requestId, response] of this._responses) { - this._totalSize -= response.body.length; - response.body = ''; - response.evicted = true; - if (this._totalSize < this._maxTotalSize) - break; - } - } - } - - getBase64EncodedResponse(requestId) { - const response = this._responses.get(requestId); - if (!response) - throw new Error(`Request "${requestId}" is not found`); - if (response.evicted) - return {base64body: '', evicted: true}; - let result = response.body; - if (response.encodings && response.encodings.length) { - for (const encoding of response.encodings) - result = convertString(result, encoding, 'uncompressed'); - } - return {base64body: btoa(result)}; - } -} - -function responseHead(httpChannel, opt_statusCode, opt_statusText) { - const headers = []; - let status = opt_statusCode || 0; - let statusText = opt_statusText || ''; - try { - status = httpChannel.responseStatus; - statusText = httpChannel.responseStatusText; - httpChannel.visitResponseHeaders({ - visitHeader: (name, value) => headers.push({name, value}), - }); - } catch (e) { - // Response headers, status and/or statusText are not available - // when redirect did not actually hit the network. - } - return { status, statusText, headers }; -} - -function setPostData(httpChannel, postData, headers) { - if (!(httpChannel instanceof Ci.nsIUploadChannel2)) - return; - const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); - const body = atob(postData); - synthesized.setByteStringData(body, body.length); - - const overriddenHeader = (lowerCaseName) => { - if (headers) { - for (const header of headers) { - if (header.name.toLowerCase() === lowerCaseName) { - return header.value; - } - } - } - return undefined; - } - // Clear content-length, so that upload stream resets it. - httpChannel.setRequestHeader('content-length', '', false /* merge */); - let contentType = overriddenHeader('content-type'); - if (contentType === undefined) { - try { - contentType = httpChannel.getRequestHeader('content-type'); - } catch (e) { - if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) - contentType = 'application/octet-stream'; - else - throw e; - } - } - httpChannel.explicitSetUploadStream(synthesized, contentType, -1, httpChannel.requestMethod, false); -} - -function convertString(s, source, dest) { - const is = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( - Ci.nsIStringInputStream - ); - is.setByteStringData(s, s.length); - const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance( - Ci.nsIStreamLoader - ); - let result = []; - listener.init({ - onStreamComplete: function onStreamComplete( - loader, - context, - status, - length, - data - ) { - const array = Array.from(data); - const kChunk = 100000; - for (let i = 0; i < length; i += kChunk) { - const len = Math.min(kChunk, length - i); - const chunk = String.fromCharCode.apply(this, array.slice(i, i + len)); - result.push(chunk); - } - }, - }); - const converter = Cc["@mozilla.org/streamConverters;1"].getService( - Ci.nsIStreamConverterService - ).asyncConvertData( - source, - dest, - listener, - null - ); - converter.onStartRequest(null, null); - converter.onDataAvailable(null, is, 0, s.length); - converter.onStopRequest(null, null, null); - return result.join(''); -} - -const errorMap = { - 'aborted': Cr.NS_ERROR_ABORT, - 'accessdenied': Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED, - 'addressunreachable': Cr.NS_ERROR_UNKNOWN_HOST, - 'blockedbyclient': Cr.NS_ERROR_FAILURE, - 'blockedbyresponse': Cr.NS_ERROR_FAILURE, - 'connectionaborted': Cr.NS_ERROR_NET_INTERRUPT, - 'connectionclosed': Cr.NS_ERROR_FAILURE, - 'connectionfailed': Cr.NS_ERROR_FAILURE, - 'connectionrefused': Cr.NS_ERROR_CONNECTION_REFUSED, - 'connectionreset': Cr.NS_ERROR_NET_RESET, - 'internetdisconnected': Cr.NS_ERROR_OFFLINE, - 'namenotresolved': Cr.NS_ERROR_UNKNOWN_HOST, - 'timedout': Cr.NS_ERROR_NET_TIMEOUT, - 'failed': Cr.NS_ERROR_FAILURE, -}; - -PageNetwork.Events = { - Request: Symbol('PageNetwork.Events.Request'), - Response: Symbol('PageNetwork.Events.Response'), - RequestFinished: Symbol('PageNetwork.Events.RequestFinished'), - RequestFailed: Symbol('PageNetwork.Events.RequestFailed'), -}; - -var EXPORTED_SYMBOLS = ['NetworkObserver', 'PageNetwork']; -this.NetworkObserver = NetworkObserver; -this.PageNetwork = PageNetwork; \ No newline at end of file diff --git a/additions/juggler/SimpleChannel.js b/additions/juggler/SimpleChannel.js deleted file mode 100644 index a4e1b19..0000000 --- a/additions/juggler/SimpleChannel.js +++ /dev/null @@ -1,256 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; -// Note: this file should be loadabale with eval() into worker environment. -// Avoid Components.*, ChromeUtils and global const variables. - -const SIMPLE_CHANNEL_MESSAGE_NAME = 'juggler:simplechannel'; - -class SimpleChannel { - constructor(name, uid) { - this._name = name; - this._messageId = 0; - this._connectorId = 0; - this._pendingMessages = new Map(); - this._handlers = new Map(); - this._bufferedIncomingMessages = []; - this.transport = { - sendMessage: null, - dispose: () => {}, - }; - this._ready = false; - this._paused = false; - this._disposed = false; - - this._bufferedResponses = new Map(); - // This is a "unique" identifier of this end of the channel. Two SimpleChannel instances - // on the same end of the channel (e.g. two content processes) must not have the same id. - // This way, the other end can distinguish between the old peer with a new transport and a new peer. - this._uid = uid; - this._connectedToUID = undefined; - } - - bindToActor(actor) { - this.resetTransport(); - this._name = actor.actorName; - const oldReceiveMessage = actor.receiveMessage; - actor.receiveMessage = message => this._onMessage(message.data); - this.setTransport({ - sendMessage: obj => actor.sendAsyncMessage(SIMPLE_CHANNEL_MESSAGE_NAME, obj), - dispose: () => actor.receiveMessage = oldReceiveMessage, - }); - } - - resetTransport() { - this.transport.dispose(); - this.transport = { - sendMessage: null, - dispose: () => {}, - }; - this._ready = false; - } - - setTransport(transport) { - this.transport = transport; - // connection handshake: - // 1. There are two channel ends in different processes. - // 2. Both ends start in the `ready = false` state, meaning that they will - // not send any messages over transport. - // 3. Once channel end is created, it sends { ack: `READY` } message to the other end. - // 4. Eventually, at least one of the ends receives { ack: `READY` } message and responds with - // { ack: `READY_ACK` }. We assume at least one of the ends will receive { ack: "READY" } event from the other, since - // channel ends have a "parent-child" relation, i.e. one end is always created before the other one. - // 5. Once channel end receives either { ack: `READY` } or { ack: `READY_ACK` }, it transitions to `ready` state. - this.transport.sendMessage({ ack: 'READY', uid: this._uid }); - } - - pause() { - this._paused = true; - } - - resumeSoon() { - if (!this._paused) - return; - this._paused = false; - this._setTimeout(() => this._deliverBufferedIncomingMessages(), 0); - } - - _setTimeout(cb, timeout) { - // Lazy load on first call. - this._setTimeout = ChromeUtils.import('resource://gre/modules/Timer.jsm').setTimeout; - this._setTimeout(cb, timeout); - } - - _markAsReady() { - if (this._ready) - return; - this._ready = true; - for (const { message } of this._pendingMessages.values()) - this.transport.sendMessage(message); - } - - dispose() { - if (this._disposed) - return; - this._disposed = true; - for (const {resolve, reject, methodName} of this._pendingMessages.values()) - reject(new Error(`Failed "${methodName}": ${this._name} is disposed.`)); - this._pendingMessages.clear(); - this._handlers.clear(); - this.transport.dispose(); - } - - _rejectCallbacksFromConnector(connectorId) { - for (const [messageId, callback] of this._pendingMessages) { - if (callback.connectorId === connectorId) { - callback.reject(new Error(`Failed "${callback.methodName}": connector for namespace "${callback.namespace}" in channel "${this._name}" is disposed.`)); - this._pendingMessages.delete(messageId); - } - } - } - - connect(namespace) { - const connectorId = ++this._connectorId; - return { - send: (...args) => this._send(namespace, connectorId, ...args), - emit: (...args) => void this._send(namespace, connectorId, ...args).catch(e => {}), - dispose: () => this._rejectCallbacksFromConnector(connectorId), - }; - } - - register(namespace, handler) { - if (this._handlers.has(namespace)) - throw new Error('ERROR: double-register for namespace ' + namespace); - this._handlers.set(namespace, handler); - this._deliverBufferedIncomingMessages(); - return () => this.unregister(namespace); - } - - _deliverBufferedIncomingMessages() { - const bufferedRequests = this._bufferedIncomingMessages; - this._bufferedIncomingMessages = []; - for (const data of bufferedRequests) { - this._onMessage(data); - } - } - - unregister(namespace) { - this._handlers.delete(namespace); - } - - /** - * @param {string} namespace - * @param {number} connectorId - * @param {string} methodName - * @param {...*} params - * @return {!Promise<*>} - */ - async _send(namespace, connectorId, methodName, ...params) { - if (this._disposed) - throw new Error(`ERROR: channel ${this._name} is already disposed! Cannot send "${methodName}" to "${namespace}"`); - const id = ++this._messageId; - const message = {requestId: id, methodName, params, namespace}; - const promise = new Promise((resolve, reject) => { - this._pendingMessages.set(id, {connectorId, resolve, reject, methodName, namespace, message}); - }); - if (this._ready) - this.transport.sendMessage(message); - return promise; - } - - _onMessage(data) { - if (data?.ack === 'READY') { - // The "READY" and "READY_ACK" messages are a part of initialization sequence. - // This sequence happens when: - // 1. A new SimpleChannel instance is getting initialized on the other end. - // In this case, it will have a different UID and we must clear - // `this._bufferedResponses` since they are no longer relevant. - // 2. A new transport is assigned to communicate between 2 SimpleChannel instances. - // In this case, we MUST NOT clear `this._bufferedResponses` since they are used - // to address the double-dispatch issue. - if (this._connectedToUID !== data.uid) - this._bufferedResponses.clear(); - this._connectedToUID = data.uid; - this.transport.sendMessage({ ack: 'READY_ACK', uid: this._uid }); - this._markAsReady(); - return; - } - if (data?.ack === 'READY_ACK') { - if (this._connectedToUID !== data.uid) - this._bufferedResponses.clear(); - this._connectedToUID = data.uid; - this._markAsReady(); - return; - } - if (data?.ack === 'RESPONSE_ACK') { - this._bufferedResponses.delete(data.responseId); - return; - } - if (this._paused) - this._bufferedIncomingMessages.push(data); - else - this._onMessageInternal(data); - } - - async _onMessageInternal(data) { - if (data.responseId) { - this.transport.sendMessage({ ack: 'RESPONSE_ACK', responseId: data.responseId }); - const message = this._pendingMessages.get(data.responseId); - if (!message) { - // During cross-process navigation, we might receive a response for - // the message sent by another process. - return; - } - this._pendingMessages.delete(data.responseId); - if (data.error) - message.reject(new Error(data.error)); - else - message.resolve(data.result); - } else if (data.requestId) { - // When the underlying transport gets replaced, some responses might - // not get delivered. As a result, sender will repeat the same request once - // a new transport gets set. - // - // If this request was already processed, we can fulfill it with the cached response - // and fast-return. - if (this._bufferedResponses.has(data.requestId)) { - this.transport.sendMessage(this._bufferedResponses.get(data.requestId)); - return; - } - - const namespace = data.namespace; - const handler = this._handlers.get(namespace); - if (!handler) { - this._bufferedIncomingMessages.push(data); - return; - } - const method = handler[data.methodName]; - if (!method) { - this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No method "${data.methodName}" in namespace "${namespace}"`}); - return; - } - let response; - const connectedToUID = this._connectedToUID; - try { - const result = await method.call(handler, ...data.params); - response = {responseId: data.requestId, result}; - } catch (error) { - response = {responseId: data.requestId, error: `error in channel "${this._name}": exception while running method "${data.methodName}" in namespace "${namespace}": ${error.message} ${error.stack}`}; - } - // The connection might have changed during the ASYNCHRONOUS handler execution. - // We only need to buffer & send response if we are connected to the same - // end. - if (connectedToUID === this._connectedToUID) { - this._bufferedResponses.set(data.requestId, response); - this.transport.sendMessage(response); - } - } else { - dump(`WARNING: unknown message in channel "${this._name}": ${JSON.stringify(data)}\n`); - } - } -} - -var EXPORTED_SYMBOLS = ['SimpleChannel']; -this.SimpleChannel = SimpleChannel; diff --git a/additions/juggler/TargetRegistry.js b/additions/juggler/TargetRegistry.js deleted file mode 100644 index 4a73580..0000000 --- a/additions/juggler/TargetRegistry.js +++ /dev/null @@ -1,1273 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public -* License, v. 2.0. If a copy of the MPL was not distributed with this -* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); -const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js'); -const {Preferences} = ChromeUtils.import("resource://gre/modules/Preferences.jsm"); -const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm"); -const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm'); -const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); - -const Cr = Components.results; - -const helper = new Helper(); - -const IDENTITY_NAME = 'Camoufox '; -const HUNDRED_YEARS = 60 * 60 * 24 * 365 * 100; - -const ALL_PERMISSIONS = [ - 'geo', - 'desktop-notification', -]; - -let globalTabAndWindowActivationChain = Promise.resolve(); - -class DownloadInterceptor { - constructor(registry) { - this._registry = registry - this._handlerToUuid = new Map(); - this._uuidToHandler = new Map(); - } - - // - // nsIDownloadInterceptor implementation. - // - interceptDownloadRequest(externalAppHandler, request, browsingContext, outFile) { - if (!(request instanceof Ci.nsIChannel)) - return false; - const channel = request.QueryInterface(Ci.nsIChannel); - let pageTarget = this._registry._browserIdToTarget.get(channel.loadInfo.browsingContext.top.browserId); - if (!pageTarget) - return false; - - const browserContext = pageTarget.browserContext(); - const options = browserContext.downloadOptions; - if (!options) - return false; - - const uuid = helper.generateId(); - let file = null; - if (options.behavior === 'saveToDisk') { - file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - file.initWithPath(options.downloadsDir); - file.append(uuid); - - try { - file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); - } catch (e) { - dump(`WARNING: interceptDownloadRequest failed to create file: ${e}\n`); - return false; - } - } - outFile.value = file; - this._handlerToUuid.set(externalAppHandler, uuid); - this._uuidToHandler.set(uuid, externalAppHandler); - const downloadInfo = { - uuid, - browserContextId: browserContext.browserContextId, - pageTargetId: pageTarget.id(), - frameId: helper.browsingContextToFrameId(channel.loadInfo.browsingContext), - url: request.name, - suggestedFileName: externalAppHandler.suggestedFileName, - }; - this._registry.emit(TargetRegistry.Events.DownloadCreated, downloadInfo); - return true; - } - - onDownloadComplete(externalAppHandler, canceled, errorName) { - const uuid = this._handlerToUuid.get(externalAppHandler); - if (!uuid) - return; - this._handlerToUuid.delete(externalAppHandler); - this._uuidToHandler.delete(uuid); - const downloadInfo = { - uuid, - error: errorName, - }; - if (canceled === 'NS_BINDING_ABORTED') { - downloadInfo.canceled = true; - } - this._registry.emit(TargetRegistry.Events.DownloadFinished, downloadInfo); - } - - async cancelDownload(uuid) { - const externalAppHandler = this._uuidToHandler.get(uuid); - if (!externalAppHandler) { - return; - } - await externalAppHandler.cancel(Cr.NS_BINDING_ABORTED); - } -} - -const screencastService = Cc['@mozilla.org/juggler/screencast;1'].getService(Ci.nsIScreencastService); - -class TargetRegistry { - static instance() { - return TargetRegistry._instance || null; - } - - constructor() { - helper.decorateAsEventEmitter(this); - TargetRegistry._instance = this; - - this._browserContextIdToBrowserContext = new Map(); - this._userContextIdToBrowserContext = new Map(); - this._browserToTarget = new Map(); - this._browserIdToTarget = new Map(); - - this._proxiesWithClashingAuthCacheKeys = new Set(); - this._browserProxy = null; - - // Cleanup containers from previous runs (if any) - for (const identity of ContextualIdentityService.getPublicIdentities()) { - if (identity.name && identity.name.startsWith(IDENTITY_NAME)) { - ContextualIdentityService.remove(identity.userContextId); - ContextualIdentityService.closeContainerTabs(identity.userContextId); - } - } - - this._defaultContext = new BrowserContext(this, undefined, undefined); - - Services.obs.addObserver({ - observe: (subject, topic, data) => { - const browser = subject.ownerElement; - if (!browser) - return; - const target = this._browserToTarget.get(browser); - if (!target) - return; - target.emit(PageTarget.Events.Crashed); - target.dispose(); - } - }, 'oop-frameloader-crashed'); - - const onTabOpenListener = (appWindow, window, event) => { - const tab = event.target; - const userContextId = tab.userContextId; - const browserContext = this._userContextIdToBrowserContext.get(userContextId); - const hasExplicitSize = appWindow && (appWindow.chromeFlags & Ci.nsIWebBrowserChrome.JUGGLER_WINDOW_EXPLICIT_SIZE) !== 0; - const openerContext = tab.linkedBrowser.browsingContext.opener; - let openerTarget; - if (openerContext) { - // Popups usually have opener context. Get top context for the case when opener is - // an iframe. - openerTarget = this._browserIdToTarget.get(openerContext.top.browserId); - } else if (tab.openerTab) { - // Noopener popups from the same window have opener tab instead. - openerTarget = this._browserToTarget.get(tab.openerTab.linkedBrowser); - } - if (!browserContext) - throw new Error(`Internal error: cannot find context for userContextId=${userContextId}`); - const target = new PageTarget(this, window, tab, browserContext, openerTarget); - target.updateOverridesForBrowsingContext(tab.linkedBrowser.browsingContext); - if (!hasExplicitSize) - target.updateViewportSize(); - if (browserContext.videoRecordingOptions) - target._startVideoRecording(browserContext.videoRecordingOptions); - }; - - const onTabCloseListener = event => { - const tab = event.target; - const linkedBrowser = tab.linkedBrowser; - const target = this._browserToTarget.get(linkedBrowser); - if (target) - target.dispose(); - }; - - const domWindowTabListeners = new Map(); - - const onOpenWindow = async (appWindow) => { - - let domWindow; - if (appWindow instanceof Ci.nsIAppWindow) { - domWindow = appWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow); - } else { - domWindow = appWindow; - appWindow = null; - } - if (!domWindow.isChromeWindow) - return; - // In persistent mode, window might be opened long ago and might be - // already initialized. - // - // In this case, we want to keep this callback synchronous so that we will call - // `onTabOpenListener` synchronously and before the sync IPc message `juggler:content-ready`. - if (domWindow.document.readyState === 'uninitialized' || domWindow.document.readyState === 'loading') { - // For non-initialized windows, DOMContentLoaded initializes gBrowser - // and starts tab loading (see //browser/base/content/browser.js), so we - // are guaranteed to call `onTabOpenListener` before the sync IPC message - // `juggler:content-ready`. - await helper.awaitEvent(domWindow, 'DOMContentLoaded'); - } - - if (!domWindow.gBrowser) - return; - const tabContainer = domWindow.gBrowser.tabContainer; - domWindowTabListeners.set(domWindow, [ - helper.addEventListener(tabContainer, 'TabOpen', event => onTabOpenListener(appWindow, domWindow, event)), - helper.addEventListener(tabContainer, 'TabClose', onTabCloseListener), - ]); - for (const tab of domWindow.gBrowser.tabs) - onTabOpenListener(appWindow, domWindow, { target: tab }); - }; - - const onCloseWindow = window => { - const domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow); - if (!domWindow.isChromeWindow) - return; - if (!domWindow.gBrowser) - return; - - const listeners = domWindowTabListeners.get(domWindow) || []; - domWindowTabListeners.delete(domWindow); - helper.removeListeners(listeners); - for (const tab of domWindow.gBrowser.tabs) - onTabCloseListener({ target: tab }); - }; - - const extHelperAppSvc = Cc["@mozilla.org/uriloader/external-helper-app-service;1"].getService(Ci.nsIExternalHelperAppService); - this._downloadInterceptor = new DownloadInterceptor(this); - extHelperAppSvc.setDownloadInterceptor(this._downloadInterceptor); - - Services.wm.addListener({ onOpenWindow, onCloseWindow }); - for (const win of Services.wm.getEnumerator(null)) - onOpenWindow(win); - } - - // Firefox uses nsHttpAuthCache to cache authentication to the proxy. - // If we're provided with a single proxy with a multiple different authentications, then - // we should clear the nsHttpAuthCache on every request. - shouldBustHTTPAuthCacheForProxy(proxy) { - return this._proxiesWithClashingAuthCacheKeys.has(proxy); - } - - _updateProxiesWithSameAuthCacheAndDifferentCredentials() { - const proxyIdToCredentials = new Map(); - const allProxies = [...this._browserContextIdToBrowserContext.values()].map(bc => bc._proxy).filter(Boolean); - if (this._browserProxy) - allProxies.push(this._browserProxy); - const proxyAuthCacheKeyAndProxy = allProxies.map(proxy => [ - JSON.stringify({ - type: proxy.type, - host: proxy.host, - port: proxy.port, - }), - proxy, - ]); - this._proxiesWithClashingAuthCacheKeys.clear(); - - proxyAuthCacheKeyAndProxy.sort(([cacheKey1], [cacheKey2]) => cacheKey1 < cacheKey2 ? -1 : 1); - for (let i = 0; i < proxyAuthCacheKeyAndProxy.length - 1; ++i) { - const [cacheKey1, proxy1] = proxyAuthCacheKeyAndProxy[i]; - const [cacheKey2, proxy2] = proxyAuthCacheKeyAndProxy[i + 1]; - if (cacheKey1 !== cacheKey2) - continue; - if (proxy1.username === proxy2.username && proxy1.password === proxy2.password) - continue; - // `proxy1` and `proxy2` have the same caching key, but serve different credentials. - // We have to bust HTTP Auth Cache everytime there's a request that will use either of the proxies. - this._proxiesWithClashingAuthCacheKeys.add(proxy1); - this._proxiesWithClashingAuthCacheKeys.add(proxy2); - } - } - - async cancelDownload(options) { - this._downloadInterceptor.cancelDownload(options.uuid); - } - - setBrowserProxy(proxy) { - this._browserProxy = proxy; - this._updateProxiesWithSameAuthCacheAndDifferentCredentials(); - } - - getProxyInfo(channel) { - const originAttributes = channel.loadInfo && channel.loadInfo.originAttributes; - const browserContext = originAttributes ? this.browserContextForUserContextId(originAttributes.userContextId) : null; - // Prefer context proxy and fallback to browser-level proxy. - const proxyInfo = (browserContext && browserContext._proxy) || this._browserProxy; - if (!proxyInfo || proxyInfo.bypass.some(domainSuffix => channel.URI.host.endsWith(domainSuffix))) - return null; - return proxyInfo; - } - - defaultContext() { - return this._defaultContext; - } - - createBrowserContext(removeOnDetach) { - return new BrowserContext(this, helper.generateId(), removeOnDetach); - } - - browserContextForId(browserContextId) { - return this._browserContextIdToBrowserContext.get(browserContextId); - } - - browserContextForUserContextId(userContextId) { - return this._userContextIdToBrowserContext.get(userContextId); - } - - async newPage({browserContextId}) { - const browserContext = this.browserContextForId(browserContextId); - const features = "chrome,dialog=no,all"; - // See _callWithURIToLoad in browser.js for the structure of window.arguments - // window.arguments[1]: unused (bug 871161) - // [2]: referrerInfo (nsIReferrerInfo) - // [3]: postData (nsIInputStream) - // [4]: allowThirdPartyFixup (bool) - // [5]: userContextId (int) - // [6]: originPrincipal (nsIPrincipal) - // [7]: originStoragePrincipal (nsIPrincipal) - // [8]: triggeringPrincipal (nsIPrincipal) - // [9]: allowInheritPrincipal (bool) - // [10]: csp (nsIContentSecurityPolicy) - // [11]: nsOpenWindowInfo - const args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); - const urlSupports = Cc["@mozilla.org/supports-string;1"].createInstance( - Ci.nsISupportsString - ); - urlSupports.data = 'about:blank'; - args.appendElement(urlSupports); // 0 - args.appendElement(undefined); // 1 - args.appendElement(undefined); // 2 - args.appendElement(undefined); // 3 - args.appendElement(undefined); // 4 - const userContextIdSupports = Cc[ - "@mozilla.org/supports-PRUint32;1" - ].createInstance(Ci.nsISupportsPRUint32); - userContextIdSupports.data = browserContext.userContextId; - args.appendElement(userContextIdSupports); // 5 - args.appendElement(undefined); // 6 - args.appendElement(undefined); // 7 - args.appendElement(Services.scriptSecurityManager.getSystemPrincipal()); // 8 - - const window = Services.ww.openWindow(null, AppConstants.BROWSER_CHROME_URL, '_blank', features, args); - await waitForWindowReady(window); - if (window.gBrowser.browsers.length !== 1) - throw new Error(`Unexpected number of tabs in the new window: ${window.gBrowser.browsers.length}`); - const browser = window.gBrowser.browsers[0]; - let target = this._browserToTarget.get(browser); - while (!target) { - await helper.awaitEvent(this, TargetRegistry.Events.TargetCreated); - target = this._browserToTarget.get(browser); - } - browser.focus(); - if (browserContext.crossProcessCookie.settings.timezoneId) { - if (await target.hasFailedToOverrideTimezone()) - throw new Error('Failed to override timezone'); - } - return target.id(); - } - - targets() { - return Array.from(this._browserToTarget.values()); - } - - targetForBrowser(browser) { - return this._browserToTarget.get(browser); - } - - targetForBrowserId(browserId) { - return this._browserIdToTarget.get(browserId); - } -} - -class PageTarget { - constructor(registry, win, tab, browserContext, opener) { - helper.decorateAsEventEmitter(this); - - this._targetId = helper.generateId(); - this._registry = registry; - this._window = win; - this._gBrowser = win.gBrowser; - this._tab = tab; - this._linkedBrowser = tab.linkedBrowser; - this._browserContext = browserContext; - this._viewportSize = undefined; - // Set the viewport size to Camoufox's default value. - if ( - ChromeUtils.camouGetInt("window.innerWidth") - || ChromeUtils.camouGetInt("window.innerHeight") - ) { - this._viewportSize = { - width: ChromeUtils.camouGetInt("window.innerWidth") || 1280, - height: ChromeUtils.camouGetInt("window.innerHeight") || 720, - }; - } - this._initialDPPX = this._linkedBrowser.browsingContext.overrideDPPX; - this._url = 'about:blank'; - this._openerId = opener ? opener.id() : undefined; - this._actor = undefined; - this._actorSequenceNumber = 0; - this._channel = new SimpleChannel(`browser::page[${this._targetId}]`, 'target-' + this._targetId); - this._videoRecordingInfo = undefined; - this._screencastRecordingInfo = undefined; - this._dialogs = new Map(); - this.forcedColors = 'none'; - this.disableCache = false; - this.mediumOverride = ''; - this.crossProcessCookie = { - initScripts: [], - bindings: [], - interceptFileChooserDialog: false, - }; - - const navigationListener = { - QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), - onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation), - }; - this._eventListeners = [ - helper.addObserver(this._updateModalDialogs.bind(this), 'common-dialog-loaded'), - helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION), - helper.addEventListener(this._linkedBrowser, 'DOMModalDialogClosed', event => this._updateModalDialogs()), - helper.addEventListener(this._linkedBrowser, 'WillChangeBrowserRemoteness', event => this._willChangeBrowserRemoteness()), - ]; - - this._disposed = false; - browserContext.pages.add(this); - this._registry._browserToTarget.set(this._linkedBrowser, this); - this._registry._browserIdToTarget.set(this._linkedBrowser.browsingContext.browserId, this); - - this._registry.emit(TargetRegistry.Events.TargetCreated, this); - } - - async activateAndRun(callback = () => {}, { muteNotificationsPopup = false } = {}) { - const ownerWindow = this._tab.linkedBrowser.ownerGlobal; - const tabBrowser = ownerWindow.gBrowser; - // Serialize all tab-switching commands per tabbed browser - // to disallow concurrent tab switching. - const result = globalTabAndWindowActivationChain.then(async () => { - this._window.focus(); - if (tabBrowser.selectedTab !== this._tab) { - const promise = helper.awaitEvent(ownerWindow, 'TabSwitchDone'); - tabBrowser.selectedTab = this._tab; - await promise; - } - const notificationsPopup = muteNotificationsPopup ? this._linkedBrowser?.ownerDocument.getElementById('notification-popup') : null; - notificationsPopup?.style.setProperty('pointer-events', 'none'); - try { - await callback(); - } finally { - notificationsPopup?.style.removeProperty('pointer-events'); - } - }); - globalTabAndWindowActivationChain = result.catch(error => { /* swallow errors to keep chain running */ }); - return result; - } - - frameIdToBrowsingContext(frameId) { - return helper.collectAllBrowsingContexts(this._linkedBrowser.browsingContext).find(bc => helper.browsingContextToFrameId(bc) === frameId); - } - - nextActorSequenceNumber() { - return ++this._actorSequenceNumber; - } - - setActor(actor) { - this._actor = actor; - this._channel.bindToActor(actor); - } - - removeActor(actor) { - // Note: the order between setActor and removeActor is non-deterministic. - // Therefore we check that we are still bound to the actor that is being removed. - if (this._actor !== actor) - return; - this._actor = undefined; - this._channel.resetTransport(); - } - - _willChangeBrowserRemoteness() { - this.removeActor(this._actor); - } - - dialog(dialogId) { - return this._dialogs.get(dialogId); - } - - dialogs() { - return [...this._dialogs.values()]; - } - - async windowReady() { - await waitForWindowReady(this._window); - } - - linkedBrowser() { - return this._linkedBrowser; - } - - browserContext() { - return this._browserContext; - } - - updateOverridesForBrowsingContext(browsingContext = undefined) { - this.updateTouchOverride(browsingContext); - this.updateUserAgent(browsingContext); - this.updatePlatform(browsingContext); - this.updateDPPXOverride(browsingContext); - this.updateEmulatedMedia(browsingContext); - this.updateColorSchemeOverride(browsingContext); - this.updateReducedMotionOverride(browsingContext); - this.updateForcedColorsOverride(browsingContext); - this.updateForceOffline(browsingContext); - this.updateCacheDisabled(browsingContext); - } - - updateForceOffline(browsingContext = undefined) { - (browsingContext || this._linkedBrowser.browsingContext).forceOffline = this._browserContext.forceOffline; - } - - setCacheDisabled(disabled) { - this.disableCache = disabled; - this.updateCacheDisabled(); - } - - updateCacheDisabled(browsingContext = this._linkedBrowser.browsingContext) { - const enableFlags = Ci.nsIRequest.LOAD_NORMAL; - const disableFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE | - Ci.nsIRequest.INHIBIT_CACHING; - - browsingContext.defaultLoadFlags = (this._browserContext.disableCache || this.disableCache) ? disableFlags : enableFlags; - } - - updateTouchOverride(browsingContext = undefined) { - (browsingContext || this._linkedBrowser.browsingContext).touchEventsOverride = this._browserContext.touchOverride ? 'enabled' : 'none'; - } - - updateUserAgent(browsingContext = undefined) { - (browsingContext || this._linkedBrowser.browsingContext).customUserAgent = this._browserContext.defaultUserAgent; - } - - updatePlatform(browsingContext = undefined) { - (browsingContext || this._linkedBrowser.browsingContext).customPlatform = this._browserContext.defaultPlatform; - } - - updateDPPXOverride(browsingContext = undefined) { - (browsingContext || this._linkedBrowser.browsingContext).overrideDPPX = this._browserContext.deviceScaleFactor || this._initialDPPX; - } - - _updateModalDialogs() { - const prompts = new Set(this._linkedBrowser.tabDialogBox.getContentDialogManager().dialogs.map(dialog => dialog.frameContentWindow.Dialog)); - for (const dialog of this._dialogs.values()) { - if (!prompts.has(dialog.prompt())) { - this._dialogs.delete(dialog.id()); - this.emit(PageTarget.Events.DialogClosed, dialog); - } else { - prompts.delete(dialog.prompt()); - } - } - for (const prompt of prompts) { - const dialog = Dialog.createIfSupported(prompt); - if (!dialog) - continue; - this._dialogs.set(dialog.id(), dialog); - this.emit(PageTarget.Events.DialogOpened, dialog); - } - } - - async updateViewportSize() { - await waitForWindowReady(this._window); - this.updateDPPXOverride(); - - // Viewport size is defined by three arguments: - // 1. default size. Could be explicit if set as part of `window.open` call, e.g. - // `window.open(url, title, 'width=400,height=400')` - // 2. page viewport size - // 3. browserContext viewport size - // - // The "default size" (1) is only respected when the page is opened. - // Otherwise, explicitly set page viewport prevales over browser context - // default viewport. - - // Camoufox is already handling viewport size, so we don't need to set it here. - if ( - ChromeUtils.camouGetInt("window.outerWidth") || - ChromeUtils.camouGetInt("window.outerHeight") || - ChromeUtils.camouGetInt("window.innerWidth") || - ChromeUtils.camouGetInt("window.innerHeight") - ) { - return; - } - - const viewportSize = this._viewportSize || this._browserContext.defaultViewportSize; - - if (viewportSize) { - const {width, height} = viewportSize; - this._linkedBrowser.style.setProperty('width', width + 'px'); - this._linkedBrowser.style.setProperty('height', height + 'px'); - this._linkedBrowser.style.setProperty('box-sizing', 'content-box'); - this._linkedBrowser.closest('.browserStack').style.setProperty('overflow', 'auto'); - this._linkedBrowser.closest('.browserStack').style.setProperty('contain', 'size'); - this._linkedBrowser.closest('.browserStack').style.setProperty('scrollbar-width', 'none'); - this._linkedBrowser.browsingContext.inRDMPane = true; - - const stackRect = this._linkedBrowser.closest('.browserStack').getBoundingClientRect(); - const toolbarTop = stackRect.y; - this._window.resizeBy(width - this._window.innerWidth, height + toolbarTop - this._window.innerHeight); - - await this._channel.connect('').send('awaitViewportDimensions', { width, height }); - } else { - this._linkedBrowser.style.removeProperty('width'); - this._linkedBrowser.style.removeProperty('height'); - this._linkedBrowser.style.removeProperty('box-sizing'); - this._linkedBrowser.closest('.browserStack').style.removeProperty('overflow'); - this._linkedBrowser.closest('.browserStack').style.removeProperty('contain'); - this._linkedBrowser.closest('.browserStack').style.removeProperty('scrollbar-width'); - this._linkedBrowser.browsingContext.inRDMPane = false; - - const actualSize = this._linkedBrowser.getBoundingClientRect(); - await this._channel.connect('').send('awaitViewportDimensions', { - width: actualSize.width, - height: actualSize.height, - }); - } - } - - setEmulatedMedia(mediumOverride) { - this.mediumOverride = mediumOverride || ''; - this.updateEmulatedMedia(); - } - - updateEmulatedMedia(browsingContext = undefined) { - (browsingContext || this._linkedBrowser.browsingContext).mediumOverride = this.mediumOverride; - } - - setColorScheme(colorScheme) { - this.colorScheme = fromProtocolColorScheme(colorScheme); - this.updateColorSchemeOverride(); - } - - updateColorSchemeOverride(browsingContext = undefined) { - (browsingContext || this._linkedBrowser.browsingContext).prefersColorSchemeOverride = this.colorScheme || 'dark'; - } - - setReducedMotion(reducedMotion) { - this.reducedMotion = fromProtocolReducedMotion(reducedMotion); - this.updateReducedMotionOverride(); - } - - updateReducedMotionOverride(browsingContext = undefined) { - (browsingContext || this._linkedBrowser.browsingContext).prefersReducedMotionOverride = this.reducedMotion || this._browserContext.reducedMotion || 'none'; - } - - setForcedColors(forcedColors) { - this.forcedColors = fromProtocolForcedColors(forcedColors); - this.updateForcedColorsOverride(); - } - - updateForcedColorsOverride(browsingContext = undefined) { - const isActive = this.forcedColors === 'active' || this._browserContext.forcedColors === 'active'; - (browsingContext || this._linkedBrowser.browsingContext).forcedColorsOverride = isActive ? 'active' : 'none'; - } - - async setInterceptFileChooserDialog(enabled) { - this.crossProcessCookie.interceptFileChooserDialog = enabled; - this._updateCrossProcessCookie(); - await this._channel.connect('').send('setInterceptFileChooserDialog', enabled).catch(e => {}); - } - - async setViewportSize(viewportSize) { - this._viewportSize = viewportSize; - await this.updateViewportSize(); - } - - close(runBeforeUnload = false) { - this._gBrowser.removeTab(this._tab, { - skipPermitUnload: !runBeforeUnload, - }); - } - - channel() { - return this._channel; - } - - id() { - return this._targetId; - } - - info() { - return { - targetId: this.id(), - type: 'page', - browserContextId: this._browserContext.browserContextId, - openerId: this._openerId, - }; - } - - _onNavigated(aLocation) { - this._url = aLocation.spec; - this._browserContext.grantPermissionsToOrigin(this._url); - } - - _updateCrossProcessCookie() { - Services.ppmm.sharedData.set('juggler:page-cookie-' + this._linkedBrowser.browsingContext.browserId, this.crossProcessCookie); - Services.ppmm.sharedData.flush(); - } - - async ensurePermissions() { - await this._channel.connect('').send('ensurePermissions', {}).catch(e => void e); - } - - async setInitScripts(scripts) { - this.crossProcessCookie.initScripts = scripts; - this._updateCrossProcessCookie(); - await this.pushInitScripts(); - } - - async pushInitScripts() { - await this._channel.connect('').send('setInitScripts', [...this._browserContext.crossProcessCookie.initScripts, ...this.crossProcessCookie.initScripts]).catch(e => void e); - } - - async addBinding(worldName, name, script) { - this.crossProcessCookie.bindings.push({ worldName, name, script }); - this._updateCrossProcessCookie(); - await this._channel.connect('').send('addBinding', { worldName, name, script }).catch(e => void e); - } - - async applyContextSetting(name, value) { - await this._channel.connect('').send('applyContextSetting', { name, value }).catch(e => void e); - } - - async hasFailedToOverrideTimezone() { - return await this._channel.connect('').send('hasFailedToOverrideTimezone').catch(e => true); - } - - async _startVideoRecording({width, height, dir}) { - // On Mac the window may not yet be visible when TargetCreated and its - // NSWindow.windowNumber may be -1, so we wait until the window is known - // to be initialized and visible. - await this.windowReady(); - const file = PathUtils.join(dir, helper.generateId() + '.webm'); - if (width < 10 || width > 10000 || height < 10 || height > 10000) - throw new Error("Invalid size"); - - const docShell = this._gBrowser.ownerGlobal.docShell; - // Exclude address bar and navigation control from the video. - const rect = this.linkedBrowser().getBoundingClientRect(); - const devicePixelRatio = this._window.devicePixelRatio; - let sessionId; - const registry = this._registry; - const screencastClient = { - QueryInterface: ChromeUtils.generateQI([Ci.nsIScreencastServiceClient]), - screencastFrame(data, deviceWidth, deviceHeight) { - }, - screencastStopped() { - registry.emit(TargetRegistry.Events.ScreencastStopped, sessionId); - }, - }; - const viewport = this._viewportSize || this._browserContext.defaultViewportSize || { width: 0, height: 0 }; - sessionId = screencastService.startVideoRecording(screencastClient, docShell, true, file, width, height, 0, viewport.width, viewport.height, devicePixelRatio * rect.top); - this._videoRecordingInfo = { sessionId, file }; - this.emit(PageTarget.Events.ScreencastStarted); - } - - _stopVideoRecording() { - if (!this._videoRecordingInfo) - throw new Error('No video recording in progress'); - const videoRecordingInfo = this._videoRecordingInfo; - this._videoRecordingInfo = undefined; - screencastService.stopVideoRecording(videoRecordingInfo.sessionId); - } - - videoRecordingInfo() { - return this._videoRecordingInfo; - } - - async startScreencast({ width, height, quality }) { - // On Mac the window may not yet be visible when TargetCreated and its - // NSWindow.windowNumber may be -1, so we wait until the window is known - // to be initialized and visible. - await this.windowReady(); - if (width < 10 || width > 10000 || height < 10 || height > 10000) - throw new Error("Invalid size"); - - const docShell = this._gBrowser.ownerGlobal.docShell; - // Exclude address bar and navigation control from the video. - const rect = this.linkedBrowser().getBoundingClientRect(); - const devicePixelRatio = this._window.devicePixelRatio; - - const self = this; - const screencastClient = { - QueryInterface: ChromeUtils.generateQI([Ci.nsIScreencastServiceClient]), - screencastFrame(data, deviceWidth, deviceHeight) { - if (self._screencastRecordingInfo) - self.emit(PageTarget.Events.ScreencastFrame, { data, deviceWidth, deviceHeight }); - }, - screencastStopped() { - }, - }; - const viewport = this._viewportSize || this._browserContext.defaultViewportSize || { width: 0, height: 0 }; - const screencastId = screencastService.startVideoRecording(screencastClient, docShell, false, '', width, height, quality || 90, viewport.width, viewport.height, devicePixelRatio * rect.top); - this._screencastRecordingInfo = { screencastId }; - return { screencastId }; - } - - screencastFrameAck({ screencastId }) { - if (!this._screencastRecordingInfo || this._screencastRecordingInfo.screencastId !== screencastId) - return; - screencastService.screencastFrameAck(screencastId); - } - - stopScreencast() { - if (!this._screencastRecordingInfo) - throw new Error('No screencast in progress'); - const { screencastId } = this._screencastRecordingInfo; - this._screencastRecordingInfo = undefined; - screencastService.stopVideoRecording(screencastId); - } - - ensureContextMenuClosed() { - // Close context menu, if any, since it might capture mouse events on Linux - // and prevent browser shutdown on MacOS. - const doc = this._linkedBrowser.ownerDocument; - const contextMenu = doc.getElementById('contentAreaContextMenu'); - if (contextMenu) - contextMenu.hidePopup(); - const autocompletePopup = doc.getElementById('PopupAutoComplete'); - if (autocompletePopup) - autocompletePopup.hidePopup(); - const selectPopup = doc.getElementById('ContentSelectDropdown')?.menupopup; - if (selectPopup) - selectPopup.hidePopup() - } - - dispose() { - this.ensureContextMenuClosed(); - this._disposed = true; - if (this._videoRecordingInfo) - this._stopVideoRecording(); - if (this._screencastRecordingInfo) - this.stopScreencast(); - this._browserContext.pages.delete(this); - this._registry._browserToTarget.delete(this._linkedBrowser); - this._registry._browserIdToTarget.delete(this._linkedBrowser.browsingContext.browserId); - try { - helper.removeListeners(this._eventListeners); - } catch (e) { - // In some cases, removing listeners from this._linkedBrowser fails - // because it is already half-destroyed. - if (e) - dump(e.message + '\n' + e.stack + '\n'); - } - this._registry.emit(TargetRegistry.Events.TargetDestroyed, this); - } -} - -PageTarget.Events = { - ScreencastStarted: Symbol('PageTarget.ScreencastStarted'), - ScreencastFrame: Symbol('PageTarget.ScreencastFrame'), - Crashed: Symbol('PageTarget.Crashed'), - DialogOpened: Symbol('PageTarget.DialogOpened'), - DialogClosed: Symbol('PageTarget.DialogClosed'), -}; - -function fromProtocolColorScheme(colorScheme) { - if (colorScheme === 'light' || colorScheme === 'dark') - return colorScheme; - if (colorScheme === null || colorScheme === 'no-preference') - return undefined; - throw new Error('Unknown color scheme: ' + colorScheme); -} - -function fromProtocolReducedMotion(reducedMotion) { - if (reducedMotion === 'reduce' || reducedMotion === 'no-preference') - return reducedMotion; - if (reducedMotion === null) - return undefined; - throw new Error('Unknown reduced motion: ' + reducedMotion); -} - -function fromProtocolForcedColors(forcedColors) { - if (forcedColors === 'active' || forcedColors === 'none') - return forcedColors; - if (!forcedColors) - return 'none'; - throw new Error('Unknown forced colors: ' + forcedColors); -} - -class BrowserContext { - constructor(registry, browserContextId, removeOnDetach) { - this._registry = registry; - this.browserContextId = browserContextId; - // Default context has userContextId === 0, but we pass undefined to many APIs just in case. - this.userContextId = 0; - if (browserContextId !== undefined) { - const identity = ContextualIdentityService.create(IDENTITY_NAME + browserContextId); - this.userContextId = identity.userContextId; - } - this._principals = []; - // Maps origins to the permission lists. - this._permissions = new Map(); - this._registry._browserContextIdToBrowserContext.set(this.browserContextId, this); - this._registry._userContextIdToBrowserContext.set(this.userContextId, this); - this._proxy = null; - this.removeOnDetach = removeOnDetach; - this.extraHTTPHeaders = undefined; - this.httpCredentials = undefined; - this.requestInterceptionEnabled = undefined; - this.ignoreHTTPSErrors = undefined; - this.downloadOptions = undefined; - this.defaultViewportSize = undefined; - this.deviceScaleFactor = undefined; - this.defaultUserAgent = null; - this.defaultPlatform = null; - this.touchOverride = false; - this.forceOffline = false; - this.disableCache = false; - this.colorScheme = 'none'; - this.forcedColors = 'none'; - this.reducedMotion = 'none'; - this.videoRecordingOptions = undefined; - this.crossProcessCookie = { - initScripts: [], - bindings: [], - settings: {}, - }; - this.pages = new Set(); - } - - _updateCrossProcessCookie() { - Services.ppmm.sharedData.set('juggler:context-cookie-' + this.userContextId, this.crossProcessCookie); - Services.ppmm.sharedData.flush(); - } - - setColorScheme(colorScheme) { - this.colorScheme = fromProtocolColorScheme(colorScheme); - for (const page of this.pages) - page.updateColorSchemeOverride(); - } - - setReducedMotion(reducedMotion) { - this.reducedMotion = fromProtocolReducedMotion(reducedMotion); - for (const page of this.pages) - page.updateReducedMotionOverride(); - } - - setForcedColors(forcedColors) { - this.forcedColors = fromProtocolForcedColors(forcedColors); - for (const page of this.pages) - page.updateForcedColorsOverride(); - } - - async destroy() { - if (this.userContextId !== 0) { - ContextualIdentityService.remove(this.userContextId); - for (const page of this.pages) - page.close(); - if (this.pages.size) { - await new Promise(f => { - const listener = helper.on(this._registry, TargetRegistry.Events.TargetDestroyed, () => { - if (!this.pages.size) { - helper.removeListeners([listener]); - f(); - } - }); - }); - } - } - this._registry._browserContextIdToBrowserContext.delete(this.browserContextId); - this._registry._userContextIdToBrowserContext.delete(this.userContextId); - this._registry._updateProxiesWithSameAuthCacheAndDifferentCredentials(); - } - - setProxy(proxy) { - // Clear AuthCache. - Services.obs.notifyObservers(null, "net:clear-active-logins"); - this._proxy = proxy; - this._registry._updateProxiesWithSameAuthCacheAndDifferentCredentials(); - } - - setIgnoreHTTPSErrors(ignoreHTTPSErrors) { - if (this.ignoreHTTPSErrors === ignoreHTTPSErrors) - return; - this.ignoreHTTPSErrors = ignoreHTTPSErrors; - const certOverrideService = Cc[ - "@mozilla.org/security/certoverride;1" - ].getService(Ci.nsICertOverrideService); - if (ignoreHTTPSErrors) { - Preferences.set("network.stricttransportsecurity.preloadlist", false); - Preferences.set("security.cert_pinning.enforcement_level", 0); - certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(true, this.userContextId); - } else { - certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(false, this.userContextId); - } - } - - setDefaultUserAgent(userAgent) { - this.defaultUserAgent = userAgent; - for (const page of this.pages) - page.updateUserAgent(); - } - - setDefaultPlatform(platform) { - this.defaultPlatform = platform; - for (const page of this.pages) - page.updatePlatform(); - } - - setTouchOverride(touchOverride) { - this.touchOverride = touchOverride; - for (const page of this.pages) - page.updateTouchOverride(); - } - - setForceOffline(forceOffline) { - this.forceOffline = forceOffline; - for (const page of this.pages) - page.updateForceOffline(); - } - - setCacheDisabled(disabled) { - this.disableCache = disabled; - for (const page of this.pages) - page.updateCacheDisabled(); - } - - async setDefaultViewport(viewport) { - // Camoufox: only override the set viewport if a new one was passed - if ( - ChromeUtils.camouGetInt("window.innerWidth") - || ChromeUtils.camouGetInt("window.innerHeight") - ) { - if (viewport.viewportSize?.width == 1280 && viewport.viewportSize?.height == 720) { - return; - } - } - this.defaultViewportSize = viewport ? viewport.viewportSize : undefined; - this.deviceScaleFactor = viewport ? viewport.deviceScaleFactor : undefined; - await Promise.all(Array.from(this.pages).map(page => page.updateViewportSize())); - } - - async setInitScripts(scripts) { - this.crossProcessCookie.initScripts = scripts; - this._updateCrossProcessCookie(); - await Promise.all(Array.from(this.pages).map(page => page.pushInitScripts())); - } - - async addBinding(worldName, name, script) { - this.crossProcessCookie.bindings.push({ worldName, name, script }); - this._updateCrossProcessCookie(); - await Promise.all(Array.from(this.pages).map(page => page.addBinding(worldName, name, script))); - } - - async applySetting(name, value) { - this.crossProcessCookie.settings[name] = value; - this._updateCrossProcessCookie(); - await Promise.all(Array.from(this.pages).map(page => page.applyContextSetting(name, value))); - } - - async grantPermissions(origin, permissions) { - this._permissions.set(origin, permissions); - const promises = []; - for (const page of this.pages) { - if (origin === '*' || page._url.startsWith(origin)) { - this.grantPermissionsToOrigin(page._url); - promises.push(page.ensurePermissions()); - } - } - await Promise.all(promises); - } - - resetPermissions() { - for (const principal of this._principals) { - for (const permission of ALL_PERMISSIONS) - Services.perms.removeFromPrincipal(principal, permission); - } - this._principals = []; - this._permissions.clear(); - } - - grantPermissionsToOrigin(url) { - let origin = Array.from(this._permissions.keys()).find(key => url.startsWith(key)); - if (!origin) - origin = '*'; - - const permissions = this._permissions.get(origin); - if (!permissions) - return; - - const attrs = { userContextId: this.userContextId || undefined }; - const principal = Services.scriptSecurityManager.createContentPrincipal(NetUtil.newURI(url), attrs); - this._principals.push(principal); - for (const permission of ALL_PERMISSIONS) { - const action = permissions.includes(permission) ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION; - Services.perms.addFromPrincipal(principal, permission, action, Ci.nsIPermissionManager.EXPIRE_NEVER, 0 /* expireTime */); - } - } - - setCookies(cookies) { - const protocolToSameSite = { - [undefined]: Ci.nsICookie.SAMESITE_NONE, - 'Lax': Ci.nsICookie.SAMESITE_LAX, - 'Strict': Ci.nsICookie.SAMESITE_STRICT, - }; - for (const cookie of cookies) { - const uri = cookie.url ? NetUtil.newURI(cookie.url) : null; - let domain = cookie.domain; - if (!domain) { - if (!uri) - throw new Error('At least one of the url and domain needs to be specified'); - domain = uri.host; - } - let path = cookie.path; - if (!path) - path = uri ? dirPath(uri.filePath) : '/'; - let secure = false; - if (cookie.secure !== undefined) - secure = cookie.secure; - else if (uri && uri.scheme === 'https') - secure = true; - Services.cookies.add( - domain, - path, - cookie.name, - cookie.value, - secure, - cookie.httpOnly || false, - cookie.expires === undefined || cookie.expires === -1 /* isSession */, - cookie.expires === undefined ? Date.now() + HUNDRED_YEARS : cookie.expires, - { userContextId: this.userContextId || undefined } /* originAttributes */, - protocolToSameSite[cookie.sameSite], - Ci.nsICookie.SCHEME_UNSET - ); - } - } - - clearCookies() { - Services.cookies.removeCookiesWithOriginAttributes(JSON.stringify({ userContextId: this.userContextId || undefined })); - } - - getCookies() { - const result = []; - const sameSiteToProtocol = { - [Ci.nsICookie.SAMESITE_NONE]: 'None', - [Ci.nsICookie.SAMESITE_LAX]: 'Lax', - [Ci.nsICookie.SAMESITE_STRICT]: 'Strict', - }; - for (let cookie of Services.cookies.cookies) { - if (cookie.originAttributes.userContextId !== this.userContextId) - continue; - if (cookie.host === 'addons.mozilla.org') - continue; - result.push({ - name: cookie.name, - value: cookie.value, - domain: cookie.host, - path: cookie.path, - expires: cookie.isSession ? -1 : cookie.expiry, - size: cookie.name.length + cookie.value.length, - httpOnly: cookie.isHttpOnly, - secure: cookie.isSecure, - session: cookie.isSession, - sameSite: sameSiteToProtocol[cookie.sameSite], - }); - } - return result; - } - - async setVideoRecordingOptions(options) { - this.videoRecordingOptions = options; - const promises = []; - for (const page of this.pages) { - if (options) - promises.push(page._startVideoRecording(options)); - else if (page._videoRecordingInfo) - promises.push(page._stopVideoRecording()); - } - await Promise.all(promises); - } -} - -class Dialog { - static createIfSupported(prompt) { - const type = prompt.args.promptType; - switch (type) { - case 'alert': - case 'alertCheck': - return new Dialog(prompt, 'alert'); - case 'prompt': - return new Dialog(prompt, 'prompt'); - case 'confirm': - case 'confirmCheck': - return new Dialog(prompt, 'confirm'); - case 'confirmEx': - return new Dialog(prompt, 'beforeunload'); - default: - return null; - }; - } - - constructor(prompt, type) { - this._id = helper.generateId(); - this._type = type; - this._prompt = prompt; - } - - id() { - return this._id; - } - - message() { - return this._prompt.ui.infoBody.textContent; - } - - type() { - return this._type; - } - - prompt() { - return this._prompt; - } - - dismiss() { - if (this._prompt.ui.button1) - this._prompt.ui.button1.click(); - else - this._prompt.ui.button0.click(); - } - - defaultValue() { - return this._prompt.ui.loginTextbox.value; - } - - accept(promptValue) { - if (typeof promptValue === 'string' && this._type === 'prompt') - this._prompt.ui.loginTextbox.value = promptValue; - this._prompt.ui.button0.click(); - } -} - - -function dirPath(path) { - return path.substring(0, path.lastIndexOf('/') + 1); -} - -async function waitForWindowReady(window) { - if (window.delayedStartupPromise) { - await window.delayedStartupPromise; - } else { - await new Promise((resolve => { - Services.obs.addObserver(function observer(aSubject, aTopic) { - if (window == aSubject) { - Services.obs.removeObserver(observer, aTopic); - resolve(); - } - }, "browser-delayed-startup-finished"); - })); - } - if (window.document.readyState !== 'complete') - await helper.awaitEvent(window, 'load'); -} - -TargetRegistry.Events = { - TargetCreated: Symbol('TargetRegistry.Events.TargetCreated'), - TargetDestroyed: Symbol('TargetRegistry.Events.TargetDestroyed'), - DownloadCreated: Symbol('TargetRegistry.Events.DownloadCreated'), - DownloadFinished: Symbol('TargetRegistry.Events.DownloadFinished'), - ScreencastStopped: Symbol('TargetRegistry.ScreencastStopped'), -}; - -var EXPORTED_SYMBOLS = ['TargetRegistry', 'PageTarget']; -this.TargetRegistry = TargetRegistry; -this.PageTarget = PageTarget; \ No newline at end of file diff --git a/additions/juggler/components/Juggler.js b/additions/juggler/components/Juggler.js deleted file mode 100644 index b272815..0000000 --- a/additions/juggler/components/Juggler.js +++ /dev/null @@ -1,166 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -var EXPORTED_SYMBOLS = ["Juggler", "JugglerFactory"]; - -const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); -const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); -const {ComponentUtils} = ChromeUtils.import("resource://gre/modules/ComponentUtils.jsm"); -const {Dispatcher} = ChromeUtils.import("chrome://juggler/content/protocol/Dispatcher.js"); -const {BrowserHandler} = ChromeUtils.import("chrome://juggler/content/protocol/BrowserHandler.js"); -const {NetworkObserver} = ChromeUtils.import("chrome://juggler/content/NetworkObserver.js"); -const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); -const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); -const {ActorManagerParent} = ChromeUtils.import('resource://gre/modules/ActorManagerParent.jsm'); -const helper = new Helper(); - -const Cc = Components.classes; -const Ci = Components.interfaces; - -// Register JSWindowActors that will be instantiated for each frame. -ActorManagerParent.addJSWindowActors({ - JugglerFrame: { - parent: { - moduleURI: 'chrome://juggler/content/JugglerFrameParent.jsm', - }, - child: { - moduleURI: 'chrome://juggler/content/content/JugglerFrameChild.jsm', - events: { - // Normally, we instantiate an actor when a new window is created. - DOMWindowCreated: {}, - // However, for same-origin iframes, the navigation from about:blank - // to the URL will share the same window, so we need to also create - // an actor for a new document via DOMDocElementInserted. - DOMDocElementInserted: {}, - // Also, listening to DOMContentLoaded. - DOMContentLoaded: {}, - DOMWillOpenModalDialog: {}, - DOMModalDialogClosed: {}, - }, - }, - allFrames: true, - }, -}); - -let browserStartupFinishedCallback; -let browserStartupFinishedPromise = new Promise(x => browserStartupFinishedCallback = x); - -class Juggler { - get classDescription() { return "Sample command-line handler"; } - get classID() { return Components.ID('{f7a74a33-e2ab-422d-b022-4fb213dd2639}'); } - get contractID() { return "@mozilla.org/remote/juggler;1" } - get QueryInterface() { - return ChromeUtils.generateQI([ Ci.nsICommandLineHandler, Ci.nsIObserver ]); - } - get helpInfo() { - return " --juggler Enable Juggler automation\n"; - } - - handle(cmdLine) { - // flag has to be consumed in nsICommandLineHandler:handle - // to avoid issues on macos. See Marionette.jsm::handle() for more details. - // TODO: remove after Bug 1724251 is fixed. - cmdLine.handleFlag("juggler-pipe", false); - } - - // This flow is taken from Remote agent and Marionette. - // See https://github.com/mozilla/gecko-dev/blob/0c1b4921830e6af8bc951da01d7772de2fe60a08/remote/components/RemoteAgent.jsm#L302 - async observe(subject, topic) { - switch (topic) { - case "profile-after-change": - Services.obs.addObserver(this, "command-line-startup"); - Services.obs.addObserver(this, "browser-idle-startup-tasks-finished"); - break; - case "command-line-startup": - Services.obs.removeObserver(this, topic); - const cmdLine = subject; - const jugglerPipeFlag = cmdLine.handleFlag('juggler-pipe', false); - if (!jugglerPipeFlag) - return; - - this._silent = cmdLine.findFlag('silent', false) >= 0; - if (this._silent) { - Services.startup.enterLastWindowClosingSurvivalArea(); - browserStartupFinishedCallback(); - } - Services.obs.addObserver(this, "final-ui-startup"); - break; - case "browser-idle-startup-tasks-finished": - browserStartupFinishedCallback(); - break; - // Used to wait until the initial application window has been opened. - case "final-ui-startup": - Services.obs.removeObserver(this, topic); - - const targetRegistry = new TargetRegistry(); - new NetworkObserver(targetRegistry); - - const loadStyleSheet = () => { - if (Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless) { - const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Components.interfaces.nsIStyleSheetService); - const ioService = Cc["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService); - const uri = ioService.newURI('chrome://juggler/content/content/hidden-scrollbars.css', null, null); - styleSheetService.loadAndRegisterSheet(uri, styleSheetService.AGENT_SHEET); - } - }; - - // Force create hidden window here, otherwise its creation later closes the web socket! - // Since https://phabricator.services.mozilla.com/D219834, hiddenDOMWindow is only available on MacOS. - if (Services.appShell.hasHiddenWindow) { - Services.appShell.hiddenDOMWindow; - } - - let pipeStopped = false; - let browserHandler; - const pipe = Cc['@mozilla.org/juggler/remotedebuggingpipe;1'].getService(Ci.nsIRemoteDebuggingPipe); - const connection = { - QueryInterface: ChromeUtils.generateQI([Ci.nsIRemoteDebuggingPipeClient]), - receiveMessage(message) { - if (this.onmessage) - this.onmessage({ data: message }); - }, - disconnected() { - if (browserHandler) - browserHandler['Browser.close'](); - }, - send(message) { - if (pipeStopped) { - // We are missing the response to Browser.close, - // but everything works fine. Once we actually need it, - // we have to stop the pipe after the response is sent. - return; - } - pipe.sendMessage(message); - }, - }; - pipe.init(connection); - ChromeUtils.camouDebug('Juggler pipe initialized'); - const dispatcher = new Dispatcher(connection); - ChromeUtils.camouDebug('Dispatcher created'); - browserHandler = new BrowserHandler(dispatcher.rootSession(), dispatcher, targetRegistry, browserStartupFinishedPromise, () => { - ChromeUtils.camouDebug('BrowserHandler cleanup callback called'); - if (this._silent) - Services.startup.exitLastWindowClosingSurvivalArea(); - connection.onclose(); - pipe.stop(); - pipeStopped = true; - }); - ChromeUtils.camouDebug('BrowserHandler created'); - dispatcher.rootSession().setHandler(browserHandler); - ChromeUtils.camouDebug('BrowserHandler set as root session handler'); - loadStyleSheet(); - dump(`\nJuggler listening to the pipe\n`); - break; - } - } - -} - -const jugglerInstance = new Juggler(); - -// This is used by the XPCOM codepath which expects a constructor -var JugglerFactory = function() { - return jugglerInstance; -}; - diff --git a/additions/juggler/components/components.conf b/additions/juggler/components/components.conf deleted file mode 100644 index e5bc652..0000000 --- a/additions/juggler/components/components.conf +++ /dev/null @@ -1,18 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -Classes = [ - # Juggler - { - "cid": "{f7a74a33-e2ab-422d-b022-4fb213dd2639}", - "contract_ids": ["@mozilla.org/remote/juggler;1"], - "categories": { - "command-line-handler": "m-remote", - "profile-after-change": "Juggler", - }, - "jsm": "chrome://juggler/content/components/Juggler.js", - "constructor": "JugglerFactory", - }, -] - diff --git a/additions/juggler/components/moz.build b/additions/juggler/components/moz.build deleted file mode 100644 index bab81f8..0000000 --- a/additions/juggler/components/moz.build +++ /dev/null @@ -1,6 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -XPCOM_MANIFESTS += ["components.conf"] - diff --git a/additions/juggler/content/FrameTree.js b/additions/juggler/content/FrameTree.js deleted file mode 100644 index e4a7db1..0000000 --- a/additions/juggler/content/FrameTree.js +++ /dev/null @@ -1,723 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); -const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js'); -const {Runtime} = ChromeUtils.import('chrome://juggler/content/content/Runtime.js'); - -const helper = new Helper(); - -class FrameTree { - constructor(rootBrowsingContext) { - helper.decorateAsEventEmitter(this); - - this._rootBrowsingContext = rootBrowsingContext; - - this._browsingContextGroup = rootBrowsingContext.group; - if (!this._browsingContextGroup.__jugglerFrameTrees) - this._browsingContextGroup.__jugglerFrameTrees = new Set(); - this._browsingContextGroup.__jugglerFrameTrees.add(this); - this._isolatedWorlds = new Map(); - - this._webSocketEventService = Cc[ - "@mozilla.org/websocketevent/service;1" - ].getService(Ci.nsIWebSocketEventService); - - this._runtime = new Runtime(false /* isWorker */); - this._workers = new Map(); - this._frameIdToFrame = new Map(); - this._pageReady = false; - this._javaScriptDisabled = false; - for (const browsingContext of helper.collectAllBrowsingContexts(rootBrowsingContext)) - this._createFrame(browsingContext); - this._mainFrame = this.frameForBrowsingContext(rootBrowsingContext); - - const webProgress = rootBrowsingContext.docShell.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebProgress); - this.QueryInterface = ChromeUtils.generateQI([ - Ci.nsIWebProgressListener, - Ci.nsIWebProgressListener2, - Ci.nsISupportsWeakReference, - ]); - - this._wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].createInstance(Ci.nsIWorkerDebuggerManager); - this._wdmListener = { - QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerManagerListener]), - onRegister: this._onWorkerCreated.bind(this), - onUnregister: this._onWorkerDestroyed.bind(this), - }; - this._wdm.addListener(this._wdmListener); - for (const workerDebugger of this._wdm.getWorkerDebuggerEnumerator()) - this._onWorkerCreated(workerDebugger); - - const flags = Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT | - Ci.nsIWebProgress.NOTIFY_LOCATION; - this._eventListeners = [ - helper.addObserver((docShell, topic, loadIdentifier) => { - const frame = this.frameForDocShell(docShell); - if (!frame) - return; - frame._pendingNavigationId = helper.toProtocolNavigationId(loadIdentifier); - this.emit(FrameTree.Events.NavigationStarted, frame); - }, 'juggler-navigation-started-renderer'), - helper.addObserver(this._onDOMWindowCreated.bind(this), 'content-document-global-created'), - helper.addObserver(this._onDOMWindowCreated.bind(this), 'juggler-dom-window-reused'), - helper.addObserver((browsingContext, topic, why) => { - this._onBrowsingContextAttached(browsingContext); - }, 'browsing-context-attached'), - helper.addObserver((browsingContext, topic, why) => { - this._onBrowsingContextDetached(browsingContext); - }, 'browsing-context-discarded'), - helper.addObserver((subject, topic, eventInfo) => { - const [type, jugglerEventId] = eventInfo.split(' '); - this.emit(FrameTree.Events.InputEvent, { type, jugglerEventId: +(jugglerEventId ?? '0') }); - }, 'juggler-mouse-event-hit-renderer'), - helper.addProgressListener(webProgress, this, flags), - ]; - - this._dragEventListeners = []; - } - - workers() { - return [...this._workers.values()]; - } - - runtime() { - return this._runtime; - } - - setInitScripts(scripts) { - for (const world of this._isolatedWorlds.values()) - world._scriptsToEvaluateOnNewDocument = []; - - for (let { worldName, script } of scripts) { - worldName = worldName || ''; - const existing = this._isolatedWorlds.has(worldName); - const world = this._ensureWorld(worldName); - world._scriptsToEvaluateOnNewDocument.push(script); - // FIXME: 'should inherit http credentials from browser context' fails without this - if (worldName && !existing) { - for (const frame of this.frames()) - frame._createIsolatedContext(worldName); - } - } - } - - _ensureWorld(worldName) { - worldName = worldName || ''; - let world = this._isolatedWorlds.get(worldName); - if (!world) { - world = new IsolatedWorld(worldName); - this._isolatedWorlds.set(worldName, world); - } - return world; - } - - _frameForWorker(workerDebugger) { - if (workerDebugger.type !== Ci.nsIWorkerDebugger.TYPE_DEDICATED) - return null; - if (!workerDebugger.window) - return null; - return this.frameForDocShell(workerDebugger.window.docShell); - } - - _onDOMWindowCreated(window) { - const frame = this.frameForDocShell(window.docShell); - if (!frame) - return; - frame._onGlobalObjectCleared(); - } - - setJavaScriptDisabled(javaScriptDisabled) { - this._javaScriptDisabled = javaScriptDisabled; - for (const frame of this.frames()) - frame._updateJavaScriptDisabled(); - } - - _onWorkerCreated(workerDebugger) { - // Note: we do not interoperate with firefox devtools. - if (workerDebugger.isInitialized) - return; - const frame = this._frameForWorker(workerDebugger); - if (!frame) - return; - const worker = new Worker(frame, workerDebugger); - this._workers.set(workerDebugger, worker); - this.emit(FrameTree.Events.WorkerCreated, worker); - } - - _onWorkerDestroyed(workerDebugger) { - const worker = this._workers.get(workerDebugger); - if (!worker) - return; - worker.dispose(); - this._workers.delete(workerDebugger); - this.emit(FrameTree.Events.WorkerDestroyed, worker); - } - - allFramesInBrowsingContextGroup(group) { - const frames = []; - for (const frameTree of (group.__jugglerFrameTrees || [])) { - for (const frame of frameTree.frames()) { - try { - // Try accessing docShell and domWindow to filter out dead frames. - // This might happen for print-preview frames, but maybe for something else as well. - frame.docShell(); - frame.domWindow(); - frames.push(frame); - } catch (e) { - dump(`WARNING: unable to access docShell and domWindow of the frame[id=${frame.id()}]\n`); - } - } - } - return frames; - } - - isPageReady() { - return this._pageReady; - } - - forcePageReady() { - if (this._pageReady) - return false; - this._pageReady = true; - this.emit(FrameTree.Events.PageReady); - return true; - } - - addBinding(worldName, name, script) { - worldName = worldName || ''; - const world = this._ensureWorld(worldName); - world._bindings.set(name, script); - for (const frame of this.frames()) - frame._addBinding(worldName, name, script); - } - - frameForBrowsingContext(browsingContext) { - if (!browsingContext) - return null; - const frameId = helper.browsingContextToFrameId(browsingContext); - return this._frameIdToFrame.get(frameId) ?? null; - } - - frameForDocShell(docShell) { - if (!docShell) - return null; - const frameId = helper.browsingContextToFrameId(docShell.browsingContext); - return this._frameIdToFrame.get(frameId) ?? null; - } - - frame(frameId) { - return this._frameIdToFrame.get(frameId) || null; - } - - frames() { - let result = []; - collect(this._mainFrame); - return result; - - function collect(frame) { - result.push(frame); - for (const subframe of frame._children) - collect(subframe); - } - } - - mainFrame() { - return this._mainFrame; - } - - dispose() { - this._browsingContextGroup.__jugglerFrameTrees.delete(this); - this._wdm.removeListener(this._wdmListener); - this._runtime.dispose(); - helper.removeListeners(this._eventListeners); - helper.removeListeners(this._dragEventListeners); - } - - onWindowEvent(event) { - if (event.type !== 'DOMDocElementInserted' || !event.target.ownerGlobal) - return; - - const docShell = event.target.ownerGlobal.docShell; - const frame = this.frameForDocShell(docShell); - if (!frame) { - dump(`WARNING: ${event.type} for unknown frame ${helper.browsingContextToFrameId(docShell.browsingContext)}\n`); - return; - } - if (frame._pendingNavigationId) { - docShell.QueryInterface(Ci.nsIWebNavigation); - this._frameNavigationCommitted(frame, docShell.currentURI.spec); - } - - if (frame === this._mainFrame) { - helper.removeListeners(this._dragEventListeners); - const chromeEventHandler = docShell.chromeEventHandler; - const options = { - mozSystemGroup: true, - capture: true, - }; - const emitInputEvent = (event) => this.emit(FrameTree.Events.InputEvent, { type: event.type, jugglerEventId: 0 }); - // Drag events are dispatched from content process, so these we don't see in the - // `juggler-mouse-event-hit-renderer` instrumentation. - this._dragEventListeners = [ - helper.addEventListener(chromeEventHandler, 'dragstart', emitInputEvent, options), - helper.addEventListener(chromeEventHandler, 'dragover', emitInputEvent, options), - ]; - } - } - - _frameNavigationCommitted(frame, url) { - for (const subframe of frame._children) - this._detachFrame(subframe); - const navigationId = frame._pendingNavigationId; - frame._pendingNavigationId = null; - frame._lastCommittedNavigationId = navigationId; - frame._url = url; - this.emit(FrameTree.Events.NavigationCommitted, frame); - if (frame === this._mainFrame) - this.forcePageReady(); - } - - onStateChange(progress, request, flag, status) { - if (!(request instanceof Ci.nsIChannel)) - return; - const channel = request.QueryInterface(Ci.nsIChannel); - const docShell = progress.DOMWindow.docShell; - const frame = this.frameForDocShell(docShell); - if (!frame) - return; - - if (!channel.isDocument) { - // Somehow, we can get worker requests here, - // while we are only interested in frame documents. - return; - } - - const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP; - if (isStop && frame._pendingNavigationId && status) { - // Navigation is aborted. - const navigationId = frame._pendingNavigationId; - frame._pendingNavigationId = null; - // Always report download navigation as failure to match other browsers. - const errorText = helper.getNetworkErrorStatusText(status); - this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, errorText); - if (frame === this._mainFrame && status !== Cr.NS_BINDING_ABORTED) - this.forcePageReady(); - } - } - - onLocationChange(progress, request, location, flags) { - const docShell = progress.DOMWindow.docShell; - const frame = this.frameForDocShell(docShell); - const sameDocumentNavigation = !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT); - if (frame && sameDocumentNavigation) { - frame._url = location.spec; - this.emit(FrameTree.Events.SameDocumentNavigation, frame); - } - } - - _onBrowsingContextAttached(browsingContext) { - // If this browsing context doesn't belong to our frame tree - do nothing. - if (browsingContext.top !== this._rootBrowsingContext) - return; - this._createFrame(browsingContext); - } - - _onBrowsingContextDetached(browsingContext) { - const frame = this.frameForBrowsingContext(browsingContext); - if (frame) - this._detachFrame(frame); - } - - _createFrame(browsingContext) { - const parentFrame = this.frameForBrowsingContext(browsingContext.parent); - if (!parentFrame && this._mainFrame) { - dump(`WARNING: found docShell with the same root, but no parent!\n`); - return; - } - const frame = new Frame(this, this._runtime, browsingContext, parentFrame); - this._frameIdToFrame.set(frame.id(), frame); - if (browsingContext.docShell?.domWindow && browsingContext.docShell?.domWindow.location) - frame._url = browsingContext.docShell.domWindow.location.href; - this.emit(FrameTree.Events.FrameAttached, frame); - // Create execution context **after** reporting frame. - // This is our protocol contract. - if (frame.domWindow()) - frame._onGlobalObjectCleared(); - return frame; - } - - _detachFrame(frame) { - // Detach all children first - for (const subframe of frame._children) - this._detachFrame(subframe); - if (frame === this._mainFrame) { - // Do not detach main frame (happens during cross-process navigation), - // as it confuses the client. - return; - } - this._frameIdToFrame.delete(frame.id()); - if (frame._parentFrame) - frame._parentFrame._children.delete(frame); - frame._parentFrame = null; - frame.dispose(); - this.emit(FrameTree.Events.FrameDetached, frame); - } -} - -FrameTree.Events = { - FrameAttached: 'frameattached', - FrameDetached: 'framedetached', - WorkerCreated: 'workercreated', - WorkerDestroyed: 'workerdestroyed', - WebSocketCreated: 'websocketcreated', - WebSocketOpened: 'websocketopened', - WebSocketClosed: 'websocketclosed', - WebSocketFrameReceived: 'websocketframereceived', - WebSocketFrameSent: 'websocketframesent', - NavigationStarted: 'navigationstarted', - NavigationCommitted: 'navigationcommitted', - NavigationAborted: 'navigationaborted', - SameDocumentNavigation: 'samedocumentnavigation', - PageReady: 'pageready', - InputEvent: 'inputevent', -}; - -class IsolatedWorld { - constructor(name) { - this._name = name; - this._scriptsToEvaluateOnNewDocument = []; - this._bindings = new Map(); - } -} - -class Frame { - constructor(frameTree, runtime, browsingContext, parentFrame) { - this._frameTree = frameTree; - this._runtime = runtime; - this._browsingContext = browsingContext; - this._children = new Set(); - this._frameId = helper.browsingContextToFrameId(browsingContext); - this._parentFrame = null; - this._url = ''; - if (parentFrame) { - this._parentFrame = parentFrame; - parentFrame._children.add(this); - } - - this.allowMW = ChromeUtils.camouGetBool('allowMainWorld', false); - this.forceScopeAccess = ChromeUtils.camouGetBool('forceScopeAccess', false); - - this.masterSandbox = undefined; - this._lastCommittedNavigationId = null; - this._pendingNavigationId = null; - - this._textInputProcessor = null; - - this._worldNameToContext = new Map(); - this._initialNavigationDone = false; - - this._webSocketListenerInnerWindowId = 0; - // WebSocketListener calls frameReceived event before webSocketOpened. - // To avoid this, serialize event reporting. - this._webSocketInfos = new Map(); - - const dispatchWebSocketFrameReceived = (webSocketSerialID, frame) => this._frameTree.emit(FrameTree.Events.WebSocketFrameReceived, { - frameId: this._frameId, - wsid: webSocketSerialID + '', - opcode: frame.opCode, - data: frame.opCode !== 1 ? btoa(frame.payload) : frame.payload, - }); - this._webSocketListener = { - QueryInterface: ChromeUtils.generateQI([Ci.nsIWebSocketEventListener, ]), - - webSocketCreated: (webSocketSerialID, uri, protocols) => { - this._frameTree.emit(FrameTree.Events.WebSocketCreated, { - frameId: this._frameId, - wsid: webSocketSerialID + '', - requestURL: uri, - }); - this._webSocketInfos.set(webSocketSerialID, { - opened: false, - pendingIncomingFrames: [], - }); - }, - - webSocketOpened: (webSocketSerialID, effectiveURI, protocols, extensions, httpChannelId) => { - this._frameTree.emit(FrameTree.Events.WebSocketOpened, { - frameId: this._frameId, - requestId: httpChannelId + '', - wsid: webSocketSerialID + '', - effectiveURL: effectiveURI, - }); - const info = this._webSocketInfos.get(webSocketSerialID); - info.opened = true; - for (const frame of info.pendingIncomingFrames) - dispatchWebSocketFrameReceived(webSocketSerialID, frame); - }, - - webSocketMessageAvailable: (webSocketSerialID, data, messageType) => { - // We don't use this event. - }, - - webSocketClosed: (webSocketSerialID, wasClean, code, reason) => { - this._webSocketInfos.delete(webSocketSerialID); - let error = ''; - if (!wasClean) { - const keys = Object.keys(Ci.nsIWebSocketChannel); - for (const key of keys) { - if (Ci.nsIWebSocketChannel[key] === code) - error = key; - } - } - this._frameTree.emit(FrameTree.Events.WebSocketClosed, { - frameId: this._frameId, - wsid: webSocketSerialID + '', - error, - }); - }, - - frameReceived: (webSocketSerialID, frame) => { - // Report only text and binary frames. - if (frame.opCode !== 1 && frame.opCode !== 2) - return; - const info = this._webSocketInfos.get(webSocketSerialID); - if (info.opened) - dispatchWebSocketFrameReceived(webSocketSerialID, frame); - else - info.pendingIncomingFrames.push(frame); - }, - - frameSent: (webSocketSerialID, frame) => { - // Report only text and binary frames. - if (frame.opCode !== 1 && frame.opCode !== 2) - return; - this._frameTree.emit(FrameTree.Events.WebSocketFrameSent, { - frameId: this._frameId, - wsid: webSocketSerialID + '', - opcode: frame.opCode, - data: frame.opCode !== 1 ? btoa(frame.payload) : frame.payload, - }); - }, - }; - } - - // Camoufox: Add a "God mode" master sandbox with it's own compartment - getMasterSandbox() { - if (!this.masterSandbox) { - this.masterSandbox = Cu.Sandbox( - Services.scriptSecurityManager.getSystemPrincipal(), - { - sandboxPrototype: this.domWindow(), - wantComponents: false, - wantExportHelpers: false, - wantXrays: true, - freshCompartment: true, - } - ); - } - return this.masterSandbox; - } - - _createIsolatedContext(name, useMaster=false) { - let sandbox; - // Camoufox: Use the master sandbox (with system principle scope access) - if (useMaster && this.forceScopeAccess) { - sandbox = this.getMasterSandbox(); - } else { - // Standard access (run in domWindow principal) - sandbox = Cu.Sandbox([this.domWindow()], { - sandboxPrototype: this.domWindow(), - wantComponents: false, - wantExportHelpers: false, - wantXrays: true, - }); - } - const world = this._runtime.createExecutionContext(this.domWindow(), sandbox, { - frameId: this.id(), - name, - }); - // Camoufox: Create a main world for the isolated context - if (this.allowMW) { - const mainWorld = this._runtime.createMW(this.domWindow(), this.domWindow()); - world.mainEquivalent = mainWorld; - } - this._worldNameToContext.set(name, world); - return world; - } - - unsafeObject(objectId) { - for (const context of this._worldNameToContext.values()) { - const result = context.unsafeObject(objectId); - if (result) - return result.object; - } - throw new Error('Cannot find object with id = ' + objectId); - } - - dispose() { - for (const context of this._worldNameToContext.values()) - this._runtime.destroyExecutionContext(context); - this._worldNameToContext.clear(); - } - - _addBinding(worldName, name, script) { - let executionContext = this._worldNameToContext.get(worldName); - if (worldName && !executionContext) - executionContext = this._createIsolatedContext(worldName); - if (executionContext) - executionContext.addBinding(name, script); - } - - _onGlobalObjectCleared() { - const webSocketService = this._frameTree._webSocketEventService; - if (this._webSocketListenerInnerWindowId && webSocketService.hasListenerFor(this._webSocketListenerInnerWindowId)) - webSocketService.removeListener(this._webSocketListenerInnerWindowId, this._webSocketListener); - this._webSocketListenerInnerWindowId = this.domWindow().windowGlobalChild.innerWindowId; - webSocketService.addListener(this._webSocketListenerInnerWindowId, this._webSocketListener); - for (const context of this._worldNameToContext.values()) - this._runtime.destroyExecutionContext(context); - this._worldNameToContext.clear(); - // Camoufox: Scope the initial execution context to prevent leaks - this._createIsolatedContext('', true); - for (const [name, world] of this._frameTree._isolatedWorlds) { - if (name) - this._createIsolatedContext(name); - const executionContext = this._worldNameToContext.get(name); - // Add bindings before evaluating scripts. - for (const [name, script] of world._bindings) - executionContext.addBinding(name, script); - for (const script of world._scriptsToEvaluateOnNewDocument) - executionContext.evaluateScriptSafely(script); - } - - const url = this.domWindow().location?.href; - if (url === 'about:blank' && !this._url) { - // Sometimes FrameTree is created too early, before the location has been set. - this._url = url; - this._frameTree.emit(FrameTree.Events.NavigationCommitted, this); - } - - this._updateJavaScriptDisabled(); - } - - _updateJavaScriptDisabled() { - if (this._browsingContext.currentWindowContext) - this._browsingContext.currentWindowContext.allowJavascript = !this._frameTree._javaScriptDisabled; - } - - mainExecutionContext() { - return this._worldNameToContext.get(''); - } - - textInputProcessor() { - if (!this._textInputProcessor) { - this._textInputProcessor = Cc["@mozilla.org/text-input-processor;1"].createInstance(Ci.nsITextInputProcessor); - } - this._textInputProcessor.beginInputTransactionForTests(this.docShell().DOMWindow); - return this._textInputProcessor; - } - - pendingNavigationId() { - return this._pendingNavigationId; - } - - lastCommittedNavigationId() { - return this._lastCommittedNavigationId; - } - - docShell() { - return this._browsingContext.docShell; - } - - domWindow() { - return this.docShell()?.domWindow; - } - - name() { - const frameElement = this.domWindow()?.frameElement; - let name = ''; - if (frameElement) - name = frameElement.getAttribute('name') || frameElement.getAttribute('id') || ''; - return name; - } - - parentFrame() { - return this._parentFrame; - } - - id() { - return this._frameId; - } - - url() { - return this._url; - } - -} - -class Worker { - constructor(frame, workerDebugger) { - this._frame = frame; - this._workerId = helper.generateId(); - this._workerDebugger = workerDebugger; - - workerDebugger.initialize('chrome://juggler/content/content/WorkerMain.js'); - - this._channel = new SimpleChannel(`content::worker[${this._workerId}]`, 'worker-' + this._workerId); - this._channel.setTransport({ - sendMessage: obj => workerDebugger.postMessage(JSON.stringify(obj)), - dispose: () => {}, - }); - this._workerDebuggerListener = { - QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerListener]), - onMessage: msg => void this._channel._onMessage(JSON.parse(msg)), - onClose: () => void this._channel.dispose(), - onError: (filename, lineno, message) => { - dump(`WARNING: Error in worker: ${message} @${filename}:${lineno}\n`); - }, - }; - workerDebugger.addListener(this._workerDebuggerListener); - } - - channel() { - return this._channel; - } - - frame() { - return this._frame; - } - - id() { - return this._workerId; - } - - url() { - return this._workerDebugger.url; - } - - dispose() { - this._channel.dispose(); - this._workerDebugger.removeListener(this._workerDebuggerListener); - } -} - -function channelId(channel) { - if (channel instanceof Ci.nsIIdentChannel) { - const identChannel = channel.QueryInterface(Ci.nsIIdentChannel); - return String(identChannel.channelId); - } - return helper.generateId(); -} - - -var EXPORTED_SYMBOLS = ['FrameTree']; -this.FrameTree = FrameTree; - diff --git a/additions/juggler/content/JugglerFrameChild.jsm b/additions/juggler/content/JugglerFrameChild.jsm deleted file mode 100644 index 05d9147..0000000 --- a/additions/juggler/content/JugglerFrameChild.jsm +++ /dev/null @@ -1,86 +0,0 @@ -"use strict"; - -const { Helper } = ChromeUtils.import('chrome://juggler/content/Helper.js'); -const { initialize } = ChromeUtils.import('chrome://juggler/content/content/main.js'); - -const Ci = Components.interfaces; -const helper = new Helper(); - -let sameProcessInstanceNumber = 0; - -const topBrowingContextToAgents = new Map(); - -class JugglerFrameChild extends JSWindowActorChild { - constructor() { - super(); - - this._eventListeners = []; - } - - handleEvent(aEvent) { - const agents = this._agents(); - if (!agents) - return; - if (aEvent.type === 'DOMWillOpenModalDialog') { - agents.channel.pause(); - return; - } - if (aEvent.type === 'DOMModalDialogClosed') { - agents.channel.resumeSoon(); - return; - } - if (aEvent.target === this.document) { - agents.pageAgent.onWindowEvent(aEvent); - agents.frameTree.onWindowEvent(aEvent); - } - } - - _agents() { - return topBrowingContextToAgents.get(this.browsingContext.top); - } - - actorCreated() { - this.actorName = `content::${this.browsingContext.browserId}/${this.browsingContext.id}/${++sameProcessInstanceNumber}`; - - this._eventListeners.push(helper.addEventListener(this.contentWindow, 'load', event => { - this._agents()?.pageAgent.onWindowEvent(event); - })); - - if (this.document.documentURI.startsWith('moz-extension://')) - return; - - // Child frame events will be forwarded to related top-level agents. - if (this.browsingContext.parent) - return; - - let agents = topBrowingContextToAgents.get(this.browsingContext); - if (!agents) { - agents = initialize(this.browsingContext, this.docShell); - topBrowingContextToAgents.set(this.browsingContext, agents); - } - agents.channel.bindToActor(this); - agents.actor = this; - } - - didDestroy() { - helper.removeListeners(this._eventListeners); - - if (this.browsingContext.parent) - return; - - const agents = topBrowingContextToAgents.get(this.browsingContext); - // The agents are already re-bound to a new actor. - if (agents?.actor !== this) - return; - - topBrowingContextToAgents.delete(this.browsingContext); - - agents.channel.resetTransport(); - agents.pageAgent.dispose(); - agents.frameTree.dispose(); - } - - receiveMessage() { } -} - -var EXPORTED_SYMBOLS = ['JugglerFrameChild']; diff --git a/additions/juggler/content/PageAgent.js b/additions/juggler/content/PageAgent.js deleted file mode 100644 index 226bea6..0000000 --- a/additions/juggler/content/PageAgent.js +++ /dev/null @@ -1,714 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); -const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm'); -const {setTimeout} = ChromeUtils.import('resource://gre/modules/Timer.jsm'); - -const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService( - Ci.nsIDragService -); -const obs = Cc["@mozilla.org/observer-service;1"].getService( - Ci.nsIObserverService -); - -const helper = new Helper(); - -class WorkerData { - constructor(pageAgent, browserChannel, worker) { - this._workerRuntime = worker.channel().connect('runtime'); - this._browserWorker = browserChannel.connect(worker.id()); - this._worker = worker; - const emit = name => { - return (...args) => this._browserWorker.emit(name, ...args); - }; - this._eventListeners = [ - worker.channel().register('runtime', { - runtimeConsole: emit('runtimeConsole'), - runtimeExecutionContextCreated: emit('runtimeExecutionContextCreated'), - runtimeExecutionContextDestroyed: emit('runtimeExecutionContextDestroyed'), - }), - browserChannel.register(worker.id(), { - evaluate: (options) => this._workerRuntime.send('evaluate', options), - callFunction: (options) => this._workerRuntime.send('callFunction', options), - getObjectProperties: (options) => this._workerRuntime.send('getObjectProperties', options), - disposeObject: (options) => this._workerRuntime.send('disposeObject', options), - }), - ]; - } - - dispose() { - this._workerRuntime.dispose(); - this._browserWorker.dispose(); - helper.removeListeners(this._eventListeners); - } -} - -class PageAgent { - constructor(browserChannel, frameTree) { - this._browserChannel = browserChannel; - this._browserPage = browserChannel.connect('page'); - this._frameTree = frameTree; - this._runtime = frameTree.runtime(); - - this._workerData = new Map(); - - const docShell = frameTree.mainFrame().docShell(); - this._docShell = docShell; - - // Dispatch frameAttached events for all initial frames - for (const frame of this._frameTree.frames()) { - this._onFrameAttached(frame); - if (frame.url()) - this._onNavigationCommitted(frame); - if (frame.pendingNavigationId()) - this._onNavigationStarted(frame); - } - - // Report created workers. - for (const worker of this._frameTree.workers()) - this._onWorkerCreated(worker); - - // Report execution contexts. - this._browserPage.emit('runtimeExecutionContextsCleared', {}); - for (const context of this._runtime.executionContexts()) - this._onExecutionContextCreated(context); - - if (this._frameTree.isPageReady()) { - this._browserPage.emit('pageReady', {}); - const mainFrame = this._frameTree.mainFrame(); - const domWindow = mainFrame.domWindow(); - const document = domWindow ? domWindow.document : null; - const readyState = document ? document.readyState : null; - // Sometimes we initialize later than the first about:blank page is opened. - // In this case, the page might've been loaded already, and we need to issue - // the `DOMContentLoaded` and `load` events. - if (mainFrame.url() === 'about:blank' && readyState === 'complete') - this._emitAllEvents(this._frameTree.mainFrame()); - } - - this._eventListeners = [ - helper.addObserver(this._linkClicked.bind(this, false), 'juggler-link-click'), - helper.addObserver(this._linkClicked.bind(this, true), 'juggler-link-click-sync'), - helper.addObserver(this._onWindowOpenInNewContext.bind(this), 'juggler-window-open-in-new-context'), - helper.addObserver(this._filePickerShown.bind(this), 'juggler-file-picker-shown'), - helper.addObserver(this._onDocumentOpenLoad.bind(this), 'juggler-document-open-loaded'), - helper.on(this._frameTree, 'frameattached', this._onFrameAttached.bind(this)), - helper.on(this._frameTree, 'framedetached', this._onFrameDetached.bind(this)), - helper.on(this._frameTree, 'navigationstarted', this._onNavigationStarted.bind(this)), - helper.on(this._frameTree, 'navigationcommitted', this._onNavigationCommitted.bind(this)), - helper.on(this._frameTree, 'navigationaborted', this._onNavigationAborted.bind(this)), - helper.on(this._frameTree, 'samedocumentnavigation', this._onSameDocumentNavigation.bind(this)), - helper.on(this._frameTree, 'pageready', () => this._browserPage.emit('pageReady', {})), - helper.on(this._frameTree, 'workercreated', this._onWorkerCreated.bind(this)), - helper.on(this._frameTree, 'workerdestroyed', this._onWorkerDestroyed.bind(this)), - helper.on(this._frameTree, 'websocketcreated', event => this._browserPage.emit('webSocketCreated', event)), - helper.on(this._frameTree, 'websocketopened', event => this._browserPage.emit('webSocketOpened', event)), - helper.on(this._frameTree, 'websocketframesent', event => this._browserPage.emit('webSocketFrameSent', event)), - helper.on(this._frameTree, 'websocketframereceived', event => this._browserPage.emit('webSocketFrameReceived', event)), - helper.on(this._frameTree, 'websocketclosed', event => this._browserPage.emit('webSocketClosed', event)), - helper.on(this._frameTree, 'inputevent', inputEvent => { - this._browserPage.emit('pageInputEvent', inputEvent); - if (inputEvent.type === 'dragstart') { - // After the dragStart event is dispatched and handled by Web, - // it might or might not create a new drag session, depending on its preventing default. - setTimeout(() => { - const session = this._getCurrentDragSession(); - this._browserPage.emit('pageInputEvent', { type: 'juggler-drag-finalized', dragSessionStarted: !!session }); - }, 0); - } - }), - helper.addObserver(this._onWindowOpen.bind(this), 'webNavigation-createdNavigationTarget-from-js'), - this._runtime.events.onErrorFromWorker((domWindow, message, stack) => { - const frame = this._frameTree.frameForDocShell(domWindow.docShell); - if (!frame) - return; - this._browserPage.emit('pageUncaughtError', { - frameId: frame.id(), - message, - stack, - }); - }), - this._runtime.events.onConsoleMessage(msg => this._browserPage.emit('runtimeConsole', msg)), - this._runtime.events.onRuntimeError(this._onRuntimeError.bind(this)), - this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)), - this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)), - this._runtime.events.onBindingCalled(this._onBindingCalled.bind(this)), - browserChannel.register('page', { - adoptNode: this._adoptNode.bind(this), - crash: this._crash.bind(this), - describeNode: this._describeNode.bind(this), - dispatchKeyEvent: this._dispatchKeyEvent.bind(this), - dispatchDragEvent: this._dispatchDragEvent.bind(this), - dispatchTouchEvent: this._dispatchTouchEvent.bind(this), - dispatchTapEvent: this._dispatchTapEvent.bind(this), - getContentQuads: this._getContentQuads.bind(this), - getFullAXTree: this._getFullAXTree.bind(this), - insertText: this._insertText.bind(this), - scrollIntoViewIfNeeded: this._scrollIntoViewIfNeeded.bind(this), - setFileInputFiles: this._setFileInputFiles.bind(this), - evaluate: this._runtime.evaluate.bind(this._runtime), - callFunction: this._runtime.callFunction.bind(this._runtime), - getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime), - disposeObject: this._runtime.disposeObject.bind(this._runtime), - }), - ]; - } - - _emitAllEvents(frame) { - this._browserPage.emit('pageEventFired', { - frameId: frame.id(), - name: 'DOMContentLoaded', - }); - this._browserPage.emit('pageEventFired', { - frameId: frame.id(), - name: 'load', - }); - } - - _onExecutionContextCreated(executionContext) { - this._browserPage.emit('runtimeExecutionContextCreated', { - executionContextId: executionContext.id(), - auxData: executionContext.auxData(), - }); - } - - _onExecutionContextDestroyed(executionContext) { - this._browserPage.emit('runtimeExecutionContextDestroyed', { - executionContextId: executionContext.id(), - }); - } - - _onWorkerCreated(worker) { - const workerData = new WorkerData(this, this._browserChannel, worker); - this._workerData.set(worker.id(), workerData); - this._browserPage.emit('pageWorkerCreated', { - workerId: worker.id(), - frameId: worker.frame().id(), - url: worker.url(), - }); - } - - _onWorkerDestroyed(worker) { - const workerData = this._workerData.get(worker.id()); - if (!workerData) - return; - this._workerData.delete(worker.id()); - workerData.dispose(); - this._browserPage.emit('pageWorkerDestroyed', { - workerId: worker.id(), - }); - } - - _onWindowOpen(subject) { - if (!(subject instanceof Ci.nsIPropertyBag2)) - return; - const props = subject.QueryInterface(Ci.nsIPropertyBag2); - const hasUrl = props.hasKey('url'); - const createdDocShell = props.getPropertyAsInterface('createdTabDocShell', Ci.nsIDocShell); - if (!hasUrl && createdDocShell === this._docShell && this._frameTree.forcePageReady()) - this._emitAllEvents(this._frameTree.mainFrame()); - } - - _linkClicked(sync, anchorElement) { - if (anchorElement.ownerGlobal.docShell !== this._docShell) - return; - this._browserPage.emit('pageLinkClicked', { phase: sync ? 'after' : 'before' }); - } - - _onWindowOpenInNewContext(docShell) { - // TODO: unify this with _onWindowOpen if possible. - const frame = this._frameTree.frameForDocShell(docShell); - if (!frame) - return; - this._browserPage.emit('pageWillOpenNewWindowAsynchronously'); - } - - _filePickerShown(inputElement) { - const frame = this._findFrameForNode(inputElement); - if (!frame) - return; - this._browserPage.emit('pageFileChooserOpened', { - executionContextId: frame.mainExecutionContext().id(), - element: frame.mainExecutionContext().rawValueToRemoteObject(inputElement) - }); - } - - _findFrameForNode(node) { - return this._frameTree.frames().find(frame => { - const doc = frame.domWindow().document; - return node === doc || node.ownerDocument === doc; - }); - } - - onWindowEvent(event) { - if (event.type !== 'DOMContentLoaded' && event.type !== 'load') - return; - if (!event.target.ownerGlobal) - return; - const docShell = event.target.ownerGlobal.docShell; - const frame = this._frameTree.frameForDocShell(docShell); - if (!frame) - return; - this._browserPage.emit('pageEventFired', { - frameId: frame.id(), - name: event.type, - }); - } - - _onRuntimeError({ executionContext, message, stack }) { - this._browserPage.emit('pageUncaughtError', { - frameId: executionContext.auxData().frameId, - message: message.toString(), - stack: stack.toString(), - }); - } - - _onDocumentOpenLoad(document) { - const docShell = document.ownerGlobal.docShell; - const frame = this._frameTree.frameForDocShell(docShell); - if (!frame) - return; - this._browserPage.emit('pageEventFired', { - frameId: frame.id(), - name: 'load' - }); - } - - _onNavigationStarted(frame) { - this._browserPage.emit('pageNavigationStarted', { - frameId: frame.id(), - navigationId: frame.pendingNavigationId(), - }); - } - - _onNavigationAborted(frame, navigationId, errorText) { - this._browserPage.emit('pageNavigationAborted', { - frameId: frame.id(), - navigationId, - errorText, - }); - if (!frame._initialNavigationDone && frame !== this._frameTree.mainFrame()) - this._emitAllEvents(frame); - frame._initialNavigationDone = true; - } - - _onSameDocumentNavigation(frame) { - this._browserPage.emit('pageSameDocumentNavigation', { - frameId: frame.id(), - url: frame.url(), - }); - } - - _onNavigationCommitted(frame) { - this._browserPage.emit('pageNavigationCommitted', { - frameId: frame.id(), - navigationId: frame.lastCommittedNavigationId() || undefined, - url: frame.url(), - name: frame.name(), - }); - frame._initialNavigationDone = true; - } - - _onFrameAttached(frame) { - this._browserPage.emit('pageFrameAttached', { - frameId: frame.id(), - parentFrameId: frame.parentFrame() ? frame.parentFrame().id() : undefined, - }); - } - - _onFrameDetached(frame) { - this._browserPage.emit('pageFrameDetached', { - frameId: frame.id(), - }); - } - - _onBindingCalled({executionContextId, name, payload}) { - this._browserPage.emit('pageBindingCalled', { - executionContextId, - name, - payload - }); - } - - dispose() { - for (const workerData of this._workerData.values()) - workerData.dispose(); - this._workerData.clear(); - helper.removeListeners(this._eventListeners); - } - - async _adoptNode({frameId, objectId, executionContextId}) { - const frame = this._frameTree.frame(frameId); - if (!frame) - throw new Error('Failed to find frame with id = ' + frameId); - let unsafeObject; - if (!objectId) { - unsafeObject = frame.domWindow().frameElement; - } else { - unsafeObject = frame.unsafeObject(objectId); - } - const context = this._runtime.findExecutionContext(executionContextId); - const fromPrincipal = unsafeObject.nodePrincipal; - const toFrame = this._frameTree.frame(context.auxData().frameId); - const toPrincipal = toFrame.domWindow().document.nodePrincipal; - if (!toPrincipal.subsumes(fromPrincipal)) - return { remoteObject: null }; - return { remoteObject: context.rawValueToRemoteObject(unsafeObject) }; - } - - async _setFileInputFiles({objectId, frameId, files}) { - const frame = this._frameTree.frame(frameId); - if (!frame) - throw new Error('Failed to find frame with id = ' + frameId); - const unsafeObject = frame.unsafeObject(objectId); - if (!unsafeObject) - throw new Error('Object is not input!'); - let nsFiles; - if (unsafeObject.webkitdirectory) { - nsFiles = await new Directory(files[0]).getFiles(true); - } else { - nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath))); - } - unsafeObject.mozSetFileArray(nsFiles); - const events = [ - new (frame.domWindow().Event)('input', { bubbles: true, cancelable: true, composed: true }), - new (frame.domWindow().Event)('change', { bubbles: true, cancelable: true, composed: true }), - ]; - for (const event of events) - unsafeObject.dispatchEvent(event); - } - - _getContentQuads({objectId, frameId}) { - const frame = this._frameTree.frame(frameId); - if (!frame) - throw new Error('Failed to find frame with id = ' + frameId); - const unsafeObject = frame.unsafeObject(objectId); - if (!unsafeObject.getBoxQuads) - throw new Error('RemoteObject is not a node'); - const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document, recurseWhenNoFrame: true}).map(quad => { - return { - p1: {x: quad.p1.x, y: quad.p1.y}, - p2: {x: quad.p2.x, y: quad.p2.y}, - p3: {x: quad.p3.x, y: quad.p3.y}, - p4: {x: quad.p4.x, y: quad.p4.y}, - }; - }); - return {quads}; - } - - _describeNode({objectId, frameId}) { - const frame = this._frameTree.frame(frameId); - if (!frame) - throw new Error('Failed to find frame with id = ' + frameId); - const unsafeObject = frame.unsafeObject(objectId); - const browsingContextGroup = frame.docShell().browsingContext.group; - const frames = this._frameTree.allFramesInBrowsingContextGroup(browsingContextGroup); - let contentFrame; - let ownerFrame; - for (const frame of frames) { - if (unsafeObject.contentWindow && frame.docShell() === unsafeObject.contentWindow.docShell) - contentFrame = frame; - const document = frame.domWindow().document; - if (unsafeObject === document || unsafeObject.ownerDocument === document) - ownerFrame = frame; - } - return { - contentFrameId: contentFrame ? contentFrame.id() : undefined, - ownerFrameId: ownerFrame ? ownerFrame.id() : undefined, - }; - } - - async _scrollIntoViewIfNeeded({objectId, frameId, rect}) { - const frame = this._frameTree.frame(frameId); - if (!frame) - throw new Error('Failed to find frame with id = ' + frameId); - const unsafeObject = frame.unsafeObject(objectId); - if (!unsafeObject.isConnected) - throw new Error('Node is detached from document'); - if (!rect) - rect = { x: -1, y: -1, width: -1, height: -1}; - if (unsafeObject.scrollRectIntoViewIfNeeded) - unsafeObject.scrollRectIntoViewIfNeeded(rect.x, rect.y, rect.width, rect.height); - else - throw new Error('Node does not have a layout object'); - } - - _getNodeBoundingBox(unsafeObject) { - if (!unsafeObject.getBoxQuads) - throw new Error('RemoteObject is not a node'); - const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document}); - if (!quads.length) - return; - let x1 = Infinity; - let y1 = Infinity; - let x2 = -Infinity; - let y2 = -Infinity; - for (const quad of quads) { - const boundingBox = quad.getBounds(); - x1 = Math.min(boundingBox.x, x1); - y1 = Math.min(boundingBox.y, y1); - x2 = Math.max(boundingBox.x + boundingBox.width, x2); - y2 = Math.max(boundingBox.y + boundingBox.height, y2); - } - return {x: x1, y: y1, width: x2 - x1, height: y2 - y1}; - } - - async _dispatchKeyEvent({type, keyCode, code, key, repeat, location, text}) { - const frame = this._frameTree.mainFrame(); - const tip = frame.textInputProcessor(); - let keyEvent = new (frame.domWindow().KeyboardEvent)("", { - key, - code, - location, - repeat, - keyCode - }); - if (type === 'keydown') { - if (text && text !== key) { - tip.commitCompositionWith(text, keyEvent); - } else { - const flags = 0; - tip.keydown(keyEvent, flags); - } - } else if (type === 'keyup') { - if (text) - throw new Error(`keyup does not support text option`); - const flags = 0; - tip.keyup(keyEvent, flags); - } else { - throw new Error(`Unknown type ${type}`); - } - } - - async _dispatchTouchEvent({type, touchPoints, modifiers}) { - const frame = this._frameTree.mainFrame(); - const defaultPrevented = frame.domWindow().windowUtils.sendTouchEvent( - type.toLowerCase(), - touchPoints.map((point, id) => id), - touchPoints.map(point => point.x), - touchPoints.map(point => point.y), - touchPoints.map(point => point.radiusX === undefined ? 1.0 : point.radiusX), - touchPoints.map(point => point.radiusY === undefined ? 1.0 : point.radiusY), - touchPoints.map(point => point.rotationAngle === undefined ? 0.0 : point.rotationAngle), - touchPoints.map(point => point.force === undefined ? 1.0 : point.force), - touchPoints.map(point => 0), - touchPoints.map(point => 0), - touchPoints.map(point => 0), - modifiers); - return {defaultPrevented}; - } - - async _dispatchTapEvent({x, y, modifiers}) { - // Force a layout at the point in question, because touch events - // do not seem to trigger one like mouse events. - this._frameTree.mainFrame().domWindow().windowUtils.elementFromPoint( - x, - y, - false /* aIgnoreRootScrollFrame */, - true /* aFlushLayout */); - - await this._dispatchTouchEvent({ - type: 'touchstart', - modifiers, - touchPoints: [{x, y}] - }); - await this._dispatchTouchEvent({ - type: 'touchend', - modifiers, - touchPoints: [{x, y}] - }); - } - - _getCurrentDragSession() { - const frame = this._frameTree.mainFrame(); - const domWindow = frame?.domWindow(); - return domWindow ? dragService.getCurrentSession(domWindow) : undefined; - } - - async _dispatchDragEvent({type, x, y, modifiers}) { - const session = this._getCurrentDragSession(); - const dropEffect = session.dataTransfer.dropEffect; - - if ((type === 'drop' && dropEffect !== 'none') || type === 'dragover') { - const win = this._frameTree.mainFrame().domWindow(); - win.windowUtils.jugglerSendMouseEvent( - type, - x, - y, - 0, /*button*/ - 0, /*clickCount*/ - modifiers, - false /*aIgnoreRootScrollFrame*/, - 0.0 /*pressure*/, - 0 /*inputSource*/, - false /*isDOMEventSynthesized*/, - false /*isWidgetEventSynthesized*/, - 0 /*buttons*/, - win.windowUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */, - false /*disablePointerEvent*/, - ); - return; - } - if (type === 'dragend') { - const session = this._getCurrentDragSession(); - session?.endDragSession(true); - return; - } - } - - async _insertText({text}) { - const frame = this._frameTree.mainFrame(); - frame.textInputProcessor().commitCompositionWith(text); - } - - async _crash() { - dump(`Crashing intentionally\n`); - // This is to intentionally crash the frame. - // We crash by using js-ctypes and dereferencing - // a bad pointer. The crash should happen immediately - // upon loading this frame script. - const { ctypes } = ChromeUtils.import('resource://gre/modules/ctypes.jsm'); - ChromeUtils.privateNoteIntentionalCrash(); - const zero = new ctypes.intptr_t(8); - const badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t)); - badptr.contents; - } - - async _getFullAXTree({objectId}) { - let unsafeObject = null; - if (objectId) { - unsafeObject = this._frameTree.mainFrame().unsafeObject(objectId); - if (!unsafeObject) - throw new Error(`No object found for id "${objectId}"`); - } - - const service = Cc["@mozilla.org/accessibilityService;1"] - .getService(Ci.nsIAccessibilityService); - const document = this._frameTree.mainFrame().domWindow().document; - const docAcc = service.getAccessibleFor(document); - - while (docAcc.document.isUpdatePendingForJugglerAccessibility) - await new Promise(x => this._frameTree.mainFrame().domWindow().requestAnimationFrame(x)); - - async function waitForQuiet() { - let state = {}; - docAcc.getState(state, {}); - if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0) - return; - let resolve, reject; - const promise = new Promise((x, y) => {resolve = x, reject = y}); - let eventObserver = { - observe(subject, topic) { - if (topic !== "accessible-event") { - return; - } - - // If event type does not match expected type, skip the event. - let event = subject.QueryInterface(Ci.nsIAccessibleEvent); - if (event.eventType !== Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) { - return; - } - - // If event's accessible does not match expected accessible, - // skip the event. - if (event.accessible !== docAcc) { - return; - } - - Services.obs.removeObserver(this, "accessible-event"); - resolve(); - }, - }; - Services.obs.addObserver(eventObserver, "accessible-event"); - return promise; - } - function buildNode(accElement) { - let a = {}, b = {}; - accElement.getState(a, b); - const tree = { - role: service.getStringRole(accElement.role), - name: accElement.name || '', - }; - if (unsafeObject && unsafeObject === accElement.DOMNode) - tree.foundObject = true; - for (const userStringProperty of [ - 'value', - 'description' - ]) { - tree[userStringProperty] = accElement[userStringProperty] || undefined; - } - - const states = {}; - for (const name of service.getStringStates(a.value, b.value)) - states[name] = true; - for (const name of ['selected', - 'focused', - 'pressed', - 'focusable', - 'required', - 'invalid', - 'modal', - 'editable', - 'busy', - 'checked', - 'multiselectable']) { - if (states[name]) - tree[name] = true; - } - - if (states['multi line']) - tree['multiline'] = true; - if (states['editable'] && states['readonly']) - tree['readonly'] = true; - if (states['checked']) - tree['checked'] = true; - if (states['mixed']) - tree['checked'] = 'mixed'; - if (states['expanded']) - tree['expanded'] = true; - else if (states['collapsed']) - tree['expanded'] = false; - if (!states['enabled']) - tree['disabled'] = true; - - const attributes = {}; - if (accElement.attributes) { - for (const { key, value } of accElement.attributes.enumerate()) { - attributes[key] = value; - } - } - for (const numericalProperty of ['level']) { - if (numericalProperty in attributes) - tree[numericalProperty] = parseFloat(attributes[numericalProperty]); - } - for (const stringProperty of ['tag', 'roledescription', 'valuetext', 'orientation', 'autocomplete', 'keyshortcuts', 'haspopup']) { - if (stringProperty in attributes) - tree[stringProperty] = attributes[stringProperty]; - } - const children = []; - - for (let child = accElement.firstChild; child; child = child.nextSibling) { - children.push(buildNode(child)); - } - if (children.length) - tree.children = children; - return tree; - } - await waitForQuiet(); - return { - tree: buildNode(docAcc) - }; - } -} - -var EXPORTED_SYMBOLS = ['PageAgent']; -this.PageAgent = PageAgent; - diff --git a/additions/juggler/content/Runtime.js b/additions/juggler/content/Runtime.js deleted file mode 100644 index a3e2ce2..0000000 --- a/additions/juggler/content/Runtime.js +++ /dev/null @@ -1,695 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; -// Note: this file should be loadabale with eval() into worker environment. -// Avoid Components.*, ChromeUtils and global const variables. - -if (!this.Debugger) { - // Worker has a Debugger defined already. - const {addDebuggerToGlobal} = ChromeUtils.import("resource://gre/modules/jsdebugger.jsm", {}); - addDebuggerToGlobal(Components.utils.getGlobalForObject(this)); -} - -let lastId = 0; -function generateId() { - return 'id-' + (++lastId); -} - -const consoleLevelToProtocolType = { - 'dir': 'dir', - 'log': 'log', - 'debug': 'debug', - 'info': 'info', - 'error': 'error', - 'warn': 'warning', - 'dirxml': 'dirxml', - 'table': 'table', - 'trace': 'trace', - 'clear': 'clear', - 'group': 'startGroup', - 'groupCollapsed': 'startGroupCollapsed', - 'groupEnd': 'endGroup', - 'assert': 'assert', - 'profile': 'profile', - 'profileEnd': 'profileEnd', - 'count': 'count', - 'countReset': 'countReset', - 'time': null, - 'timeLog': 'timeLog', - 'timeEnd': 'timeEnd', - 'timeStamp': 'timeStamp', -}; - -const disallowedMessageCategories = new Set([ - 'XPConnect JavaScript', - 'component javascript', - 'chrome javascript', - 'chrome registration', - 'XBL', - 'XBL Prototype Handler', - 'XBL Content Sink', - 'xbl javascript', -]); - -class Runtime { - constructor(isWorker = false) { - this._debugger = new Debugger(); - this._pendingPromises = new Map(); - this._executionContexts = new Map(); - this._windowToExecutionContext = new Map(); - this._eventListeners = []; - if (isWorker) { - this._registerWorkerConsoleHandler(); - } else { - this._registerConsoleServiceListener(Services); - this._registerConsoleAPIListener(Services); - } - // We can't use event listener here to be compatible with Worker Global Context. - // Use plain callbacks instead. - this.events = { - onConsoleMessage: createEvent(), - onRuntimeError: createEvent(), - onErrorFromWorker: createEvent(), - onExecutionContextCreated: createEvent(), - onExecutionContextDestroyed: createEvent(), - onBindingCalled: createEvent(), - }; - } - - executionContexts() { - return [...this._executionContexts.values()]; - } - - async evaluate({executionContextId, expression, returnByValue}) { - const executionContext = this.findExecutionContext(executionContextId); - if (!executionContext) - throw new Error('Failed to find execution context with id = ' + executionContextId); - const exceptionDetails = {}; - let result = await executionContext.evaluateScript(expression, exceptionDetails); - if (!result) - return {exceptionDetails}; - if (returnByValue) - result = executionContext.ensureSerializedToValue(result); - return {result}; - } - - async callFunction({executionContextId, functionDeclaration, args, returnByValue}) { - const executionContext = this.findExecutionContext(executionContextId); - if (!executionContext) - throw new Error('Failed to find execution context with id = ' + executionContextId); - - // Hijack the utilityScript.evaluate function to evaluate in the main world - if ( - ChromeUtils.camouGetBool('allowMainWorld', false) && - functionDeclaration.includes('utilityScript.evaluate') && - args.length >= 4 && - args[3].value && - typeof args[3].value === 'string' && - args[3].value.startsWith('mw:')) { - ChromeUtils.camouDebug(`Evaluating in main world: ${args[3].value}`); - const mainWorldScript = args[3].value.substring(3); - - // Get the main world execution context - const mainContext = executionContext.mainEquivalent; - if (!mainContext) { - throw new Error(`Main world injection is not enabled.`); - } - // Extract arguments for the main world function - const functionArgs = args[5]?.value?.a || []; - const exceptionDetails = {}; - const result = mainContext.executeInGlobal(mainWorldScript, functionArgs, exceptionDetails); - if (!result) - return {exceptionDetails}; - return {result}; - } - - const exceptionDetails = {}; - let result = await executionContext.evaluateFunction(functionDeclaration, args, exceptionDetails); - if (!result) - return {exceptionDetails}; - if (returnByValue) - result = executionContext.ensureSerializedToValue(result); - return {result}; - } - - async getObjectProperties({executionContextId, objectId}) { - const executionContext = this.findExecutionContext(executionContextId); - if (!executionContext) - throw new Error('Failed to find execution context with id = ' + executionContextId); - return {properties: executionContext.getObjectProperties(objectId)}; - } - - async disposeObject({executionContextId, objectId}) { - const executionContext = this.findExecutionContext(executionContextId); - if (!executionContext) - throw new Error('Failed to find execution context with id = ' + executionContextId); - return executionContext.disposeObject(objectId); - } - - _registerConsoleServiceListener(Services) { - const Ci = Components.interfaces; - const consoleServiceListener = { - QueryInterface: ChromeUtils.generateQI([Ci.nsIConsoleListener]), - - observe: message => { - if (!(message instanceof Ci.nsIScriptError) || !message.outerWindowID || - !message.category || disallowedMessageCategories.has(message.category)) { - return; - } - const errorWindow = Services.wm.getOuterWindowWithId(message.outerWindowID); - if (message.category === 'Web Worker' && message.logLevel === Ci.nsIConsoleMessage.error) { - emitEvent(this.events.onErrorFromWorker, errorWindow, message.message, '' + message.stack); - return; - } - const executionContext = this._windowToExecutionContext.get(errorWindow); - if (!executionContext) { - return; - } - const typeNames = { - [Ci.nsIConsoleMessage.debug]: 'debug', - [Ci.nsIConsoleMessage.info]: 'info', - [Ci.nsIConsoleMessage.warn]: 'warn', - [Ci.nsIConsoleMessage.error]: 'error', - }; - if (!message.hasException) { - emitEvent(this.events.onConsoleMessage, { - args: [{ - value: message.message, - }], - type: typeNames[message.logLevel], - executionContextId: executionContext.id(), - location: { - lineNumber: message.lineNumber, - columnNumber: message.columnNumber, - url: message.sourceName, - }, - }); - } else { - emitEvent(this.events.onRuntimeError, { - executionContext, - message: message.errorMessage, - stack: message.stack?.toString() || '', - }); - } - }, - }; - Services.console.registerListener(consoleServiceListener); - this._eventListeners.push(() => Services.console.unregisterListener(consoleServiceListener)); - } - - _registerConsoleAPIListener(Services) { - const Ci = Components.interfaces; - const Cc = Components.classes; - const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(Ci.nsIConsoleAPIStorage); - const onMessage = ({ wrappedJSObject }) => { - const executionContext = Array.from(this._executionContexts.values()).find(context => { - // There is no easy way to determine isolated world context and we normally don't write - // objects to console from utility worlds so we always return main world context here. - if (context._isIsolatedWorldContext()) - return false; - const domWindow = context._domWindow; - try { - // `windowGlobalChild` might be dead already; accessing it will throw an error, message in a console, - // and infinite recursion. - return domWindow && domWindow.windowGlobalChild.innerWindowId === wrappedJSObject.innerID; - } catch (e) { - return false; - } - }); - if (!executionContext) - return; - this._onConsoleMessage(executionContext, wrappedJSObject); - } - ConsoleAPIStorage.addLogEventListener( - onMessage, - Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) - ); - this._eventListeners.push(() => ConsoleAPIStorage.removeLogEventListener(onMessage)); - } - - _registerWorkerConsoleHandler() { - setConsoleEventHandler(message => { - const executionContext = Array.from(this._executionContexts.values())[0]; - this._onConsoleMessage(executionContext, message); - }); - this._eventListeners.push(() => setConsoleEventHandler(null)); - } - - _onConsoleMessage(executionContext, message) { - const type = consoleLevelToProtocolType[message.level]; - if (!type) - return; - const args = message.arguments.map(arg => executionContext.rawValueToRemoteObject(arg)); - emitEvent(this.events.onConsoleMessage, { - args, - type, - executionContextId: executionContext.id(), - location: { - lineNumber: message.lineNumber - 1, - columnNumber: message.columnNumber - 1, - url: message.filename, - }, - }); - } - - dispose() { - for (const tearDown of this._eventListeners) - tearDown.call(null); - this._eventListeners = []; - } - - async _awaitPromise(executionContext, obj, exceptionDetails = {}) { - if (obj.promiseState === 'fulfilled') - return {success: true, obj: obj.promiseValue}; - if (obj.promiseState === 'rejected') { - const debuggee = executionContext._debuggee; - exceptionDetails.text = debuggee.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}, {useInnerBindings: true}).return; - exceptionDetails.stack = debuggee.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}, {useInnerBindings: true}).return; - return {success: false, obj: null}; - } - let resolve, reject; - const promise = new Promise((a, b) => { - resolve = a; - reject = b; - }); - this._pendingPromises.set(obj.promiseID, {resolve, reject, executionContext, exceptionDetails}); - if (this._pendingPromises.size === 1) - this._debugger.onPromiseSettled = this._onPromiseSettled.bind(this); - return await promise; - } - - _onPromiseSettled(obj) { - const pendingPromise = this._pendingPromises.get(obj.promiseID); - if (!pendingPromise) - return; - this._pendingPromises.delete(obj.promiseID); - if (!this._pendingPromises.size) - this._debugger.onPromiseSettled = undefined; - - if (obj.promiseState === 'fulfilled') { - pendingPromise.resolve({success: true, obj: obj.promiseValue}); - return; - }; - const debuggee = pendingPromise.executionContext._debuggee; - pendingPromise.exceptionDetails.text = debuggee.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}, {useInnerBindings: true}).return; - pendingPromise.exceptionDetails.stack = debuggee.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}, {useInnerBindings: true}).return; - pendingPromise.resolve({success: false, obj: null}); - } - - createExecutionContext(domWindow, contextGlobal, auxData) { - // Note: domWindow is null for workers. - const context = new ExecutionContext(this, domWindow, contextGlobal, auxData); - this._executionContexts.set(context._id, context); - if (domWindow) - this._windowToExecutionContext.set(domWindow, context); - emitEvent(this.events.onExecutionContextCreated, context); - return context; - } - - createMW(domWindow, contextGlobal) { - const context = new MainWorldContext(this, domWindow, contextGlobal); - return context; - } - - findExecutionContext(executionContextId) { - const executionContext = this._executionContexts.get(executionContextId); - if (!executionContext) - throw new Error('Failed to find execution context with id = ' + executionContextId); - return executionContext; - } - - destroyExecutionContext(destroyedContext) { - for (const [promiseID, {reject, executionContext}] of this._pendingPromises) { - if (executionContext === destroyedContext) { - reject(new Error('Execution context was destroyed!')); - this._pendingPromises.delete(promiseID); - } - } - if (!this._pendingPromises.size) - this._debugger.onPromiseSettled = undefined; - this._debugger.removeDebuggee(destroyedContext._contextGlobal); - this._executionContexts.delete(destroyedContext._id); - if (destroyedContext._domWindow) - this._windowToExecutionContext.delete(destroyedContext._domWindow); - emitEvent(this.events.onExecutionContextDestroyed, destroyedContext); - } -} - -class MainWorldContext { - constructor(runtime, domWindow, contextGlobal) { - this._runtime = runtime; - this._domWindow = domWindow; - this._contextGlobal = contextGlobal; - this._debuggee = runtime._debugger.addDebuggee(contextGlobal); - } - - _getResult(completionValue, exceptionDetails = {}) { - if (!completionValue) { - exceptionDetails.text = "Evaluation terminated"; - return {success: false, obj: null}; - } - - if (completionValue.throw) { - const result = this._debuggee.executeInGlobalWithBindings(` - (function(error) { - try { - if (error instanceof Error) { - return error.toString(); - } - return String(error); - } catch(e) { - return "Unknown error occurred"; - } - })(e) - `, { e: completionValue.throw }); - - exceptionDetails.text = result.return || "Unknown error"; - return {success: false, obj: null}; - } - - return {success: true, obj: completionValue.return}; - } - - executeInGlobal(script, args = [], exceptionDetails = {}) { - try { - const wrappedScript = ` - (() => { - let _s = (${script}); - let _r = typeof _s === 'function' - ? _s(${args.map(arg => JSON.stringify(arg)).join(', ')}) - : _s; - return JSON.stringify({value: _r}); - })() - `; - - const result = this._debuggee.executeInGlobal(wrappedScript); - - let {success, obj} = this._getResult(result, exceptionDetails); - if (!success) { - return {exceptionDetails}; - } - return JSON.parse(obj); - } catch (e) { - exceptionDetails.text = e.message; - exceptionDetails.stack = e.stack; - return {exceptionDetails}; - } - } -} - -class ExecutionContext { - constructor(runtime, domWindow, contextGlobal, auxData) { - this._runtime = runtime; - this._domWindow = domWindow; - this._contextGlobal = contextGlobal; - this._debuggee = runtime._debugger.addDebuggee(contextGlobal); - this._remoteObjects = new Map(); - this._id = generateId(); - this._auxData = auxData; - this._jsonStringifyObject = this._debuggee.executeInGlobal(`((stringify, object) => { - const oldToJSON = Date.prototype?.toJSON; - if (oldToJSON) - Date.prototype.toJSON = undefined; - const oldArrayToJSON = Array.prototype.toJSON; - const oldArrayHadToJSON = Array.prototype.hasOwnProperty('toJSON'); - if (oldArrayHadToJSON) - Array.prototype.toJSON = undefined; - - let hasSymbol = false; - const result = stringify(object, (key, value) => { - if (typeof value === 'symbol') - hasSymbol = true; - return value; - }); - - if (oldToJSON) - Date.prototype.toJSON = oldToJSON; - if (oldArrayHadToJSON) - Array.prototype.toJSON = oldArrayToJSON; - - return hasSymbol ? undefined : result; - }).bind(null, JSON.stringify.bind(JSON))`).return; - - this.mainEquivalent = undefined; - } - - id() { - return this._id; - } - - auxData() { - return this._auxData; - } - - _isIsolatedWorldContext() { - return !!this._auxData.name; - } - - async evaluateScript(script, exceptionDetails = {}) { - const userInputHelper = this._domWindow ? this._domWindow.windowUtils.setHandlingUserInput(true) : null; - if (this._domWindow && this._domWindow.document) - this._domWindow.document.notifyUserGestureActivation(); - - let {success, obj} = this._getResult(this._debuggee.executeInGlobal(script), exceptionDetails); - userInputHelper && userInputHelper.destruct(); - if (!success) - return null; - if (obj && obj.isPromise) { - const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails); - if (!awaitResult.success) - return null; - obj = awaitResult.obj; - } - return this._createRemoteObject(obj); - } - - evaluateScriptSafely(script) { - try { - this._debuggee.executeInGlobal(script); - } catch (e) { - dump(`WARNING: ${e.message}\n${e.stack}\n`); - } - } - - async evaluateFunction(functionText, args, exceptionDetails = {}) { - const funEvaluation = this._getResult(this._debuggee.executeInGlobal('(' + functionText + ')'), exceptionDetails); - if (!funEvaluation.success) - return null; - if (!funEvaluation.obj.callable) - throw new Error('functionText does not evaluate to a function!'); - args = args.map(arg => { - if (arg.objectId) { - if (!this._remoteObjects.has(arg.objectId)) - throw new Error('Cannot find object with id = ' + arg.objectId); - return this._remoteObjects.get(arg.objectId); - } - switch (arg.unserializableValue) { - case 'Infinity': return Infinity; - case '-Infinity': return -Infinity; - case '-0': return -0; - case 'NaN': return NaN; - default: return this._toDebugger(arg.value); - } - }); - const userInputHelper = this._domWindow ? this._domWindow.windowUtils.setHandlingUserInput(true) : null; - if (this._domWindow && this._domWindow.document) - this._domWindow.document.notifyUserGestureActivation(); - let {success, obj} = this._getResult(funEvaluation.obj.apply(null, args), exceptionDetails); - userInputHelper && userInputHelper.destruct(); - if (!success) - return null; - if (obj && obj.isPromise) { - const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails); - if (!awaitResult.success) - return null; - obj = awaitResult.obj; - } - return this._createRemoteObject(obj); - } - - addBinding(name, script) { - Cu.exportFunction((...args) => { - emitEvent(this._runtime.events.onBindingCalled, { - executionContextId: this._id, - name, - payload: args[0], - }); - }, this._contextGlobal, { - defineAs: name, - }); - this.evaluateScriptSafely(script); - } - - unsafeObject(objectId) { - if (!this._remoteObjects.has(objectId)) - return; - return { object: this._remoteObjects.get(objectId).unsafeDereference() }; - } - - rawValueToRemoteObject(rawValue) { - const debuggerObj = this._debuggee.makeDebuggeeValue(rawValue); - return this._createRemoteObject(debuggerObj); - } - - _instanceOf(debuggerObj, rawObj, className) { - if (this._domWindow) - return rawObj instanceof this._domWindow[className]; - return this._debuggee.executeInGlobalWithBindings('o instanceof this[className]', {o: debuggerObj, className: this._debuggee.makeDebuggeeValue(className)}, {useInnerBindings: true}).return; - } - - _createRemoteObject(debuggerObj) { - if (debuggerObj instanceof Debugger.Object) { - const objectId = generateId(); - this._remoteObjects.set(objectId, debuggerObj); - const rawObj = debuggerObj.unsafeDereference(); - const type = typeof rawObj; - let subtype = undefined; - if (debuggerObj.isProxy) - subtype = 'proxy'; - else if (Array.isArray(rawObj)) - subtype = 'array'; - else if (Object.is(rawObj, null)) - subtype = 'null'; - else if (typeof Node !== 'undefined' && Node.isInstance(rawObj)) - subtype = 'node'; - else if (this._instanceOf(debuggerObj, rawObj, 'RegExp')) - subtype = 'regexp'; - else if (this._instanceOf(debuggerObj, rawObj, 'Date')) - subtype = 'date'; - else if (this._instanceOf(debuggerObj, rawObj, 'Map')) - subtype = 'map'; - else if (this._instanceOf(debuggerObj, rawObj, 'Set')) - subtype = 'set'; - else if (this._instanceOf(debuggerObj, rawObj, 'WeakMap')) - subtype = 'weakmap'; - else if (this._instanceOf(debuggerObj, rawObj, 'WeakSet')) - subtype = 'weakset'; - else if (this._instanceOf(debuggerObj, rawObj, 'Error')) - subtype = 'error'; - else if (this._instanceOf(debuggerObj, rawObj, 'Promise')) - subtype = 'promise'; - else if ((this._instanceOf(debuggerObj, rawObj, 'Int8Array')) || (this._instanceOf(debuggerObj, rawObj, 'Uint8Array')) || - (this._instanceOf(debuggerObj, rawObj, 'Uint8ClampedArray')) || (this._instanceOf(debuggerObj, rawObj, 'Int16Array')) || - (this._instanceOf(debuggerObj, rawObj, 'Uint16Array')) || (this._instanceOf(debuggerObj, rawObj, 'Int32Array')) || - (this._instanceOf(debuggerObj, rawObj, 'Uint32Array')) || (this._instanceOf(debuggerObj, rawObj, 'Float32Array')) || - (this._instanceOf(debuggerObj, rawObj, 'Float64Array'))) { - subtype = 'typedarray'; - } - return {objectId, type, subtype}; - } - if (typeof debuggerObj === 'symbol') { - const objectId = generateId(); - this._remoteObjects.set(objectId, debuggerObj); - return {objectId, type: 'symbol'}; - } - - let unserializableValue = undefined; - if (Object.is(debuggerObj, NaN)) - unserializableValue = 'NaN'; - else if (Object.is(debuggerObj, -0)) - unserializableValue = '-0'; - else if (Object.is(debuggerObj, Infinity)) - unserializableValue = 'Infinity'; - else if (Object.is(debuggerObj, -Infinity)) - unserializableValue = '-Infinity'; - return unserializableValue ? {unserializableValue} : {value: debuggerObj}; - } - - ensureSerializedToValue(protocolObject) { - if (!protocolObject.objectId) - return protocolObject; - const obj = this._remoteObjects.get(protocolObject.objectId); - this._remoteObjects.delete(protocolObject.objectId); - return {value: this._serialize(obj)}; - } - - _toDebugger(obj) { - if (typeof obj !== 'object') - return obj; - if (obj === null) - return obj; - const properties = {}; - for (let [key, value] of Object.entries(obj)) { - properties[key] = { - configurable: true, - writable: true, - enumerable: true, - value: this._toDebugger(value), - }; - } - const baseObject = Array.isArray(obj) ? '([])' : '({})'; - const debuggerObj = this._debuggee.executeInGlobal(baseObject).return; - debuggerObj.defineProperties(properties); - return debuggerObj; - } - - _serialize(obj) { - const result = this._debuggee.executeInGlobalWithBindings('stringify(e)', {e: obj, stringify: this._jsonStringifyObject}, {useInnerBindings: true}); - if (result.throw) - throw new Error('Object is not serializable'); - return result.return === undefined ? undefined : JSON.parse(result.return); - } - - disposeObject(objectId) { - this._remoteObjects.delete(objectId); - } - - getObjectProperties(objectId) { - if (!this._remoteObjects.has(objectId)) - throw new Error('Cannot find object with id = ' + arg.objectId); - const result = []; - for (let obj = this._remoteObjects.get(objectId); obj; obj = obj.proto) { - for (const propertyName of obj.getOwnPropertyNames()) { - const descriptor = obj.getOwnPropertyDescriptor(propertyName); - if (!descriptor.enumerable) - continue; - result.push({ - name: propertyName, - value: this._createRemoteObject(descriptor.value), - }); - } - } - return result; - } - - _getResult(completionValue, exceptionDetails = {}) { - if (!completionValue) - throw new Error('evaluation terminated'); - if (completionValue.throw) { - if (this._debuggee.executeInGlobalWithBindings('e instanceof Error', {e: completionValue.throw}, {useInnerBindings: true}).return) { - exceptionDetails.text = this._debuggee.executeInGlobalWithBindings('e.message', {e: completionValue.throw}, {useInnerBindings: true}).return; - exceptionDetails.stack = this._debuggee.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}, {useInnerBindings: true}).return; - } else { - exceptionDetails.value = this._serialize(completionValue.throw); - } - return {success: false, obj: null}; - } - return {success: true, obj: completionValue.return}; - } -} - -const listenersSymbol = Symbol('listeners'); - -function createEvent() { - const listeners = new Set(); - const subscribeFunction = listener => { - listeners.add(listener); - return () => listeners.delete(listener); - } - subscribeFunction[listenersSymbol] = listeners; - return subscribeFunction; -} - -function emitEvent(event, ...args) { - let listeners = event[listenersSymbol]; - if (!listeners || !listeners.size) - return; - listeners = new Set(listeners); - for (const listener of listeners) - listener.call(null, ...args); -} - -var EXPORTED_SYMBOLS = ['Runtime']; -this.Runtime = Runtime; diff --git a/additions/juggler/content/WorkerMain.js b/additions/juggler/content/WorkerMain.js deleted file mode 100644 index 9be6885..0000000 --- a/additions/juggler/content/WorkerMain.js +++ /dev/null @@ -1,78 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; -loadSubScript('chrome://juggler/content/content/Runtime.js'); -loadSubScript('chrome://juggler/content/SimpleChannel.js'); - -// SimpleChannel in worker is never replaced: its lifetime matches the lifetime -// of the worker itself, so anything would work as a unique identifier. -const channel = new SimpleChannel('worker::content', 'unique_identifier'); -const eventListener = event => channel._onMessage(JSON.parse(event.data)); -this.addEventListener('message', eventListener); -channel.setTransport({ - sendMessage: msg => postMessage(JSON.stringify(msg)), - dispose: () => this.removeEventListener('message', eventListener), -}); - -const runtime = new Runtime(true /* isWorker */); - -(() => { - // Create execution context in the runtime only when the script - // source was actually evaluated in it. - const dbg = new Debugger(global); - if (dbg.findScripts({global}).length) { - runtime.createExecutionContext(null /* domWindow */, global, {}); - } else { - dbg.onNewScript = function(s) { - dbg.onNewScript = undefined; - dbg.removeAllDebuggees(); - runtime.createExecutionContext(null /* domWindow */, global, {}); - }; - } -})(); - -class RuntimeAgent { - constructor(runtime, channel) { - this._runtime = runtime; - this._browserRuntime = channel.connect('runtime'); - - for (const context of this._runtime.executionContexts()) - this._onExecutionContextCreated(context); - - this._eventListeners = [ - this._runtime.events.onConsoleMessage(msg => this._browserRuntime.emit('runtimeConsole', msg)), - this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)), - this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)), - channel.register('runtime', { - evaluate: this._runtime.evaluate.bind(this._runtime), - callFunction: this._runtime.callFunction.bind(this._runtime), - getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime), - disposeObject: this._runtime.disposeObject.bind(this._runtime), - }), - ]; - } - - _onExecutionContextCreated(executionContext) { - this._browserRuntime.emit('runtimeExecutionContextCreated', { - executionContextId: executionContext.id(), - auxData: executionContext.auxData(), - }); - } - - _onExecutionContextDestroyed(executionContext) { - this._browserRuntime.emit('runtimeExecutionContextDestroyed', { - executionContextId: executionContext.id(), - }); - } - - dispose() { - for (const disposer of this._eventListeners) - disposer(); - this._eventListeners = []; - } -} - -new RuntimeAgent(runtime, channel); - diff --git a/additions/juggler/content/hidden-scrollbars.css b/additions/juggler/content/hidden-scrollbars.css deleted file mode 100644 index 26fc0db..0000000 --- a/additions/juggler/content/hidden-scrollbars.css +++ /dev/null @@ -1,7 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -* { - scrollbar-width: none !important; -} diff --git a/additions/juggler/content/main.js b/additions/juggler/content/main.js deleted file mode 100644 index 7eaa704..0000000 --- a/additions/juggler/content/main.js +++ /dev/null @@ -1,119 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); -const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js'); -const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js'); -const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js'); - -const helper = new Helper(); - -function initialize(browsingContext, docShell) { - const data = { channel: undefined, pageAgent: undefined, frameTree: undefined, failedToOverrideTimezone: false }; - - const applySetting = { - geolocation: (geolocation) => { - if (geolocation) { - docShell.setGeolocationOverride({ - coords: { - latitude: geolocation.latitude, - longitude: geolocation.longitude, - accuracy: geolocation.accuracy, - altitude: NaN, - altitudeAccuracy: NaN, - heading: NaN, - speed: NaN, - }, - address: null, - timestamp: Date.now() - }); - } else { - docShell.setGeolocationOverride(null); - } - }, - - bypassCSP: (bypassCSP) => { - docShell.bypassCSPEnabled = bypassCSP; - }, - - timezoneId: (timezoneId) => { - data.failedToOverrideTimezone = !docShell.overrideTimezone(timezoneId); - }, - - locale: (locale) => { - docShell.languageOverride = locale; - }, - - javaScriptDisabled: (javaScriptDisabled) => { - data.frameTree.setJavaScriptDisabled(javaScriptDisabled); - }, - }; - - const contextCrossProcessCookie = Services.cpmm.sharedData.get('juggler:context-cookie-' + browsingContext.originAttributes.userContextId) || { initScripts: [], bindings: [], settings: {} }; - const pageCrossProcessCookie = Services.cpmm.sharedData.get('juggler:page-cookie-' + browsingContext.browserId) || { initScripts: [], bindings: [], interceptFileChooserDialog: false }; - - // Enforce focused state for all top level documents. - docShell.overrideHasFocus = true; - docShell.forceActiveState = true; - docShell.disallowBFCache = true; - data.frameTree = new FrameTree(browsingContext); - for (const [name, value] of Object.entries(contextCrossProcessCookie.settings)) { - if (value !== undefined) - applySetting[name](value); - } - for (const { worldName, name, script } of [...contextCrossProcessCookie.bindings, ...pageCrossProcessCookie.bindings]) - data.frameTree.addBinding(worldName, name, script); - data.frameTree.setInitScripts([...contextCrossProcessCookie.initScripts, ...pageCrossProcessCookie.initScripts]); - data.channel = new SimpleChannel('', 'process-' + Services.appinfo.processID); - data.pageAgent = new PageAgent(data.channel, data.frameTree); - docShell.fileInputInterceptionEnabled = !!pageCrossProcessCookie.interceptFileChooserDialog; - - data.channel.register('', { - setInitScripts(scripts) { - data.frameTree.setInitScripts(scripts); - }, - - addBinding({worldName, name, script}) { - data.frameTree.addBinding(worldName, name, script); - }, - - applyContextSetting({name, value}) { - applySetting[name](value); - }, - - setInterceptFileChooserDialog(enabled) { - docShell.fileInputInterceptionEnabled = !!enabled; - }, - - ensurePermissions() { - // noop, just a rountrip. - }, - - hasFailedToOverrideTimezone() { - return data.failedToOverrideTimezone; - }, - - async awaitViewportDimensions({width, height}) { - const win = docShell.domWindow; - if (win.innerWidth === width && win.innerHeight === height) - return; - await new Promise(resolve => { - const listener = helper.addEventListener(win, 'resize', () => { - if (win.innerWidth === width && win.innerHeight === height) { - helper.removeListeners([listener]); - resolve(); - } - }); - }); - }, - - dispose() { - }, - }); - - return data; -} - -var EXPORTED_SYMBOLS = ['initialize']; -this.initialize = initialize; diff --git a/additions/juggler/jar.mn b/additions/juggler/jar.mn deleted file mode 100644 index 7f3ecf5..0000000 --- a/additions/juggler/jar.mn +++ /dev/null @@ -1,27 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -juggler.jar: -% content juggler %content/ - - content/components/Juggler.js (components/Juggler.js) - - content/Helper.js (Helper.js) - content/NetworkObserver.js (NetworkObserver.js) - content/TargetRegistry.js (TargetRegistry.js) - content/SimpleChannel.js (SimpleChannel.js) - content/JugglerFrameParent.jsm (JugglerFrameParent.jsm) - content/protocol/PrimitiveTypes.js (protocol/PrimitiveTypes.js) - content/protocol/Protocol.js (protocol/Protocol.js) - content/protocol/Dispatcher.js (protocol/Dispatcher.js) - content/protocol/PageHandler.js (protocol/PageHandler.js) - content/protocol/BrowserHandler.js (protocol/BrowserHandler.js) - content/content/JugglerFrameChild.jsm (content/JugglerFrameChild.jsm) - content/content/main.js (content/main.js) - content/content/FrameTree.js (content/FrameTree.js) - content/content/PageAgent.js (content/PageAgent.js) - content/content/Runtime.js (content/Runtime.js) - content/content/WorkerMain.js (content/WorkerMain.js) - content/content/hidden-scrollbars.css (content/hidden-scrollbars.css) - diff --git a/additions/juggler/moz.build b/additions/juggler/moz.build deleted file mode 100644 index 905c20c..0000000 --- a/additions/juggler/moz.build +++ /dev/null @@ -1,10 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -DIRS += ["components", "screencast", "pipe"] - -JAR_MANIFESTS += ["jar.mn"] -with Files("**"): - BUG_COMPONENT = ("Testing", "Juggler") - diff --git a/additions/juggler/pipe/components.conf b/additions/juggler/pipe/components.conf deleted file mode 100644 index db13a00..0000000 --- a/additions/juggler/pipe/components.conf +++ /dev/null @@ -1,15 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -Classes = [ - { - 'cid': '{d69ecefe-3df7-4d11-9dc7-f604edb96da2}', - 'contract_ids': ['@mozilla.org/juggler/remotedebuggingpipe;1'], - 'type': 'nsIRemoteDebuggingPipe', - 'constructor': 'mozilla::nsRemoteDebuggingPipe::GetSingleton', - 'headers': ['/juggler/pipe/nsRemoteDebuggingPipe.h'], - }, -] diff --git a/additions/juggler/pipe/moz.build b/additions/juggler/pipe/moz.build deleted file mode 100644 index b56c697..0000000 --- a/additions/juggler/pipe/moz.build +++ /dev/null @@ -1,24 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -XPIDL_SOURCES += [ - 'nsIRemoteDebuggingPipe.idl', -] - -XPIDL_MODULE = 'jugglerpipe' - -SOURCES += [ - 'nsRemoteDebuggingPipe.cpp', -] - -XPCOM_MANIFESTS += [ - 'components.conf', -] - -LOCAL_INCLUDES += [ -] - -FINAL_LIBRARY = 'xul' diff --git a/additions/juggler/pipe/nsIRemoteDebuggingPipe.idl b/additions/juggler/pipe/nsIRemoteDebuggingPipe.idl deleted file mode 100644 index ac91b63..0000000 --- a/additions/juggler/pipe/nsIRemoteDebuggingPipe.idl +++ /dev/null @@ -1,20 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "nsISupports.idl" - -[scriptable, uuid(7910c231-971a-4653-abdc-a8599a986c4c)] -interface nsIRemoteDebuggingPipeClient : nsISupports -{ - void receiveMessage(in AString message); - void disconnected(); -}; - -[scriptable, uuid(b7bfb66b-fd46-4aa2-b4ad-396177186d94)] -interface nsIRemoteDebuggingPipe : nsISupports -{ - void init(in nsIRemoteDebuggingPipeClient client); - void sendMessage(in AString message); - void stop(); -}; diff --git a/additions/juggler/pipe/nsRemoteDebuggingPipe.cpp b/additions/juggler/pipe/nsRemoteDebuggingPipe.cpp deleted file mode 100644 index abcb0a7..0000000 --- a/additions/juggler/pipe/nsRemoteDebuggingPipe.cpp +++ /dev/null @@ -1,223 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "nsRemoteDebuggingPipe.h" - -#include -#if defined(_WIN32) -#include -#include -#else -#include -#include -#include -#endif - -#include "mozilla/StaticPtr.h" -#include "nsISupportsPrimitives.h" -#include "nsThreadUtils.h" - -namespace mozilla { - -NS_IMPL_ISUPPORTS(nsRemoteDebuggingPipe, nsIRemoteDebuggingPipe) - -namespace { - -StaticRefPtr gPipe; - -const size_t kWritePacketSize = 1 << 16; - -#if defined(_WIN32) -HANDLE readHandle; -HANDLE writeHandle; -#else -const int readFD = 3; -const int writeFD = 4; -#endif - -size_t ReadBytes(void* buffer, size_t size, bool exact_size) -{ - size_t bytesRead = 0; - while (bytesRead < size) { -#if defined(_WIN32) - DWORD sizeRead = 0; - bool hadError = !ReadFile(readHandle, static_cast(buffer) + bytesRead, - size - bytesRead, &sizeRead, nullptr); -#else - int sizeRead = read(readFD, static_cast(buffer) + bytesRead, - size - bytesRead); - if (sizeRead < 0 && errno == EINTR) - continue; - bool hadError = sizeRead <= 0; -#endif - if (hadError) { - return 0; - } - bytesRead += sizeRead; - if (!exact_size) - break; - } - return bytesRead; -} - -void WriteBytes(const char* bytes, size_t size) -{ - size_t totalWritten = 0; - while (totalWritten < size) { - size_t length = size - totalWritten; - if (length > kWritePacketSize) - length = kWritePacketSize; -#if defined(_WIN32) - DWORD bytesWritten = 0; - bool hadError = !WriteFile(writeHandle, bytes + totalWritten, static_cast(length), &bytesWritten, nullptr); -#else - int bytesWritten = write(writeFD, bytes + totalWritten, length); - if (bytesWritten < 0 && errno == EINTR) - continue; - bool hadError = bytesWritten <= 0; -#endif - if (hadError) - return; - totalWritten += bytesWritten; - } -} - -} // namespace - -// static -already_AddRefed nsRemoteDebuggingPipe::GetSingleton() { - if (!gPipe) { - gPipe = new nsRemoteDebuggingPipe(); - } - return do_AddRef(gPipe); -} - -nsRemoteDebuggingPipe::nsRemoteDebuggingPipe() = default; - -nsRemoteDebuggingPipe::~nsRemoteDebuggingPipe() = default; - -nsresult nsRemoteDebuggingPipe::Init(nsIRemoteDebuggingPipeClient* aClient) { - MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread."); - if (mClient) { - return NS_ERROR_FAILURE; - } - mClient = aClient; - - MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("Pipe Reader", getter_AddRefs(mReaderThread))); - MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("Pipe Writer", getter_AddRefs(mWriterThread))); - -#if defined(_WIN32) - CHAR pipeReadStr[20]; - CHAR pipeWriteStr[20]; - GetEnvironmentVariableA("PW_PIPE_READ", pipeReadStr, 20); - GetEnvironmentVariableA("PW_PIPE_WRITE", pipeWriteStr, 20); - readHandle = reinterpret_cast(atoi(pipeReadStr)); - writeHandle = reinterpret_cast(atoi(pipeWriteStr)); -#endif - - MOZ_ALWAYS_SUCCEEDS(mReaderThread->Dispatch(NewRunnableMethod( - "nsRemoteDebuggingPipe::ReaderLoop", - this, &nsRemoteDebuggingPipe::ReaderLoop), nsIThread::DISPATCH_NORMAL)); - return NS_OK; -} - -nsresult nsRemoteDebuggingPipe::Stop() { - MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread."); - if (!mClient) { - return NS_ERROR_FAILURE; - } - m_terminated = true; - mClient = nullptr; - // Cancel pending synchronous read. -#if defined(_WIN32) - CancelIoEx(readHandle, nullptr); - CloseHandle(readHandle); - CloseHandle(writeHandle); -#else - shutdown(readFD, SHUT_RDWR); - shutdown(writeFD, SHUT_RDWR); -#endif - mReaderThread->Shutdown(); - mReaderThread = nullptr; - mWriterThread->Shutdown(); - mWriterThread = nullptr; - return NS_OK; -} - -void nsRemoteDebuggingPipe::ReaderLoop() { - const size_t bufSize = 256 * 1024; - std::vector buffer; - buffer.resize(bufSize); - std::vector line; - while (!m_terminated) { - size_t size = ReadBytes(buffer.data(), bufSize, false); - if (!size) { - nsCOMPtr runnable = NewRunnableMethod<>( - "nsRemoteDebuggingPipe::Disconnected", - this, &nsRemoteDebuggingPipe::Disconnected); - NS_DispatchToMainThread(runnable.forget()); - break; - } - size_t start = 0; - size_t end = line.size(); - line.insert(line.end(), buffer.begin(), buffer.begin() + size); - while (true) { - for (; end < line.size(); ++end) { - if (line[end] == '\0') { - break; - } - } - if (end == line.size()) { - break; - } - if (end > start) { - nsCString message; - message.Append(line.data() + start, end - start); - nsCOMPtr runnable = NewRunnableMethod( - "nsRemoteDebuggingPipe::ReceiveMessage", - this, &nsRemoteDebuggingPipe::ReceiveMessage, std::move(message)); - NS_DispatchToMainThread(runnable.forget()); - } - ++end; - start = end; - } - if (start != 0 && start < line.size()) { - memmove(line.data(), line.data() + start, line.size() - start); - } - line.resize(line.size() - start); - } -} - -void nsRemoteDebuggingPipe::ReceiveMessage(const nsCString& aMessage) { - MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread."); - if (mClient) { - NS_ConvertUTF8toUTF16 utf16(aMessage); - mClient->ReceiveMessage(utf16); - } -} - -void nsRemoteDebuggingPipe::Disconnected() { - MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread."); - if (mClient) - mClient->Disconnected(); -} - -nsresult nsRemoteDebuggingPipe::SendMessage(const nsAString& aMessage) { - MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread."); - if (!mClient) { - return NS_ERROR_FAILURE; - } - NS_ConvertUTF16toUTF8 utf8(aMessage); - nsCOMPtr runnable = NS_NewRunnableFunction( - "nsRemoteDebuggingPipe::SendMessage", - [message = std::move(utf8)] { - const nsCString& flat = PromiseFlatCString(message); - WriteBytes(flat.Data(), flat.Length()); - WriteBytes("\0", 1); - }); - MOZ_ALWAYS_SUCCEEDS(mWriterThread->Dispatch(runnable.forget(), nsIThread::DISPATCH_NORMAL)); - return NS_OK; -} - -} // namespace mozilla diff --git a/additions/juggler/pipe/nsRemoteDebuggingPipe.h b/additions/juggler/pipe/nsRemoteDebuggingPipe.h deleted file mode 100644 index be4cb26..0000000 --- a/additions/juggler/pipe/nsRemoteDebuggingPipe.h +++ /dev/null @@ -1,34 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#pragma once - -#include -#include "nsCOMPtr.h" -#include "nsIRemoteDebuggingPipe.h" -#include "nsThread.h" - -namespace mozilla { - -class nsRemoteDebuggingPipe final : public nsIRemoteDebuggingPipe { - public: - NS_DECL_THREADSAFE_ISUPPORTS - NS_DECL_NSIREMOTEDEBUGGINGPIPE - - static already_AddRefed GetSingleton(); - nsRemoteDebuggingPipe(); - - private: - void ReaderLoop(); - void ReceiveMessage(const nsCString& aMessage); - void Disconnected(); - ~nsRemoteDebuggingPipe(); - - RefPtr mClient; - nsCOMPtr mReaderThread; - nsCOMPtr mWriterThread; - std::atomic m_terminated { false }; -}; - -} // namespace mozilla diff --git a/additions/juggler/protocol/BrowserHandler.js b/additions/juggler/protocol/BrowserHandler.js deleted file mode 100644 index 81aeee4..0000000 --- a/additions/juggler/protocol/BrowserHandler.js +++ /dev/null @@ -1,318 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const {AddonManager} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm"); -const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); -const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); -const {PageHandler} = ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js"); -const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); - -const helper = new Helper(); - -class BrowserHandler { - constructor(session, dispatcher, targetRegistry, startCompletePromise, onclose) { - this._session = session; - this._dispatcher = dispatcher; - this._targetRegistry = targetRegistry; - this._enabled = false; - this._attachToDefaultContext = false; - this._eventListeners = []; - this._createdBrowserContextIds = new Set(); - this._attachedSessions = new Map(); - this._onclose = onclose; - this._startCompletePromise = startCompletePromise; - } - - async ['Browser.enable']({attachToDefaultContext, userPrefs = []}) { - if (this._enabled) - return; - await this._startCompletePromise; - this._enabled = true; - this._attachToDefaultContext = attachToDefaultContext; - - for (const { name, value } of userPrefs) { - if (value === true || value === false) - Services.prefs.setBoolPref(name, value); - else if (typeof value === 'string') - Services.prefs.setStringPref(name, value); - else if (typeof value === 'number') - Services.prefs.setIntPref(name, value); - else - throw new Error(`Preference "${name}" has unsupported value: ${JSON.stringify(value)}`); - } - - this._eventListeners = [ - helper.on(this._targetRegistry, TargetRegistry.Events.TargetCreated, this._onTargetCreated.bind(this)), - helper.on(this._targetRegistry, TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)), - helper.on(this._targetRegistry, TargetRegistry.Events.DownloadCreated, this._onDownloadCreated.bind(this)), - helper.on(this._targetRegistry, TargetRegistry.Events.DownloadFinished, this._onDownloadFinished.bind(this)), - helper.on(this._targetRegistry, TargetRegistry.Events.ScreencastStopped, sessionId => { - this._session.emitEvent('Browser.videoRecordingFinished', {screencastId: '' + sessionId}); - }) - ]; - - for (const target of this._targetRegistry.targets()) - this._onTargetCreated(target); - } - - async ['Browser.createBrowserContext']({removeOnDetach}) { - if (!this._enabled) - throw new Error('Browser domain is not enabled'); - const browserContext = this._targetRegistry.createBrowserContext(removeOnDetach); - this._createdBrowserContextIds.add(browserContext.browserContextId); - return {browserContextId: browserContext.browserContextId}; - } - - async ['Browser.removeBrowserContext']({browserContextId}) { - if (!this._enabled) - throw new Error('Browser domain is not enabled'); - await this._targetRegistry.browserContextForId(browserContextId).destroy(); - this._createdBrowserContextIds.delete(browserContextId); - } - - dispose() { - helper.removeListeners(this._eventListeners); - for (const [target, session] of this._attachedSessions) - this._dispatcher.destroySession(session); - this._attachedSessions.clear(); - for (const browserContextId of this._createdBrowserContextIds) { - const browserContext = this._targetRegistry.browserContextForId(browserContextId); - if (browserContext.removeOnDetach) - browserContext.destroy(); - } - this._createdBrowserContextIds.clear(); - } - - _shouldAttachToTarget(target) { - if (this._createdBrowserContextIds.has(target._browserContext.browserContextId)) - return true; - return this._attachToDefaultContext && target._browserContext === this._targetRegistry.defaultContext(); - } - - _onTargetCreated(target) { - if (!this._shouldAttachToTarget(target)) - return; - const channel = target.channel(); - const session = this._dispatcher.createSession(); - this._attachedSessions.set(target, session); - this._session.emitEvent('Browser.attachedToTarget', { - sessionId: session.sessionId(), - targetInfo: target.info() - }); - session.setHandler(new PageHandler(target, session, channel)); - } - - _onTargetDestroyed(target) { - const session = this._attachedSessions.get(target); - if (!session) - return; - this._attachedSessions.delete(target); - this._dispatcher.destroySession(session); - this._session.emitEvent('Browser.detachedFromTarget', { - sessionId: session.sessionId(), - targetId: target.id(), - }); - } - - _onDownloadCreated(downloadInfo) { - this._session.emitEvent('Browser.downloadCreated', downloadInfo); - } - - _onDownloadFinished(downloadInfo) { - this._session.emitEvent('Browser.downloadFinished', downloadInfo); - } - - async ['Browser.cancelDownload']({uuid}) { - await this._targetRegistry.cancelDownload({uuid}); - } - - async ['Browser.newPage']({browserContextId}) { - const targetId = await this._targetRegistry.newPage({browserContextId}); - return {targetId}; - } - - async ['Browser.close']() { - let browserWindow = Services.wm.getMostRecentWindow( - "navigator:browser" - ); - if (browserWindow && browserWindow.gBrowserInit) { - // idleTasksFinishedPromise does not resolve when the window - // is closed early enough, so we race against window closure. - await Promise.race([ - browserWindow.gBrowserInit.idleTasksFinishedPromise, - waitForWindowClosed(browserWindow), - ]); - } - await this._startCompletePromise; - this._onclose(); - Services.startup.quit(Ci.nsIAppStartup.eForceQuit); - } - - async ['Browser.grantPermissions']({browserContextId, origin, permissions}) { - await this._targetRegistry.browserContextForId(browserContextId).grantPermissions(origin, permissions); - } - - async ['Browser.resetPermissions']({browserContextId}) { - this._targetRegistry.browserContextForId(browserContextId).resetPermissions(); - } - - ['Browser.setExtraHTTPHeaders']({browserContextId, headers}) { - this._targetRegistry.browserContextForId(browserContextId).extraHTTPHeaders = headers; - } - - ['Browser.clearCache']() { - // Clearing only the context cache does not work: https://bugzilla.mozilla.org/show_bug.cgi?id=1819147 - Services.cache2.clear(); - ChromeUtils.clearStyleSheetCache(); - } - - ['Browser.setHTTPCredentials']({browserContextId, credentials}) { - this._targetRegistry.browserContextForId(browserContextId).httpCredentials = nullToUndefined(credentials); - } - - async ['Browser.setBrowserProxy']({type, host, port, bypass, username, password}) { - this._targetRegistry.setBrowserProxy({ type, host, port, bypass, username, password}); - } - - async ['Browser.setContextProxy']({browserContextId, type, host, port, bypass, username, password}) { - const browserContext = this._targetRegistry.browserContextForId(browserContextId); - browserContext.setProxy({ type, host, port, bypass, username, password }); - } - - ['Browser.setRequestInterception']({browserContextId, enabled}) { - this._targetRegistry.browserContextForId(browserContextId).requestInterceptionEnabled = enabled; - } - - ['Browser.setCacheDisabled']({browserContextId, cacheDisabled}) { - this._targetRegistry.browserContextForId(browserContextId).setCacheDisabled(cacheDisabled); - } - - ['Browser.setIgnoreHTTPSErrors']({browserContextId, ignoreHTTPSErrors}) { - this._targetRegistry.browserContextForId(browserContextId).setIgnoreHTTPSErrors(nullToUndefined(ignoreHTTPSErrors)); - } - - ['Browser.setDownloadOptions']({browserContextId, downloadOptions}) { - this._targetRegistry.browserContextForId(browserContextId).downloadOptions = nullToUndefined(downloadOptions); - } - - async ['Browser.setGeolocationOverride']({browserContextId, geolocation}) { - await this._targetRegistry.browserContextForId(browserContextId).applySetting('geolocation', nullToUndefined(geolocation)); - } - - async ['Browser.setOnlineOverride']({browserContextId, override}) { - const forceOffline = override === 'offline'; - await this._targetRegistry.browserContextForId(browserContextId).setForceOffline(forceOffline); - } - - async ['Browser.setColorScheme']({browserContextId, colorScheme}) { - await this._targetRegistry.browserContextForId(browserContextId).setColorScheme(nullToUndefined(colorScheme)); - } - - async ['Browser.setReducedMotion']({browserContextId, reducedMotion}) { - await this._targetRegistry.browserContextForId(browserContextId).setReducedMotion(nullToUndefined(reducedMotion)); - } - - async ['Browser.setForcedColors']({browserContextId, forcedColors}) { - await this._targetRegistry.browserContextForId(browserContextId).setForcedColors(nullToUndefined(forcedColors)); - } - - async ['Browser.setContrast']({browserContextId, contrast}) { - return; // TODO: Implement - } - - async ['Browser.setVideoRecordingOptions']({browserContextId, options}) { - await this._targetRegistry.browserContextForId(browserContextId).setVideoRecordingOptions(options); - } - - async ['Browser.setUserAgentOverride']({browserContextId, userAgent}) { - await this._targetRegistry.browserContextForId(browserContextId).setDefaultUserAgent(userAgent); - } - - async ['Browser.setPlatformOverride']({browserContextId, platform}) { - await this._targetRegistry.browserContextForId(browserContextId).setDefaultPlatform(platform); - } - - async ['Browser.setBypassCSP']({browserContextId, bypassCSP}) { - await this._targetRegistry.browserContextForId(browserContextId).applySetting('bypassCSP', nullToUndefined(bypassCSP)); - } - - async ['Browser.setJavaScriptDisabled']({browserContextId, javaScriptDisabled}) { - await this._targetRegistry.browserContextForId(browserContextId).applySetting('javaScriptDisabled', nullToUndefined(javaScriptDisabled)); - } - - async ['Browser.setLocaleOverride']({browserContextId, locale}) { - await this._targetRegistry.browserContextForId(browserContextId).applySetting('locale', nullToUndefined(locale)); - } - - async ['Browser.setTimezoneOverride']({browserContextId, timezoneId}) { - await this._targetRegistry.browserContextForId(browserContextId).applySetting('timezoneId', nullToUndefined(timezoneId)); - } - - async ['Browser.setTouchOverride']({browserContextId, hasTouch}) { - await this._targetRegistry.browserContextForId(browserContextId).setTouchOverride(nullToUndefined(hasTouch)); - } - - async ['Browser.setDefaultViewport']({browserContextId, viewport}) { - await this._targetRegistry.browserContextForId(browserContextId).setDefaultViewport(nullToUndefined(viewport)); - } - - async ['Browser.setInitScripts']({browserContextId, scripts}) { - await this._targetRegistry.browserContextForId(browserContextId).setInitScripts(scripts); - } - - async ['Browser.addBinding']({browserContextId, worldName, name, script}) { - await this._targetRegistry.browserContextForId(browserContextId).addBinding(worldName, name, script); - } - - ['Browser.setCookies']({browserContextId, cookies}) { - this._targetRegistry.browserContextForId(browserContextId).setCookies(cookies); - } - - ['Browser.clearCookies']({browserContextId}) { - this._targetRegistry.browserContextForId(browserContextId).clearCookies(); - } - - ['Browser.getCookies']({browserContextId}) { - const cookies = this._targetRegistry.browserContextForId(browserContextId).getCookies(); - return {cookies}; - } - - async ['Browser.getInfo']() { - const version = AppConstants.MOZ_APP_VERSION_DISPLAY; - const userAgent = Components.classes["@mozilla.org/network/protocol;1?name=http"] - .getService(Components.interfaces.nsIHttpProtocolHandler) - .userAgent; - return {version: 'Firefox/' + version, userAgent}; - } -} - -async function waitForWindowClosed(browserWindow) { - if (browserWindow.closed) - return; - await new Promise((resolve => { - const listener = { - onCloseWindow: window => { - let domWindow; - if (window instanceof Ci.nsIAppWindow) - domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow); - else - domWindow = window; - if (domWindow === browserWindow) { - Services.wm.removeListener(listener); - resolve(); - } - }, - }; - Services.wm.addListener(listener); - })); -} - -function nullToUndefined(value) { - return value === null ? undefined : value; -} - -var EXPORTED_SYMBOLS = ['BrowserHandler']; -this.BrowserHandler = BrowserHandler; diff --git a/additions/juggler/protocol/Dispatcher.js b/additions/juggler/protocol/Dispatcher.js deleted file mode 100644 index cf24b89..0000000 --- a/additions/juggler/protocol/Dispatcher.js +++ /dev/null @@ -1,193 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const {protocol, checkScheme} = ChromeUtils.import("chrome://juggler/content/protocol/Protocol.js"); -const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); - -const helper = new Helper(); -// Camoufox: Exclude redundant internal events from logs. -const EXCLUDED_DBG = ['Page.navigationStarted', 'Page.frameAttached', 'Runtime.executionContextCreated', 'Runtime.console', 'Page.navigationAborted', 'Page.eventFired']; - -class Dispatcher { - /** - * @param {Connection} connection - */ - constructor(connection) { - this._connection = connection; - this._connection.onmessage = this._dispatch.bind(this); - this._connection.onclose = this._dispose.bind(this); - this._sessions = new Map(); - this._rootSession = new ProtocolSession(this, undefined); - } - - rootSession() { - return this._rootSession; - } - - createSession() { - const session = new ProtocolSession(this, helper.generateId()); - this._sessions.set(session.sessionId(), session); - return session; - } - - destroySession(session) { - this._sessions.delete(session.sessionId()); - session._dispose(); - } - - _dispose() { - this._connection.onmessage = null; - this._connection.onclose = null; - this._rootSession._dispose(); - this._rootSession = null; - this._sessions.clear(); - } - - async _dispatch(event) { - const data = JSON.parse(event.data); - - if (ChromeUtils.isCamouDebug()) - ChromeUtils.camouDebug(`[${new Date().toLocaleString()}]` - + `\nReceived message: ${safeJsonStringify(data)}`); - - const id = data.id; - const sessionId = data.sessionId; - delete data.sessionId; - try { - const session = sessionId ? this._sessions.get(sessionId) : this._rootSession; - if (!session) - throw new Error(`ERROR: cannot find session with id "${sessionId}"`); - const method = data.method; - const params = data.params || {}; - if (!id) - throw new Error(`ERROR: every message must have an 'id' parameter`); - if (!method) - throw new Error(`ERROR: every message must have a 'method' parameter`); - - const [domain, methodName] = method.split('.'); - const descriptor = protocol.domains[domain] ? protocol.domains[domain].methods[methodName] : null; - if (!descriptor) - throw new Error(`ERROR: method '${method}' is not supported`); - let details = {}; - if (!checkScheme(descriptor.params || {}, params, details)) - throw new Error(`ERROR: failed to call method '${method}' with parameters ${JSON.stringify(params, null, 2)}\n${details.error}`); - - const result = await session.dispatch(method, params); - - details = {}; - if ((descriptor.returns || result) && !checkScheme(descriptor.returns, result, details)) - throw new Error(`ERROR: failed to dispatch method '${method}' result ${JSON.stringify(result, null, 2)}\n${details.error}`); - - this._connection.send(JSON.stringify({id, sessionId, result})); - } catch (e) { - dump(` - ERROR: ${e.message} ${e.stack} - `); - this._connection.send(JSON.stringify({id, sessionId, error: { - message: e.message, - data: e.stack - }})); - } - } - - _emitEvent(sessionId, eventName, params) { - const [domain, eName] = eventName.split('.'); - - // Camoufox: Log internal events - if (ChromeUtils.isCamouDebug() && !EXCLUDED_DBG.includes(eventName) && domain !== 'Network') { - ChromeUtils.camouDebug(`[${new Date().toLocaleString()}]` - + `\nInternal event: ${eventName}\nParams: ${JSON.stringify(params, null, 2)}`); - } - - const scheme = protocol.domains[domain] ? protocol.domains[domain].events[eName] : null; - if (!scheme) - throw new Error(`ERROR: event '${eventName}' is not supported`); - const details = {}; - if (!checkScheme(scheme, params || {}, details)) - throw new Error(`ERROR: failed to emit event '${eventName}' ${JSON.stringify(params, null, 2)}\n${details.error}`); - this._connection.send(JSON.stringify({method: eventName, params, sessionId})); - } -} - -class ProtocolSession { - constructor(dispatcher, sessionId) { - this._sessionId = sessionId; - this._dispatcher = dispatcher; - this._handler = null; - } - - sessionId() { - return this._sessionId; - } - - setHandler(handler) { - this._handler = handler; - } - - _dispose() { - if (this._handler) - this._handler.dispose(); - this._handler = null; - this._dispatcher = null; - } - - emitEvent(eventName, params) { - if (!this._dispatcher) - throw new Error(`Session has been disposed.`); - this._dispatcher._emitEvent(this._sessionId, eventName, params); - } - - async dispatch(method, params) { - if (!this._handler) - throw new Error(`Session does not have a handler!`); - if (!this._handler[method]) - throw new Error(`Handler for does not implement method "${method}"`); - return await this._handler[method](params); - } -} - -this.EXPORTED_SYMBOLS = ['Dispatcher']; -this.Dispatcher = Dispatcher; - - -function formatDate(date) { - const pad = (num) => String(num).padStart(2, '0'); - return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; -} - -function truncateObject(obj, maxDepth = 8, maxLength = 100) { - if (maxDepth < 0) return '[Max Depth Reached]'; - - if (typeof obj !== 'object' || obj === null) { - return typeof obj === 'string' ? truncateString(obj, maxLength) : obj; - } - - if (Array.isArray(obj)) { - return obj.slice(0, 10).map(item => truncateObject(item, maxDepth - 1, maxLength)); - } - - const truncated = {}; - for (const [key, value] of Object.entries(obj)) { - if (Object.keys(truncated).length >= 10) { - truncated['...'] = '[Truncated]'; - break; - } - truncated[key] = truncateObject(value, maxDepth - 1, maxLength); - } - return truncated; -} - -function truncateString(str, maxLength) { - if (str.length <= maxLength) return str; - ChromeUtils.camouDebug(`String length: ${str.length}`); - return str.substr(0, maxLength) + '... [truncated]'; -} - -function safeJsonStringify(data) { - try { - return JSON.stringify(truncateObject(data), null, 2); - } catch (error) { - return `[Unable to stringify: ${error.message}]`; - } -} \ No newline at end of file diff --git a/additions/juggler/protocol/PageHandler.js b/additions/juggler/protocol/PageHandler.js deleted file mode 100644 index 213ede0..0000000 --- a/additions/juggler/protocol/PageHandler.js +++ /dev/null @@ -1,725 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public -* License, v. 2.0. If a copy of the MPL was not distributed with this -* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const {Helper, EventWatcher} = ChromeUtils.import('chrome://juggler/content/Helper.js'); -const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm'); -const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js'); -const {PageTarget} = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js'); -const {setTimeout} = ChromeUtils.import('resource://gre/modules/Timer.jsm'); - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; -const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; -const helper = new Helper(); - -function hashConsoleMessage(params) { - return params.location.lineNumber + ':' + params.location.columnNumber + ':' + params.location.url; -} - -class WorkerHandler { - constructor(session, contentChannel, workerId) { - this._session = session; - this._contentWorker = contentChannel.connect(workerId); - this._workerConsoleMessages = new Set(); - this._workerId = workerId; - - const emitWrappedProtocolEvent = eventName => { - return params => { - this._session.emitEvent('Page.dispatchMessageFromWorker', { - workerId, - message: JSON.stringify({method: eventName, params}), - }); - } - } - - this._eventListeners = [ - contentChannel.register(workerId, { - runtimeConsole: (params) => { - this._workerConsoleMessages.add(hashConsoleMessage(params)); - emitWrappedProtocolEvent('Runtime.console')(params); - }, - runtimeExecutionContextCreated: emitWrappedProtocolEvent('Runtime.executionContextCreated'), - runtimeExecutionContextDestroyed: emitWrappedProtocolEvent('Runtime.executionContextDestroyed'), - }), - ]; - } - - async sendMessage(message) { - const [domain, method] = message.method.split('.'); - if (domain !== 'Runtime') - throw new Error('ERROR: can only dispatch to Runtime domain inside worker'); - const result = await this._contentWorker.send(method, message.params); - this._session.emitEvent('Page.dispatchMessageFromWorker', { - workerId: this._workerId, - message: JSON.stringify({result, id: message.id}), - }); - } - - dispose() { - this._contentWorker.dispose(); - helper.removeListeners(this._eventListeners); - } -} - -class PageHandler { - constructor(target, session, contentChannel) { - this._session = session; - this._contentChannel = contentChannel; - this._contentPage = contentChannel.connect('page'); - this._workers = new Map(); - - this._pageTarget = target; - this._pageNetwork = PageNetwork.forPageTarget(target); - - const emitProtocolEvent = eventName => { - return (...args) => this._session.emitEvent(eventName, ...args); - } - - this._isDragging = false; - - // Camoufox: set a random default cursor position - let random_val = (max_val) => Math.floor(Math.random() * max_val); - - // Try to fetch the viewport size - this._defaultCursorPos = { - x: random_val(this._pageTarget._viewportSize?.width || 1280), - y: random_val(this._pageTarget._viewportSize?.height || 720), - }; - this._lastMousePosition = { ...this._defaultCursorPos }; - this._lastTrackedPos = { ...this._defaultCursorPos }; - - this._reportedFrameIds = new Set(); - this._networkEventsForUnreportedFrameIds = new Map(); - - // `Page.ready` protocol event is emitted whenever page has completed initialization, e.g. - // finished all the transient navigations to the `about:blank`. - // - // We'd like to avoid reporting meaningful events before the `Page.ready` since they are likely - // to be ignored by the protocol clients. - this._isPageReady = false; - - if (this._pageTarget.videoRecordingInfo()) - this._onVideoRecordingStarted(); - - this._pageEventSink = {}; - helper.decorateAsEventEmitter(this._pageEventSink); - - this._pendingEventWatchers = new Set(); - this._eventListeners = [ - helper.on(this._pageTarget, PageTarget.Events.DialogOpened, this._onDialogOpened.bind(this)), - helper.on(this._pageTarget, PageTarget.Events.DialogClosed, this._onDialogClosed.bind(this)), - helper.on(this._pageTarget, PageTarget.Events.Crashed, () => { - this._session.emitEvent('Page.crashed', {}); - }), - helper.on(this._pageTarget, PageTarget.Events.ScreencastStarted, this._onVideoRecordingStarted.bind(this)), - helper.on(this._pageTarget, PageTarget.Events.ScreencastFrame, this._onScreencastFrame.bind(this)), - helper.on(this._pageNetwork, PageNetwork.Events.Request, this._handleNetworkEvent.bind(this, 'Network.requestWillBeSent')), - helper.on(this._pageNetwork, PageNetwork.Events.Response, this._handleNetworkEvent.bind(this, 'Network.responseReceived')), - helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._handleNetworkEvent.bind(this, 'Network.requestFinished')), - helper.on(this._pageNetwork, PageNetwork.Events.RequestFailed, this._handleNetworkEvent.bind(this, 'Network.requestFailed')), - contentChannel.register('page', { - pageBindingCalled: emitProtocolEvent('Page.bindingCalled'), - pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'), - pageEventFired: emitProtocolEvent('Page.eventFired'), - pageFileChooserOpened: emitProtocolEvent('Page.fileChooserOpened'), - pageFrameAttached: this._onFrameAttached.bind(this), - pageFrameDetached: emitProtocolEvent('Page.frameDetached'), - pageLinkClicked: emitProtocolEvent('Page.linkClicked'), - pageWillOpenNewWindowAsynchronously: emitProtocolEvent('Page.willOpenNewWindowAsynchronously'), - pageNavigationAborted: emitProtocolEvent('Page.navigationAborted'), - pageNavigationCommitted: emitProtocolEvent('Page.navigationCommitted'), - pageNavigationStarted: emitProtocolEvent('Page.navigationStarted'), - pageReady: this._onPageReady.bind(this), - pageInputEvent: (event) => this._pageEventSink.emit(event.type, event), - pageSameDocumentNavigation: emitProtocolEvent('Page.sameDocumentNavigation'), - pageUncaughtError: emitProtocolEvent('Page.uncaughtError'), - pageWorkerCreated: this._onWorkerCreated.bind(this), - pageWorkerDestroyed: this._onWorkerDestroyed.bind(this), - runtimeConsole: params => { - const consoleMessageHash = hashConsoleMessage(params); - for (const worker of this._workers.values()) { - if (worker._workerConsoleMessages.has(consoleMessageHash)) { - worker._workerConsoleMessages.delete(consoleMessageHash); - return; - } - } - this._session.emitEvent('Runtime.console', params); - }, - runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'), - runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'), - runtimeExecutionContextsCleared: emitProtocolEvent('Runtime.executionContextsCleared'), - - webSocketCreated: emitProtocolEvent('Page.webSocketCreated'), - webSocketOpened: emitProtocolEvent('Page.webSocketOpened'), - webSocketClosed: emitProtocolEvent('Page.webSocketClosed'), - webSocketFrameReceived: emitProtocolEvent('Page.webSocketFrameReceived'), - webSocketFrameSent: emitProtocolEvent('Page.webSocketFrameSent'), - }), - ]; - } - - async dispose() { - this._contentPage.dispose(); - for (const watcher of this._pendingEventWatchers) - watcher.dispose(); - helper.removeListeners(this._eventListeners); - } - - _onVideoRecordingStarted() { - const info = this._pageTarget.videoRecordingInfo(); - this._session.emitEvent('Page.videoRecordingStarted', { screencastId: info.sessionId, file: info.file }); - } - - _onScreencastFrame(params) { - this._session.emitEvent('Page.screencastFrame', params); - } - - _onPageReady(event) { - this._isPageReady = true; - this._session.emitEvent('Page.ready'); - for (const dialog of this._pageTarget.dialogs()) - this._onDialogOpened(dialog); - } - - _onDialogOpened(dialog) { - if (!this._isPageReady) - return; - this._session.emitEvent('Page.dialogOpened', { - dialogId: dialog.id(), - type: dialog.type(), - message: dialog.message(), - defaultValue: dialog.defaultValue(), - }); - } - - _onDialogClosed(dialog) { - if (!this._isPageReady) - return; - this._session.emitEvent('Page.dialogClosed', { dialogId: dialog.id(), }); - } - - _onWorkerCreated({workerId, frameId, url}) { - const worker = new WorkerHandler(this._session, this._contentChannel, workerId); - this._workers.set(workerId, worker); - this._session.emitEvent('Page.workerCreated', {workerId, frameId, url}); - } - - _onWorkerDestroyed({workerId}) { - const worker = this._workers.get(workerId); - if (!worker) - return; - this._workers.delete(workerId); - worker.dispose(); - this._session.emitEvent('Page.workerDestroyed', {workerId}); - } - - _handleNetworkEvent(protocolEventName, eventDetails, frameId) { - if (!this._reportedFrameIds.has(frameId)) { - let events = this._networkEventsForUnreportedFrameIds.get(frameId); - if (!events) { - events = []; - this._networkEventsForUnreportedFrameIds.set(frameId, events); - } - events.push({eventName: protocolEventName, eventDetails}); - } else { - this._session.emitEvent(protocolEventName, eventDetails); - } - } - - _onFrameAttached({frameId, parentFrameId}) { - this._session.emitEvent('Page.frameAttached', {frameId, parentFrameId}); - this._reportedFrameIds.add(frameId); - const events = this._networkEventsForUnreportedFrameIds.get(frameId) || []; - this._networkEventsForUnreportedFrameIds.delete(frameId); - for (const {eventName, eventDetails} of events) - this._session.emitEvent(eventName, eventDetails); - } - - async ['Page.close']({runBeforeUnload}) { - // Postpone target close to deliver response in session. - Services.tm.dispatchToMainThread(() => { - this._pageTarget.close(runBeforeUnload); - }); - } - - async ['Page.setViewportSize']({viewportSize}) { - await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize); - } - - async ['Runtime.evaluate'](options) { - return await this._contentPage.send('evaluate', options); - } - - async ['Runtime.callFunction'](options) { - return await this._contentPage.send('callFunction', options); - } - - async ['Runtime.getObjectProperties'](options) { - return await this._contentPage.send('getObjectProperties', options); - } - - async ['Runtime.disposeObject'](options) { - return await this._contentPage.send('disposeObject', options); - } - - async ['Heap.collectGarbage']() { - Services.obs.notifyObservers(null, "child-gc-request"); - Cu.forceGC(); - Services.obs.notifyObservers(null, "child-cc-request"); - Cu.forceCC(); - } - - async ['Network.getResponseBody']({requestId}) { - return this._pageNetwork.getResponseBody(requestId); - } - - async ['Network.setExtraHTTPHeaders']({headers}) { - this._pageNetwork.setExtraHTTPHeaders(headers); - } - - async ['Network.setRequestInterception']({enabled}) { - if (enabled) - this._pageNetwork.enableRequestInterception(); - else - this._pageNetwork.disableRequestInterception(); - } - - async ['Network.resumeInterceptedRequest']({requestId, url, method, headers, postData}) { - this._pageNetwork.resumeInterceptedRequest(requestId, url, method, headers, postData); - } - - async ['Network.abortInterceptedRequest']({requestId, errorCode}) { - this._pageNetwork.abortInterceptedRequest(requestId, errorCode); - } - - async ['Network.fulfillInterceptedRequest']({requestId, status, statusText, headers, base64body}) { - this._pageNetwork.fulfillInterceptedRequest(requestId, status, statusText, headers, base64body); - } - - async ['Accessibility.getFullAXTree'](params) { - return await this._contentPage.send('getFullAXTree', params); - } - - async ['Page.setFileInputFiles'](options) { - return await this._contentPage.send('setFileInputFiles', options); - } - - async ['Page.setEmulatedMedia']({colorScheme, type, reducedMotion, forcedColors}) { - this._pageTarget.setColorScheme(colorScheme || null); - this._pageTarget.setReducedMotion(reducedMotion || null); - this._pageTarget.setForcedColors(forcedColors || null); - this._pageTarget.setEmulatedMedia(type); - } - - async ['Page.bringToFront'](options) { - await this._pageTarget.activateAndRun(() => {}); - } - - async ['Page.setCacheDisabled']({cacheDisabled}) { - return await this._pageTarget.setCacheDisabled(cacheDisabled); - } - - async ['Page.addBinding']({ worldName, name, script }) { - return await this._pageTarget.addBinding(worldName, name, script); - } - - async ['Page.adoptNode'](options) { - return await this._contentPage.send('adoptNode', options); - } - - async ['Page.screenshot']({ mimeType, clip, omitDeviceScaleFactor, quality = 80}) { - const rect = new DOMRect(clip.x, clip.y, clip.width, clip.height); - - const browsingContext = this._pageTarget.linkedBrowser().browsingContext; - // `win.devicePixelRatio` returns a non-overriden value to priveleged code. - // See https://bugzilla.mozilla.org/show_bug.cgi?id=1761032 - // See https://phabricator.services.mozilla.com/D141323 - const devicePixelRatio = browsingContext.overrideDPPX || this._pageTarget._window.devicePixelRatio; - const scale = omitDeviceScaleFactor ? 1 : devicePixelRatio; - const canvasWidth = rect.width * scale; - const canvasHeight = rect.height * scale; - - const MAX_CANVAS_DIMENSIONS = 32767; - const MAX_CANVAS_AREA = 472907776; - if (canvasWidth > MAX_CANVAS_DIMENSIONS || canvasHeight > MAX_CANVAS_DIMENSIONS) - throw new Error('Cannot take screenshot larger than ' + MAX_CANVAS_DIMENSIONS); - if (canvasWidth * canvasHeight > MAX_CANVAS_AREA) - throw new Error('Cannot take screenshot with more than ' + MAX_CANVAS_AREA + ' pixels'); - - let snapshot; - while (!snapshot) { - try { - //TODO(fission): browsingContext will change in case of cross-group navigation. - snapshot = await browsingContext.currentWindowGlobal.drawSnapshot( - rect, - scale, - "rgb(255,255,255)" - ); - } catch (e) { - // The currentWindowGlobal.drawSnapshot might throw - // NS_ERROR_LOSS_OF_SIGNIFICANT_DATA if called during navigation. - // wait a little and re-try. - await new Promise(x => setTimeout(x, 50)); - } - } - - const win = browsingContext.topChromeWindow.ownerGlobal; - const canvas = win.document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas'); - canvas.width = canvasWidth; - canvas.height = canvasHeight; - let ctx = canvas.getContext('2d'); - ctx.drawImage(snapshot, 0, 0); - snapshot.close(); - - if (mimeType === 'image/jpeg') { - if (quality < 0 || quality > 100) - throw new Error('Quality must be an integer value between 0 and 100; received ' + quality); - quality /= 100; - } else { - quality = undefined; - } - const dataURL = canvas.toDataURL(mimeType, quality); - return { data: dataURL.substring(dataURL.indexOf(',') + 1) }; - } - - async ['Page.getContentQuads'](options) { - return await this._contentPage.send('getContentQuads', options); - } - - async ['Page.navigate']({frameId, url, referer}) { - const browsingContext = this._pageTarget.frameIdToBrowsingContext(frameId); - let sameDocumentNavigation = false; - try { - const uri = NetUtil.newURI(url); - // This is the same check that verifes browser-side if this is the same-document navigation. - // See CanonicalBrowsingContext::SupportsLoadingInParent. - sameDocumentNavigation = browsingContext.currentURI && uri.hasRef && uri.equalsExceptRef(browsingContext.currentURI); - } catch (e) { - throw new Error(`Invalid url: "${url}"`); - } - let referrerURI = null; - let referrerInfo = null; - if (referer) { - try { - referrerURI = NetUtil.newURI(referer); - const ReferrerInfo = Components.Constructor( - '@mozilla.org/referrer-info;1', - 'nsIReferrerInfo', - 'init' - ); - referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.UNSAFE_URL, true, referrerURI); - } catch (e) { - throw new Error(`Invalid referer: "${referer}"`); - } - } - - let navigationId; - const unsubscribe = helper.addObserver((browsingContext, topic, loadIdentifier) => { - navigationId = helper.toProtocolNavigationId(loadIdentifier); - }, 'juggler-navigation-started-browser'); - browsingContext.loadURI(Services.io.newURI(url), { - triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), - loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK, - referrerInfo, - // postData: null, - // headers: null, - // Fake user activation. - hasValidUserGestureActivation: true, - }); - unsubscribe(); - - if (ChromeUtils.camouGetBool('memorysaver', false)) { - ChromeUtils.camouDebug('Clearing all memory...'); - Services.obs.notifyObservers(null, "child-gc-request"); - Cu.forceGC(); - Services.obs.notifyObservers(null, "child-cc-request"); - Cu.forceCC(); - } - - return { - navigationId: sameDocumentNavigation ? null : navigationId, - }; - } - - async ['Page.goBack']({}) { - const browsingContext = this._pageTarget.linkedBrowser().browsingContext; - if (!browsingContext.embedderElement?.canGoBack) - return { success: false }; - browsingContext.goBack(); - return { success: true }; - } - - async ['Page.goForward']({}) { - const browsingContext = this._pageTarget.linkedBrowser().browsingContext; - if (!browsingContext.embedderElement?.canGoForward) - return { success: false }; - browsingContext.goForward(); - return { success: true }; - } - - async ['Page.reload']() { - await this._pageTarget.activateAndRun(() => { - const doc = this._pageTarget._tab.linkedBrowser.ownerDocument; - doc.getElementById('Browser:Reload').doCommand(); - }); - } - - async ['Page.describeNode'](options) { - return await this._contentPage.send('describeNode', options); - } - - async ['Page.scrollIntoViewIfNeeded'](options) { - return await this._contentPage.send('scrollIntoViewIfNeeded', options); - } - - async ['Page.setInitScripts']({ scripts }) { - return await this._pageTarget.setInitScripts(scripts); - } - - async ['Page.dispatchKeyEvent']({type, keyCode, code, key, repeat, location, text}) { - // key events don't fire if we are dragging. - if (this._isDragging) { - if (type === 'keydown' && key === 'Escape') { - await this._contentPage.send('dispatchDragEvent', { - type: 'dragover', - x: this._lastMousePosition.x, - y: this._lastMousePosition.y, - modifiers: 0 - }); - await this._contentPage.send('dispatchDragEvent', {type: 'dragend'}); - this._isDragging = false; - } - return; - } - return await this._contentPage.send('dispatchKeyEvent', {type, keyCode, code, key, repeat, location, text}); - } - - async ['Page.dispatchTouchEvent'](options) { - return await this._contentPage.send('dispatchTouchEvent', options); - } - - async ['Page.dispatchTapEvent'](options) { - return await this._contentPage.send('dispatchTapEvent', options); - } - - async ['Page.dispatchMouseEvent']({type, x, y, button, clickCount, modifiers, buttons}) { - const win = this._pageTarget._window; - const sendEvents = async (types) => { - // 1. Scroll element to the desired location first; the coordinates are relative to the element. - this._pageTarget._linkedBrowser.scrollRectIntoViewIfNeeded(x, y, 0, 0); - // 2. Get element's bounding box in the browser after the scroll is completed. - const boundingBox = this._pageTarget._linkedBrowser.getBoundingClientRect(); - // 3. Make sure compositor is flushed after scrolling. - if (win.windowUtils.flushApzRepaints()) - await helper.awaitTopic('apz-repaints-flushed'); - - const watcher = new EventWatcher(this._pageEventSink, types, this._pendingEventWatchers); - const sendMouseEvent = async (eventType, eventX, eventY) => { - const jugglerEventId = win.windowUtils.jugglerSendMouseEvent( - eventType, - eventX + boundingBox.left, - eventY + boundingBox.top, - button, - clickCount, - modifiers, - false /* aIgnoreRootScrollFrame */, - 0.0 /* pressure */, - 0 /* inputSource */, - false /* isDOMEventSynthesized */, - false /* isWidgetEventSynthesized */, - buttons, - win.windowUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */, - false /* disablePointerEvent */ - ); - await watcher.ensureEvent(eventType, eventObject => eventObject.jugglerEventId === jugglerEventId); - }; - for (const type of types) { - if (type === 'mousemove' && ChromeUtils.camouGetBool('humanize', false)) { - let trajectory = ChromeUtils.camouGetMouseTrajectory(this._lastTrackedPos.x, this._lastTrackedPos.y, x, y); - for (let i = 2; i < trajectory.length - 2; i += 2) { - let currentX = trajectory[i]; - let currentY = trajectory[i + 1]; - // Skip movement that is out of bounds - if (currentX < 0 || currentY < 0 || currentX > boundingBox.width || currentY > boundingBox.height) { - continue; - } - await sendMouseEvent(type, currentX, currentY); - await new Promise(resolve => setTimeout(resolve, 10)); - } - } else { - // Call the function for the current event - await sendMouseEvent(type, x, y); - } - } - await watcher.dispose(); - }; - - // We must switch to proper tab in the tabbed browser so that - // 1. Event is dispatched to a proper renderer. - // 2. We receive an ack from the renderer for the dispatched event. - await this._pageTarget.activateAndRun(async () => { - this._pageTarget.ensureContextMenuClosed(); - // If someone asks us to dispatch mouse event outside of viewport, then we normally would drop it. - const boundingBox = this._pageTarget._linkedBrowser.getBoundingClientRect(); - if (x < 0 || y < 0 || x > boundingBox.width || y > boundingBox.height) { - if (type !== 'mousemove') - return; - - // A special hack: if someone tries to do `mousemove` outside of - // viewport coordinates, then move the mouse off from the Web Content. - // This way we can eliminate all the hover effects. - // NOTE: since this won't go inside the renderer, there's no need to wait for ACK. - win.windowUtils.sendMouseEvent( - 'mousemove', - this._defaultCursorPos.x, - this._defaultCursorPos.y, - button, - clickCount, - modifiers, - false /* aIgnoreRootScrollFrame */, - 0.0 /* pressure */, - 0 /* inputSource */, - false /* isDOMEventSynthesized */, - false /* isWidgetEventSynthesized */, - buttons, - win.windowUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */, - false /* disablePointerEvent */ - ); - return; - } - - if (type === 'mousedown') { - if (this._isDragging) - return; - - const eventNames = button === 2 ? ['mousedown', 'contextmenu'] : ['mousedown']; - await sendEvents(eventNames); - return; - } - - if (type === 'mousemove') { - this._lastMousePosition = { x, y }; - if (this._isDragging) { - const watcher = new EventWatcher(this._pageEventSink, ['dragover'], this._pendingEventWatchers); - await this._contentPage.send('dispatchDragEvent', {type:'dragover', x, y, modifiers}); - await watcher.ensureEventsAndDispose(['dragover']); - return; - } - - const watcher = new EventWatcher(this._pageEventSink, ['dragstart', 'juggler-drag-finalized'], this._pendingEventWatchers); - await sendEvents(['mousemove']); - this._lastTrackedPos = { x, y }; - - // The order of events after 'mousemove' is sent: - // 1. [dragstart] - might or might NOT be emitted - // 2. [mousemove] - always emitted. This was awaited as part of `sendEvents` call. - // 3. [juggler-drag-finalized] - only emitted if dragstart was emitted. - - if (watcher.hasEvent('dragstart')) { - const eventObject = await watcher.ensureEvent('juggler-drag-finalized'); - this._isDragging = eventObject.dragSessionStarted; - } - watcher.dispose(); - return; - } - - if (type === 'mouseup') { - if (this._isDragging) { - const watcher = new EventWatcher(this._pageEventSink, ['dragover'], this._pendingEventWatchers); - await this._contentPage.send('dispatchDragEvent', {type: 'dragover', x, y, modifiers}); - await this._contentPage.send('dispatchDragEvent', {type: 'drop', x, y, modifiers}); - await this._contentPage.send('dispatchDragEvent', {type: 'dragend', x, y, modifiers}); - // NOTE: - // - 'drop' event might not be dispatched at all, depending on dropAction. - // - 'dragend' event might not be dispatched at all, if the source element was removed - // during drag. However, it'll be dispatched synchronously in the renderer. - await watcher.ensureEventsAndDispose(['dragover']); - this._isDragging = false; - } else { - await sendEvents(['mouseup']); - } - return; - } - }, { muteNotificationsPopup: true }); - } - - async ['Page.dispatchWheelEvent']({x, y, button, deltaX, deltaY, deltaZ, modifiers }) { - const deltaMode = 0; // WheelEvent.DOM_DELTA_PIXEL - const lineOrPageDeltaX = deltaX > 0 ? Math.floor(deltaX) : Math.ceil(deltaX); - const lineOrPageDeltaY = deltaY > 0 ? Math.floor(deltaY) : Math.ceil(deltaY); - - await this._pageTarget.activateAndRun(async () => { - this._pageTarget.ensureContextMenuClosed(); - - // 1. Scroll element to the desired location first; the coordinates are relative to the element. - this._pageTarget._linkedBrowser.scrollRectIntoViewIfNeeded(x, y, 0, 0); - // 2. Get element's bounding box in the browser after the scroll is completed. - const boundingBox = this._pageTarget._linkedBrowser.getBoundingClientRect(); - - const win = this._pageTarget._window; - // 3. Make sure compositor is flushed after scrolling. - if (win.windowUtils.flushApzRepaints()) - await helper.awaitTopic('apz-repaints-flushed'); - - win.windowUtils.sendWheelEvent( - x + boundingBox.left, - y + boundingBox.top, - deltaX, - deltaY, - deltaZ, - deltaMode, - modifiers, - lineOrPageDeltaX, - lineOrPageDeltaY, - 0 /* options */); - }, { muteNotificationsPopup: true }); - } - - async ['Page.insertText'](options) { - return await this._contentPage.send('insertText', options); - } - - async ['Page.crash'](options) { - return await this._contentPage.send('crash', options); - } - - async ['Page.handleDialog']({dialogId, accept, promptText}) { - const dialog = this._pageTarget.dialog(dialogId); - if (!dialog) - throw new Error('Failed to find dialog with id = ' + dialogId); - if (accept) - dialog.accept(promptText); - else - dialog.dismiss(); - } - - async ['Page.setInterceptFileChooserDialog']({ enabled }) { - return await this._pageTarget.setInterceptFileChooserDialog(enabled); - } - - async ['Page.startScreencast'](options) { - return await this._pageTarget.startScreencast(options); - } - - async ['Page.screencastFrameAck'](options) { - await this._pageTarget.screencastFrameAck(options); - } - - async ['Page.stopScreencast'](options) { - await this._pageTarget.stopScreencast(options); - } - - async ['Page.sendMessageToWorker']({workerId, message}) { - const worker = this._workers.get(workerId); - if (!worker) - throw new Error('ERROR: cannot find worker with id ' + workerId); - return await worker.sendMessage(JSON.parse(message)); - } -} - -var EXPORTED_SYMBOLS = ['PageHandler']; -this.PageHandler = PageHandler; diff --git a/additions/juggler/protocol/PrimitiveTypes.js b/additions/juggler/protocol/PrimitiveTypes.js deleted file mode 100644 index 5799038..0000000 --- a/additions/juggler/protocol/PrimitiveTypes.js +++ /dev/null @@ -1,147 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const t = {}; - -t.String = function(x, details = {}, path = ['']) { - if (typeof x === 'string' || typeof x === 'String') - return true; - details.error = `Expected "${path.join('.')}" to be |string|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`; - return false; -} - -t.Number = function(x, details = {}, path = ['']) { - if (typeof x === 'number') - return true; - details.error = `Expected "${path.join('.')}" to be |number|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`; - return false; -} - -t.Boolean = function(x, details = {}, path = ['']) { - if (typeof x === 'boolean') - return true; - details.error = `Expected "${path.join('.')}" to be |boolean|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`; - return false; -} - -t.Null = function(x, details = {}, path = ['']) { - if (Object.is(x, null)) - return true; - details.error = `Expected "${path.join('.')}" to be \`null\`; found \`${JSON.stringify(x)}\` instead.`; - return false; -} - -t.Undefined = function(x, details = {}, path = ['']) { - if (Object.is(x, undefined)) - return true; - details.error = `Expected "${path.join('.')}" to be \`undefined\`; found \`${JSON.stringify(x)}\` instead.`; - return false; -} - -t.Any = x => true, - -t.Enum = function(values) { - return function(x, details = {}, path = ['']) { - if (values.indexOf(x) !== -1) - return true; - details.error = `Expected "${path.join('.')}" to be one of [${values.join(', ')}]; found \`${JSON.stringify(x)}\` (${typeof x}) instead.`; - return false; - } -} - -t.Nullable = function(scheme) { - return function(x, details = {}, path = ['']) { - if (Object.is(x, null)) - return true; - return checkScheme(scheme, x, details, path); - } -} - -t.Optional = function(scheme) { - return function(x, details = {}, path = ['']) { - if (Object.is(x, undefined)) - return true; - return checkScheme(scheme, x, details, path); - } -} - -t.Array = function(scheme) { - return function(x, details = {}, path = ['']) { - if (!Array.isArray(x)) { - details.error = `Expected "${path.join('.')}" to be an array; found \`${JSON.stringify(x)}\` (${typeof x}) instead.`; - return false; - } - const lastPathElement = path[path.length - 1]; - for (let i = 0; i < x.length; ++i) { - path[path.length - 1] = lastPathElement + `[${i}]`; - if (!checkScheme(scheme, x[i], details, path)) - return false; - } - path[path.length - 1] = lastPathElement; - return true; - } -} - -t.Recursive = function(types, schemeName) { - return function(x, details = {}, path = ['']) { - const scheme = types[schemeName]; - return checkScheme(scheme, x, details, path); - } -} - -function beauty(path, obj) { - if (path.length === 1) - return `object ${JSON.stringify(obj, null, 2)}`; - return `property "${path.join('.')}" - ${JSON.stringify(obj, null, 2)}`; -} - -function checkScheme(scheme, x, details = {}, path = ['']) { - if (!scheme) - throw new Error(`ILLDEFINED SCHEME: ${path.join('.')}`); - if (typeof scheme === 'object') { - if (!x) { - details.error = `Object "${path.join('.')}" is undefined, but has some scheme`; - return false; - } - for (const [propertyName, aScheme] of Object.entries(scheme)) { - path.push(propertyName); - const result = checkScheme(aScheme, x[propertyName], details, path); - path.pop(); - if (!result) - return false; - } - for (const propertyName of Object.keys(x)) { - if (!scheme[propertyName]) { - path.push(propertyName); - details.error = `Found ${beauty(path, x[propertyName])} which is not described in this scheme`; - return false; - } - } - return true; - } - return scheme(x, details, path); -} - -/* - -function test(scheme, obj) { - const details = {}; - if (!checkScheme(scheme, obj, details)) { - dump(`FAILED: ${JSON.stringify(obj)} - details.error: ${details.error} - `); - } else { - dump(`SUCCESS: ${JSON.stringify(obj)} -`); - } -} - -test(t.Array(t.String), ['a', 'b', 2, 'c']); -test(t.Either(t.String, t.Number), {}); - -*/ - -this.t = t; -this.checkScheme = checkScheme; -this.EXPORTED_SYMBOLS = ['t', 'checkScheme']; diff --git a/additions/juggler/protocol/Protocol.js b/additions/juggler/protocol/Protocol.js deleted file mode 100644 index 08ca173..0000000 --- a/additions/juggler/protocol/Protocol.js +++ /dev/null @@ -1,1019 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js'); - -// Protocol-specific types. -const browserTypes = {}; - -browserTypes.TargetInfo = { - type: t.Enum(['page']), - targetId: t.String, - browserContextId: t.Optional(t.String), - // PageId of parent tab, if any. - openerId: t.Optional(t.String), -}; - -browserTypes.UserPreference = { - name: t.String, - value: t.Any, -}; - -browserTypes.CookieOptions = { - name: t.String, - value: t.String, - url: t.Optional(t.String), - domain: t.Optional(t.String), - path: t.Optional(t.String), - secure: t.Optional(t.Boolean), - httpOnly: t.Optional(t.Boolean), - sameSite: t.Optional(t.Enum(['Strict', 'Lax', 'None'])), - expires: t.Optional(t.Number), -}; - -browserTypes.Cookie = { - name: t.String, - domain: t.String, - path: t.String, - value: t.String, - expires: t.Number, - size: t.Number, - httpOnly: t.Boolean, - secure: t.Boolean, - session: t.Boolean, - sameSite: t.Enum(['Strict', 'Lax', 'None']), -}; - -browserTypes.Geolocation = { - latitude: t.Number, - longitude: t.Number, - accuracy: t.Optional(t.Number), -}; - -browserTypes.DownloadOptions = { - behavior: t.Optional(t.Enum(['saveToDisk', 'cancel'])), - downloadsDir: t.Optional(t.String), -}; - -const pageTypes = {}; -pageTypes.DOMPoint = { - x: t.Number, - y: t.Number, -}; - -pageTypes.Rect = { - x: t.Number, - y: t.Number, - width: t.Number, - height: t.Number, -}; - -pageTypes.Size = { - width: t.Number, - height: t.Number, -}; - -pageTypes.Viewport = { - viewportSize: pageTypes.Size, - deviceScaleFactor: t.Optional(t.Number), -}; - -pageTypes.DOMQuad = { - p1: pageTypes.DOMPoint, - p2: pageTypes.DOMPoint, - p3: pageTypes.DOMPoint, - p4: pageTypes.DOMPoint, -}; - -pageTypes.TouchPoint = { - x: t.Number, - y: t.Number, - radiusX: t.Optional(t.Number), - radiusY: t.Optional(t.Number), - rotationAngle: t.Optional(t.Number), - force: t.Optional(t.Number), -}; - -pageTypes.Clip = { - x: t.Number, - y: t.Number, - width: t.Number, - height: t.Number, -}; - -pageTypes.InitScript = { - script: t.String, - worldName: t.Optional(t.String), -}; - -const runtimeTypes = {}; -runtimeTypes.RemoteObject = { - type: t.Optional(t.Enum(['object', 'function', 'undefined', 'string', 'number', 'boolean', 'symbol', 'bigint'])), - subtype: t.Optional(t.Enum(['array', 'null', 'node', 'regexp', 'date', 'map', 'set', 'weakmap', 'weakset', 'error', 'proxy', 'promise', 'typedarray'])), - objectId: t.Optional(t.String), - unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])), - value: t.Any -}; - -runtimeTypes.ObjectProperty = { - name: t.String, - value: runtimeTypes.RemoteObject, -}; - -runtimeTypes.ScriptLocation = { - columnNumber: t.Number, - lineNumber: t.Number, - url: t.String, -}; - -runtimeTypes.ExceptionDetails = { - text: t.Optional(t.String), - stack: t.Optional(t.String), - value: t.Optional(t.Any), -}; - -runtimeTypes.CallFunctionArgument = { - objectId: t.Optional(t.String), - unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])), - value: t.Any, -}; - -runtimeTypes.AuxData = { - frameId: t.Optional(t.String), - name: t.Optional(t.String), -}; - -const axTypes = {}; -axTypes.AXTree = { - role: t.String, - name: t.String, - children: t.Optional(t.Array(t.Recursive(axTypes, 'AXTree'))), - - selected: t.Optional(t.Boolean), - focused: t.Optional(t.Boolean), - pressed: t.Optional(t.Boolean), - focusable: t.Optional(t.Boolean), - haspopup: t.Optional(t.String), - required: t.Optional(t.Boolean), - invalid: t.Optional(t.Boolean), - modal: t.Optional(t.Boolean), - editable: t.Optional(t.Boolean), - busy: t.Optional(t.Boolean), - multiline: t.Optional(t.Boolean), - readonly: t.Optional(t.Boolean), - checked: t.Optional(t.Enum(['mixed', true])), - expanded: t.Optional(t.Boolean), - disabled: t.Optional(t.Boolean), - multiselectable: t.Optional(t.Boolean), - - value: t.Optional(t.String), - description: t.Optional(t.String), - - roledescription: t.Optional(t.String), - valuetext: t.Optional(t.String), - orientation: t.Optional(t.String), - autocomplete: t.Optional(t.String), - keyshortcuts: t.Optional(t.String), - - level: t.Optional(t.Number), - - tag: t.Optional(t.String), - - foundObject: t.Optional(t.Boolean), -} - -const networkTypes = {}; - -networkTypes.HTTPHeader = { - name: t.String, - value: t.String, -}; - -networkTypes.HTTPCredentials = { - username: t.String, - password: t.String, - origin: t.Optional(t.String), -}; - -networkTypes.SecurityDetails = { - protocol: t.String, - subjectName: t.String, - issuer: t.String, - validFrom: t.Number, - validTo: t.Number, -}; - -networkTypes.ResourceTiming = { - startTime: t.Number, - domainLookupStart: t.Number, - domainLookupEnd: t.Number, - connectStart: t.Number, - secureConnectionStart: t.Number, - connectEnd: t.Number, - requestStart: t.Number, - responseStart: t.Number, -}; - -const Browser = { - targets: ['browser'], - - types: browserTypes, - - events: { - 'attachedToTarget': { - sessionId: t.String, - targetInfo: browserTypes.TargetInfo, - }, - 'detachedFromTarget': { - sessionId: t.String, - targetId: t.String, - }, - 'downloadCreated': { - uuid: t.String, - browserContextId: t.Optional(t.String), - pageTargetId: t.String, - frameId: t.String, - url: t.String, - suggestedFileName: t.String, - }, - 'downloadFinished': { - uuid: t.String, - canceled: t.Optional(t.Boolean), - error: t.Optional(t.String), - }, - 'videoRecordingFinished': { - screencastId: t.String, - }, - }, - - methods: { - 'enable': { - params: { - attachToDefaultContext: t.Boolean, - userPrefs: t.Optional(t.Array(browserTypes.UserPreference)), - }, - }, - 'createBrowserContext': { - params: { - removeOnDetach: t.Optional(t.Boolean), - }, - returns: { - browserContextId: t.String, - }, - }, - 'removeBrowserContext': { - params: { - browserContextId: t.String, - }, - }, - 'newPage': { - params: { - browserContextId: t.Optional(t.String), - }, - returns: { - targetId: t.String, - } - }, - 'close': {}, - 'getInfo': { - returns: { - userAgent: t.String, - version: t.String, - }, - }, - 'setExtraHTTPHeaders': { - params: { - browserContextId: t.Optional(t.String), - headers: t.Array(networkTypes.HTTPHeader), - }, - }, - 'clearCache': {}, - 'setBrowserProxy': { - params: { - type: t.Enum(['http', 'https', 'socks', 'socks4']), - bypass: t.Array(t.String), - host: t.String, - port: t.Number, - username: t.Optional(t.String), - password: t.Optional(t.String), - }, - }, - 'setContextProxy': { - params: { - browserContextId: t.Optional(t.String), - type: t.Enum(['http', 'https', 'socks', 'socks4']), - bypass: t.Array(t.String), - host: t.String, - port: t.Number, - username: t.Optional(t.String), - password: t.Optional(t.String), - }, - }, - 'setHTTPCredentials': { - params: { - browserContextId: t.Optional(t.String), - credentials: t.Nullable(networkTypes.HTTPCredentials), - }, - }, - 'setRequestInterception': { - params: { - browserContextId: t.Optional(t.String), - enabled: t.Boolean, - }, - }, - 'setCacheDisabled': { - params: { - browserContextId: t.Optional(t.String), - cacheDisabled: t.Boolean, - }, - }, - 'setGeolocationOverride': { - params: { - browserContextId: t.Optional(t.String), - geolocation: t.Nullable(browserTypes.Geolocation), - } - }, - 'setUserAgentOverride': { - params: { - browserContextId: t.Optional(t.String), - userAgent: t.Nullable(t.String), - } - }, - 'setPlatformOverride': { - params: { - browserContextId: t.Optional(t.String), - platform: t.Nullable(t.String), - } - }, - 'setBypassCSP': { - params: { - browserContextId: t.Optional(t.String), - bypassCSP: t.Nullable(t.Boolean), - } - }, - 'setIgnoreHTTPSErrors': { - params: { - browserContextId: t.Optional(t.String), - ignoreHTTPSErrors: t.Nullable(t.Boolean), - } - }, - 'setJavaScriptDisabled': { - params: { - browserContextId: t.Optional(t.String), - javaScriptDisabled: t.Boolean, - } - }, - 'setLocaleOverride': { - params: { - browserContextId: t.Optional(t.String), - locale: t.Nullable(t.String), - } - }, - 'setTimezoneOverride': { - params: { - browserContextId: t.Optional(t.String), - timezoneId: t.Nullable(t.String), - } - }, - 'setDownloadOptions': { - params: { - browserContextId: t.Optional(t.String), - downloadOptions: t.Nullable(browserTypes.DownloadOptions), - } - }, - 'setTouchOverride': { - params: { - browserContextId: t.Optional(t.String), - hasTouch: t.Nullable(t.Boolean), - } - }, - 'setDefaultViewport': { - params: { - browserContextId: t.Optional(t.String), - viewport: t.Nullable(pageTypes.Viewport), - } - }, - 'setInitScripts': { - params: { - browserContextId: t.Optional(t.String), - scripts: t.Array(pageTypes.InitScript), - } - }, - 'addBinding': { - params: { - browserContextId: t.Optional(t.String), - worldName: t.Optional(t.String), - name: t.String, - script: t.String, - }, - }, - 'grantPermissions': { - params: { - origin: t.String, - browserContextId: t.Optional(t.String), - permissions: t.Array(t.String), - }, - }, - 'resetPermissions': { - params: { - browserContextId: t.Optional(t.String), - } - }, - 'setCookies': { - params: { - browserContextId: t.Optional(t.String), - cookies: t.Array(browserTypes.CookieOptions), - } - }, - 'clearCookies': { - params: { - browserContextId: t.Optional(t.String), - } - }, - 'getCookies': { - params: { - browserContextId: t.Optional(t.String) - }, - returns: { - cookies: t.Array(browserTypes.Cookie), - }, - }, - 'setOnlineOverride': { - params: { - browserContextId: t.Optional(t.String), - override: t.Nullable(t.Enum(['online', 'offline'])), - } - }, - 'setColorScheme': { - params: { - browserContextId: t.Optional(t.String), - colorScheme: t.Nullable(t.Enum(['dark', 'light', 'no-preference'])), - }, - }, - 'setReducedMotion': { - params: { - browserContextId: t.Optional(t.String), - reducedMotion: t.Nullable(t.Enum(['reduce', 'no-preference'])), - }, - }, - 'setForcedColors': { - params: { - browserContextId: t.Optional(t.String), - forcedColors: t.Nullable(t.Enum(['active', 'none'])), - }, - }, - 'setContrast': { - params: { - browserContextId: t.Optional(t.String), - contrast: t.Nullable(t.Enum(['less', 'more', 'custom', 'no-preference'])), - }, - }, - 'setVideoRecordingOptions': { - params: { - browserContextId: t.Optional(t.String), - options: t.Optional({ - dir: t.String, - width: t.Number, - height: t.Number, - }), - }, - }, - 'cancelDownload': { - params: { - uuid: t.Optional(t.String), - } - } - }, -}; - -const Heap = { - targets: ['page'], - types: {}, - events: {}, - methods: { - 'collectGarbage': { - params: {}, - }, - }, -}; - -const Network = { - targets: ['page'], - types: networkTypes, - events: { - 'requestWillBeSent': { - // frameId may be absent for redirected requests. - frameId: t.Optional(t.String), - requestId: t.String, - // RequestID of redirected request. - redirectedFrom: t.Optional(t.String), - postData: t.Optional(t.String), - headers: t.Array(networkTypes.HTTPHeader), - isIntercepted: t.Boolean, - url: t.String, - method: t.String, - navigationId: t.Optional(t.String), - cause: t.String, - internalCause: t.String, - }, - 'responseReceived': { - securityDetails: t.Nullable(networkTypes.SecurityDetails), - requestId: t.String, - fromCache: t.Boolean, - remoteIPAddress: t.Optional(t.String), - remotePort: t.Optional(t.Number), - status: t.Number, - statusText: t.String, - headers: t.Array(networkTypes.HTTPHeader), - timing: networkTypes.ResourceTiming, - fromServiceWorker: t.Boolean, - }, - 'requestFinished': { - requestId: t.String, - responseEndTime: t.Number, - transferSize: t.Number, - encodedBodySize: t.Number, - protocolVersion: t.Optional(t.String), - }, - 'requestFailed': { - requestId: t.String, - errorCode: t.String, - }, - }, - methods: { - 'setRequestInterception': { - params: { - enabled: t.Boolean, - }, - }, - 'setExtraHTTPHeaders': { - params: { - headers: t.Array(networkTypes.HTTPHeader), - }, - }, - 'abortInterceptedRequest': { - params: { - requestId: t.String, - errorCode: t.String, - }, - }, - 'resumeInterceptedRequest': { - params: { - requestId: t.String, - url: t.Optional(t.String), - method: t.Optional(t.String), - headers: t.Optional(t.Array(networkTypes.HTTPHeader)), - postData: t.Optional(t.String), - }, - }, - 'fulfillInterceptedRequest': { - params: { - requestId: t.String, - status: t.Number, - statusText: t.String, - headers: t.Array(networkTypes.HTTPHeader), - base64body: t.Optional(t.String), // base64-encoded - }, - }, - 'getResponseBody': { - params: { - requestId: t.String, - }, - returns: { - base64body: t.String, - evicted: t.Optional(t.Boolean), - }, - }, - }, -}; - -const Runtime = { - targets: ['page'], - types: runtimeTypes, - events: { - 'executionContextCreated': { - executionContextId: t.String, - auxData: runtimeTypes.AuxData, - }, - 'executionContextDestroyed': { - executionContextId: t.String, - }, - 'executionContextsCleared': { - }, - 'console': { - executionContextId: t.String, - args: t.Array(runtimeTypes.RemoteObject), - type: t.String, - location: runtimeTypes.ScriptLocation, - }, - }, - methods: { - 'evaluate': { - params: { - // Pass frameId here. - executionContextId: t.String, - expression: t.String, - returnByValue: t.Optional(t.Boolean), - }, - - returns: { - result: t.Optional(runtimeTypes.RemoteObject), - exceptionDetails: t.Optional(runtimeTypes.ExceptionDetails), - } - }, - 'callFunction': { - params: { - // Pass frameId here. - executionContextId: t.String, - functionDeclaration: t.String, - returnByValue: t.Optional(t.Boolean), - args: t.Array(runtimeTypes.CallFunctionArgument), - }, - - returns: { - result: t.Optional(runtimeTypes.RemoteObject), - exceptionDetails: t.Optional(runtimeTypes.ExceptionDetails), - } - }, - 'disposeObject': { - params: { - executionContextId: t.String, - objectId: t.String, - }, - }, - - 'getObjectProperties': { - params: { - executionContextId: t.String, - objectId: t.String, - }, - - returns: { - properties: t.Array(runtimeTypes.ObjectProperty), - } - }, - }, -}; - -const Page = { - targets: ['page'], - - types: pageTypes, - events: { - 'ready': { - }, - 'crashed': { - }, - 'eventFired': { - frameId: t.String, - name: t.Enum(['load', 'DOMContentLoaded']), - }, - 'uncaughtError': { - frameId: t.String, - message: t.String, - stack: t.String, - }, - 'frameAttached': { - frameId: t.String, - parentFrameId: t.Optional(t.String), - }, - 'frameDetached': { - frameId: t.String, - }, - 'navigationStarted': { - frameId: t.String, - navigationId: t.String, - }, - 'navigationCommitted': { - frameId: t.String, - // |navigationId| can only be null in response to enable. - navigationId: t.Optional(t.String), - url: t.String, - // frame.id or frame.name - name: t.String, - }, - 'navigationAborted': { - frameId: t.String, - navigationId: t.String, - errorText: t.String, - }, - 'sameDocumentNavigation': { - frameId: t.String, - url: t.String, - }, - 'dialogOpened': { - dialogId: t.String, - type: t.Enum(['prompt', 'alert', 'confirm', 'beforeunload']), - message: t.String, - defaultValue: t.Optional(t.String), - }, - 'dialogClosed': { - dialogId: t.String, - }, - 'bindingCalled': { - executionContextId: t.String, - name: t.String, - payload: t.Any, - }, - 'linkClicked': { - phase: t.Enum(['before', 'after']), - }, - 'willOpenNewWindowAsynchronously': {}, - 'fileChooserOpened': { - executionContextId: t.String, - element: runtimeTypes.RemoteObject - }, - 'workerCreated': { - workerId: t.String, - frameId: t.String, - url: t.String, - }, - 'workerDestroyed': { - workerId: t.String, - }, - 'dispatchMessageFromWorker': { - workerId: t.String, - message: t.String, - }, - 'videoRecordingStarted': { - screencastId: t.String, - file: t.String, - }, - 'webSocketCreated': { - frameId: t.String, - wsid: t.String, - requestURL: t.String, - }, - 'webSocketOpened': { - frameId: t.String, - requestId: t.String, - wsid: t.String, - effectiveURL: t.String, - }, - 'webSocketClosed': { - frameId: t.String, - wsid: t.String, - error: t.String, - }, - 'webSocketFrameSent': { - frameId: t.String, - wsid: t.String, - opcode: t.Number, - data: t.String, - }, - 'webSocketFrameReceived': { - frameId: t.String, - wsid: t.String, - opcode: t.Number, - data: t.String, - }, - 'screencastFrame': { - data: t.String, - deviceWidth: t.Number, - deviceHeight: t.Number, - }, - }, - - methods: { - 'close': { - params: { - runBeforeUnload: t.Optional(t.Boolean), - }, - }, - 'setFileInputFiles': { - params: { - frameId: t.String, - objectId: t.String, - files: t.Array(t.String), - }, - }, - 'addBinding': { - params: { - worldName: t.Optional(t.String), - name: t.String, - script: t.String, - }, - }, - 'setViewportSize': { - params: { - viewportSize: t.Nullable(pageTypes.Size), - }, - }, - 'bringToFront': { - params: { - }, - }, - 'setEmulatedMedia': { - params: { - type: t.Optional(t.Enum(['screen', 'print', ''])), - colorScheme: t.Optional(t.Enum(['dark', 'light', 'no-preference'])), - reducedMotion: t.Optional(t.Enum(['reduce', 'no-preference'])), - forcedColors: t.Optional(t.Enum(['active', 'none'])), - }, - }, - 'setCacheDisabled': { - params: { - cacheDisabled: t.Boolean, - }, - }, - 'describeNode': { - params: { - frameId: t.String, - objectId: t.String, - }, - returns: { - contentFrameId: t.Optional(t.String), - ownerFrameId: t.Optional(t.String), - }, - }, - 'scrollIntoViewIfNeeded': { - params: { - frameId: t.String, - objectId: t.String, - rect: t.Optional(pageTypes.Rect), - }, - }, - 'setInitScripts': { - params: { - scripts: t.Array(pageTypes.InitScript) - } - }, - 'navigate': { - params: { - frameId: t.String, - url: t.String, - referer: t.Optional(t.String), - }, - returns: { - navigationId: t.Nullable(t.String), - } - }, - 'goBack': { - params: { - frameId: t.String, - }, - returns: { - success: t.Boolean, - }, - }, - 'goForward': { - params: { - frameId: t.String, - }, - returns: { - success: t.Boolean, - }, - }, - 'reload': { - params: { }, - }, - 'adoptNode': { - params: { - frameId: t.String, - // Missing objectId adopts frame owner. - objectId: t.Optional(t.String), - executionContextId: t.String, - }, - returns: { - remoteObject: t.Nullable(runtimeTypes.RemoteObject), - }, - }, - 'screenshot': { - params: { - mimeType: t.Enum(['image/png', 'image/jpeg']), - clip: pageTypes.Clip, - quality: t.Optional(t.Number), - omitDeviceScaleFactor: t.Optional(t.Boolean), - }, - returns: { - data: t.String, - } - }, - 'getContentQuads': { - params: { - frameId: t.String, - objectId: t.String, - }, - returns: { - quads: t.Array(pageTypes.DOMQuad), - }, - }, - 'dispatchKeyEvent': { - params: { - type: t.String, - key: t.String, - keyCode: t.Number, - location: t.Number, - code: t.String, - repeat: t.Boolean, - text: t.Optional(t.String), - } - }, - 'dispatchTouchEvent': { - params: { - type: t.Enum(['touchStart', 'touchEnd', 'touchMove', 'touchCancel']), - touchPoints: t.Array(pageTypes.TouchPoint), - modifiers: t.Number, - }, - returns: { - defaultPrevented: t.Boolean, - } - }, - 'dispatchTapEvent': { - params: { - x: t.Number, - y: t.Number, - modifiers: t.Number, - } - }, - 'dispatchMouseEvent': { - params: { - type: t.Enum(['mousedown', 'mousemove', 'mouseup']), - button: t.Number, - x: t.Number, - y: t.Number, - modifiers: t.Number, - clickCount: t.Optional(t.Number), - buttons: t.Number, - } - }, - 'dispatchWheelEvent': { - params: { - x: t.Number, - y: t.Number, - deltaX: t.Number, - deltaY: t.Number, - deltaZ: t.Number, - modifiers: t.Number, - } - }, - 'insertText': { - params: { - text: t.String, - } - }, - 'crash': { - params: {} - }, - 'handleDialog': { - params: { - dialogId: t.String, - accept: t.Boolean, - promptText: t.Optional(t.String), - }, - }, - 'setInterceptFileChooserDialog': { - params: { - enabled: t.Boolean, - }, - }, - 'sendMessageToWorker': { - params: { - frameId: t.String, - workerId: t.String, - message: t.String, - }, - }, - 'startScreencast': { - params: { - width: t.Number, - height: t.Number, - quality: t.Number, - }, - returns: { - screencastId: t.String, - }, - }, - 'screencastFrameAck': { - params: { - screencastId: t.String, - }, - }, - 'stopScreencast': { - }, - }, -}; - - -const Accessibility = { - targets: ['page'], - types: axTypes, - events: {}, - methods: { - 'getFullAXTree': { - params: { - objectId: t.Optional(t.String), - }, - returns: { - tree: axTypes.AXTree - }, - } - } -} - -this.protocol = { - domains: {Browser, Heap, Page, Runtime, Network, Accessibility}, -}; -this.checkScheme = checkScheme; -this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme']; diff --git a/additions/juggler/screencast/HeadlessWindowCapturer.cpp b/additions/juggler/screencast/HeadlessWindowCapturer.cpp deleted file mode 100644 index 75200db..0000000 --- a/additions/juggler/screencast/HeadlessWindowCapturer.cpp +++ /dev/null @@ -1,150 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "HeadlessWindowCapturer.h" - -#include "api/video/i420_buffer.h" -#include "HeadlessWidget.h" -#include "libyuv.h" -#include "mozilla/EndianUtils.h" -#include "mozilla/gfx/DataSurfaceHelpers.h" -#include "rtc_base/ref_counted_object.h" -#include "rtc_base/time_utils.h" -#include "api/scoped_refptr.h" - -using namespace mozilla::widget; -using namespace webrtc; - -namespace mozilla { - -rtc::scoped_refptr HeadlessWindowCapturer::Create(HeadlessWidget* headlessWindow) { - return rtc::scoped_refptr( - new rtc::RefCountedObject(headlessWindow) - ); -} - -HeadlessWindowCapturer::HeadlessWindowCapturer(mozilla::widget::HeadlessWidget* window) - : mWindow(window) { -} -HeadlessWindowCapturer::~HeadlessWindowCapturer() { - StopCapture(); -} - - -void HeadlessWindowCapturer::RegisterCaptureDataCallback(rtc::VideoSinkInterface* dataCallback) { - rtc::CritScope lock2(&_callBackCs); - _dataCallBacks.insert(dataCallback); -} - -void HeadlessWindowCapturer::RegisterCaptureDataCallback(webrtc::RawVideoSinkInterface* dataCallback) { -} - -void HeadlessWindowCapturer::DeRegisterCaptureDataCallback(rtc::VideoSinkInterface* dataCallback) { - rtc::CritScope lock2(&_callBackCs); - auto it = _dataCallBacks.find(dataCallback); - if (it != _dataCallBacks.end()) { - _dataCallBacks.erase(it); - } -} - -void HeadlessWindowCapturer::RegisterRawFrameCallback(webrtc::RawFrameCallback* rawFrameCallback) { - rtc::CritScope lock2(&_callBackCs); - _rawFrameCallbacks.insert(rawFrameCallback); -} - -void HeadlessWindowCapturer::DeRegisterRawFrameCallback(webrtc::RawFrameCallback* rawFrameCallback) { - rtc::CritScope lock2(&_callBackCs); - auto it = _rawFrameCallbacks.find(rawFrameCallback); - if (it != _rawFrameCallbacks.end()) { - _rawFrameCallbacks.erase(it); - } -} - -void HeadlessWindowCapturer::NotifyFrameCaptured(const webrtc::VideoFrame& frame) { - rtc::CritScope lock2(&_callBackCs); - for (auto dataCallBack : _dataCallBacks) - dataCallBack->OnFrame(frame); -} - -int32_t HeadlessWindowCapturer::StopCaptureIfAllClientsClose() { - if (_dataCallBacks.empty()) { - return StopCapture(); - } else { - return 0; - } -} - -int32_t HeadlessWindowCapturer::StartCapture(const webrtc::VideoCaptureCapability& capability) { - mWindow->SetSnapshotListener([this] (RefPtr&& dataSurface){ - if (!NS_IsInCompositorThread()) { - fprintf(stderr, "SnapshotListener is called not on the Compositor thread!\n"); - return; - } - - if (dataSurface->GetFormat() != gfx::SurfaceFormat::B8G8R8A8) { - fprintf(stderr, "Unexpected snapshot surface format: %hhd\n", dataSurface->GetFormat()); - return; - } - - webrtc::VideoCaptureCapability frameInfo; - frameInfo.width = dataSurface->GetSize().width; - frameInfo.height = dataSurface->GetSize().height; -#if MOZ_LITTLE_ENDIAN() - frameInfo.videoType = VideoType::kARGB; -#else - frameInfo.videoType = VideoType::kBGRA; -#endif - - { - rtc::CritScope lock2(&_callBackCs); - for (auto rawFrameCallback : _rawFrameCallbacks) { - rawFrameCallback->OnRawFrame(dataSurface->GetData(), dataSurface->Stride(), frameInfo); - } - if (!_dataCallBacks.size()) - return; - } - - int width = dataSurface->GetSize().width; - int height = dataSurface->GetSize().height; - rtc::scoped_refptr buffer = I420Buffer::Create(width, height); - - gfx::DataSourceSurface::ScopedMap map(dataSurface.get(), gfx::DataSourceSurface::MapType::READ); - if (!map.IsMapped()) { - fprintf(stderr, "Failed to map snapshot bytes!\n"); - return; - } - -#if MOZ_LITTLE_ENDIAN() - const int conversionResult = libyuv::ARGBToI420( -#else - const int conversionResult = libyuv::BGRAToI420( -#endif - map.GetData(), map.GetStride(), - buffer->MutableDataY(), buffer->StrideY(), - buffer->MutableDataU(), buffer->StrideU(), - buffer->MutableDataV(), buffer->StrideV(), - width, height); - if (conversionResult != 0) { - fprintf(stderr, "Failed to convert capture frame to I420: %d\n", conversionResult); - return; - } - - VideoFrame captureFrame(buffer, 0, rtc::TimeMillis(), kVideoRotation_0); - NotifyFrameCaptured(captureFrame); - }); - return 0; -} - -int32_t HeadlessWindowCapturer::StopCapture() { - if (!CaptureStarted()) - return 0; - mWindow->SetSnapshotListener(nullptr); - return 0; -} - -bool HeadlessWindowCapturer::CaptureStarted() { - return true; -} - -} // namespace mozilla diff --git a/additions/juggler/screencast/HeadlessWindowCapturer.h b/additions/juggler/screencast/HeadlessWindowCapturer.h deleted file mode 100644 index cb1bcbb..0000000 --- a/additions/juggler/screencast/HeadlessWindowCapturer.h +++ /dev/null @@ -1,65 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#pragma once - -#include -#include -#include "api/video/video_frame.h" -#include "api/video/video_sink_interface.h" -#include "modules/video_capture/video_capture.h" -#include "rtc_base/deprecated/recursive_critical_section.h" -#include "video_engine/desktop_capture_impl.h" - -class nsIWidget; - -namespace mozilla { - -namespace widget { -class HeadlessWidget; -} - -class HeadlessWindowCapturer : public webrtc::VideoCaptureModuleEx { - public: - static rtc::scoped_refptr Create(mozilla::widget::HeadlessWidget*); - - void RegisterCaptureDataCallback( - rtc::VideoSinkInterface* dataCallback) override; - void DeRegisterCaptureDataCallback( - rtc::VideoSinkInterface* dataCallback) override; - int32_t StopCaptureIfAllClientsClose() override; - - void RegisterRawFrameCallback(webrtc::RawFrameCallback* rawFrameCallback) override; - void RegisterCaptureDataCallback(webrtc::RawVideoSinkInterface* dataCallback) override; - void DeRegisterRawFrameCallback(webrtc::RawFrameCallback* rawFrameCallback) override; - - int32_t SetCaptureRotation(webrtc::VideoRotation) override { return -1; } - bool SetApplyRotation(bool) override { return false; } - bool GetApplyRotation() override { return true; } - - const char* CurrentDeviceName() const override { return "Headless window"; } - - // Platform dependent - int32_t StartCapture(const webrtc::VideoCaptureCapability& capability) override; - bool FocusOnSelectedSource() override { return false; } - int32_t StopCapture() override; - bool CaptureStarted() override; - int32_t CaptureSettings(webrtc::VideoCaptureCapability& settings) override { - return -1; - } - - protected: - HeadlessWindowCapturer(mozilla::widget::HeadlessWidget*); - ~HeadlessWindowCapturer() override; - - private: - void NotifyFrameCaptured(const webrtc::VideoFrame& frame); - - RefPtr mWindow; - rtc::RecursiveCriticalSection _callBackCs; - std::set*> _dataCallBacks; - std::set _rawFrameCallbacks; -}; - -} // namespace mozilla diff --git a/additions/juggler/screencast/ScreencastEncoder.cpp b/additions/juggler/screencast/ScreencastEncoder.cpp deleted file mode 100644 index 701361a..0000000 --- a/additions/juggler/screencast/ScreencastEncoder.cpp +++ /dev/null @@ -1,404 +0,0 @@ -/* - * Copyright (c) 2010, The WebM Project authors. All rights reserved. - * Copyright (c) 2013 The Chromium Authors. All rights reserved. - * Copyright (C) 2020 Microsoft Corporation. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "ScreencastEncoder.h" - -#include -#include -#include -#include -#include -#include "nsIThread.h" -#include "nsThreadUtils.h" -#include "WebMFileWriter.h" -#include "api/video/video_frame.h" - -namespace mozilla { - -namespace { - -struct VpxCodecDeleter { - void operator()(vpx_codec_ctx_t* codec) { - if (codec) { - vpx_codec_err_t ret = vpx_codec_destroy(codec); - if (ret != VPX_CODEC_OK) - fprintf(stderr, "Failed to destroy codec: %s\n", vpx_codec_error(codec)); - } - } -}; - -using ScopedVpxCodec = std::unique_ptr; - -// Number of timebase unints per one frame. -constexpr int timeScale = 1000; - -// Defines the dimension of a macro block. This is used to compute the active -// map for the encoder. -const int kMacroBlockSize = 16; - -void createImage(unsigned int width, unsigned int height, - std::unique_ptr& out_image, - std::unique_ptr& out_image_buffer, - int& out_buffer_size) { - std::unique_ptr image(new vpx_image_t()); - memset(image.get(), 0, sizeof(vpx_image_t)); - - // libvpx seems to require both to be assigned. - image->d_w = width; - image->w = width; - image->d_h = height; - image->h = height; - - // I420 - image->fmt = VPX_IMG_FMT_YV12; - image->x_chroma_shift = 1; - image->y_chroma_shift = 1; - - // libyuv's fast-path requires 16-byte aligned pointers and strides, so pad - // the Y, U and V planes' strides to multiples of 16 bytes. - const int y_stride = ((image->w - 1) & ~15) + 16; - const int uv_unaligned_stride = y_stride >> image->x_chroma_shift; - const int uv_stride = ((uv_unaligned_stride - 1) & ~15) + 16; - - // libvpx accesses the source image in macro blocks, and will over-read - // if the image is not padded out to the next macroblock: crbug.com/119633. - // Pad the Y, U and V planes' height out to compensate. - // Assuming macroblocks are 16x16, aligning the planes' strides above also - // macroblock aligned them. - static_assert(kMacroBlockSize == 16, "macroblock_size_not_16"); - const int y_rows = ((image->h - 1) & ~(kMacroBlockSize-1)) + kMacroBlockSize; - const int uv_rows = y_rows >> image->y_chroma_shift; - - // Allocate a YUV buffer large enough for the aligned data & padding. - out_buffer_size = y_stride * y_rows + 2*uv_stride * uv_rows; - std::unique_ptr image_buffer(new uint8_t[out_buffer_size]); - - // Reset image value to 128 so we just need to fill in the y plane. - memset(image_buffer.get(), 128, out_buffer_size); - - // Fill in the information for |image_|. - unsigned char* uchar_buffer = - reinterpret_cast(image_buffer.get()); - image->planes[0] = uchar_buffer; - image->planes[1] = image->planes[0] + y_stride * y_rows; - image->planes[2] = image->planes[1] + uv_stride * uv_rows; - image->stride[0] = y_stride; - image->stride[1] = uv_stride; - image->stride[2] = uv_stride; - - out_image = std::move(image); - out_image_buffer = std::move(image_buffer); -} - -} // namespace - -class ScreencastEncoder::VPXFrame { -public: - VPXFrame(rtc::scoped_refptr&& buffer, const gfx::IntMargin& margin) - : m_frameBuffer(std::move(buffer)) - , m_margin(margin) - { } - - void setDuration(TimeDuration duration) { m_duration = duration; } - TimeDuration duration() const { return m_duration; } - - void convertToVpxImage(vpx_image_t* image) - { - if (m_frameBuffer->type() != webrtc::VideoFrameBuffer::Type::kI420) { - fprintf(stderr, "convertToVpxImage unexpected frame buffer type: %d\n", m_frameBuffer->type()); - return; - } - - auto src = m_frameBuffer->GetI420(); - const int y_stride = image->stride[VPX_PLANE_Y]; - MOZ_ASSERT(image->stride[VPX_PLANE_U] == image->stride[VPX_PLANE_V]); - const int uv_stride = image->stride[1]; - uint8_t* y_data = image->planes[VPX_PLANE_Y]; - uint8_t* u_data = image->planes[VPX_PLANE_U]; - uint8_t* v_data = image->planes[VPX_PLANE_V]; - - /** - * Let's say we have the following image of 6x3 pixels (same number = same pixel value): - * 112233 - * 112233 - * 445566 - * In I420 format (see https://en.wikipedia.org/wiki/YUV), the image will have the following data planes: - * Y [stride_Y = 6]: - * 112233 - * 112233 - * 445566 - * U [stride_U = 3] - this plane has aggregate for each 2x2 pixels: - * 123 - * 456 - * V [stride_V = 3] - this plane has aggregate for each 2x2 pixels: - * 123 - * 456 - * - * To crop this image efficiently, we can move src_Y/U/V pointer and - * adjust the src_width and src_height. However, we must cut off only **even** - * amount of lines and columns to retain semantic of U and V planes which - * contain only 1/4 of pixel information. - */ - int yuvTopOffset = m_margin.top + (m_margin.top & 1); - int yuvLeftOffset = m_margin.left + (m_margin.left & 1); - - double src_width = src->width() - yuvLeftOffset; - double src_height = src->height() - yuvTopOffset; - - if (src_width > image->w || src_height > image->h) { - double scale = std::min(image->w / src_width, image->h / src_height); - double dst_width = src_width * scale; - if (dst_width > image->w) { - src_width *= image->w / dst_width; - dst_width = image->w; - } - double dst_height = src_height * scale; - if (dst_height > image->h) { - src_height *= image->h / dst_height; - dst_height = image->h; - } - libyuv::I420Scale(src->DataY() + yuvTopOffset * src->StrideY() + yuvLeftOffset, src->StrideY(), - src->DataU() + (yuvTopOffset * src->StrideU() + yuvLeftOffset) / 2, src->StrideU(), - src->DataV() + (yuvTopOffset * src->StrideV() + yuvLeftOffset) / 2, src->StrideV(), - src_width, src_height, - y_data, y_stride, - u_data, uv_stride, - v_data, uv_stride, - dst_width, dst_height, - libyuv::kFilterBilinear); - } else { - int width = std::min(image->w, src_width); - int height = std::min(image->h, src_height); - - libyuv::I420Copy(src->DataY() + yuvTopOffset * src->StrideY() + yuvLeftOffset, src->StrideY(), - src->DataU() + (yuvTopOffset * src->StrideU() + yuvLeftOffset) / 2, src->StrideU(), - src->DataV() + (yuvTopOffset * src->StrideV() + yuvLeftOffset) / 2, src->StrideV(), - y_data, y_stride, - u_data, uv_stride, - v_data, uv_stride, - width, height); - } - } - -private: - rtc::scoped_refptr m_frameBuffer; - gfx::IntMargin m_margin; - TimeDuration m_duration; -}; - - -class ScreencastEncoder::VPXCodec { -public: - VPXCodec(ScopedVpxCodec codec, vpx_codec_enc_cfg_t cfg, FILE* file) - : m_codec(std::move(codec)) - , m_cfg(cfg) - , m_file(file) - , m_writer(new WebMFileWriter(file, &m_cfg)) - { - nsresult rv = NS_NewNamedThread("Screencast enc", getter_AddRefs(m_encoderQueue)); - if (rv != NS_OK) { - fprintf(stderr, "ScreencastEncoder::VPXCodec failed to spawn thread %d\n", rv); - return; - } - - createImage(cfg.g_w, cfg.g_h, m_image, m_imageBuffer, m_imageBufferSize); - } - - ~VPXCodec() { - m_encoderQueue->Shutdown(); - m_encoderQueue = nullptr; - } - - void encodeFrameAsync(std::unique_ptr&& frame) - { - m_encoderQueue->Dispatch(NS_NewRunnableFunction("VPXCodec::encodeFrameAsync", [this, frame = std::move(frame)] { - memset(m_imageBuffer.get(), 128, m_imageBufferSize); - frame->convertToVpxImage(m_image.get()); - - double frameCount = frame->duration().ToSeconds() * fps; - // For long duration repeat frame at 1 fps to ensure last frame duration is short enough. - // TODO: figure out why simply passing duration doesn't work well. - for (;frameCount > 1.5; frameCount -= 1) { - encodeFrame(m_image.get(), timeScale); - } - encodeFrame(m_image.get(), std::max(1, frameCount * timeScale)); - })); - } - - void finishAsync(std::function&& callback) - { - m_encoderQueue->Dispatch(NS_NewRunnableFunction("VPXCodec::finishAsync", [this, callback = std::move(callback)] { - finish(); - callback(); - })); - } - -private: - bool encodeFrame(vpx_image_t *img, int duration) - { - vpx_codec_iter_t iter = nullptr; - const vpx_codec_cx_pkt_t *pkt = nullptr; - int flags = 0; - const vpx_codec_err_t res = vpx_codec_encode(m_codec.get(), img, m_pts, duration, flags, VPX_DL_REALTIME); - if (res != VPX_CODEC_OK) { - fprintf(stderr, "Failed to encode frame: %s\n", vpx_codec_error(m_codec.get())); - return false; - } - - bool gotPkts = false; - while ((pkt = vpx_codec_get_cx_data(m_codec.get(), &iter)) != nullptr) { - gotPkts = true; - - if (pkt->kind == VPX_CODEC_CX_FRAME_PKT) { - m_writer->writeFrame(pkt); - ++m_frameCount; - // fprintf(stderr, " #%03d %spts=%" PRId64 " sz=%zd\n", m_frameCount, (pkt->data.frame.flags & VPX_FRAME_IS_KEY) != 0 ? "[K] " : "", pkt->data.frame.pts, pkt->data.frame.sz); - m_pts += pkt->data.frame.duration; - } - } - - return gotPkts; - } - - void finish() - { - // Flush encoder. - while (encodeFrame(nullptr, 1)) - ++m_frameCount; - - m_writer->finish(); - fclose(m_file); - // fprintf(stderr, "ScreencastEncoder::finish %d frames\n", m_frameCount); - } - - RefPtr m_encoderQueue; - ScopedVpxCodec m_codec; - vpx_codec_enc_cfg_t m_cfg; - FILE* m_file { nullptr }; - std::unique_ptr m_writer; - int m_frameCount { 0 }; - int64_t m_pts { 0 }; - std::unique_ptr m_imageBuffer; - int m_imageBufferSize { 0 }; - std::unique_ptr m_image; -}; - -ScreencastEncoder::ScreencastEncoder(std::unique_ptr vpxCodec, const gfx::IntMargin& margin) - : m_vpxCodec(std::move(vpxCodec)) - , m_margin(margin) -{ -} - -ScreencastEncoder::~ScreencastEncoder() -{ -} - -std::unique_ptr ScreencastEncoder::create(nsCString& errorString, const nsCString& filePath, int width, int height, const gfx::IntMargin& margin) -{ - vpx_codec_iface_t* codec_interface = vpx_codec_vp8_cx(); - if (!codec_interface) { - errorString = "Codec not found."; - return nullptr; - } - - if (width <= 0 || height <= 0 || (width % 2) != 0 || (height % 2) != 0) { - errorString.AppendPrintf("Invalid frame size: %dx%d", width, height); - return nullptr; - } - - vpx_codec_enc_cfg_t cfg; - memset(&cfg, 0, sizeof(cfg)); - vpx_codec_err_t error = vpx_codec_enc_config_default(codec_interface, &cfg, 0); - if (error) { - errorString.AppendPrintf("Failed to get default codec config: %s", vpx_codec_err_to_string(error)); - return nullptr; - } - - cfg.g_w = width; - cfg.g_h = height; - cfg.g_timebase.num = 1; - cfg.g_timebase.den = fps * timeScale; - cfg.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT; - - ScopedVpxCodec codec(new vpx_codec_ctx_t); - if (vpx_codec_enc_init(codec.get(), codec_interface, &cfg, 0)) { - errorString.AppendPrintf("Failed to initialize encoder: %s", vpx_codec_error(codec.get())); - return nullptr; - } - - FILE* file = fopen(filePath.get(), "wb"); - if (!file) { - errorString.AppendPrintf("Failed to open file '%s' for writing: %s", filePath.get(), strerror(errno)); - return nullptr; - } - - std::unique_ptr vpxCodec(new VPXCodec(std::move(codec), cfg, file)); - // fprintf(stderr, "ScreencastEncoder initialized with: %s\n", vpx_codec_iface_name(codec_interface)); - return std::make_unique(std::move(vpxCodec), margin); -} - -void ScreencastEncoder::flushLastFrame() -{ - TimeStamp now = TimeStamp::Now(); - if (m_lastFrameTimestamp) { - // If previous frame encoding failed for some rason leave the timestampt intact. - if (!m_lastFrame) - return; - - m_lastFrame->setDuration(now - m_lastFrameTimestamp); - m_vpxCodec->encodeFrameAsync(std::move(m_lastFrame)); - } - m_lastFrameTimestamp = now; -} - -void ScreencastEncoder::encodeFrame(const webrtc::VideoFrame& videoFrame) -{ - // fprintf(stderr, "ScreencastEncoder::encodeFrame\n"); - flushLastFrame(); - - m_lastFrame = std::make_unique(videoFrame.video_frame_buffer(), m_margin); -} - -void ScreencastEncoder::finish(std::function&& callback) -{ - if (!m_vpxCodec) { - callback(); - return; - } - - flushLastFrame(); - m_vpxCodec->finishAsync([callback = std::move(callback)] () mutable { - NS_DispatchToMainThread(NS_NewRunnableFunction("ScreencastEncoder::finish callback", std::move(callback))); - }); -} - - -} // namespace mozilla diff --git a/additions/juggler/screencast/ScreencastEncoder.h b/additions/juggler/screencast/ScreencastEncoder.h deleted file mode 100644 index 883ad01..0000000 --- a/additions/juggler/screencast/ScreencastEncoder.h +++ /dev/null @@ -1,45 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#pragma once - -#include -#include -#include "mozilla/gfx/Rect.h" -#include "mozilla/Maybe.h" -#include "mozilla/TimeStamp.h" -#include "nsISupportsImpl.h" -#include "nsStringFwd.h" - -namespace webrtc { -class VideoFrame; -} - -namespace mozilla { - -class ScreencastEncoder { -public: - static constexpr int fps = 25; - - static std::unique_ptr create(nsCString& errorString, const nsCString& filePath, int width, int height, const gfx::IntMargin& margin); - - class VPXCodec; - ScreencastEncoder(std::unique_ptr, const gfx::IntMargin& margin); - ~ScreencastEncoder(); - - void encodeFrame(const webrtc::VideoFrame& videoFrame); - - void finish(std::function&& callback); - -private: - void flushLastFrame(); - - std::unique_ptr m_vpxCodec; - gfx::IntMargin m_margin; - TimeStamp m_lastFrameTimestamp; - class VPXFrame; - std::unique_ptr m_lastFrame; -}; - -} // namespace mozilla diff --git a/additions/juggler/screencast/WebMFileWriter.cpp b/additions/juggler/screencast/WebMFileWriter.cpp deleted file mode 100644 index f720b30..0000000 --- a/additions/juggler/screencast/WebMFileWriter.cpp +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2014 The WebM project authors. All Rights Reserved. - */ - -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "WebMFileWriter.h" - -#include -#include "mkvmuxer/mkvmuxerutil.h" - -namespace mozilla { - -WebMFileWriter::WebMFileWriter(FILE* file, vpx_codec_enc_cfg_t* cfg) - : m_cfg(cfg) - , m_writer(new mkvmuxer::MkvWriter(file)) - , m_segment(new mkvmuxer::Segment()) { - m_segment->Init(m_writer.get()); - m_segment->set_mode(mkvmuxer::Segment::kFile); - m_segment->OutputCues(true); - - mkvmuxer::SegmentInfo* info = m_segment->GetSegmentInfo(); - std::string version = "Playwright " + std::string(vpx_codec_version_str()); - info->set_writing_app(version.c_str()); - - // Add vp8 track. - m_videoTrackId = m_segment->AddVideoTrack( - static_cast(m_cfg->g_w), static_cast(m_cfg->g_h), 0); - if (!m_videoTrackId) { - fprintf(stderr, "Failed to add video track\n"); - } -} - -WebMFileWriter::~WebMFileWriter() {} - -void WebMFileWriter::writeFrame(const vpx_codec_cx_pkt_t* pkt) { - int64_t pts_ns = pkt->data.frame.pts * 1000000000ll * m_cfg->g_timebase.num / - m_cfg->g_timebase.den; - m_segment->AddFrame(static_cast(pkt->data.frame.buf), - pkt->data.frame.sz, m_videoTrackId, pts_ns, - pkt->data.frame.flags & VPX_FRAME_IS_KEY); -} - -void WebMFileWriter::finish() { - m_segment->Finalize(); -} - -} // namespace mozilla diff --git a/additions/juggler/screencast/WebMFileWriter.h b/additions/juggler/screencast/WebMFileWriter.h deleted file mode 100644 index 4a7fd06..0000000 --- a/additions/juggler/screencast/WebMFileWriter.h +++ /dev/null @@ -1,32 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#pragma once - -#include -#include -#include -#include "vpx/vpx_encoder.h" - -#include "mkvmuxer/mkvmuxer.h" -#include "mkvmuxer/mkvwriter.h" - -namespace mozilla { - -class WebMFileWriter { -public: - WebMFileWriter(FILE*, vpx_codec_enc_cfg_t* cfg); - ~WebMFileWriter(); - - void writeFrame(const vpx_codec_cx_pkt_t* pkt); - void finish(); - -private: - vpx_codec_enc_cfg_t* m_cfg = nullptr; - std::unique_ptr m_writer; - std::unique_ptr m_segment; - uint64_t m_videoTrackId = 0; -}; - -} // namespace mozilla diff --git a/additions/juggler/screencast/components.conf b/additions/juggler/screencast/components.conf deleted file mode 100644 index 6298739..0000000 --- a/additions/juggler/screencast/components.conf +++ /dev/null @@ -1,15 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -Classes = [ - { - 'cid': '{d8c4d9e0-9462-445e-9e43-68d3872ad1de}', - 'contract_ids': ['@mozilla.org/juggler/screencast;1'], - 'type': 'nsIScreencastService', - 'constructor': 'mozilla::nsScreencastService::GetSingleton', - 'headers': ['/juggler/screencast/nsScreencastService.h'], - }, -] diff --git a/additions/juggler/screencast/moz.build b/additions/juggler/screencast/moz.build deleted file mode 100644 index f89c54e..0000000 --- a/additions/juggler/screencast/moz.build +++ /dev/null @@ -1,49 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -XPIDL_SOURCES += [ - 'nsIScreencastService.idl', -] - -XPIDL_MODULE = 'jugglerscreencast' - -SOURCES += [ - 'HeadlessWindowCapturer.cpp', - 'nsScreencastService.cpp', - 'ScreencastEncoder.cpp', -] - -XPCOM_MANIFESTS += [ - 'components.conf', -] - -LOCAL_INCLUDES += [ - '/dom/media/systemservices', - '/media/libyuv/libyuv/include', - '/third_party/abseil-cpp', - '/third_party/libwebrtc', -] - -LOCAL_INCLUDES += [ - '/widget', - '/widget/headless', -] - -LOCAL_INCLUDES += [ - '/third_party/aom/third_party/libwebm', -] - -SOURCES += [ - '/third_party/aom/third_party/libwebm/mkvmuxer/mkvmuxer.cc', - '/third_party/aom/third_party/libwebm/mkvmuxer/mkvmuxerutil.cc', - '/third_party/aom/third_party/libwebm/mkvmuxer/mkvwriter.cc', - 'WebMFileWriter.cpp', -] - -include('/dom/media/webrtc/third_party_build/webrtc.mozbuild') -include('/ipc/chromium/chromium-config.mozbuild') - -FINAL_LIBRARY = 'xul' diff --git a/additions/juggler/screencast/nsIScreencastService.idl b/additions/juggler/screencast/nsIScreencastService.idl deleted file mode 100644 index 16c9437..0000000 --- a/additions/juggler/screencast/nsIScreencastService.idl +++ /dev/null @@ -1,31 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "nsISupports.idl" - -interface nsIDocShell; - -[scriptable, uuid(0b5d32c4-aeeb-11eb-8529-0242ac130003)] -interface nsIScreencastServiceClient : nsISupports -{ - void screencastFrame(in AString frame, in uint32_t deviceWidth, in uint32_t deviceHeight); - - void screencastStopped(); -}; - -/** - * Service for recording window video. - */ -[scriptable, uuid(d8c4d9e0-9462-445e-9e43-68d3872ad1de)] -interface nsIScreencastService : nsISupports -{ - AString startVideoRecording(in nsIScreencastServiceClient client, in nsIDocShell docShell, in boolean isVideo, in ACString fileName, in uint32_t width, in uint32_t height, in uint32_t quality, in uint32_t viewportWidth, in uint32_t viewportHeight, in uint32_t offset_top); - - /** - * Will emit 'juggler-screencast-stopped' when the video file is saved. - */ - void stopVideoRecording(in AString sessionId); - - void screencastFrameAck(in AString sessionId); -}; diff --git a/additions/juggler/screencast/nsScreencastService.cpp b/additions/juggler/screencast/nsScreencastService.cpp deleted file mode 100644 index 062a851..0000000 --- a/additions/juggler/screencast/nsScreencastService.cpp +++ /dev/null @@ -1,394 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "nsScreencastService.h" - -#include "ScreencastEncoder.h" -#include "HeadlessWidget.h" -#include "HeadlessWindowCapturer.h" -#include "mozilla/Base64.h" -#include "mozilla/ClearOnShutdown.h" -#include "mozilla/PresShell.h" -#include "mozilla/StaticPtr.h" -#include "nsIDocShell.h" -#include "nsIObserverService.h" -#include "nsIRandomGenerator.h" -#include "nsISupportsPrimitives.h" -#include "nsThreadManager.h" -#include "nsView.h" -#include "nsViewManager.h" -#include "modules/desktop_capture/desktop_capturer.h" -#include "modules/desktop_capture/desktop_capture_options.h" -#include "modules/desktop_capture/desktop_frame.h" -#include "modules/video_capture/video_capture.h" -#include "mozilla/widget/PlatformWidgetTypes.h" -#include "video_engine/desktop_capture_impl.h" -#include "VideoEngine.h" - -extern "C" { -#include "jpeglib.h" -} -#include - -using namespace mozilla::widget; - -namespace mozilla { - -NS_IMPL_ISUPPORTS(nsScreencastService, nsIScreencastService) - -namespace { - -const int kMaxFramesInFlight = 1; - -StaticRefPtr gScreencastService; - -rtc::scoped_refptr CreateWindowCapturer(nsIWidget* widget) { - if (gfxPlatform::IsHeadless()) { - HeadlessWidget* headlessWidget = static_cast(widget); - return HeadlessWindowCapturer::Create(headlessWidget); - } - uintptr_t rawWindowId = reinterpret_cast(widget->GetNativeData(NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID)); - if (!rawWindowId) { - fprintf(stderr, "Failed to get native window id\n"); - return nullptr; - } - nsCString windowId; - windowId.AppendPrintf("%" PRIuPTR, rawWindowId); - bool captureCursor = false; - static int moduleId = 0; - return rtc::scoped_refptr(webrtc::DesktopCaptureImpl::Create(++moduleId, windowId.get(), camera::CaptureDeviceType::Window, captureCursor)); -} - -nsresult generateUid(nsString& uid) { - nsresult rv = NS_OK; - nsCOMPtr rg = do_GetService("@mozilla.org/security/random-generator;1", &rv); - NS_ENSURE_SUCCESS(rv, rv); - - uint8_t* buffer; - const int kLen = 16; - rv = rg->GenerateRandomBytes(kLen, &buffer); - NS_ENSURE_SUCCESS(rv, rv); - - for (int i = 0; i < kLen; i++) { - uid.AppendPrintf("%02x", buffer[i]); - } - free(buffer); - return rv; -} -} - -class nsScreencastService::Session : public rtc::VideoSinkInterface, - public webrtc::RawFrameCallback { - Session( - nsIScreencastServiceClient* client, - nsIWidget* widget, - rtc::scoped_refptr&& capturer, - std::unique_ptr encoder, - int width, int height, - int viewportWidth, int viewportHeight, - gfx::IntMargin margin, - uint32_t jpegQuality) - : mClient(client) - , mWidget(widget) - , mCaptureModule(std::move(capturer)) - , mEncoder(std::move(encoder)) - , mJpegQuality(jpegQuality) - , mWidth(width) - , mHeight(height) - , mViewportWidth(viewportWidth) - , mViewportHeight(viewportHeight) - , mMargin(margin) { - } - ~Session() override = default; - - public: - NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Session) - static RefPtr Create( - nsIScreencastServiceClient* client, - nsIWidget* widget, - rtc::scoped_refptr&& capturer, - std::unique_ptr encoder, - int width, int height, - int viewportWidth, int viewportHeight, - gfx::IntMargin margin, - uint32_t jpegQuality) { - return do_AddRef(new Session(client, widget, std::move(capturer), std::move(encoder), width, height, viewportWidth, viewportHeight, margin, jpegQuality)); - } - - rtc::scoped_refptr ReuseCapturer(nsIWidget* widget) { - if (mWidget == widget) - return mCaptureModule; - return nullptr; - } - - bool Start() { - webrtc::VideoCaptureCapability capability; - // The size is ignored in fact. - capability.width = 1280; - capability.height = 960; - capability.maxFPS = ScreencastEncoder::fps; - capability.videoType = webrtc::VideoType::kI420; - int error = mCaptureModule->StartCaptureCounted(capability); - if (error) { - fprintf(stderr, "StartCapture error %d\n", error); - return false; - } - - if (mEncoder) - mCaptureModule->RegisterCaptureDataCallback(this); - else - mCaptureModule->RegisterRawFrameCallback(this); - return true; - } - - void Stop() { - if (mStopped) { - fprintf(stderr, "Screencast session has already been stopped\n"); - return; - } - mStopped = true; - if (mEncoder) - mCaptureModule->DeRegisterCaptureDataCallback(this); - else - mCaptureModule->DeRegisterRawFrameCallback(this); - mCaptureModule->StopCaptureCounted(); - if (mEncoder) { - mEncoder->finish([this, protect = RefPtr{this}] { - NS_DispatchToMainThread(NS_NewRunnableFunction( - "NotifyScreencastStopped", [this, protect = std::move(protect)]() -> void { - mClient->ScreencastStopped(); - })); - }); - } else { - mClient->ScreencastStopped(); - } - } - - void ScreencastFrameAck() { - if (mFramesInFlight.load() == 0) { - fprintf(stderr, "ScreencastFrameAck is called while there are no inflight frames\n"); - return; - } - mFramesInFlight.fetch_sub(1); - } - - // These callbacks end up running on the VideoCapture thread. - void OnFrame(const webrtc::VideoFrame& videoFrame) override { - if (!mEncoder) - return; - mEncoder->encodeFrame(videoFrame); - } - - // These callbacks end up running on the VideoCapture thread. - void OnRawFrame(uint8_t* videoFrame, size_t videoFrameStride, const webrtc::VideoCaptureCapability& frameInfo) override { - int pageWidth = frameInfo.width - mMargin.LeftRight(); - int pageHeight = frameInfo.height - mMargin.TopBottom(); - // Frame size is 1x1 when browser window is minimized. - if (pageWidth <= 1 || pageHeight <= 1) - return; - // Headed Firefox brings sizes in sync slowly. - if (mViewportWidth && pageWidth > mViewportWidth) - pageWidth = mViewportWidth; - if (mViewportHeight && pageHeight > mViewportHeight) - pageHeight = mViewportHeight; - - if (mFramesInFlight.load() >= kMaxFramesInFlight) - return; - - int screenshotWidth = pageWidth; - int screenshotHeight = pageHeight; - int screenshotTopMargin = mMargin.TopBottom(); - std::unique_ptr canvas; - uint8_t* canvasPtr = videoFrame; - int canvasStride = videoFrameStride; - - if (mWidth < pageWidth || mHeight < pageHeight) { - double scale = std::min(1., std::min((double)mWidth / pageWidth, (double)mHeight / pageHeight)); - int canvasWidth = frameInfo.width * scale; - int canvasHeight = frameInfo.height * scale; - canvasStride = canvasWidth * 4; - - screenshotWidth *= scale; - screenshotHeight *= scale; - screenshotTopMargin *= scale; - - canvas.reset(new uint8_t[canvasWidth * canvasHeight * 4]); - canvasPtr = canvas.get(); - libyuv::ARGBScale(videoFrame, - videoFrameStride, - frameInfo.width, - frameInfo.height, - canvasPtr, - canvasStride, - canvasWidth, - canvasHeight, - libyuv::kFilterBilinear); - } - - jpeg_compress_struct info; - jpeg_error_mgr error; - info.err = jpeg_std_error(&error); - jpeg_create_compress(&info); - - unsigned char* bufferPtr = nullptr; - unsigned long bufferSize; - jpeg_mem_dest(&info, &bufferPtr, &bufferSize); - - info.image_width = screenshotWidth; - info.image_height = screenshotHeight; - -#if MOZ_LITTLE_ENDIAN() - if (frameInfo.videoType == webrtc::VideoType::kARGB) - info.in_color_space = JCS_EXT_BGRA; - if (frameInfo.videoType == webrtc::VideoType::kBGRA) - info.in_color_space = JCS_EXT_ARGB; -#else - if (frameInfo.videoType == webrtc::VideoType::kARGB) - info.in_color_space = JCS_EXT_ARGB; - if (frameInfo.videoType == webrtc::VideoType::kBGRA) - info.in_color_space = JCS_EXT_BGRA; -#endif - - // # of color components in input image - info.input_components = 4; - - jpeg_set_defaults(&info); - jpeg_set_quality(&info, mJpegQuality, true); - - jpeg_start_compress(&info, true); - while (info.next_scanline < info.image_height) { - JSAMPROW row = canvasPtr + (screenshotTopMargin + info.next_scanline) * canvasStride; - if (jpeg_write_scanlines(&info, &row, 1) != 1) { - fprintf(stderr, "JPEG library failed to encode line\n"); - break; - } - } - - jpeg_finish_compress(&info); - jpeg_destroy_compress(&info); - - nsCString base64; - nsresult rv = mozilla::Base64Encode(reinterpret_cast(bufferPtr), bufferSize, base64); - free(bufferPtr); - if (NS_WARN_IF(NS_FAILED(rv))) { - return; - } - - mFramesInFlight.fetch_add(1); - NS_DispatchToMainThread(NS_NewRunnableFunction( - "NotifyScreencastFrame", [this, protect = RefPtr{this}, base64, pageWidth, pageHeight]() -> void { - if (mStopped) - return; - NS_ConvertUTF8toUTF16 utf16(base64); - mClient->ScreencastFrame(utf16, pageWidth, pageHeight); - })); - } - - private: - RefPtr mClient; - nsIWidget* mWidget; - rtc::scoped_refptr mCaptureModule; - std::unique_ptr mEncoder; - uint32_t mJpegQuality; - bool mStopped = false; - std::atomic mFramesInFlight = 0; - int mWidth; - int mHeight; - int mViewportWidth; - int mViewportHeight; - gfx::IntMargin mMargin; -}; - - -// static -already_AddRefed nsScreencastService::GetSingleton() { - if (gScreencastService) { - return do_AddRef(gScreencastService); - } - - gScreencastService = new nsScreencastService(); - // ClearOnShutdown(&gScreencastService); - return do_AddRef(gScreencastService); -} - -nsScreencastService::nsScreencastService() = default; - -nsScreencastService::~nsScreencastService() { -} - -nsresult nsScreencastService::StartVideoRecording(nsIScreencastServiceClient* aClient, nsIDocShell* aDocShell, bool isVideo, const nsACString& aVideoFileName, uint32_t width, uint32_t height, uint32_t quality, uint32_t viewportWidth, uint32_t viewportHeight, uint32_t offsetTop, nsAString& sessionId) { - MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Screencast service must be started on the Main thread."); - - PresShell* presShell = aDocShell->GetPresShell(); - if (!presShell) - return NS_ERROR_UNEXPECTED; - nsViewManager* viewManager = presShell->GetViewManager(); - if (!viewManager) - return NS_ERROR_UNEXPECTED; - nsView* view = viewManager->GetRootView(); - if (!view) - return NS_ERROR_UNEXPECTED; - nsIWidget* widget = view->GetWidget(); - - rtc::scoped_refptr capturer = nullptr; - for (auto& it : mIdToSession) { - capturer = it.second->ReuseCapturer(widget); - if (capturer) - break; - } - if (!capturer) - capturer = CreateWindowCapturer(widget); - if (!capturer) - return NS_ERROR_FAILURE; - - gfx::IntMargin margin; - auto bounds = widget->GetScreenBounds().ToUnknownRect(); - auto clientBounds = widget->GetClientBounds().ToUnknownRect(); - // Crop the image to exclude frame (if any). - margin = bounds - clientBounds; - // Crop the image to exclude controls. - margin.top += offsetTop; - - nsCString error; - std::unique_ptr encoder; - if (isVideo) { - encoder = ScreencastEncoder::create(error, PromiseFlatCString(aVideoFileName), width, height, margin); - if (!encoder) { - fprintf(stderr, "Failed to create ScreencastEncoder: %s\n", error.get()); - return NS_ERROR_FAILURE; - } - } - - nsString uid; - nsresult rv = generateUid(uid); - NS_ENSURE_SUCCESS(rv, rv); - sessionId = uid; - - auto session = Session::Create(aClient, widget, std::move(capturer), std::move(encoder), width, height, viewportWidth, viewportHeight, margin, isVideo ? 0 : quality); - if (!session->Start()) - return NS_ERROR_FAILURE; - mIdToSession.emplace(sessionId, std::move(session)); - return NS_OK; -} - -nsresult nsScreencastService::StopVideoRecording(const nsAString& aSessionId) { - nsString sessionId(aSessionId); - auto it = mIdToSession.find(sessionId); - if (it == mIdToSession.end()) - return NS_ERROR_INVALID_ARG; - it->second->Stop(); - mIdToSession.erase(it); - return NS_OK; -} - -nsresult nsScreencastService::ScreencastFrameAck(const nsAString& aSessionId) { - nsString sessionId(aSessionId); - auto it = mIdToSession.find(sessionId); - if (it == mIdToSession.end()) - return NS_ERROR_INVALID_ARG; - it->second->ScreencastFrameAck(); - return NS_OK; -} - -} // namespace mozilla diff --git a/additions/juggler/screencast/nsScreencastService.h b/additions/juggler/screencast/nsScreencastService.h deleted file mode 100644 index 419603e..0000000 --- a/additions/juggler/screencast/nsScreencastService.h +++ /dev/null @@ -1,29 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#pragma once - -#include -#include -#include "nsIScreencastService.h" - -namespace mozilla { - -class nsScreencastService final : public nsIScreencastService { - public: - NS_DECL_ISUPPORTS - NS_DECL_NSISCREENCASTSERVICE - - static already_AddRefed GetSingleton(); - - nsScreencastService(); - - private: - ~nsScreencastService(); - - class Session; - std::map> mIdToSession; -}; - -} // namespace mozilla