remove juggler

This commit is contained in:
oneflux 2025-04-21 20:06:04 -07:00
parent 62160c6a7e
commit c3a76418f9
38 changed files with 0 additions and 9408 deletions

View file

@ -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 '<unknown error>';
}
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;

View file

@ -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);
}
}

View file

@ -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] || '<unknown>',
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;

View file

@ -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;

File diff suppressed because it is too large Load diff

View file

@ -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;
};

View file

@ -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",
},
]

View file

@ -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"]

View file

@ -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;

View file

@ -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'];

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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;
}

View file

@ -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;

View file

@ -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)

View file

@ -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")

View file

@ -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'],
},
]

View file

@ -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'

View file

@ -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();
};

View file

@ -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 <cstring>
#if defined(_WIN32)
#include <io.h>
#include <windows.h>
#else
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#endif
#include "mozilla/StaticPtr.h"
#include "nsISupportsPrimitives.h"
#include "nsThreadUtils.h"
namespace mozilla {
NS_IMPL_ISUPPORTS(nsRemoteDebuggingPipe, nsIRemoteDebuggingPipe)
namespace {
StaticRefPtr<nsRemoteDebuggingPipe> 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<char*>(buffer) + bytesRead,
size - bytesRead, &sizeRead, nullptr);
#else
int sizeRead = read(readFD, static_cast<char*>(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<DWORD>(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<nsIRemoteDebuggingPipe> 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<HANDLE>(atoi(pipeReadStr));
writeHandle = reinterpret_cast<HANDLE>(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<char> buffer;
buffer.resize(bufSize);
std::vector<char> line;
while (!m_terminated) {
size_t size = ReadBytes(buffer.data(), bufSize, false);
if (!size) {
nsCOMPtr<nsIRunnable> 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<nsIRunnable> runnable = NewRunnableMethod<nsCString>(
"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<nsIRunnable> 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

View file

@ -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 <memory>
#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<nsIRemoteDebuggingPipe> GetSingleton();
nsRemoteDebuggingPipe();
private:
void ReaderLoop();
void ReceiveMessage(const nsCString& aMessage);
void Disconnected();
~nsRemoteDebuggingPipe();
RefPtr<nsIRemoteDebuggingPipeClient> mClient;
nsCOMPtr<nsIThread> mReaderThread;
nsCOMPtr<nsIThread> mWriterThread;
std::atomic<bool> m_terminated { false };
};
} // namespace mozilla

View file

@ -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;

View file

@ -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}]`;
}
}

View file

@ -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;

View file

@ -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 = ['<root>']) {
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 = ['<root>']) {
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 = ['<root>']) {
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 = ['<root>']) {
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 = ['<root>']) {
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 = ['<root>']) {
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 = ['<root>']) {
if (Object.is(x, null))
return true;
return checkScheme(scheme, x, details, path);
}
}
t.Optional = function(scheme) {
return function(x, details = {}, path = ['<root>']) {
if (Object.is(x, undefined))
return true;
return checkScheme(scheme, x, details, path);
}
}
t.Array = function(scheme) {
return function(x, details = {}, path = ['<root>']) {
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 = ['<root>']) {
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 = ['<root>']) {
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'];

File diff suppressed because it is too large Load diff

View file

@ -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<webrtc::VideoCaptureModuleEx> HeadlessWindowCapturer::Create(HeadlessWidget* headlessWindow) {
return rtc::scoped_refptr<webrtc::VideoCaptureModuleEx>(
new rtc::RefCountedObject<HeadlessWindowCapturer>(headlessWindow)
);
}
HeadlessWindowCapturer::HeadlessWindowCapturer(mozilla::widget::HeadlessWidget* window)
: mWindow(window) {
}
HeadlessWindowCapturer::~HeadlessWindowCapturer() {
StopCapture();
}
void HeadlessWindowCapturer::RegisterCaptureDataCallback(rtc::VideoSinkInterface<webrtc::VideoFrame>* dataCallback) {
rtc::CritScope lock2(&_callBackCs);
_dataCallBacks.insert(dataCallback);
}
void HeadlessWindowCapturer::RegisterCaptureDataCallback(webrtc::RawVideoSinkInterface* dataCallback) {
}
void HeadlessWindowCapturer::DeRegisterCaptureDataCallback(rtc::VideoSinkInterface<webrtc::VideoFrame>* 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<gfx::DataSourceSurface>&& 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<I420Buffer> 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

View file

@ -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 <memory>
#include <set>
#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<webrtc::VideoCaptureModuleEx> Create(mozilla::widget::HeadlessWidget*);
void RegisterCaptureDataCallback(
rtc::VideoSinkInterface<webrtc::VideoFrame>* dataCallback) override;
void DeRegisterCaptureDataCallback(
rtc::VideoSinkInterface<webrtc::VideoFrame>* 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<mozilla::widget::HeadlessWidget> mWindow;
rtc::RecursiveCriticalSection _callBackCs;
std::set<rtc::VideoSinkInterface<webrtc::VideoFrame>*> _dataCallBacks;
std::set<webrtc::RawFrameCallback*> _rawFrameCallbacks;
};
} // namespace mozilla

View file

@ -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 <algorithm>
#include <libyuv.h>
#include <vpx/vp8.h>
#include <vpx/vp8cx.h>
#include <vpx/vpx_encoder.h>
#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<vpx_codec_ctx_t, VpxCodecDeleter>;
// 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<vpx_image_t>& out_image,
std::unique_ptr<uint8_t[]>& out_image_buffer,
int& out_buffer_size) {
std::unique_ptr<vpx_image_t> 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<uint8_t[]> 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<unsigned char*>(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<webrtc::VideoFrameBuffer>&& 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<int>(image->w, src_width);
int height = std::min<int>(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<webrtc::VideoFrameBuffer> 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<VPXFrame>&& 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<int>(1, frameCount * timeScale));
}));
}
void finishAsync(std::function<void()>&& 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<nsIThread> m_encoderQueue;
ScopedVpxCodec m_codec;
vpx_codec_enc_cfg_t m_cfg;
FILE* m_file { nullptr };
std::unique_ptr<WebMFileWriter> m_writer;
int m_frameCount { 0 };
int64_t m_pts { 0 };
std::unique_ptr<uint8_t[]> m_imageBuffer;
int m_imageBufferSize { 0 };
std::unique_ptr<vpx_image_t> m_image;
};
ScreencastEncoder::ScreencastEncoder(std::unique_ptr<VPXCodec> vpxCodec, const gfx::IntMargin& margin)
: m_vpxCodec(std::move(vpxCodec))
, m_margin(margin)
{
}
ScreencastEncoder::~ScreencastEncoder()
{
}
std::unique_ptr<ScreencastEncoder> 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> 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<ScreencastEncoder>(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<VPXFrame>(videoFrame.video_frame_buffer(), m_margin);
}
void ScreencastEncoder::finish(std::function<void()>&& 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

View file

@ -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 <functional>
#include <memory>
#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<ScreencastEncoder> create(nsCString& errorString, const nsCString& filePath, int width, int height, const gfx::IntMargin& margin);
class VPXCodec;
ScreencastEncoder(std::unique_ptr<VPXCodec>, const gfx::IntMargin& margin);
~ScreencastEncoder();
void encodeFrame(const webrtc::VideoFrame& videoFrame);
void finish(std::function<void()>&& callback);
private:
void flushLastFrame();
std::unique_ptr<VPXCodec> m_vpxCodec;
gfx::IntMargin m_margin;
TimeStamp m_lastFrameTimestamp;
class VPXFrame;
std::unique_ptr<VPXFrame> m_lastFrame;
};
} // namespace mozilla

View file

@ -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 <string>
#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<int>(m_cfg->g_w), static_cast<int>(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<uint8_t*>(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

View file

@ -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 <memory>
#include <stdio.h>
#include <stdlib.h>
#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<mkvmuxer::MkvWriter> m_writer;
std::unique_ptr<mkvmuxer::Segment> m_segment;
uint64_t m_videoTrackId = 0;
};
} // namespace mozilla

View file

@ -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'],
},
]

View file

@ -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'

View file

@ -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);
};

View file

@ -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 <libyuv.h>
using namespace mozilla::widget;
namespace mozilla {
NS_IMPL_ISUPPORTS(nsScreencastService, nsIScreencastService)
namespace {
const int kMaxFramesInFlight = 1;
StaticRefPtr<nsScreencastService> gScreencastService;
rtc::scoped_refptr<webrtc::VideoCaptureModuleEx> CreateWindowCapturer(nsIWidget* widget) {
if (gfxPlatform::IsHeadless()) {
HeadlessWidget* headlessWidget = static_cast<HeadlessWidget*>(widget);
return HeadlessWindowCapturer::Create(headlessWidget);
}
uintptr_t rawWindowId = reinterpret_cast<uintptr_t>(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::VideoCaptureModuleEx>(webrtc::DesktopCaptureImpl::Create(++moduleId, windowId.get(), camera::CaptureDeviceType::Window, captureCursor));
}
nsresult generateUid(nsString& uid) {
nsresult rv = NS_OK;
nsCOMPtr<nsIRandomGenerator> 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<webrtc::VideoFrame>,
public webrtc::RawFrameCallback {
Session(
nsIScreencastServiceClient* client,
nsIWidget* widget,
rtc::scoped_refptr<webrtc::VideoCaptureModuleEx>&& capturer,
std::unique_ptr<ScreencastEncoder> 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<Session> Create(
nsIScreencastServiceClient* client,
nsIWidget* widget,
rtc::scoped_refptr<webrtc::VideoCaptureModuleEx>&& capturer,
std::unique_ptr<ScreencastEncoder> 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<webrtc::VideoCaptureModuleEx> 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<uint8_t[]> 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<char *>(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<nsIScreencastServiceClient> mClient;
nsIWidget* mWidget;
rtc::scoped_refptr<webrtc::VideoCaptureModuleEx> mCaptureModule;
std::unique_ptr<ScreencastEncoder> mEncoder;
uint32_t mJpegQuality;
bool mStopped = false;
std::atomic<uint32_t> mFramesInFlight = 0;
int mWidth;
int mHeight;
int mViewportWidth;
int mViewportHeight;
gfx::IntMargin mMargin;
};
// static
already_AddRefed<nsIScreencastService> 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<webrtc::VideoCaptureModuleEx> 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<ScreencastEncoder> 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

View file

@ -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 <memory>
#include <map>
#include "nsIScreencastService.h"
namespace mozilla {
class nsScreencastService final : public nsIScreencastService {
public:
NS_DECL_ISUPPORTS
NS_DECL_NSISCREENCASTSERVICE
static already_AddRefed<nsIScreencastService> GetSingleton();
nsScreencastService();
private:
~nsScreencastService();
class Session;
std::map<nsString, RefPtr<Session>> mIdToSession;
};
} // namespace mozilla