mirror of
https://forge.fsky.io/oneflux/omegafox.git
synced 2026-02-10 06:42:04 -08:00
remove juggler
This commit is contained in:
parent
62160c6a7e
commit
c3a76418f9
38 changed files with 0 additions and 9408 deletions
|
|
@ -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;
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -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"]
|
||||
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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'];
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
@ -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'],
|
||||
},
|
||||
]
|
||||
|
|
@ -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'
|
||||
|
|
@ -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();
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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}]`;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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'],
|
||||
},
|
||||
]
|
||||
|
|
@ -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'
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue