forked from mirrors/gecko-dev
bug 1523104: remote: initial cdp prototype; r=ochameau
This commit is contained in:
parent
09050a5d42
commit
06faaf9146
57 changed files with 21868 additions and 1 deletions
|
|
@ -282,6 +282,15 @@ modules/libpref/test/unit/*data/**
|
|||
# Only contains non-standard test files.
|
||||
python/**
|
||||
|
||||
# Remote agent
|
||||
remote/pref/remote.js
|
||||
remote/Protocol.jsm
|
||||
remote/server/HTTPD.jsm
|
||||
remote/server/Packet.jsm
|
||||
remote/server/Socket.jsm
|
||||
remote/server/Stream.jsm
|
||||
remote/test/demo.js
|
||||
|
||||
# security/ exclusions (pref files).
|
||||
security/manager/ssl/security-prefs.js
|
||||
|
||||
|
|
|
|||
|
|
@ -183,7 +183,14 @@
|
|||
|
||||
@RESPATH@/components/Push.manifest
|
||||
|
||||
; Remote control protocol
|
||||
; CDP remote agent
|
||||
#ifdef ENABLE_REMOTE_AGENT
|
||||
@RESPATH@/components/RemoteAgent.js
|
||||
@RESPATH@/components/RemoteAgent.manifest
|
||||
@RESPATH@/defaults/pref/remote.js
|
||||
#endif
|
||||
|
||||
; Marionette remote control protocol
|
||||
#ifdef ENABLE_MARIONETTE
|
||||
@RESPATH@/chrome/marionette@JAREXT@
|
||||
@RESPATH@/chrome/marionette.manifest
|
||||
|
|
|
|||
7
remote/.eslintrc.js
Normal file
7
remote/.eslintrc.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
"rules": {
|
||||
"max-len": "off",
|
||||
}
|
||||
};
|
||||
72
remote/Actor.jsm
Normal file
72
remote/Actor.jsm
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = [
|
||||
"MessageChannelActorChild",
|
||||
"RemoteAgentActorChild",
|
||||
];
|
||||
|
||||
const {ActorChild} = ChromeUtils.import("resource://gre/modules/ActorChild.jsm");
|
||||
const {Log} = ChromeUtils.import("chrome://remote/content/Log.jsm");
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", Log.get);
|
||||
|
||||
// TODO(ato):
|
||||
// This used to have more stuff on it, but now only really does logging,
|
||||
// and I'm sure there's a better way to get the message manager logs.
|
||||
this.RemoteAgentActorChild = class extends ActorChild {
|
||||
get browsingContext() {
|
||||
return this.docShell.browsingContext;
|
||||
}
|
||||
|
||||
sendAsyncMessage(name, data = {}) {
|
||||
log.trace(`<--(message ${name}) ${JSON.stringify(data)}`);
|
||||
super.sendAsyncMessage(name, data);
|
||||
}
|
||||
|
||||
receiveMessage(name, data) {
|
||||
log.trace(`(message ${name})--> ${JSON.stringify(data)}`);
|
||||
super.receiveMessage(name, data);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO(ato): Move to MessageChannel.jsm?
|
||||
// TODO(ato): This can eventually be replaced by ActorChild and IPDL generation
|
||||
// TODO(ato): Can we find a shorter name?
|
||||
this.MessageChannelActorChild = class extends RemoteAgentActorChild {
|
||||
constructor(despatcher) {
|
||||
super(despatcher);
|
||||
this.name = `RemoteAgent:${this.constructor.name}`;
|
||||
}
|
||||
|
||||
emit(eventName, params = {}) {
|
||||
this.send({eventName, params});
|
||||
}
|
||||
|
||||
send(message = {}) {
|
||||
this.sendAsyncMessage(this.name, message);
|
||||
}
|
||||
|
||||
// nsIMessageListener
|
||||
|
||||
async receiveMessage({name, data}) {
|
||||
const {id, methodName, params} = data;
|
||||
|
||||
try {
|
||||
const func = this[methodName];
|
||||
if (!func) {
|
||||
throw new Error("Unknown method: " + methodName);
|
||||
}
|
||||
|
||||
const result = await func.call(this, params);
|
||||
this.send({id, result});
|
||||
} catch ({message, stack}) {
|
||||
const error = `${message}\n${stack}`;
|
||||
this.send({id, error});
|
||||
}
|
||||
}
|
||||
};
|
||||
25
remote/Collections.jsm
Normal file
25
remote/Collections.jsm
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["AtomicMap"];
|
||||
|
||||
this.AtomicMap = class extends Map {
|
||||
set(key, value) {
|
||||
if (this.has(key)) {
|
||||
throw new RangeError("Key already used: " + key);
|
||||
}
|
||||
super.set(key, value);
|
||||
}
|
||||
|
||||
pop(key) {
|
||||
if (!super.has(key)) {
|
||||
throw new RangeError("No such key in map: " + key);
|
||||
}
|
||||
const rv = super.get(key);
|
||||
super.delete(key);
|
||||
return rv;
|
||||
}
|
||||
};
|
||||
69
remote/Connection.jsm
Normal file
69
remote/Connection.jsm
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Connection"];
|
||||
|
||||
const {Log} = ChromeUtils.import("chrome://remote/content/Log.jsm");
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", Log.get);
|
||||
|
||||
this.Connection = class {
|
||||
constructor(connID, transport, socketListener) {
|
||||
this.id = connID;
|
||||
this.transport = transport;
|
||||
this.socketListener = socketListener;
|
||||
|
||||
this.transport.hooks = this;
|
||||
this.onmessage = () => {};
|
||||
}
|
||||
|
||||
send(message) {
|
||||
log.trace(`<-(connection ${this.id}) ${JSON.stringify(message)}`);
|
||||
// TODO(ato): Check return types
|
||||
this.transport.send(message);
|
||||
}
|
||||
|
||||
error(id, e) {
|
||||
const error = {
|
||||
message: e.message,
|
||||
data: e.stack,
|
||||
};
|
||||
this.send({id, error});
|
||||
}
|
||||
|
||||
deserialize(data) {
|
||||
const id = data.id;
|
||||
const method = data.method;
|
||||
// TODO(ato): what if params is falsy?
|
||||
const params = data.params || {};
|
||||
|
||||
// TODO(ato): Do protocol validation (Protocol.jsm)
|
||||
|
||||
return {id, method, params};
|
||||
}
|
||||
|
||||
// transport hooks
|
||||
|
||||
onPacket(packet) {
|
||||
log.trace(`(connection ${this.id})-> ${JSON.stringify(packet)}`);
|
||||
|
||||
let message = {id: null};
|
||||
try {
|
||||
message = this.deserialize(packet);
|
||||
this.onmessage.call(null, message);
|
||||
} catch (e) {
|
||||
log.warn(e);
|
||||
this.error(message.id, e);
|
||||
}
|
||||
}
|
||||
|
||||
onClosed(status) {}
|
||||
|
||||
toString() {
|
||||
return `[object Connection ${this.id}]`;
|
||||
}
|
||||
};
|
||||
98
remote/ConsoleServiceObserver.jsm
Normal file
98
remote/ConsoleServiceObserver.jsm
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["ConsoleServiceObserver"];
|
||||
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
this.ConsoleServiceObserver = class {
|
||||
constructor() {
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.running) {
|
||||
Services.console.registerListener(this);
|
||||
this.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.running) {
|
||||
Services.console.unregisterListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
// nsIConsoleListener
|
||||
|
||||
/**
|
||||
* Takes all script error messages belonging to the current window
|
||||
* and emits a "console-service-message" event.
|
||||
*
|
||||
* @param {nsIConsoleMessage} message
|
||||
* Message originating from the nsIConsoleService.
|
||||
*/
|
||||
observe(message) {
|
||||
let entry;
|
||||
if (message instanceof Ci.nsIScriptError) {
|
||||
entry = fromScriptError(message);
|
||||
} else {
|
||||
entry = fromConsoleMessage(message);
|
||||
}
|
||||
|
||||
// TODO(ato): This doesn't work for some reason:
|
||||
Services.mm.broadcastAsyncMessage("RemoteAgent:Log:OnConsoleServiceMessage", entry);
|
||||
}
|
||||
|
||||
// XPCOM
|
||||
|
||||
get QueryInterface() {
|
||||
return ChromeUtils.generateQI([Ci.nsIConsoleListener]);
|
||||
}
|
||||
};
|
||||
|
||||
function fromConsoleMessage(message) {
|
||||
const levels = {
|
||||
[Ci.nsIConsoleMessage.debug]: "verbose",
|
||||
[Ci.nsIConsoleMessage.info]: "info",
|
||||
[Ci.nsIConsoleMessage.warn]: "warning",
|
||||
[Ci.nsIConsoleMessage.error]: "error",
|
||||
};
|
||||
const level = levels[message.logLevel];
|
||||
|
||||
return {
|
||||
source: "javascript",
|
||||
level,
|
||||
text: message.message,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function fromScriptError(error) {
|
||||
const {flags, errorMessage, sourceName, lineNumber, stack} = error;
|
||||
|
||||
// lossy reduction from bitmask to CDP string level
|
||||
let level = "verbose";
|
||||
if ((flags & Ci.nsIScriptError.exceptionFlag) ||
|
||||
(flags & Ci.nsIScriptError.errorFlag)) {
|
||||
level = "error";
|
||||
} else if ((flags & Ci.nsIScriptError.warningFlag) ||
|
||||
(flags & Ci.nsIScriptError.strictFlag)) {
|
||||
level = "warning";
|
||||
} else if (flags & Ci.nsIScriptError.infoFlag) {
|
||||
level = "info";
|
||||
}
|
||||
|
||||
return {
|
||||
source: "javascript",
|
||||
level,
|
||||
text: errorMessage,
|
||||
timestamp: Date.now(),
|
||||
url: sourceName,
|
||||
lineNumber,
|
||||
stackTrace: stack,
|
||||
};
|
||||
}
|
||||
73
remote/Debugger.jsm
Normal file
73
remote/Debugger.jsm
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Debugger"];
|
||||
|
||||
const {Connection} = ChromeUtils.import("chrome://remote/content/Connection.jsm");
|
||||
const {Session} = ChromeUtils.import("chrome://remote/content/Session.jsm");
|
||||
const {SocketListener} = ChromeUtils.import("chrome://remote/content/server/Socket.jsm");
|
||||
|
||||
/**
|
||||
* Represents a debuggee target (a browsing context, typically a tab)
|
||||
* that clients can connect to and debug.
|
||||
*
|
||||
* Debugger#listen starts a WebSocket listener,
|
||||
* and for each accepted connection a new Session is created.
|
||||
* There can be multiple sessions per target.
|
||||
* The session's lifetime is equal to the lifetime of the debugger connection.
|
||||
*/
|
||||
this.Debugger = class {
|
||||
constructor(target) {
|
||||
this.target = target;
|
||||
this.listener = null;
|
||||
this.sessions = new Map();
|
||||
this.nextConnID = 0;
|
||||
}
|
||||
|
||||
get connected() {
|
||||
return !!this.listener && this.listener.listening;
|
||||
}
|
||||
|
||||
listen() {
|
||||
if (this.listener) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listener = new SocketListener();
|
||||
this.listener.on("accepted", this.onConnectionAccepted.bind(this));
|
||||
|
||||
this.listener.listen("ws", 0 /* atomically allocated port */);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.listener.off("accepted", this.onConnectionAccepted.bind(this));
|
||||
for (const [conn, session] of this.sessions) {
|
||||
session.destructor();
|
||||
conn.close();
|
||||
}
|
||||
this.listener.close();
|
||||
this.listener = null;
|
||||
this.sessions.clear();
|
||||
}
|
||||
|
||||
onConnectionAccepted(transport, listener) {
|
||||
const conn = new Connection(this.nextConnID++, transport, listener);
|
||||
transport.ready();
|
||||
this.sessions.set(conn, new Session(conn, this.target));
|
||||
}
|
||||
|
||||
get url() {
|
||||
if (this.connected) {
|
||||
const {network, host, port} = this.listener;
|
||||
return `${network}://${host}:${port}/`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `[object Debugger ${this.url || "disconnected"}]`;
|
||||
}
|
||||
};
|
||||
37
remote/Domain.jsm
Normal file
37
remote/Domain.jsm
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Domain"];
|
||||
|
||||
const {EventEmitter} = ChromeUtils.import("chrome://remote/content/EventEmitter.jsm");
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
this.Domain = class {
|
||||
constructor(session, target) {
|
||||
this.session = session;
|
||||
this.target = target;
|
||||
this.name = this.constructor.name;
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
destructor() {}
|
||||
|
||||
get browser() {
|
||||
return this.target.browser;
|
||||
}
|
||||
|
||||
get mm() {
|
||||
return this.browser.mm;
|
||||
}
|
||||
};
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(Domain, {
|
||||
Log: "chrome://remote/content/domain/Log.jsm",
|
||||
Network: "chrome://remote/content/domain/Network.jsm",
|
||||
Page: "chrome://remote/content/domain/Page.jsm",
|
||||
Runtime: "chrome://remote/content/domain/Runtime.jsm",
|
||||
});
|
||||
63
remote/Error.jsm
Normal file
63
remote/Error.jsm
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = [
|
||||
"FatalError",
|
||||
"UnsupportedError",
|
||||
];
|
||||
|
||||
const {Log} = ChromeUtils.import("chrome://remote/content/Log.jsm");
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", Log.get);
|
||||
|
||||
class RemoteAgentError extends Error {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.notify();
|
||||
}
|
||||
|
||||
notify() {
|
||||
Cu.reportError(this);
|
||||
log.error(formatError(this), this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A fatal error that it is not possible to recover from
|
||||
* or send back to the client.
|
||||
*
|
||||
* Constructing this error will cause the application to quit.
|
||||
*/
|
||||
this.FatalError = class extends RemoteAgentError {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.quit();
|
||||
}
|
||||
|
||||
notify() {
|
||||
log.fatal(formatError(this, {stack: true}));
|
||||
}
|
||||
|
||||
quit(mode = Ci.nsIAppStartup.eForceQuit) {
|
||||
Services.startup.quit(mode);
|
||||
}
|
||||
};
|
||||
|
||||
this.UnsupportedError = class extends RemoteAgentError {};
|
||||
|
||||
function formatError(error, {stack = false} = {}) {
|
||||
const s = [];
|
||||
s.push(`${error.name}: ${error.message}`);
|
||||
s.push("");
|
||||
if (stack) {
|
||||
s.push("Stacktrace:");
|
||||
}
|
||||
s.push(error.stack);
|
||||
|
||||
return s.join("\n");
|
||||
}
|
||||
159
remote/EventEmitter.jsm
Normal file
159
remote/EventEmitter.jsm
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["EventEmitter"];
|
||||
|
||||
const LISTENERS = Symbol("EventEmitter/listeners");
|
||||
const ONCE_ORIGINAL_LISTENER = Symbol("EventEmitter/once-original-listener");
|
||||
|
||||
const BAD_LISTENER = "Listener must be a function " +
|
||||
"or an object that has an onevent function";
|
||||
|
||||
this.EventEmitter = class {
|
||||
constructor() {
|
||||
this.listeners = new Map();
|
||||
}
|
||||
|
||||
static on(target, type, listener) {
|
||||
if (typeof listener != "function" && !isEventHandler(listener)) {
|
||||
throw new TypeError(BAD_LISTENER);
|
||||
}
|
||||
|
||||
if (!(LISTENERS in target)) {
|
||||
target[LISTENERS] = new Map();
|
||||
}
|
||||
|
||||
const events = target[LISTENERS];
|
||||
if (events.has(type)) {
|
||||
events.get(type).add(listener);
|
||||
} else {
|
||||
events.set(type, new Set([listener]));
|
||||
}
|
||||
}
|
||||
|
||||
static off(target, type = undefined, listener = undefined) {
|
||||
if (!target[LISTENERS]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (listener) {
|
||||
EventEmitter._removeListener(target, type, listener);
|
||||
} else if (type) {
|
||||
EventEmitter._removeTypedListeners(target, type);
|
||||
} else {
|
||||
EventEmitter._removeAllListeners(target);
|
||||
}
|
||||
}
|
||||
|
||||
static _removeListener(target, type, listener) {
|
||||
const events = target[LISTENERS];
|
||||
|
||||
const listenersForType = events.get(type);
|
||||
if (!listenersForType) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (listenersForType.has(listener)) {
|
||||
listenersForType.delete(listener);
|
||||
} else {
|
||||
for (const value of listenersForType.values()) {
|
||||
if (ONCE_ORIGINAL_LISTENER in value &&
|
||||
value[ONCE_ORIGINAL_LISTENER] === listener) {
|
||||
listenersForType.delete(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static _removeTypedListeners(target, type) {
|
||||
const events = target[LISTENERS];
|
||||
if (events.has(type)) {
|
||||
events.delete(type);
|
||||
}
|
||||
}
|
||||
|
||||
static _removeAllListeners(target) {
|
||||
const events = target[LISTENERS];
|
||||
events.clear();
|
||||
}
|
||||
|
||||
static once(target, type, listener) {
|
||||
return new Promise(resolve => {
|
||||
const newListener = (first, ...rest) => {
|
||||
EventEmitter.off(target, type, listener);
|
||||
invoke(listener, target, type, first, ...rest);
|
||||
resolve(first);
|
||||
};
|
||||
|
||||
newListener[ONCE_ORIGINAL_LISTENER] = listener;
|
||||
EventEmitter.on(target, type, newListener);
|
||||
});
|
||||
}
|
||||
|
||||
static emit(target, type, ...rest) {
|
||||
if (!(LISTENERS in target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [candidate, listeners] of target[LISTENERS]) {
|
||||
if (!type.match(expr(candidate))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const listener of listeners) {
|
||||
invoke(listener, target, type, ...rest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static decorate(target) {
|
||||
const descriptors = Object.getOwnPropertyDescriptors(this.prototype);
|
||||
delete descriptors.constructor;
|
||||
return Object.defineProperties(target, descriptors);
|
||||
}
|
||||
|
||||
on(...args) {
|
||||
EventEmitter.on(this, ...args);
|
||||
}
|
||||
|
||||
off(...args) {
|
||||
EventEmitter.off(this, ...args);
|
||||
}
|
||||
|
||||
once(...args) {
|
||||
EventEmitter.once(this, ...args);
|
||||
}
|
||||
|
||||
emit(...args) {
|
||||
EventEmitter.emit(this, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
function invoke(listener, target, type, ...args) {
|
||||
if (!listener) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isEventHandler(listener)) {
|
||||
listener.onevent(type, ...args);
|
||||
} else {
|
||||
listener.call(target, ...args);
|
||||
}
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
function isEventHandler(listener) {
|
||||
return listener && "onevent" in listener &&
|
||||
typeof listener.onevent == "function";
|
||||
}
|
||||
|
||||
function expr(type) {
|
||||
return new RegExp(type.replace("*", "([a-zA-Z.:-]+)"));
|
||||
}
|
||||
93
remote/Handler.jsm
Normal file
93
remote/Handler.jsm
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = [
|
||||
"TargetListHandler",
|
||||
"ProtocolHandler",
|
||||
];
|
||||
|
||||
const {Log} = ChromeUtils.import("chrome://remote/content/Log.jsm");
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
const {Protocol} = ChromeUtils.import("chrome://remote/content/Protocol.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", Log.get);
|
||||
|
||||
class Handler {
|
||||
register(server) {
|
||||
server.registerPathHandler(this.path, this.rawHandle);
|
||||
}
|
||||
|
||||
rawHandle(request, response) {
|
||||
log.trace(`(${request._scheme})-> ${request._method} ${request._path}`);
|
||||
|
||||
try {
|
||||
this.handle(request, response);
|
||||
} catch (e) {
|
||||
log.warn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class JSONHandler extends Handler {
|
||||
register(server) {
|
||||
server.registerPathHandler(this.path, (req, resp) => {
|
||||
this.rawHandle(req, new JSONWriter(resp));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.TargetListHandler = class extends JSONHandler {
|
||||
constructor(targets) {
|
||||
super();
|
||||
this.targets = targets;
|
||||
}
|
||||
|
||||
get path() {
|
||||
return "/json/list";
|
||||
}
|
||||
|
||||
handle(request, response) {
|
||||
response.write([...this.targets]);
|
||||
}
|
||||
};
|
||||
|
||||
this.ProtocolHandler = class extends JSONHandler {
|
||||
get path() {
|
||||
return "/json/protocol";
|
||||
}
|
||||
|
||||
handle(request, response) {
|
||||
response.write(Protocol.Description);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps an httpd.js response and serialises anything passed to
|
||||
* write() to JSON.
|
||||
*/
|
||||
class JSONWriter {
|
||||
constructor(response) {
|
||||
this._response = response;
|
||||
}
|
||||
|
||||
/** Filters out null and empty strings. */
|
||||
_replacer(key, value) {
|
||||
if (value === null || (typeof value == "string" && value.length == 0)) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
write(data) {
|
||||
try {
|
||||
const json = JSON.stringify(data, this._replacer, "\t");
|
||||
this._response.write(json);
|
||||
} catch (e) {
|
||||
log.error(`Unable to serialise JSON: ${e.message}`, e);
|
||||
this._response.write("");
|
||||
}
|
||||
}
|
||||
}
|
||||
20
remote/Log.jsm
Normal file
20
remote/Log.jsm
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Log"];
|
||||
|
||||
/** E10s compatible wrapper for the standard logger from Log.jsm. */
|
||||
this.Log = class {
|
||||
static get() {
|
||||
const StdLog = ChromeUtils.import("resource://gre/modules/Log.jsm").Log;
|
||||
const logger = StdLog.repository.getLogger("RemoteAgent");
|
||||
if (logger.ownAppenders.length == 0) {
|
||||
logger.addAppender(new StdLog.DumpAppender());
|
||||
logger.manageLevelFromPref("remote.log.level");
|
||||
}
|
||||
return logger;
|
||||
}
|
||||
};
|
||||
90
remote/MessageChannel.jsm
Normal file
90
remote/MessageChannel.jsm
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["MessageChannel"];
|
||||
|
||||
const {AtomicMap} = ChromeUtils.import("chrome://remote/content/Collections.jsm");
|
||||
const {FatalError} = ChromeUtils.import("chrome://remote/content/Error.jsm");
|
||||
const {Log} = ChromeUtils.import("chrome://remote/content/Log.jsm");
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", Log.get);
|
||||
|
||||
this.MessageChannel = class {
|
||||
constructor(target, channelName, messageManager) {
|
||||
this.target = target;
|
||||
this.name = channelName;
|
||||
this.mm = messageManager;
|
||||
this.mm.addMessageListener(this.name, this);
|
||||
|
||||
this.ids = 0;
|
||||
this.pending = new AtomicMap();
|
||||
}
|
||||
|
||||
destructor() {
|
||||
this.mm.removeMessageListener(this.name, this);
|
||||
this.ids = 0;
|
||||
this.pending.clear();
|
||||
}
|
||||
|
||||
send(methodName, params = {}) {
|
||||
const id = ++this.ids;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
this.pending.set(id, {resolve, reject});
|
||||
});
|
||||
|
||||
const msg = {id, methodName, params};
|
||||
log.trace(`(channel ${this.name})--> ${JSON.stringify(msg)}`);
|
||||
this.mm.sendAsyncMessage(this.name, msg);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
onevent() {}
|
||||
|
||||
onresponse(id, result) {
|
||||
const {resolve} = this.pending.pop(id);
|
||||
resolve(result);
|
||||
}
|
||||
|
||||
onerror(id, error) {
|
||||
const {reject} = this.pending.pop(id);
|
||||
reject(new Error(error));
|
||||
}
|
||||
|
||||
receiveMessage({data}) {
|
||||
log.trace(`<--(channel ${this.name}) ${JSON.stringify(data)}`);
|
||||
|
||||
if (data.methodName) {
|
||||
throw new FatalError("Circular message channel!", this);
|
||||
}
|
||||
|
||||
if (data.id) {
|
||||
const {id, error, result} = data;
|
||||
if (error) {
|
||||
this.onerror(id, error);
|
||||
} else {
|
||||
this.onresponse(id, result);
|
||||
}
|
||||
} else {
|
||||
const {eventName, params = {}} = data;
|
||||
this.onevent(eventName, params);
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `[object MessageChannel ${this.name}]`;
|
||||
}
|
||||
|
||||
// XPCOM
|
||||
|
||||
get QueryInterface() {
|
||||
return ChromeUtils.generateQI([
|
||||
Ci.nsIMessageListener,
|
||||
Ci.nsISupportsWeakReference,
|
||||
]);
|
||||
}
|
||||
};
|
||||
30
remote/Observer.jsm
Normal file
30
remote/Observer.jsm
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Observer"];
|
||||
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
this.Observer = class {
|
||||
static observe(type, observer) {
|
||||
Services.obs.addObserver(observer, type);
|
||||
}
|
||||
|
||||
static unobserve(type, observer) {
|
||||
Services.obs.removeObserver(observer, type);
|
||||
}
|
||||
|
||||
static once(type, observer = () => {}) {
|
||||
return new Promise(resolve => {
|
||||
const wrappedObserver = (first, ...rest) => {
|
||||
Observer.unobserve(type, wrappedObserver);
|
||||
observer.call(first, ...rest);
|
||||
resolve();
|
||||
};
|
||||
Observer.observe(type, wrappedObserver);
|
||||
});
|
||||
}
|
||||
};
|
||||
12
remote/Prefs.jsm
Normal file
12
remote/Prefs.jsm
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["RecommendedPreferences"];
|
||||
|
||||
const RecommendedPreferences = {
|
||||
// Allow the application to have focus even when it runs in the background.
|
||||
"focusmanager.testmode": true,
|
||||
};
|
||||
17410
remote/Protocol.jsm
Normal file
17410
remote/Protocol.jsm
Normal file
File diff suppressed because it is too large
Load diff
20
remote/README
Normal file
20
remote/README
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
The Firefox remote agent is a low-level debugging interface based on the
|
||||
CDP protocol.
|
||||
|
||||
With it, you can inspect the state and control execution of documents
|
||||
running in web content, instrument Gecko in interesting ways, simulate
|
||||
user interaction for automation purposes, and debug JavaScript execution.
|
||||
|
||||
This component provides an experimental and partial implementation
|
||||
of a remote devtools interface using the CDP protocol and transport layer.
|
||||
|
||||
See https://firefox-source-docs.mozilla.org/remote/ for documentation.
|
||||
|
||||
The remote agent is not by default included in Firefox builds.
|
||||
To build it, put this in your mozconfig:
|
||||
|
||||
ac_add_options --enable-cdp
|
||||
|
||||
This exposes a --debug flag you can use to start the remote agent:
|
||||
|
||||
% ./mach run --setpref "browser.fission.simulate=true" -- --debug
|
||||
357
remote/RemoteAgent.js
Normal file
357
remote/RemoteAgent.js
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
/* 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 {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
ActorManagerParent: "resource://gre/modules/ActorManagerParent.jsm",
|
||||
ConsoleServiceObserver: "chrome://remote/content/ConsoleServiceObserver.jsm",
|
||||
HttpServer: "chrome://remote/content/server/HTTPD.jsm",
|
||||
Log: "chrome://remote/content/Log.jsm",
|
||||
NetUtil: "resource://gre/modules/NetUtil.jsm",
|
||||
Observer: "chrome://remote/content/Observer.jsm",
|
||||
Preferences: "resource://gre/modules/Preferences.jsm",
|
||||
ProtocolHandler: "chrome://remote/content/Handler.jsm",
|
||||
RecommendedPreferences: "chrome://remote/content/Prefs.jsm",
|
||||
TabObserver: "chrome://remote/content/WindowManager.jsm",
|
||||
Target: "chrome://remote/content/Target.jsm",
|
||||
TargetListHandler: "chrome://remote/content/Handler.jsm",
|
||||
});
|
||||
XPCOMUtils.defineLazyGetter(this, "log", Log.get);
|
||||
|
||||
const ENABLED = "remote.enabled";
|
||||
const FORCE_LOCAL = "remote.force-local";
|
||||
const HTTPD = "remote.httpd";
|
||||
const SCHEME = `${HTTPD}.scheme`;
|
||||
const HOST = `${HTTPD}.host`;
|
||||
const PORT = `${HTTPD}.port`;
|
||||
|
||||
const DEFAULT_HOST = "localhost";
|
||||
const DEFAULT_PORT = 9222;
|
||||
const LOOPBACKS = ["localhost", "127.0.0.1", "::1"];
|
||||
|
||||
const ACTORS = {
|
||||
DOM: {
|
||||
child: {
|
||||
module: "chrome://remote/content/actor/DOMChild.jsm",
|
||||
events: {
|
||||
DOMContentLoaded: {},
|
||||
DOMWindowCreated: {once: true},
|
||||
pageshow: {mozSystemGroup: true},
|
||||
pagehide: {mozSystemGroup: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Log: {
|
||||
child: {
|
||||
module: "chrome://remote/content/actor/LogChild.jsm",
|
||||
observers: [
|
||||
"console-api-log-event",
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
class ParentRemoteAgent {
|
||||
constructor() {
|
||||
this.server = null;
|
||||
this.targets = new Targets();
|
||||
|
||||
// TODO(ato): We need a way to dynamically load actors,
|
||||
// otherwise these actors may risk being instantiated
|
||||
// without the remote agent running!
|
||||
ActorManagerParent.addActors(ACTORS);
|
||||
ActorManagerParent.flush();
|
||||
|
||||
this.consoleService = new ConsoleServiceObserver();
|
||||
|
||||
this.tabs = new TabObserver({registerExisting: true});
|
||||
this.tabs.on("open", tab => this.targets.connect(tab.linkedBrowser));
|
||||
this.tabs.on("close", tab => this.targets.disconnect(tab.linkedBrowser));
|
||||
|
||||
Services.ppmm.addMessageListener("RemoteAgent:IsRunning", this);
|
||||
}
|
||||
|
||||
// nsIRemoteAgent
|
||||
|
||||
get listening() {
|
||||
return !!this.server && !this.server._socketClosed;
|
||||
}
|
||||
|
||||
listen(address) {
|
||||
if (!(address instanceof Ci.nsIURI)) {
|
||||
throw new TypeError(`Expected nsIURI: ${address}`);
|
||||
}
|
||||
|
||||
let {host, port} = address;
|
||||
if (Preferences.get(FORCE_LOCAL) && !LOOPBACKS.includes(host)) {
|
||||
throw new Error("Restricted to loopback devices");
|
||||
}
|
||||
|
||||
// nsIServerSocket uses -1 for atomic port allocation
|
||||
if (port === 0) {
|
||||
port = -1;
|
||||
}
|
||||
|
||||
if (this.listening) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.server = new HttpServer();
|
||||
const targetList = new TargetListHandler(this.targets);
|
||||
const protocol = new ProtocolHandler();
|
||||
targetList.register(this.server);
|
||||
protocol.register(this.server);
|
||||
|
||||
try {
|
||||
this.server._start(port, host);
|
||||
log.info(`Remote debugging agent listening on ${this.scheme}://${this.host}:${this.port}/`);
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to start agent on ${port}: ${e.message}`, e);
|
||||
}
|
||||
|
||||
Preferences.set(RecommendedPreferences);
|
||||
Preferences.set(SCHEME, this.scheme);
|
||||
Preferences.set(HOST, this.host);
|
||||
Preferences.set(PORT, this.port);
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.listening) {
|
||||
try {
|
||||
await this.server.stop();
|
||||
|
||||
Preferences.resetBranch(HTTPD);
|
||||
Preferences.reset(Object.keys(RecommendedPreferences));
|
||||
|
||||
this.consoleService.stop();
|
||||
this.tabs.stop();
|
||||
this.targets.clear();
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to stop agent: ${e.message}`, e);
|
||||
}
|
||||
|
||||
log.info("Stopped listening");
|
||||
}
|
||||
}
|
||||
|
||||
get scheme() {
|
||||
if (!this.server) {
|
||||
return null;
|
||||
}
|
||||
return this.server.identity.primaryScheme;
|
||||
}
|
||||
|
||||
get host() {
|
||||
if (!this.server) {
|
||||
return null;
|
||||
}
|
||||
return this.server.identity.primaryHost;
|
||||
}
|
||||
|
||||
get port() {
|
||||
if (!this.server) {
|
||||
return null;
|
||||
}
|
||||
return this.server.identity.primaryPort;
|
||||
}
|
||||
|
||||
// nsICommandLineHandler
|
||||
|
||||
async handle(cmdLine) {
|
||||
let flag;
|
||||
try {
|
||||
flag = cmdLine.handleFlagWithParam("debug", false);
|
||||
} catch (e) {
|
||||
flag = cmdLine.handleFlag("debug", false);
|
||||
}
|
||||
|
||||
if (!flag) {
|
||||
return;
|
||||
}
|
||||
|
||||
let host, port;
|
||||
if (typeof flag == "string") {
|
||||
[host, port] = flag.split(":");
|
||||
}
|
||||
|
||||
let addr;
|
||||
try {
|
||||
addr = NetUtil.newURI(`http://${host || DEFAULT_HOST}:${port || DEFAULT_PORT}/`);
|
||||
} catch (e) {
|
||||
log.fatal(`Expected address syntax [<host>]:<port>: ${flag}`);
|
||||
cmdLine.preventDefault = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await Observer.once("sessionstore-windows-restored");
|
||||
await this.tabs.start();
|
||||
|
||||
this.consoleService.start();
|
||||
|
||||
try {
|
||||
this.listen(addr);
|
||||
} catch ({message}) {
|
||||
this.close();
|
||||
log.fatal(`Unable to start remote agent on ${addr.spec}: ${message}`);
|
||||
cmdLine.preventDefault = true;
|
||||
}
|
||||
}
|
||||
|
||||
get helpInfo() {
|
||||
return " --debug [<host>][:<port>] Start the Firefox remote agent, which is a low-level\n" +
|
||||
" debugging interface based on the CDP protocol. Defaults to\n" +
|
||||
" listen on port 9222.\n";
|
||||
}
|
||||
|
||||
// nsIMessageListener
|
||||
|
||||
receiveMessage({name}) {
|
||||
switch (name) {
|
||||
case "RemoteAgent:IsRunning":
|
||||
return this.listening;
|
||||
|
||||
default:
|
||||
log.warn(`Unknown IPC message to parent process: ${name}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// XPCOM
|
||||
|
||||
get QueryInterface() {
|
||||
return ChromeUtils.generateQI([
|
||||
Ci.nsICommandLineHandler,
|
||||
Ci.nsIRemoteAgent,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class Targets {
|
||||
constructor() {
|
||||
// browser context ID -> Target<XULElement>
|
||||
this._targets = new Map();
|
||||
}
|
||||
|
||||
/** @param {XULElement|Target} subject */
|
||||
connect(subject) {
|
||||
let target = subject;
|
||||
if (!(subject instanceof Target)) {
|
||||
target = new Target(subject);
|
||||
}
|
||||
|
||||
target.connect();
|
||||
this._targets.set(target.id, target);
|
||||
}
|
||||
|
||||
/** @param {XULElement|Target} subject */
|
||||
disconnect(subject) {
|
||||
let target = subject;
|
||||
if (!(subject instanceof Target)) {
|
||||
target = this._targets.get(subject.browsingContext.id);
|
||||
}
|
||||
|
||||
if (target) {
|
||||
target.disconnect();
|
||||
this._targets.delete(target.id);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (const target of this) {
|
||||
this.disconnect(target);
|
||||
}
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._targets.size;
|
||||
}
|
||||
|
||||
* [Symbol.iterator]() {
|
||||
for (const target of this._targets.values()) {
|
||||
yield target;
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return [...this];
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `[object Targets ${this.size}]`;
|
||||
}
|
||||
}
|
||||
|
||||
class ChildRemoteAgent {
|
||||
get listening() {
|
||||
const reply = Services.cpmm.sendSyncMessage("RemoteAgent:IsListening");
|
||||
if (reply.length == 0) {
|
||||
log.error("No reply from parent process");
|
||||
throw Cr.NS_ERROR_ABORT;
|
||||
}
|
||||
return reply[0];
|
||||
}
|
||||
|
||||
listen() {
|
||||
throw Cr.NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
close() {
|
||||
throw Cr.NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
get scheme() {
|
||||
Preferences.get(SCHEME, null);
|
||||
}
|
||||
get host() {
|
||||
Preferences.get(HOST, null);
|
||||
}
|
||||
get port() {
|
||||
Preferences.get(PORT, null);
|
||||
}
|
||||
|
||||
// XPCOM
|
||||
|
||||
get QueryInterface() {
|
||||
return ChromeUtils.generateQI([Ci.nsIRemoteAgent]);
|
||||
}
|
||||
}
|
||||
|
||||
const RemoteAgentFactory = {
|
||||
instance_: null,
|
||||
|
||||
createInstance(outer, iid) {
|
||||
if (outer) {
|
||||
throw Cr.NS_ERROR_NO_AGGREGATION;
|
||||
}
|
||||
if (!Preferences.get(ENABLED, false)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.instance_) {
|
||||
if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
|
||||
this.instance_ = new ChildRemoteAgent();
|
||||
} else {
|
||||
this.instance_ = new ParentRemoteAgent();
|
||||
}
|
||||
}
|
||||
|
||||
return this.instance_.QueryInterface(iid);
|
||||
},
|
||||
};
|
||||
|
||||
function RemoteAgent() {}
|
||||
|
||||
RemoteAgent.prototype = {
|
||||
classDescription: "Remote Agent",
|
||||
classID: Components.ID("{8f685a9d-8181-46d6-a71d-869289099c6d}"),
|
||||
contractID: "@mozilla.org/remote/agent",
|
||||
_xpcom_factory: RemoteAgentFactory, /* eslint-disable-line */
|
||||
};
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RemoteAgent]);
|
||||
3
remote/RemoteAgent.manifest
Normal file
3
remote/RemoteAgent.manifest
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
component {8f685a9d-8181-46d6-a71d-869289099c6d} RemoteAgent.js
|
||||
contract @mozilla.org/remote/agent {8f685a9d-8181-46d6-a71d-869289099c6d}
|
||||
category command-line-handler m-remote @mozilla.org/remote/agent
|
||||
156
remote/Session.jsm
Normal file
156
remote/Session.jsm
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Session"];
|
||||
|
||||
const {Domain} = ChromeUtils.import("chrome://remote/content/Domain.jsm");
|
||||
const {formatError} = ChromeUtils.import("chrome://remote/content/Error.jsm");
|
||||
const {Protocol} = ChromeUtils.import("chrome://remote/content/Protocol.jsm");
|
||||
|
||||
this.Session = class {
|
||||
constructor(connection, target) {
|
||||
this.connection = connection;
|
||||
this.target = target;
|
||||
|
||||
this.domains = new Domains(this);
|
||||
|
||||
this.connection.onmessage = this.despatch.bind(this);
|
||||
}
|
||||
|
||||
destructor() {
|
||||
this.connection.onmessage = null;
|
||||
this.domains.clear();
|
||||
}
|
||||
|
||||
async despatch({id, method, params}) {
|
||||
try {
|
||||
if (typeof id == "undefined") {
|
||||
throw new TypeError("Message missing 'id' field");
|
||||
}
|
||||
if (typeof method == "undefined") {
|
||||
throw new TypeError("Message missing 'method' field");
|
||||
}
|
||||
|
||||
const [domainName, methodName] = split(method, ".", 1);
|
||||
assertSchema(domainName, methodName, params);
|
||||
|
||||
const inst = this.domains.get(domainName);
|
||||
const methodFn = inst[methodName];
|
||||
if (!methodFn || typeof methodFn != "function") {
|
||||
throw new Error(`Method implementation of ${method} missing`);
|
||||
}
|
||||
|
||||
const result = await methodFn.call(inst, params);
|
||||
this.connection.send({id, result});
|
||||
} catch (e) {
|
||||
const error = formatError(e, {stack: true});
|
||||
this.connection.send({id, error});
|
||||
}
|
||||
}
|
||||
|
||||
// EventEmitter
|
||||
|
||||
onevent(eventName, params) {
|
||||
this.connection.send({method: eventName, params});
|
||||
}
|
||||
};
|
||||
|
||||
function assertSchema(domainName, methodName, params) {
|
||||
const domain = Domain[domainName];
|
||||
if (!domain) {
|
||||
throw new TypeError("No such domain: " + domainName);
|
||||
}
|
||||
if (!domain.schema) {
|
||||
throw new Error(`Domain ${domainName} missing schema description`);
|
||||
}
|
||||
|
||||
let details = {};
|
||||
const descriptor = (domain.schema.methods || {})[methodName];
|
||||
if (!Protocol.checkSchema(descriptor.params || {}, params, details)) {
|
||||
const {errorType, propertyName, propertyValue} = details;
|
||||
throw new TypeError(`${domainName}.${methodName} called ` +
|
||||
`with ${errorType} ${propertyName}: ${propertyValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
class Domains extends Map {
|
||||
constructor(session) {
|
||||
super();
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
get(name) {
|
||||
let inst = super.get(name);
|
||||
if (!inst) {
|
||||
inst = this.new(name);
|
||||
this.set(inst);
|
||||
}
|
||||
return inst;
|
||||
}
|
||||
|
||||
set(domain) {
|
||||
super.set(domain.name, domain);
|
||||
}
|
||||
|
||||
new(name) {
|
||||
const Cls = Domain[name];
|
||||
if (!Cls) {
|
||||
throw new Error("No such domain: " + name);
|
||||
}
|
||||
|
||||
const inst = new Cls(this.session, this.session.target);
|
||||
inst.on("*", this.session);
|
||||
|
||||
return inst;
|
||||
}
|
||||
|
||||
delete(name) {
|
||||
const inst = super.get(name);
|
||||
if (inst) {
|
||||
inst.off("*");
|
||||
inst.destructor();
|
||||
super.delete(inst.name);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (const domainName of this.keys()) {
|
||||
this.delete(domainName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Split s by sep, returning list of substrings.
|
||||
* If max is given, at most max splits are done.
|
||||
* If max is 0, there is no limit on the number of splits.
|
||||
*/
|
||||
function split(s, sep, max = 0) {
|
||||
if (typeof s != "string" ||
|
||||
typeof sep != "string" ||
|
||||
typeof max != "number") {
|
||||
throw new TypeError();
|
||||
}
|
||||
if (!Number.isInteger(max) || max < 0) {
|
||||
throw new RangeError();
|
||||
}
|
||||
|
||||
const rv = [];
|
||||
let i = 0;
|
||||
|
||||
while (rv.length < max) {
|
||||
const si = s.indexOf(sep, i);
|
||||
if (!si) {
|
||||
break;
|
||||
}
|
||||
|
||||
rv.push(s.substring(i, si));
|
||||
i = si + sep.length;
|
||||
}
|
||||
|
||||
rv.push(s.substring(i));
|
||||
return rv;
|
||||
}
|
||||
77
remote/Sync.jsm
Normal file
77
remote/Sync.jsm
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = [
|
||||
"DOMContentLoadedPromise",
|
||||
"EventPromise",
|
||||
];
|
||||
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
/**
|
||||
* Wait for a single event to be fired on a specific EventListener.
|
||||
*
|
||||
* The returned promise is guaranteed to not be called before the
|
||||
* next event tick after the event listener is called, so that all
|
||||
* other event listeners for the element are executed before the
|
||||
* handler is executed. For example:
|
||||
*
|
||||
* const promise = new EventPromise(element, "myEvent");
|
||||
* // same event tick here
|
||||
* await promise;
|
||||
* // next event tick here
|
||||
*
|
||||
* @param {EventListener} listener
|
||||
* Object which receives a notification (an object that implements
|
||||
* the Event interface) when an event of the specificed type occurs.
|
||||
* @param {string} type
|
||||
* Case-sensitive string representing the event type to listen for.
|
||||
* @param {boolean?} [false] options.capture
|
||||
* Indicates the event will be despatched to this subject,
|
||||
* before it bubbles down to any EventTarget beneath it in the
|
||||
* DOM tree.
|
||||
* @param {boolean?} [false] options.wantsUntrusted
|
||||
* Receive synthetic events despatched by web content.
|
||||
* @param {boolean?} [false] options.mozSystemGroup
|
||||
* Determines whether to add listener to the system group.
|
||||
*
|
||||
* @return {Promise.<Event>}
|
||||
*
|
||||
* @throws {TypeError}
|
||||
*/
|
||||
function EventPromise(listener, type, options = {
|
||||
capture: false,
|
||||
wantsUntrusted: false,
|
||||
mozSystemGroup: false,
|
||||
}) {
|
||||
|
||||
if (!listener || !("addEventListener" in listener)) {
|
||||
throw new TypeError();
|
||||
}
|
||||
if (typeof type != "string") {
|
||||
throw new TypeError();
|
||||
}
|
||||
if (("capture" in options && typeof options.capture != "boolean") ||
|
||||
("wantsUntrusted" in options && typeof options.wantsUntrusted != "boolean") ||
|
||||
("mozSystemGroup" in options && typeof options.mozSystemGroup != "boolean")) {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
options.once = true;
|
||||
|
||||
return new Promise(resolve => {
|
||||
listener.addEventListener(type, event => {
|
||||
Services.tm.dispatchToMainThread(() => resolve(event));
|
||||
}, options);
|
||||
});
|
||||
}
|
||||
|
||||
function DOMContentLoadedPromise(window, options = {mozSystemGroup: true}) {
|
||||
if (window.document.readyState == "complete") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new EventPromise(window, "DOMContentLoaded", options);
|
||||
}
|
||||
132
remote/Target.jsm
Normal file
132
remote/Target.jsm
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Target"];
|
||||
|
||||
const {Debugger} = ChromeUtils.import("chrome://remote/content/Debugger.jsm");
|
||||
const {EventEmitter} = ChromeUtils.import("chrome://remote/content/EventEmitter.jsm");
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "Favicons",
|
||||
"@mozilla.org/browser/favicon-service;1", "nsIFaviconService");
|
||||
|
||||
/** A debugging target. */
|
||||
this.Target = class {
|
||||
constructor(browser) {
|
||||
this.browser = browser;
|
||||
this.debugger = new Debugger(this);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
connect() {
|
||||
Services.obs.addObserver(this, "message-manager-disconnect");
|
||||
this.debugger.listen();
|
||||
this.emit("connect");
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
Services.obs.removeObserver(this, "message-manager-disconnect");
|
||||
// TODO(ato): Disconnect existing client sockets
|
||||
this.debugger.close();
|
||||
this.emit("disconnect");
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.browsingContext.id;
|
||||
}
|
||||
|
||||
get browsingContext() {
|
||||
return this.browser.browsingContext;
|
||||
}
|
||||
|
||||
get mm() {
|
||||
return this.browser.messageManager;
|
||||
}
|
||||
|
||||
get window() {
|
||||
return this.browser.ownerGlobal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the content browser remains attached
|
||||
* to its parent chrome window.
|
||||
*
|
||||
* We determine this by checking if the <browser> element
|
||||
* is still attached to the DOM.
|
||||
*
|
||||
* @return {boolean}
|
||||
* True if target's browser is still attached,
|
||||
* false if it has been disconnected.
|
||||
*/
|
||||
get closed() {
|
||||
return !this.browser || !this.browser.isConnected;
|
||||
}
|
||||
|
||||
get description() {
|
||||
return "";
|
||||
}
|
||||
|
||||
get frontendURL() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return {Promise.<String=>} */
|
||||
get faviconUrl() {
|
||||
return new Promise((resolve, reject) => {
|
||||
Favicons.getFaviconURLForPage(this.browser.currentURI, url => {
|
||||
if (url) {
|
||||
resolve(url.spec);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get type() {
|
||||
return "page";
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this.browser.currentURI.spec;
|
||||
}
|
||||
|
||||
get wsDebuggerURL() {
|
||||
return this.debugger.url;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `[object Target ${this.id}]`;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
description: this.description,
|
||||
devtoolsFrontendUrl: this.frontendURL,
|
||||
// TODO(ato): toJSON cannot be marked async )-:
|
||||
faviconUrl: "",
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
type: this.type,
|
||||
url: this.url,
|
||||
webSocketDebuggerUrl: this.wsDebuggerURL,
|
||||
};
|
||||
}
|
||||
|
||||
// nsIObserver
|
||||
|
||||
observe(subject, topic, data) {
|
||||
if (subject === this.mm && subject == "message-manager-disconnect") {
|
||||
// disconnect debugging target if <browser> is disconnected,
|
||||
// otherwise this is just a host process change
|
||||
if (this.closed) {
|
||||
this.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
246
remote/WindowManager.jsm
Normal file
246
remote/WindowManager.jsm
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = [
|
||||
"BrowserObserver",
|
||||
"TabObserver",
|
||||
"WindowObserver",
|
||||
"WindowManager",
|
||||
];
|
||||
|
||||
const {DOMContentLoadedPromise} = ChromeUtils.import("chrome://remote/content/Sync.jsm");
|
||||
const {EventEmitter} = ChromeUtils.import("chrome://remote/content/EventEmitter.jsm");
|
||||
const {Log} = ChromeUtils.import("chrome://remote/content/Log.jsm");
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", Log.get);
|
||||
|
||||
/**
|
||||
* The WindowManager provides tooling for application-agnostic observation
|
||||
* of windows, tabs, and content browsers as they are created and destroyed.
|
||||
*/
|
||||
|
||||
// TODO(ato):
|
||||
//
|
||||
// The DOM team is working on pulling browsing context related behaviour,
|
||||
// such as window and tab handling, out of product code and into the platform.
|
||||
// This will have implication for the remote agent,
|
||||
// and as the platform gains support for product-independent events
|
||||
// we can likely get rid of this entire module.
|
||||
//
|
||||
// Seen below, BrowserObserver in particular tries to emulate content
|
||||
// browser tracking across host process changes.
|
||||
|
||||
/**
|
||||
* Observes DOMWindows as they open and close.
|
||||
*
|
||||
* The WindowObserver.Event.Open event fires when a window opens.
|
||||
* The WindowObserver.Event.Close event fires when a window closes.
|
||||
*/
|
||||
this.WindowObserver = class {
|
||||
/**
|
||||
* @param {boolean?} [false] registerExisting
|
||||
* Events will be despatched for the ChromeWindows that exist
|
||||
* at the time the observer is started.
|
||||
*/
|
||||
constructor({registerExisting = false} = {}) {
|
||||
this.registerExisting = registerExisting;
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.registerExisting) {
|
||||
for (const window of Services.wm.getEnumerator("navigator:browser")) {
|
||||
await this.onOpenWindow(window);
|
||||
}
|
||||
}
|
||||
|
||||
Services.wm.addListener(this);
|
||||
}
|
||||
|
||||
stop() {
|
||||
Services.wm.removeListener(this);
|
||||
}
|
||||
|
||||
// nsIWindowMediatorListener
|
||||
|
||||
async onOpenWindow(xulWindow) {
|
||||
const window = xulWindow
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow);
|
||||
await new DOMContentLoadedPromise(window);
|
||||
this.emit("open", window);
|
||||
}
|
||||
|
||||
onCloseWindow(xulWindow) {
|
||||
const window = xulWindow
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow);
|
||||
this.emit("close", window);
|
||||
}
|
||||
|
||||
// XPCOM
|
||||
|
||||
get QueryInterface() {
|
||||
return ChromeUtils.generateQI([Ci.nsIWindowMediatorListener]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Observe Firefox tabs as they open and close.
|
||||
*
|
||||
* "open" fires when a tab opens.
|
||||
* "close" fires when a tab closes.
|
||||
*/
|
||||
this.TabObserver = class {
|
||||
/**
|
||||
* @param {boolean?} [false] registerExisting
|
||||
* Events will be fired for ChromeWIndows and their respective tabs
|
||||
* at the time when the observer is started.
|
||||
*/
|
||||
constructor({registerExisting = false} = {}) {
|
||||
this.windows = new WindowObserver({registerExisting});
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.windows.on("open", this.onWindowOpen.bind(this));
|
||||
this.windows.on("close", this.onWindowClose.bind(this));
|
||||
await this.windows.start();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.windows.off("open");
|
||||
this.windows.off("close");
|
||||
this.windows.stop();
|
||||
}
|
||||
|
||||
onTabOpen(tab) {
|
||||
this.emit("open", tab);
|
||||
}
|
||||
|
||||
onTabClose(tab) {
|
||||
this.emit("close", tab);
|
||||
}
|
||||
|
||||
// WindowObserver
|
||||
|
||||
async onWindowOpen(window) {
|
||||
if (!window.gBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const tab of window.gBrowser.tabs) {
|
||||
this.onTabOpen(tab);
|
||||
}
|
||||
|
||||
window.addEventListener("TabOpen", ({target}) => this.onTabOpen(target));
|
||||
window.addEventListener("TabClose", ({target}) => this.onTabClose(target));
|
||||
}
|
||||
|
||||
onWindowClose(window) {
|
||||
// TODO(ato): Is TabClose fired when the window closes?
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* BrowserObserver is more powerful than TabObserver,
|
||||
* as it watches for any content browser appearing anywhere in Gecko.
|
||||
* TabObserver on the other hand is limited to browsers associated with a tab.
|
||||
*
|
||||
* This class is currently not used by the remote agent,
|
||||
* but leave it in here because we may have use for it later
|
||||
* if we decide to allow Marionette-style chrome automation.
|
||||
*/
|
||||
this.BrowserObserver = class {
|
||||
constructor() {
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
start() {
|
||||
// TODO(ato): Theoretically it would be better to use ChromeWindow#getGroupMessageManager("Browsers")
|
||||
// TODO(ato): Browser:Init does not cover browsers living in the parent process
|
||||
Services.mm.addMessageListener("Browser:Init", this);
|
||||
Services.obs.addObserver(this, "message-manager-disconnect");
|
||||
}
|
||||
|
||||
stop() {
|
||||
Services.mm.removeMessageListener("Browser:Init", this);
|
||||
Services.obs.removeObserver(this, "message-manager-disconnect");
|
||||
}
|
||||
|
||||
onBrowserInit(browser) {
|
||||
this.emit("connected", browser);
|
||||
}
|
||||
|
||||
onMessageManagerDisconnect(browser) {
|
||||
if (!browser.isConnected) {
|
||||
this.emit("disconnected", browser);
|
||||
}
|
||||
}
|
||||
|
||||
// nsIMessageListener
|
||||
|
||||
receiveMessage({name, target}) {
|
||||
switch (name) {
|
||||
case "Browser:Init":
|
||||
this.onBrowserInit(target);
|
||||
break;
|
||||
|
||||
default:
|
||||
log.warn("Unknown IPC message form browser: " + name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// nsIObserver
|
||||
|
||||
observe(subject, topic) {
|
||||
switch (topic) {
|
||||
case "message-manager-disconnect":
|
||||
this.onMessageManagerDisconnect(subject);
|
||||
break;
|
||||
|
||||
default:
|
||||
log.warn("Unknown system observer notification: " + topic);
|
||||
}
|
||||
}
|
||||
|
||||
// XPCOM
|
||||
|
||||
get QueryInterface() {
|
||||
return ChromeUtils.generateQI([
|
||||
Ci.nsIMessageListener,
|
||||
Ci.nsIObserver,
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if WindowProxy is part of the boundary window.
|
||||
*
|
||||
* @param {DOMWindow} boundary
|
||||
* @param {DOMWindow} target
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
function isWindowIncluded(boundary, target) {
|
||||
if (target === boundary) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO(ato): Pretty sure this is not Fission compatible,
|
||||
// but then this is a problem that needs to be solved in nsIConsoleAPI.
|
||||
const {parent} = target;
|
||||
if (!parent || parent === boundary) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isWindowIncluded(boundary, parent);
|
||||
}
|
||||
|
||||
this.WindowManager = {isWindowIncluded};
|
||||
19
remote/actor/DOMChild.jsm
Normal file
19
remote/actor/DOMChild.jsm
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["DOMChild"];
|
||||
|
||||
const {RemoteAgentActorChild} = ChromeUtils.import("chrome://remote/content/Actor.jsm");
|
||||
|
||||
this.DOMChild = class extends RemoteAgentActorChild {
|
||||
handleEvent({type}) {
|
||||
const event = {
|
||||
type,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
this.sendAsyncMessage("RemoteAgent:DOM:OnEvent", event);
|
||||
}
|
||||
};
|
||||
51
remote/actor/LogChild.jsm
Normal file
51
remote/actor/LogChild.jsm
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["LogChild"];
|
||||
|
||||
const {RemoteAgentActorChild} = ChromeUtils.import("chrome://remote/content/Actor.jsm");
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {WindowManager} = ChromeUtils.import("chrome://remote/content/WindowManager.jsm");
|
||||
|
||||
this.LogChild = class extends RemoteAgentActorChild {
|
||||
observe(subject, topic) {
|
||||
const event = subject.wrappedJSObject;
|
||||
|
||||
if (this.isEventRelevant(event)) {
|
||||
const entry = {
|
||||
source: "javascript",
|
||||
level: reduceLevel(event.level),
|
||||
text: event.arguments.join(" "),
|
||||
timestamp: event.timeStamp,
|
||||
url: event.fileName,
|
||||
lineNumber: event.lineNumber,
|
||||
};
|
||||
this.sendAsyncMessage("RemoteAgent:Log:OnConsoleAPIEvent", entry);
|
||||
}
|
||||
}
|
||||
|
||||
isEventRelevant({innerID}) {
|
||||
const eventWin = Services.wm.getCurrentInnerWindowWithId(innerID);
|
||||
if (!eventWin || !WindowManager.isWindowIncluded(this.content, eventWin)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
function reduceLevel(level) {
|
||||
switch (level) {
|
||||
case "exception":
|
||||
return "error";
|
||||
case "warn":
|
||||
return "warning";
|
||||
case "debug":
|
||||
return "verbose";
|
||||
case "log":
|
||||
default:
|
||||
return "info";
|
||||
}
|
||||
}
|
||||
8
remote/actor/moz.build
Normal file
8
remote/actor/moz.build
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# 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/.
|
||||
|
||||
FINAL_TARGET_FILES.actors += [
|
||||
"DOMChild.jsm",
|
||||
"LogChild.jsm",
|
||||
]
|
||||
24
remote/doc/Building.md
Normal file
24
remote/doc/Building.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
Building
|
||||
========
|
||||
|
||||
The remote agent is by default not included in Firefox builds.
|
||||
To build it, put this in your [mozconfig]:
|
||||
|
||||
ac_add_options --enable-cdp
|
||||
|
||||
This exposes a --debug flag you can use to start the remote agent:
|
||||
|
||||
% ./mach run --setpref "browser.fission.simulate=true" -- --debug
|
||||
|
||||
When you make changes to the XPCOM component you need to rebuild
|
||||
in order for the changes to take effect. The most efficient way to
|
||||
do this, provided you haven’t touched any compiled code (C++ or Rust):
|
||||
|
||||
% ./mach build faster
|
||||
|
||||
Component files include the likes of RemoteAgent.js, RemoteAgent.manifest,
|
||||
moz.build files, prefs/remote.js, and jar.mn. All the JS modules
|
||||
(files ending with `.jsm`) are symlinked into the build and can be
|
||||
changed without rebuilding.
|
||||
|
||||
[mozconfig]: ../build/buildsystem/mozconfigs.html
|
||||
14
remote/doc/Debugging.md
Normal file
14
remote/doc/Debugging.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
Debugging
|
||||
=========
|
||||
|
||||
Increasing the logging verbosity
|
||||
--------------------------------
|
||||
|
||||
To increase the internal logging verbosity you can use the
|
||||
`remote.log.level` [preference].
|
||||
|
||||
If you use mach to start the Firefox:
|
||||
|
||||
./mach run --setpref "browser.fission.simulate=true" --setpref "remote.log.level=Debug" -- --debug
|
||||
|
||||
[preference]: ./Prefs.md
|
||||
51
remote/doc/Prefs.md
Normal file
51
remote/doc/Prefs.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
Preferences
|
||||
===========
|
||||
|
||||
There are a couple of preferences associated with the remote agent:
|
||||
|
||||
|
||||
Configurable preferences
|
||||
------------------------
|
||||
|
||||
### `remote.enabled`
|
||||
|
||||
Indicates whether the remote agent is enabled. When the remote agent
|
||||
is enabled, it exposes a `--debug` flag for Firefox. When set to
|
||||
false, the remote agent will not be loaded on startup.
|
||||
|
||||
### `remote.force-local`
|
||||
|
||||
Limits the remote agent to be allowed to listen on loopback devices,
|
||||
e.g. 127.0.0.1, localhost, and ::1.
|
||||
|
||||
### `remote.log.level`
|
||||
|
||||
Defines the verbosity of the internal logger. Available levels
|
||||
are, in descending order of severity, `Trace`, `Debug`, `Config`,
|
||||
`Info`, `Warn`, `Error`, and `Fatal`. Note that the value is
|
||||
treated case-sensitively.
|
||||
|
||||
|
||||
Informative preferences
|
||||
-----------------------
|
||||
|
||||
The following preferences are set when the remote agent starts the
|
||||
HTTPD frontend:
|
||||
|
||||
### `remote.httpd.scheme`
|
||||
|
||||
Scheme the server is listening on, e.g. `httpd`.
|
||||
|
||||
### `remote.httpd.host`
|
||||
|
||||
Hostname the server is bound to.
|
||||
|
||||
### `remote.httpd.port`
|
||||
|
||||
The port bound by the server. When starting Firefox with `--debug`
|
||||
you can ask the remote agent to listen on port 0 to have the system
|
||||
atomically allocate a free port. You can then later check this
|
||||
preference to find out on what port it is listening:
|
||||
|
||||
./firefox --debug :0
|
||||
1548002326113 RemoteAgent INFO Remote debugging agent listening on http://localhost:16738/
|
||||
19
remote/doc/Testing.md
Normal file
19
remote/doc/Testing.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Testing
|
||||
=======
|
||||
|
||||
xpcshell unit tests
|
||||
-------------------
|
||||
|
||||
The remote agent has a set of [xpcshell] unit tests located in
|
||||
_remote/test/unit_. These can be run this way:
|
||||
|
||||
% ./mach test remote/test/unit
|
||||
|
||||
Because tests are run in parallel and xpcshell itself is quite
|
||||
chatty, it can sometimes be useful to run the tests in sequence:
|
||||
|
||||
% ./mach test --sequential remote/test/unit/test_Assert.js
|
||||
|
||||
The unit tests will appear as part of the `X` jobs on Treeherder.
|
||||
|
||||
[xpcshell]: https://developer.mozilla.org/en-US/docs/Mozilla/QA/Writing_xpcshell-based_unit_tests
|
||||
37
remote/doc/index.md
Normal file
37
remote/doc/index.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
CDP
|
||||
===
|
||||
|
||||
In addition to the Firefox Developer Tools _Remote Debugging Protocol_,
|
||||
also known as RDP, Firefox also has a partial implementation of
|
||||
the Chrome DevTools Protocol (CDP).
|
||||
|
||||
The Firefox remote agent is a low-level debugging interface based on
|
||||
the CDP protocol. With it, you can inspect the state and control
|
||||
execution of documents running in web content, instrument Gecko in
|
||||
interesting ways, simulate user interaction for automation purposes,
|
||||
and debug JavaScript execution.
|
||||
|
||||
|
||||
Table of Contents
|
||||
-----------------
|
||||
|
||||
* [Building](./Building.html)
|
||||
* [Debugging](./Debugging.html)
|
||||
* [Testing](./Testing.html)
|
||||
* [Preferences](./Prefs.html)
|
||||
|
||||
|
||||
Communication
|
||||
-------------
|
||||
|
||||
The mailing list for Firefox remote debugging discussion is
|
||||
[remote@lists.mozilla.org] ([subscribe], [archive]).
|
||||
|
||||
If you prefer real-time chat, there is often someone in the
|
||||
#devtools IRC channel on irc.mozilla.org. Don’t ask if you may
|
||||
ask a question just go ahead and ask, and please wait for an answer
|
||||
as we might not be in your timezone.
|
||||
|
||||
[remote@lists.mozilla.org]: mailto:remote@lists.mozilla.org
|
||||
[subscribe]: https://lists.mozilla.org/listinfo/remote
|
||||
[archive]: https://lists.mozilla.org/pipermail/remote/
|
||||
44
remote/doc/index.rst
Normal file
44
remote/doc/index.rst
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
===
|
||||
CDP
|
||||
===
|
||||
|
||||
In addition to the Firefox Developer Tools _Remote Debugging Protocol_,
|
||||
also known as RDP, Firefox also has a partial implementation of
|
||||
the Chrome DevTools Protocol (CDP).
|
||||
|
||||
The Firefox remote agent is a low-level debugging interface based on
|
||||
the CDP protocol. With it, you can inspect the state and control
|
||||
execution of documents running in web content, instrument Gecko in
|
||||
interesting ways, simulate user interaction for automation purposes,
|
||||
and debug JavaScript execution.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
Building.md
|
||||
Debugging.md
|
||||
Testing.md
|
||||
Prefs.md
|
||||
|
||||
|
||||
Bugs
|
||||
====
|
||||
|
||||
Bugs are tracked in the `DevTools :: Remote Agent` component.
|
||||
|
||||
|
||||
Communication
|
||||
=============
|
||||
|
||||
The mailing list for Firefox remote debugging discussion is
|
||||
`remote@lists.mozilla.org`_ (`subscribe`_, `archive`_).
|
||||
|
||||
If you prefer real-time chat, there is often someone in the
|
||||
#devtools IRC channel on irc.mozilla.org. Don’t ask if you may
|
||||
ask a question just go ahead and ask, and please wait for an answer
|
||||
as we might not be in your timezone.
|
||||
|
||||
.. _remote@lists.mozilla.org: mailto:remote@lists.mozilla.org
|
||||
.. _subscribe: https://lists.mozilla.org/listinfo/remote
|
||||
.. _archive: https://lists.mozilla.org/pipermail/remote/
|
||||
8
remote/doc/moz.build
Normal file
8
remote/doc/moz.build
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# 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/.
|
||||
|
||||
SPHINX_TREES["remote"] = "."
|
||||
|
||||
with Files("**"):
|
||||
SCHEDULES.exclusive = ["docs"]
|
||||
109
remote/domain/Log.jsm
Normal file
109
remote/domain/Log.jsm
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Log"];
|
||||
|
||||
const {Domain} = ChromeUtils.import("chrome://remote/content/Domain.jsm");
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {t} = ChromeUtils.import("chrome://remote/content/Protocol.jsm");
|
||||
|
||||
const {Network, Runtime} = Domain;
|
||||
|
||||
const ALLOWED_SOURCES = [
|
||||
"xml",
|
||||
"javascript",
|
||||
"network",
|
||||
"storage",
|
||||
"appcache",
|
||||
"rendering",
|
||||
"security",
|
||||
"deprecation",
|
||||
"worker",
|
||||
"violation",
|
||||
"intervention",
|
||||
"recommendation",
|
||||
"other",
|
||||
];
|
||||
|
||||
const ALLOWED_LEVELS = [
|
||||
"verbose",
|
||||
"info",
|
||||
"warning",
|
||||
"error",
|
||||
];
|
||||
|
||||
this.Log = class extends Domain {
|
||||
constructor(session, target) {
|
||||
super(session, target);
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
destructor() {
|
||||
this.disable();
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (!this.enabled) {
|
||||
this.enabled = true;
|
||||
|
||||
// TODO(ato): Using the target content browser's MM here does not work
|
||||
// because it disconnects when it suffers a host process change.
|
||||
// That forces us to listen for Everything
|
||||
// and do a target check in receiveMessage() below.
|
||||
// Perhaps we can solve reattaching message listeners in a ParentActor?
|
||||
Services.mm.addMessageListener("RemoteAgent:Log:OnConsoleAPIEvent", this);
|
||||
Services.mm.addMessageListener("RemoteAgent:Log:OnConsoleServiceMessage", this);
|
||||
}
|
||||
}
|
||||
|
||||
disable() {
|
||||
if (this.enabled) {
|
||||
Services.mm.removeMessageListener("RemoteAgent:Log:OnConsoleAPIEvent", this);
|
||||
Services.mm.removeMessageListener("RemoteAgent:Log:OnConsoleServiceMessage", this);
|
||||
this.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// nsIMessageListener
|
||||
|
||||
receiveMessage({target, name, data}) {
|
||||
// filter out Console API events that do not belong
|
||||
// to the browsing context we are operating on
|
||||
if (name == "RemoteAgent:Log:OnConsoleAPIEvent" &&
|
||||
this.target.id !== data.browsingContextId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit("Log.entryAdded", {entry: data});
|
||||
}
|
||||
|
||||
static get schema() {
|
||||
return {
|
||||
methods: {
|
||||
enable: {},
|
||||
disable: {},
|
||||
},
|
||||
events: {
|
||||
entryAdded: Log.LogEntry.schema,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
this.Log.LogEntry = {
|
||||
schema: {
|
||||
source: t.Enum(ALLOWED_SOURCES),
|
||||
level: t.Enum(ALLOWED_LEVELS),
|
||||
text: t.String,
|
||||
timestamp: Runtime.Timestamp,
|
||||
url: t.Optional(t.String),
|
||||
lineNumber: t.Optional(t.Number),
|
||||
stackTrace: t.Optional(Runtime.StackTrace.schema),
|
||||
networkRequestId: t.Optional(Network.RequestId.schema),
|
||||
workerId: t.Optional(t.String),
|
||||
args: t.Optional(t.Array(Runtime.RemoteObject.schema)),
|
||||
},
|
||||
};
|
||||
15
remote/domain/Network.jsm
Normal file
15
remote/domain/Network.jsm
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Network"];
|
||||
|
||||
const {t} = ChromeUtils.import("chrome://remote/content/Protocol.jsm");
|
||||
|
||||
this.Network = {
|
||||
MonotonicTime: {schema: t.Number},
|
||||
LoaderId: {schema: t.String},
|
||||
RequestId: {schema: t.Number},
|
||||
};
|
||||
139
remote/domain/Page.jsm
Normal file
139
remote/domain/Page.jsm
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Page"];
|
||||
|
||||
const {Domain} = ChromeUtils.import("chrome://remote/content/Domain.jsm");
|
||||
const {t} = ChromeUtils.import("chrome://remote/content/Protocol.jsm");
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {UnsupportedError} = ChromeUtils.import("chrome://remote/content/Error.jsm");
|
||||
|
||||
this.Page = class extends Domain {
|
||||
constructor(session, target) {
|
||||
super(session, target);
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
destructor() {
|
||||
this.disable();
|
||||
}
|
||||
|
||||
// commands
|
||||
|
||||
async enable() {
|
||||
if (!this.enabled) {
|
||||
this.enabled = true;
|
||||
Services.mm.addMessageListener("RemoteAgent:DOM:OnEvent", this);
|
||||
}
|
||||
}
|
||||
|
||||
disable() {
|
||||
if (this.enabled) {
|
||||
Services.mm.removeMessageListener("RemoteAgent:DOM:OnEvent", this);
|
||||
this.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async navigate({url, referrer, transitionType, frameId} = {}) {
|
||||
if (frameId) {
|
||||
throw new UnsupportedError("frameId not supported");
|
||||
}
|
||||
|
||||
const opts = {
|
||||
loadFlags: transitionToLoadFlag(transitionType),
|
||||
referrerURI: referrer,
|
||||
triggeringPrincipal: this.browser.contentPrincipal,
|
||||
};
|
||||
this.browser.webNavigation.loadURI(url, opts);
|
||||
|
||||
return {frameId: "42"};
|
||||
}
|
||||
|
||||
url() {
|
||||
return this.browsrer.currentURI.spec;
|
||||
}
|
||||
|
||||
onDOMEvent({type}) {
|
||||
const timestamp = Date.now();
|
||||
|
||||
switch (type) {
|
||||
case "DOMContentLoaded":
|
||||
this.emit("Page.domContentEventFired", {timestamp});
|
||||
break;
|
||||
|
||||
case "pageshow":
|
||||
this.emit("Page.loadEventFired", {timestamp});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// nsIMessageListener
|
||||
|
||||
receiveMessage({target, name, data}) {
|
||||
if (target !== this.target.browser) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case "RemoteAgent:DOM:OnEvent":
|
||||
this.onDOMEvent(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static get schema() {
|
||||
return {
|
||||
methods: {
|
||||
enable: {},
|
||||
disable: {},
|
||||
navigate: {
|
||||
params: {
|
||||
url: t.String,
|
||||
referrer: t.Optional(t.String),
|
||||
transitionType: t.Optional(Page.TransitionType.schema),
|
||||
frameId: t.Optional(Page.FrameId.schema),
|
||||
},
|
||||
returns: {
|
||||
frameId: Page.FrameId,
|
||||
loaderId: t.Optional(Domain.Network.LoaderId.schema),
|
||||
errorText: t.String,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
events: {
|
||||
domContentEventFired: {
|
||||
timestamp: Domain.Network.MonotonicTime.schema,
|
||||
},
|
||||
loadEventFired: {
|
||||
timestamp: Domain.Network.MonotonicTime.schema,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
this.Page.FrameId = {schema: t.String};
|
||||
this.Page.TransitionType = {
|
||||
schema: t.Enum([
|
||||
"auto_bookmark",
|
||||
"auto_subframe",
|
||||
"link",
|
||||
"manual_subframe",
|
||||
"reload",
|
||||
"typed",
|
||||
]),
|
||||
};
|
||||
|
||||
function transitionToLoadFlag(transitionType) {
|
||||
switch (transitionType) {
|
||||
case "reload":
|
||||
return Ci.nsIWebNavigation.LOAD_FLAG_IS_REFRESH;
|
||||
case "link":
|
||||
default:
|
||||
return Ci.nsIWebNavigation.LOAD_FLAG_IS_LINK;
|
||||
}
|
||||
}
|
||||
51
remote/domain/Runtime.jsm
Normal file
51
remote/domain/Runtime.jsm
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Runtime"];
|
||||
|
||||
const {t} = ChromeUtils.import("chrome://remote/content/Protocol.jsm");
|
||||
|
||||
this.Runtime = {
|
||||
StackTrace: {schema: t.String},
|
||||
|
||||
RemoteObject: {
|
||||
schema: t.Either(
|
||||
{
|
||||
type: t.Enum([
|
||||
"object",
|
||||
"function",
|
||||
"undefined",
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"symbol",
|
||||
"bigint",
|
||||
]),
|
||||
subtype: t.Optional(t.Enum([
|
||||
"array",
|
||||
"date",
|
||||
"error",
|
||||
"map",
|
||||
"node",
|
||||
"null",
|
||||
"promise",
|
||||
"proxy",
|
||||
"regexp",
|
||||
"set",
|
||||
"typedarray",
|
||||
"weakmap",
|
||||
"weakset",
|
||||
])),
|
||||
objectId: t.String,
|
||||
},
|
||||
{
|
||||
unserializableValue: t.Enum(["Infinity", "-Infinity", "-0", "NaN"]),
|
||||
},
|
||||
{
|
||||
value: t.Any,
|
||||
}),
|
||||
},
|
||||
};
|
||||
43
remote/jar.mn
Normal file
43
remote/jar.mn
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# 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/.
|
||||
|
||||
remote.jar:
|
||||
% content remote %content/
|
||||
content/Actor.jsm (Actor.jsm)
|
||||
content/Collections.jsm (Collections.jsm)
|
||||
content/Connection.jsm (Connection.jsm)
|
||||
content/ConsoleServiceObserver.jsm (ConsoleServiceObserver.jsm)
|
||||
content/Debugger.jsm (Debugger.jsm)
|
||||
content/Domain.jsm (Domain.jsm)
|
||||
content/Error.jsm (Error.jsm)
|
||||
content/EventEmitter.jsm (EventEmitter.jsm)
|
||||
content/Handler.jsm (Handler.jsm)
|
||||
content/Log.jsm (Log.jsm)
|
||||
content/MessageChannel.jsm (MessageChannel.jsm)
|
||||
content/Observer.jsm (Observer.jsm)
|
||||
content/Prefs.jsm (Prefs.jsm)
|
||||
content/Protocol.jsm (Protocol.jsm)
|
||||
content/Session.jsm (Session.jsm)
|
||||
content/Sync.jsm (Sync.jsm)
|
||||
content/Target.jsm (Target.jsm)
|
||||
content/WindowManager.jsm (WindowManager.jsm)
|
||||
|
||||
# domains
|
||||
content/domain/Log.jsm (domain/Log.jsm)
|
||||
content/domain/Network.jsm (domain/Network.jsm)
|
||||
content/domain/Page.jsm (domain/Page.jsm)
|
||||
content/domain/Runtime.jsm (domain/Runtime.jsm)
|
||||
|
||||
# actors
|
||||
content/actor/DOMChild.jsm (actor/DOMChild.jsm)
|
||||
content/actor/LogChild.jsm (actor/LogChild.jsm)
|
||||
|
||||
# transport layer
|
||||
content/server/HTTPD.jsm (../netwerk/test/httpserver/httpd.js)
|
||||
content/server/Packet.jsm (server/Packet.jsm)
|
||||
content/server/Socket.jsm (server/Socket.jsm)
|
||||
content/server/Stream.jsm (server/Stream.jsm)
|
||||
content/server/Transport.jsm (server/Transport.jsm)
|
||||
content/server/WebSocket.jsm (server/WebSocket.jsm)
|
||||
content/server/WebSocketTransport.jsm (server/WebSocketTransport.jsm)
|
||||
26
remote/moz.build
Normal file
26
remote/moz.build
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# 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 += [
|
||||
"actor",
|
||||
"pref",
|
||||
"test",
|
||||
]
|
||||
|
||||
EXTRA_COMPONENTS += [
|
||||
"RemoteAgent.js",
|
||||
"RemoteAgent.manifest",
|
||||
]
|
||||
|
||||
JAR_MANIFESTS += ["jar.mn"]
|
||||
XPIDL_MODULE = "remote"
|
||||
XPIDL_SOURCES += ["nsIRemoteAgent.idl"]
|
||||
|
||||
with Files("**"):
|
||||
BUG_COMPONENT = ("DevTools", "Remote Agent")
|
||||
|
||||
with Files("doc/**"):
|
||||
SCHEDULES.exclusive = ["docs"]
|
||||
|
||||
SPHINX_TREES["remote"] = "docs"
|
||||
43
remote/nsIRemoteAgent.idl
Normal file
43
remote/nsIRemoteAgent.idl
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/* 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"
|
||||
|
||||
%{C++
|
||||
#define NS_REMOTE_AGENT_CONTRACTID "@mozilla.org/remote/agent"
|
||||
%}
|
||||
|
||||
/**
|
||||
* Remote agent is a low-level debugging interface
|
||||
* based on the CDP protocol.
|
||||
*/
|
||||
[scriptable, uuid(ccbd36a6-a8fc-4930-b747-8be79c7a04b2)]
|
||||
interface nsIRemoteAgent : nsISupports
|
||||
{
|
||||
/**
|
||||
* Determine whether the remote agent is currently listening,
|
||||
* i.e. accepting new connections.
|
||||
*/
|
||||
readonly attribute boolean listening;
|
||||
|
||||
/**
|
||||
* Start the listener.
|
||||
*
|
||||
* Takes a network address string of the form [<host>][:<port>].
|
||||
* :0 will have the system atomically select a free port.
|
||||
*/
|
||||
void listen(in AUTF8String address);
|
||||
|
||||
/** Close the listener. */
|
||||
void close();
|
||||
|
||||
/** The network scheme, i.e. "http", the listener is currently bound to. */
|
||||
readonly attribute AUTF8String scheme;
|
||||
|
||||
/** The host name the listener is currently bound to. */
|
||||
readonly attribute AUTF8String host;
|
||||
|
||||
/** The port the listener socket is currently bound to. */
|
||||
readonly attribute long port;
|
||||
};
|
||||
1
remote/pref/moz.build
Normal file
1
remote/pref/moz.build
Normal file
|
|
@ -0,0 +1 @@
|
|||
JS_PREFERENCE_FILES += ["remote.js"]
|
||||
14
remote/pref/remote.js
Normal file
14
remote/pref/remote.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Indicates whether the remote agent is enabled.
|
||||
// If it is false, the remote agent will not be loaded.
|
||||
pref("remote.enabled", true);
|
||||
|
||||
// Limits remote agent to listen on loopback devices,
|
||||
// e.g. 127.0.0.1, localhost, and ::1.
|
||||
pref("remote.force-local", true);
|
||||
|
||||
// Defines the verbosity of the internal logger.
|
||||
//
|
||||
// Available levels are, in descending order of severity,
|
||||
// "Trace", "Debug", "Config", "Info", "Warn", "Error", and "Fatal".
|
||||
// The value is treated case-sensitively.
|
||||
pref("remote.log.level", "Info");
|
||||
413
remote/server/Packet.jsm
Normal file
413
remote/server/Packet.jsm
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
/* 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/. */
|
||||
|
||||
// This is an XPCOM service-ified copy of ../devtools/shared/transport/packets.js.
|
||||
|
||||
"use strict",
|
||||
|
||||
var EXPORTED_SYMBOLS = [
|
||||
"RawPacket",
|
||||
"Packet",
|
||||
"JSONPacket",
|
||||
"BulkPacket",
|
||||
];
|
||||
|
||||
/**
|
||||
* Packets contain read / write functionality for the different packet types
|
||||
* supported by the debugging protocol, so that a transport can focus on
|
||||
* delivery and queue management without worrying too much about the specific
|
||||
* packet types.
|
||||
*
|
||||
* They are intended to be "one use only", so a new packet should be
|
||||
* instantiated for each incoming or outgoing packet.
|
||||
*
|
||||
* A complete Packet type should expose at least the following:
|
||||
* * read(stream, scriptableStream)
|
||||
* Called when the input stream has data to read
|
||||
* * write(stream)
|
||||
* Called when the output stream is ready to write
|
||||
* * get done()
|
||||
* Returns true once the packet is done being read / written
|
||||
* * destroy()
|
||||
* Called to clean up at the end of use
|
||||
*/
|
||||
|
||||
const {Stream} = ChromeUtils.import("chrome://remote/content/server/Stream.jsm");
|
||||
|
||||
const unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
|
||||
.createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
unicodeConverter.charset = "UTF-8";
|
||||
|
||||
const defer = function() {
|
||||
let deferred = {
|
||||
promise: new Promise((resolve, reject) => {
|
||||
deferred.resolve = resolve;
|
||||
deferred.reject = reject;
|
||||
}),
|
||||
};
|
||||
return deferred;
|
||||
};
|
||||
|
||||
// The transport's previous check ensured the header length did not
|
||||
// exceed 20 characters. Here, we opt for the somewhat smaller, but still
|
||||
// large limit of 1 TiB.
|
||||
const PACKET_LENGTH_MAX = Math.pow(2, 40);
|
||||
|
||||
/**
|
||||
* A generic Packet processing object (extended by two subtypes below).
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
function Packet(transport) {
|
||||
this._transport = transport;
|
||||
this._length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to initialize a new Packet based on the incoming packet header
|
||||
* we've received so far. We try each of the types in succession, trying
|
||||
* JSON packets first since they are much more common.
|
||||
*
|
||||
* @param {string} header
|
||||
* Packet header string to attempt parsing.
|
||||
* @param {DebuggerTransport} transport
|
||||
* Transport instance that will own the packet.
|
||||
*
|
||||
* @return {Packet}
|
||||
* Parsed packet of the matching type, or null if no types matched.
|
||||
*/
|
||||
Packet.fromHeader = function(header, transport) {
|
||||
return JSONPacket.fromHeader(header, transport) ||
|
||||
BulkPacket.fromHeader(header, transport);
|
||||
};
|
||||
|
||||
Packet.prototype = {
|
||||
|
||||
get length() {
|
||||
return this._length;
|
||||
},
|
||||
|
||||
set length(length) {
|
||||
if (length > PACKET_LENGTH_MAX) {
|
||||
throw new Error("Packet length " + length +
|
||||
" exceeds the max length of " + PACKET_LENGTH_MAX);
|
||||
}
|
||||
this._length = length;
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this._transport = null;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* With a JSON packet (the typical packet type sent via the transport),
|
||||
* data is transferred as a JSON packet serialized into a string,
|
||||
* with the string length prepended to the packet, followed by a colon
|
||||
* ([length]:[packet]). The contents of the JSON packet are specified in
|
||||
* the Remote Debugging Protocol specification.
|
||||
*
|
||||
* @param {DebuggerTransport} transport
|
||||
* Transport instance that will own the packet.
|
||||
*/
|
||||
function JSONPacket(transport) {
|
||||
Packet.call(this, transport);
|
||||
this._data = "";
|
||||
this._done = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to initialize a new JSONPacket based on the incoming packet
|
||||
* header we've received so far.
|
||||
*
|
||||
* @param {string} header
|
||||
* Packet header string to attempt parsing.
|
||||
* @param {DebuggerTransport} transport
|
||||
* Transport instance that will own the packet.
|
||||
*
|
||||
* @return {JSONPacket}
|
||||
* Parsed packet, or null if it's not a match.
|
||||
*/
|
||||
JSONPacket.fromHeader = function(header, transport) {
|
||||
let match = this.HEADER_PATTERN.exec(header);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let packet = new JSONPacket(transport);
|
||||
packet.length = +match[1];
|
||||
return packet;
|
||||
};
|
||||
|
||||
JSONPacket.HEADER_PATTERN = /^(\d+):$/;
|
||||
|
||||
JSONPacket.prototype = Object.create(Packet.prototype);
|
||||
|
||||
Object.defineProperty(JSONPacket.prototype, "object", {
|
||||
/**
|
||||
* Gets the object (not the serialized string) being read or written.
|
||||
*/
|
||||
get() {
|
||||
return this._object;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the object to be sent when write() is called.
|
||||
*/
|
||||
set(object) {
|
||||
this._object = object;
|
||||
let data = JSON.stringify(object);
|
||||
this._data = unicodeConverter.ConvertFromUnicode(data);
|
||||
this.length = this._data.length;
|
||||
},
|
||||
});
|
||||
|
||||
JSONPacket.prototype.read = function(stream, scriptableStream) {
|
||||
|
||||
// Read in more packet data.
|
||||
this._readData(stream, scriptableStream);
|
||||
|
||||
if (!this.done) {
|
||||
// Don't have a complete packet yet.
|
||||
return;
|
||||
}
|
||||
|
||||
let json = this._data;
|
||||
try {
|
||||
json = unicodeConverter.ConvertToUnicode(json);
|
||||
this._object = JSON.parse(json);
|
||||
} catch (e) {
|
||||
let msg = "Error parsing incoming packet: " + json + " (" + e +
|
||||
" - " + e.stack + ")";
|
||||
console.error(msg);
|
||||
dump(msg + "\n");
|
||||
return;
|
||||
}
|
||||
|
||||
this._transport._onJSONObjectReady(this._object);
|
||||
};
|
||||
|
||||
JSONPacket.prototype._readData = function(stream, scriptableStream) {
|
||||
let bytesToRead = Math.min(
|
||||
this.length - this._data.length,
|
||||
stream.available());
|
||||
this._data += scriptableStream.readBytes(bytesToRead);
|
||||
this._done = this._data.length === this.length;
|
||||
};
|
||||
|
||||
JSONPacket.prototype.write = function(stream) {
|
||||
|
||||
if (this._outgoing === undefined) {
|
||||
// Format the serialized packet to a buffer
|
||||
this._outgoing = this.length + ":" + this._data;
|
||||
}
|
||||
|
||||
let written = stream.write(this._outgoing, this._outgoing.length);
|
||||
this._outgoing = this._outgoing.slice(written);
|
||||
this._done = !this._outgoing.length;
|
||||
};
|
||||
|
||||
Object.defineProperty(JSONPacket.prototype, "done", {
|
||||
get() {
|
||||
return this._done;
|
||||
},
|
||||
});
|
||||
|
||||
JSONPacket.prototype.toString = function() {
|
||||
return JSON.stringify(this._object, null, 2);
|
||||
};
|
||||
|
||||
/**
|
||||
* With a bulk packet, data is transferred by temporarily handing over
|
||||
* the transport's input or output stream to the application layer for
|
||||
* writing data directly. This can be much faster for large data sets,
|
||||
* and avoids various stages of copies and data duplication inherent in
|
||||
* the JSON packet type. The bulk packet looks like:
|
||||
*
|
||||
* bulk [actor] [type] [length]:[data]
|
||||
*
|
||||
* The interpretation of the data portion depends on the kind of actor and
|
||||
* the packet's type. See the Remote Debugging Protocol Stream Transport
|
||||
* spec for more details.
|
||||
*
|
||||
* @param {DebuggerTransport} transport
|
||||
* Transport instance that will own the packet.
|
||||
*/
|
||||
function BulkPacket(transport) {
|
||||
Packet.call(this, transport);
|
||||
this._done = false;
|
||||
this._readyForWriting = defer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to initialize a new BulkPacket based on the incoming packet
|
||||
* header we've received so far.
|
||||
*
|
||||
* @param {string} header
|
||||
* Packet header string to attempt parsing.
|
||||
* @param {DebuggerTransport} transport
|
||||
* Transport instance that will own the packet.
|
||||
*
|
||||
* @return {BulkPacket}
|
||||
* Parsed packet, or null if it's not a match.
|
||||
*/
|
||||
BulkPacket.fromHeader = function(header, transport) {
|
||||
let match = this.HEADER_PATTERN.exec(header);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let packet = new BulkPacket(transport);
|
||||
packet.header = {
|
||||
actor: match[1],
|
||||
type: match[2],
|
||||
length: +match[3],
|
||||
};
|
||||
return packet;
|
||||
};
|
||||
|
||||
BulkPacket.HEADER_PATTERN = /^bulk ([^: ]+) ([^: ]+) (\d+):$/;
|
||||
|
||||
BulkPacket.prototype = Object.create(Packet.prototype);
|
||||
|
||||
BulkPacket.prototype.read = function(stream) {
|
||||
// Temporarily pause monitoring of the input stream
|
||||
this._transport.pauseIncoming();
|
||||
|
||||
let deferred = defer();
|
||||
|
||||
this._transport._onBulkReadReady({
|
||||
actor: this.actor,
|
||||
type: this.type,
|
||||
length: this.length,
|
||||
copyTo: (output) => {
|
||||
let copying = StreamUtils.copyStream(stream, output, this.length);
|
||||
deferred.resolve(copying);
|
||||
return copying;
|
||||
},
|
||||
stream,
|
||||
done: deferred,
|
||||
});
|
||||
|
||||
// Await the result of reading from the stream
|
||||
deferred.promise.then(() => {
|
||||
this._done = true;
|
||||
this._transport.resumeIncoming();
|
||||
}, this._transport.close);
|
||||
|
||||
// Ensure this is only done once
|
||||
this.read = () => {
|
||||
throw new Error("Tried to read() a BulkPacket's stream multiple times.");
|
||||
};
|
||||
};
|
||||
|
||||
BulkPacket.prototype.write = function(stream) {
|
||||
if (this._outgoingHeader === undefined) {
|
||||
// Format the serialized packet header to a buffer
|
||||
this._outgoingHeader = "bulk " + this.actor + " " + this.type + " " +
|
||||
this.length + ":";
|
||||
}
|
||||
|
||||
// Write the header, or whatever's left of it to write.
|
||||
if (this._outgoingHeader.length) {
|
||||
let written = stream.write(this._outgoingHeader,
|
||||
this._outgoingHeader.length);
|
||||
this._outgoingHeader = this._outgoingHeader.slice(written);
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporarily pause the monitoring of the output stream
|
||||
this._transport.pauseOutgoing();
|
||||
|
||||
let deferred = defer();
|
||||
|
||||
this._readyForWriting.resolve({
|
||||
copyFrom: (input) => {
|
||||
let copying = StreamUtils.copyStream(input, stream, this.length);
|
||||
deferred.resolve(copying);
|
||||
return copying;
|
||||
},
|
||||
stream,
|
||||
done: deferred,
|
||||
});
|
||||
|
||||
// Await the result of writing to the stream
|
||||
deferred.promise.then(() => {
|
||||
this._done = true;
|
||||
this._transport.resumeOutgoing();
|
||||
}, this._transport.close);
|
||||
|
||||
// Ensure this is only done once
|
||||
this.write = () => {
|
||||
throw new Error("Tried to write() a BulkPacket's stream multiple times.");
|
||||
};
|
||||
};
|
||||
|
||||
Object.defineProperty(BulkPacket.prototype, "streamReadyForWriting", {
|
||||
get() {
|
||||
return this._readyForWriting.promise;
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(BulkPacket.prototype, "header", {
|
||||
get() {
|
||||
return {
|
||||
actor: this.actor,
|
||||
type: this.type,
|
||||
length: this.length,
|
||||
};
|
||||
},
|
||||
|
||||
set(header) {
|
||||
this.actor = header.actor;
|
||||
this.type = header.type;
|
||||
this.length = header.length;
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(BulkPacket.prototype, "done", {
|
||||
get() {
|
||||
return this._done;
|
||||
},
|
||||
});
|
||||
|
||||
BulkPacket.prototype.toString = function() {
|
||||
return "Bulk: " + JSON.stringify(this.header, null, 2);
|
||||
};
|
||||
|
||||
/**
|
||||
* RawPacket is used to test the transport's error handling of malformed
|
||||
* packets, by writing data directly onto the stream.
|
||||
* @param transport DebuggerTransport
|
||||
* The transport instance that will own the packet.
|
||||
* @param data string
|
||||
* The raw string to send out onto the stream.
|
||||
*/
|
||||
function RawPacket(transport, data) {
|
||||
Packet.call(this, transport);
|
||||
this._data = data;
|
||||
this.length = data.length;
|
||||
this._done = false;
|
||||
}
|
||||
|
||||
RawPacket.prototype = Object.create(Packet.prototype);
|
||||
|
||||
RawPacket.prototype.read = function() {
|
||||
// this has not yet been needed for testing
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
RawPacket.prototype.write = function(stream) {
|
||||
let written = stream.write(this._data, this._data.length);
|
||||
this._data = this._data.slice(written);
|
||||
this._done = !this._data.length;
|
||||
};
|
||||
|
||||
Object.defineProperty(RawPacket.prototype, "done", {
|
||||
get() {
|
||||
return this._done;
|
||||
},
|
||||
});
|
||||
8
remote/server/README
Normal file
8
remote/server/README
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
These files provide functionality for serving and responding to HTTP
|
||||
requests, and handling WebSocket connections. For this we rely on
|
||||
httpd.js and the chrome-only WebSocket.createServerWebSocket function.
|
||||
|
||||
Generally speaking, this is all held together with a piece of string.
|
||||
It is a known problem that we do not have a high-quality HTTPD
|
||||
implementation in central, and we’d like to move away from using
|
||||
any of this code.
|
||||
270
remote/server/Socket.jsm
Normal file
270
remote/server/Socket.jsm
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = [
|
||||
"ConnectionHandshake",
|
||||
"SocketListener",
|
||||
];
|
||||
|
||||
// This is an XPCOM-service-ified copy of ../devtools/shared/security/socket.js.
|
||||
|
||||
const {EventEmitter} = ChromeUtils.import("chrome://remote/content/EventEmitter.jsm");
|
||||
const {Log} = ChromeUtils.import("chrome://remote/content/Log.jsm");
|
||||
const {Preferences} = ChromeUtils.import("resource://gre/modules/Preferences.jsm");
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
DebuggerTransport: "chrome://remote/content/server/Transport.jsm",
|
||||
WebSocketDebuggerTransport: "chrome://remote/content/server/WebSocketTransport.jsm",
|
||||
WebSocketServer: "chrome://remote/content/server/WebSocket.jsm",
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", Log.get);
|
||||
XPCOMUtils.defineLazyGetter(this, "nsFile", () => CC("@mozilla.org/file/local;1", "nsIFile", "initWithPath"));
|
||||
|
||||
const LOOPBACKS = ["localhost", "127.0.0.1"];
|
||||
|
||||
const {KeepWhenOffline, LoopbackOnly} = Ci.nsIServerSocket;
|
||||
|
||||
this.SocketListener = class SocketListener {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.network = null;
|
||||
|
||||
this.nextConnID = 0;
|
||||
|
||||
this.onConnectionCreated = null;
|
||||
this.onConnectionClose = null;
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
get listening() {
|
||||
return !!this.socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} addr
|
||||
* [<network>][:<host>][:<port>]
|
||||
* networks: ws, unix, tcp
|
||||
*/
|
||||
listen(addr) {
|
||||
const [network, host, port] = addr.split(":");
|
||||
try {
|
||||
this._listen(network, host, port);
|
||||
} catch (e) {
|
||||
this.close();
|
||||
throw new Error(`Unable to start socket server on ${addr}: ${e.message}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
_listen(network = "tcp", host = "localhost", port = 0) {
|
||||
if (typeof network != "string" ||
|
||||
typeof host != "string" ||
|
||||
(network != "unix" && typeof port != "number")) {
|
||||
throw new TypeError();
|
||||
}
|
||||
if (network != "unix" && (!Number.isInteger(port) || port < 0)) {
|
||||
throw new RangeError();
|
||||
}
|
||||
if (!SocketListener.Networks.includes(network)) {
|
||||
throw new Error("Unexpected network: " + network);
|
||||
}
|
||||
if (!LOOPBACKS.includes(host)) {
|
||||
throw new Error("Restricted to listening on loopback devices");
|
||||
}
|
||||
|
||||
const flags = KeepWhenOffline | LoopbackOnly;
|
||||
|
||||
const backlog = 4;
|
||||
this.socket = createSocket();
|
||||
this.network = network;
|
||||
|
||||
switch (this.network) {
|
||||
case "tcp":
|
||||
case "ws":
|
||||
// -1 means kernel-assigned port in Gecko
|
||||
if (port == 0) {
|
||||
port = -1;
|
||||
}
|
||||
|
||||
this.socket.initSpecialConnection(port, flags, backlog);
|
||||
break;
|
||||
|
||||
case "unix":
|
||||
// concrete Unix socket
|
||||
if (host.startsWith("/")) {
|
||||
const file = nsFile(host);
|
||||
if (file.exists()) {
|
||||
file.remove(false);
|
||||
}
|
||||
this.socket.initWithFilename(file, Number.parseInt("666", 8), backlog);
|
||||
// abstract Unix socket
|
||||
} else {
|
||||
this.socket.initWithAbstractAddress(host, backlog);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.socket.asyncListen(this);
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
}
|
||||
// TODO(ato): removeSocketListener?
|
||||
}
|
||||
|
||||
get host() {
|
||||
if (this.socket) {
|
||||
// TODO(ato): Don't hardcode:
|
||||
return "localhost";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get port() {
|
||||
if (this.socket) {
|
||||
return this.socket.port;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onAllowedConnection(transport) {
|
||||
this.emit("accepted", transport, this);
|
||||
}
|
||||
|
||||
// nsIServerSocketListener implementation
|
||||
|
||||
onSocketAccepted(socket, socketTransport) {
|
||||
const conn = new ConnectionHandshake(this, socketTransport);
|
||||
conn.once("allowed", this.onAllowedConnection.bind(this));
|
||||
conn.handle();
|
||||
}
|
||||
|
||||
onStopListening(socket, status) {
|
||||
dump("onStopListening: " + status + "\n");
|
||||
}
|
||||
};
|
||||
|
||||
SocketListener.Networks = ["tcp", "unix", "ws"];
|
||||
|
||||
/**
|
||||
* Created by SocketListener for each accepted incoming socket.
|
||||
* This is a short-lived object used to implement a handshake,
|
||||
* before the socket transport is handed back to RemoteAgent.
|
||||
*/
|
||||
this.ConnectionHandshake = class ConnectionHandshake {
|
||||
constructor(listener, socketTransport) {
|
||||
this.listener = listener;
|
||||
this.socket = socketTransport;
|
||||
this.transport = null;
|
||||
this.destroyed = false;
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
destructor() {
|
||||
this.listener = null;
|
||||
this.socket = null;
|
||||
this.transport = null;
|
||||
this.destroyed = true;
|
||||
}
|
||||
|
||||
get address() {
|
||||
return `${this.host}:${this.port}`;
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this.socket.host;
|
||||
}
|
||||
|
||||
get port() {
|
||||
return this.socket.port;
|
||||
}
|
||||
|
||||
async handle() {
|
||||
try {
|
||||
await this.createTransport();
|
||||
this.allow();
|
||||
} catch (e) {
|
||||
this.deny(e);
|
||||
}
|
||||
}
|
||||
|
||||
async createTransport() {
|
||||
const rx = this.socket.openInputStream(0, 0, 0);
|
||||
const tx = this.socket.openOutputStream(0, 0, 0);
|
||||
|
||||
if (this.listener.network == "ws") {
|
||||
const so = await WebSocketServer.accept(this.socket, rx, tx);
|
||||
this.transport = new WebSocketDebuggerTransport(so);
|
||||
} else {
|
||||
this.transport = new DebuggerTransport(rx, tx);
|
||||
}
|
||||
|
||||
// This handles early disconnects from clients, primarily for failed TLS negotiation.
|
||||
// We don't support TLS connections in RDP, but might be useful for future blocklist.
|
||||
this.transport.hooks = {
|
||||
onClosed: reason => this.deny(reason),
|
||||
};
|
||||
// TODO(ato): Review if this is correct:
|
||||
this.transport.ready();
|
||||
}
|
||||
|
||||
allow() {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
}
|
||||
log.debug(`Accepted connection from ${this.address}`);
|
||||
this.emit("allowed", this.transport);
|
||||
this.destructor();
|
||||
}
|
||||
|
||||
deny(result) {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let err = legibleError(result);
|
||||
log.warn(`Connection from ${this.address} denied: ${err.message}`, err.stack);
|
||||
|
||||
if (this.transport) {
|
||||
this.transport.hooks = null;
|
||||
this.transport.close(result);
|
||||
}
|
||||
this.socket.close(result);
|
||||
this.destructor();
|
||||
}
|
||||
};
|
||||
|
||||
function createSocket() {
|
||||
return Cc["@mozilla.org/network/server-socket;1"]
|
||||
.createInstance(Ci.nsIServerSocket);
|
||||
}
|
||||
|
||||
// TODO(ato): Move to separate module
|
||||
function legibleError(obj) {
|
||||
if (obj instanceof Ci.nsIException) {
|
||||
for (const result in Cr) {
|
||||
if (obj.result == Cr[result]) {
|
||||
return {
|
||||
message: result,
|
||||
stack: obj.location.formattedStack,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: "nsIException",
|
||||
stack: obj,
|
||||
};
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
249
remote/server/Stream.jsm
Normal file
249
remote/server/Stream.jsm
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Stream"];
|
||||
|
||||
// This is an XPCOM service-ified copy of ../devtools/shared/transport/stream-utils.js.
|
||||
|
||||
const CC = Components.Constructor;
|
||||
|
||||
const {EventEmitter} = ChromeUtils.import("resource://gre/modules/EventEmitter.jsm");
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const IOUtil = Cc["@mozilla.org/io-util;1"].getService(Ci.nsIIOUtil);
|
||||
const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
|
||||
"nsIScriptableInputStream", "init");
|
||||
|
||||
const BUFFER_SIZE = 0x8000;
|
||||
|
||||
/**
|
||||
* This helper function (and its companion object) are used by bulk
|
||||
* senders and receivers to read and write data in and out of other streams.
|
||||
* Functions that make use of this tool are passed to callers when it is
|
||||
* time to read or write bulk data. It is highly recommended to use these
|
||||
* copier functions instead of the stream directly because the copier
|
||||
* enforces the agreed upon length. Since bulk mode reuses an existing
|
||||
* stream, the sender and receiver must write and read exactly the agreed
|
||||
* upon amount of data, or else the entire transport will be left in a
|
||||
* invalid state. Additionally, other methods of stream copying (such as
|
||||
* NetUtil.asyncCopy) close the streams involved, which would terminate
|
||||
* the debugging transport, and so it is avoided here.
|
||||
*
|
||||
* Overall, this *works*, but clearly the optimal solution would be
|
||||
* able to just use the streams directly. If it were possible to fully
|
||||
* implement nsIInputStream/nsIOutputStream in JS, wrapper streams could
|
||||
* be created to enforce the length and avoid closing, and consumers could
|
||||
* use familiar stream utilities like NetUtil.asyncCopy.
|
||||
*
|
||||
* The function takes two async streams and copies a precise number
|
||||
* of bytes from one to the other. Copying begins immediately, but may
|
||||
* complete at some future time depending on data size. Use the returned
|
||||
* promise to know when it's complete.
|
||||
*
|
||||
* @param {nsIAsyncInputStream} input
|
||||
* Stream to copy from.
|
||||
* @param {nsIAsyncOutputStream} output
|
||||
* Stream to copy to.
|
||||
* @param {number} length
|
||||
* Amount of data that needs to be copied.
|
||||
*
|
||||
* @return {Promise}
|
||||
* Promise is resolved when copying completes or rejected if any
|
||||
* (unexpected) errors occur.
|
||||
*/
|
||||
function copyStream(input, output, length) {
|
||||
let copier = new StreamCopier(input, output, length);
|
||||
return copier.copy();
|
||||
}
|
||||
|
||||
/** @class */
|
||||
function StreamCopier(input, output, length) {
|
||||
EventEmitter.decorate(this);
|
||||
this._id = StreamCopier._nextId++;
|
||||
this.input = input;
|
||||
// Save off the base output stream, since we know it's async as we've
|
||||
// required
|
||||
this.baseAsyncOutput = output;
|
||||
if (IOUtil.outputStreamIsBuffered(output)) {
|
||||
this.output = output;
|
||||
} else {
|
||||
this.output = Cc["@mozilla.org/network/buffered-output-stream;1"]
|
||||
.createInstance(Ci.nsIBufferedOutputStream);
|
||||
this.output.init(output, BUFFER_SIZE);
|
||||
}
|
||||
this._length = length;
|
||||
this._amountLeft = length;
|
||||
this._deferred = {
|
||||
promise: new Promise((resolve, reject) => {
|
||||
this._deferred.resolve = resolve;
|
||||
this._deferred.reject = reject;
|
||||
}),
|
||||
};
|
||||
|
||||
this._copy = this._copy.bind(this);
|
||||
this._flush = this._flush.bind(this);
|
||||
this._destroy = this._destroy.bind(this);
|
||||
|
||||
// Copy promise's then method up to this object.
|
||||
//
|
||||
// Allows the copier to offer a promise interface for the simple succeed
|
||||
// or fail scenarios, but also emit events (due to the EventEmitter)
|
||||
// for other states, like progress.
|
||||
this.then = this._deferred.promise.then.bind(this._deferred.promise);
|
||||
this.then(this._destroy, this._destroy);
|
||||
|
||||
// Stream ready callback starts as |_copy|, but may switch to |_flush|
|
||||
// at end if flushing would block the output stream.
|
||||
this._streamReadyCallback = this._copy;
|
||||
}
|
||||
StreamCopier._nextId = 0;
|
||||
|
||||
StreamCopier.prototype = {
|
||||
|
||||
copy() {
|
||||
// Dispatch to the next tick so that it's possible to attach a progress
|
||||
// event listener, even for extremely fast copies (like when testing).
|
||||
Services.tm.currentThread.dispatch(() => {
|
||||
try {
|
||||
this._copy();
|
||||
} catch (e) {
|
||||
this._deferred.reject(e);
|
||||
}
|
||||
}, 0);
|
||||
return this;
|
||||
},
|
||||
|
||||
_copy() {
|
||||
let bytesAvailable = this.input.available();
|
||||
let amountToCopy = Math.min(bytesAvailable, this._amountLeft);
|
||||
this._debug("Trying to copy: " + amountToCopy);
|
||||
|
||||
let bytesCopied;
|
||||
try {
|
||||
bytesCopied = this.output.writeFrom(this.input, amountToCopy);
|
||||
} catch (e) {
|
||||
if (e.result == Cr.NS_BASE_STREAM_WOULD_BLOCK) {
|
||||
this._debug("Base stream would block, will retry");
|
||||
this._debug("Waiting for output stream");
|
||||
this.baseAsyncOutput.asyncWait(this, 0, 0, Services.tm.currentThread);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
this._amountLeft -= bytesCopied;
|
||||
this._debug("Copied: " + bytesCopied +
|
||||
", Left: " + this._amountLeft);
|
||||
this._emitProgress();
|
||||
|
||||
if (this._amountLeft === 0) {
|
||||
this._debug("Copy done!");
|
||||
this._flush();
|
||||
return;
|
||||
}
|
||||
|
||||
this._debug("Waiting for input stream");
|
||||
this.input.asyncWait(this, 0, 0, Services.tm.currentThread);
|
||||
},
|
||||
|
||||
_emitProgress() {
|
||||
this.emit("progress", {
|
||||
bytesSent: this._length - this._amountLeft,
|
||||
totalBytes: this._length,
|
||||
});
|
||||
},
|
||||
|
||||
_flush() {
|
||||
try {
|
||||
this.output.flush();
|
||||
} catch (e) {
|
||||
if (e.result == Cr.NS_BASE_STREAM_WOULD_BLOCK ||
|
||||
e.result == Cr.NS_ERROR_FAILURE) {
|
||||
this._debug("Flush would block, will retry");
|
||||
this._streamReadyCallback = this._flush;
|
||||
this._debug("Waiting for output stream");
|
||||
this.baseAsyncOutput.asyncWait(this, 0, 0, Services.tm.currentThread);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
this._deferred.resolve();
|
||||
},
|
||||
|
||||
_destroy() {
|
||||
this._destroy = null;
|
||||
this._copy = null;
|
||||
this._flush = null;
|
||||
this.input = null;
|
||||
this.output = null;
|
||||
},
|
||||
|
||||
// nsIInputStreamCallback
|
||||
onInputStreamReady() {
|
||||
this._streamReadyCallback();
|
||||
},
|
||||
|
||||
// nsIOutputStreamCallback
|
||||
onOutputStreamReady() {
|
||||
this._streamReadyCallback();
|
||||
},
|
||||
|
||||
_debug() {
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Read from a stream, one byte at a time, up to the next
|
||||
* <var>delimiter</var> character, but stopping if we've read |count|
|
||||
* without finding it. Reading also terminates early if there are less
|
||||
* than <var>count</var> bytes available on the stream. In that case,
|
||||
* we only read as many bytes as the stream currently has to offer.
|
||||
*
|
||||
* @param {nsIInputStream} stream
|
||||
* Input stream to read from.
|
||||
* @param {string} delimiter
|
||||
* Character we're trying to find.
|
||||
* @param {number} count
|
||||
* Max number of characters to read while searching.
|
||||
*
|
||||
* @return {string}
|
||||
* Collected data. If the delimiter was found, this string will
|
||||
* end with it.
|
||||
*/
|
||||
// TODO: This implementation could be removed if bug 984651 is fixed,
|
||||
// which provides a native version of the same idea.
|
||||
function delimitedRead(stream, delimiter, count) {
|
||||
let scriptableStream;
|
||||
if (stream instanceof Ci.nsIScriptableInputStream) {
|
||||
scriptableStream = stream;
|
||||
} else {
|
||||
scriptableStream = new ScriptableInputStream(stream);
|
||||
}
|
||||
|
||||
let data = "";
|
||||
|
||||
// Don't exceed what's available on the stream
|
||||
count = Math.min(count, stream.available());
|
||||
|
||||
if (count <= 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
let char;
|
||||
while (char !== delimiter && count > 0) {
|
||||
char = scriptableStream.readBytes(1);
|
||||
count--;
|
||||
data += char;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
this.Stream = {
|
||||
copyStream,
|
||||
delimitedRead,
|
||||
};
|
||||
527
remote/server/Transport.jsm
Normal file
527
remote/server/Transport.jsm
Normal file
|
|
@ -0,0 +1,527 @@
|
|||
/* 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/. */
|
||||
|
||||
// This is an XPCOM service-ified copy of ../devtools/shared/transport/transport.js.
|
||||
|
||||
/* global Pipe, ScriptableInputStream */
|
||||
|
||||
"use strict";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["DebuggerTransport"];
|
||||
|
||||
const CC = Components.Constructor;
|
||||
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {EventEmitter} = ChromeUtils.import("resource://gre/modules/EventEmitter.jsm");
|
||||
const {Stream} = ChromeUtils.import("chrome://remote/content/server/Stream.jsm");
|
||||
const {
|
||||
Packet,
|
||||
JSONPacket,
|
||||
BulkPacket,
|
||||
} = ChromeUtils.import("chrome://remote/content/server/Packet.jsm");
|
||||
|
||||
const executeSoon = function(func) {
|
||||
Services.tm.dispatchToMainThread(func);
|
||||
};
|
||||
|
||||
const flags = {wantVerbose: true, wantLogging: true};
|
||||
|
||||
const dumpv =
|
||||
flags.wantVerbose ?
|
||||
function(msg) {
|
||||
dump(msg + "\n");
|
||||
} :
|
||||
function() {};
|
||||
|
||||
const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
|
||||
"nsIScriptableInputStream", "init");
|
||||
|
||||
const PACKET_HEADER_MAX = 200;
|
||||
|
||||
/**
|
||||
* An adapter that handles data transfers between the debugger client
|
||||
* and server. It can work with both nsIPipe and nsIServerSocket
|
||||
* transports so long as the properly created input and output streams
|
||||
* are specified. (However, for intra-process connections,
|
||||
* LocalDebuggerTransport, below, is more efficient than using an nsIPipe
|
||||
* pair with DebuggerTransport.)
|
||||
*
|
||||
* @param {nsIAsyncInputStream} input
|
||||
* The input stream.
|
||||
* @param {nsIAsyncOutputStream} output
|
||||
* The output stream.
|
||||
*
|
||||
* Given a DebuggerTransport instance dt:
|
||||
* 1) Set dt.hooks to a packet handler object (described below).
|
||||
* 2) Call dt.ready() to begin watching for input packets.
|
||||
* 3) Call dt.send() / dt.startBulkSend() to send packets.
|
||||
* 4) Call dt.close() to close the connection, and disengage from
|
||||
* the event loop.
|
||||
*
|
||||
* A packet handler is an object with the following methods:
|
||||
*
|
||||
* - onPacket(packet) - called when we have received a complete packet.
|
||||
* |packet| is the parsed form of the packet --- a JavaScript value, not
|
||||
* a JSON-syntax string.
|
||||
*
|
||||
* - onBulkPacket(packet) - called when we have switched to bulk packet
|
||||
* receiving mode. |packet| is an object containing:
|
||||
* * actor: Name of actor that will receive the packet
|
||||
* * type: Name of actor's method that should be called on receipt
|
||||
* * length: Size of the data to be read
|
||||
* * stream: This input stream should only be used directly if you
|
||||
* can ensure that you will read exactly |length| bytes and
|
||||
* will not close the stream when reading is complete
|
||||
* * done: If you use the stream directly (instead of |copyTo|
|
||||
* below), you must signal completion by resolving/rejecting
|
||||
* this deferred. If it's rejected, the transport will
|
||||
* be closed. If an Error is supplied as a rejection value,
|
||||
* it will be logged via |dump|. If you do use |copyTo|,
|
||||
* resolving is taken care of for you when copying completes.
|
||||
* * copyTo: A helper function for getting your data out of the
|
||||
* stream that meets the stream handling requirements above,
|
||||
* and has the following signature:
|
||||
*
|
||||
* @param nsIAsyncOutputStream {output}
|
||||
* The stream to copy to.
|
||||
*
|
||||
* @return {Promise}
|
||||
* The promise is resolved when copying completes or
|
||||
* rejected if any (unexpected) errors occur. This object
|
||||
* also emits "progress" events for each chunk that is
|
||||
* copied. See stream-utils.js.
|
||||
*
|
||||
* - onClosed(reason) - called when the connection is closed. |reason|
|
||||
* is an optional nsresult or object, typically passed when the
|
||||
* transport is closed due to some error in a underlying stream.
|
||||
*
|
||||
* See ./packets.js and the Remote Debugging Protocol specification for
|
||||
* more details on the format of these packets.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
function DebuggerTransport(input, output) {
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
this._input = input;
|
||||
this._scriptableInput = new ScriptableInputStream(input);
|
||||
this._output = output;
|
||||
|
||||
// The current incoming (possibly partial) header, which will determine
|
||||
// which type of Packet |_incoming| below will become.
|
||||
this._incomingHeader = "";
|
||||
// The current incoming Packet object
|
||||
this._incoming = null;
|
||||
// A queue of outgoing Packet objects
|
||||
this._outgoing = [];
|
||||
|
||||
this.hooks = null;
|
||||
this.active = false;
|
||||
|
||||
this._incomingEnabled = true;
|
||||
this._outgoingEnabled = true;
|
||||
|
||||
this.close = this.close.bind(this);
|
||||
}
|
||||
|
||||
DebuggerTransport.prototype = {
|
||||
/**
|
||||
* Transmit an object as a JSON packet.
|
||||
*
|
||||
* This method returns immediately, without waiting for the entire
|
||||
* packet to be transmitted, registering event handlers as needed to
|
||||
* transmit the entire packet. Packets are transmitted in the order they
|
||||
* are passed to this method.
|
||||
*/
|
||||
send(object) {
|
||||
this.emit("send", object);
|
||||
|
||||
const packet = new JSONPacket(this);
|
||||
packet.object = object;
|
||||
this._outgoing.push(packet);
|
||||
this._flushOutgoing();
|
||||
},
|
||||
|
||||
/**
|
||||
* Transmit streaming data via a bulk packet.
|
||||
*
|
||||
* This method initiates the bulk send process by queuing up the header
|
||||
* data. The caller receives eventual access to a stream for writing.
|
||||
*
|
||||
* N.B.: Do *not* attempt to close the stream handed to you, as it
|
||||
* will continue to be used by this transport afterwards. Most users
|
||||
* should instead use the provided |copyFrom| function instead.
|
||||
*
|
||||
* @param {Object} header
|
||||
* This is modeled after the format of JSON packets above, but does
|
||||
* not actually contain the data, but is instead just a routing
|
||||
* header:
|
||||
*
|
||||
* - actor: Name of actor that will receive the packet
|
||||
* - type: Name of actor's method that should be called on receipt
|
||||
* - length: Size of the data to be sent
|
||||
*
|
||||
* @return {Promise}
|
||||
* The promise will be resolved when you are allowed to write to
|
||||
* the stream with an object containing:
|
||||
*
|
||||
* - stream: This output stream should only be used directly
|
||||
* if you can ensure that you will write exactly
|
||||
* |length| bytes and will not close the stream when
|
||||
* writing is complete.
|
||||
* - done: If you use the stream directly (instead of
|
||||
* |copyFrom| below), you must signal completion by
|
||||
* resolving/rejecting this deferred. If it's
|
||||
* rejected, the transport will be closed. If an
|
||||
* Error is supplied as a rejection value, it will
|
||||
* be logged via |dump|. If you do use |copyFrom|,
|
||||
* resolving is taken care of for you when copying
|
||||
* completes.
|
||||
* - copyFrom: A helper function for getting your data onto the
|
||||
* stream that meets the stream handling requirements
|
||||
* above, and has the following signature:
|
||||
*
|
||||
* @param {nsIAsyncInputStream} input
|
||||
* The stream to copy from.
|
||||
*
|
||||
* @return {Promise}
|
||||
* The promise is resolved when copying completes
|
||||
* or rejected if any (unexpected) errors occur.
|
||||
* This object also emits "progress" events for
|
||||
* each chunkthat is copied. See stream-utils.js.
|
||||
*/
|
||||
startBulkSend(header) {
|
||||
this.emit("startbulksend", header);
|
||||
|
||||
const packet = new BulkPacket(this);
|
||||
packet.header = header;
|
||||
this._outgoing.push(packet);
|
||||
this._flushOutgoing();
|
||||
return packet.streamReadyForWriting;
|
||||
},
|
||||
|
||||
/**
|
||||
* Close the transport.
|
||||
*
|
||||
* @param {(nsresult|object)=} reason
|
||||
* The status code or error message that corresponds to the reason
|
||||
* for closing the transport (likely because a stream closed
|
||||
* or failed).
|
||||
*/
|
||||
close(reason) {
|
||||
this.emit("close", reason);
|
||||
|
||||
this.active = false;
|
||||
this._input.close();
|
||||
this._scriptableInput.close();
|
||||
this._output.close();
|
||||
this._destroyIncoming();
|
||||
this._destroyAllOutgoing();
|
||||
if (this.hooks) {
|
||||
this.hooks.onClosed(reason);
|
||||
this.hooks = null;
|
||||
}
|
||||
if (reason) {
|
||||
dumpv("Transport closed: " + reason);
|
||||
} else {
|
||||
dumpv("Transport closed.");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The currently outgoing packet (at the top of the queue).
|
||||
*/
|
||||
get _currentOutgoing() {
|
||||
return this._outgoing[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Flush data to the outgoing stream. Waits until the output
|
||||
* stream notifies us that it is ready to be written to (via
|
||||
* onOutputStreamReady).
|
||||
*/
|
||||
_flushOutgoing() {
|
||||
if (!this._outgoingEnabled || this._outgoing.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the top of the packet queue has nothing more to send, remove it.
|
||||
if (this._currentOutgoing.done) {
|
||||
this._finishCurrentOutgoing();
|
||||
}
|
||||
|
||||
if (this._outgoing.length > 0) {
|
||||
const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
|
||||
this._output.asyncWait(this, 0, 0, threadManager.currentThread);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Pause this transport's attempts to write to the output stream.
|
||||
* This is used when we've temporarily handed off our output stream for
|
||||
* writing bulk data.
|
||||
*/
|
||||
pauseOutgoing() {
|
||||
this._outgoingEnabled = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Resume this transport's attempts to write to the output stream.
|
||||
*/
|
||||
resumeOutgoing() {
|
||||
this._outgoingEnabled = true;
|
||||
this._flushOutgoing();
|
||||
},
|
||||
|
||||
// nsIOutputStreamCallback
|
||||
/**
|
||||
* This is called when the output stream is ready for more data to
|
||||
* be written. The current outgoing packet will attempt to write some
|
||||
* amount of data, but may not complete.
|
||||
*/
|
||||
onOutputStreamReady(stream) {
|
||||
if (!this._outgoingEnabled || this._outgoing.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this._currentOutgoing.write(stream);
|
||||
} catch (e) {
|
||||
if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
|
||||
this.close(e.result);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
this._flushOutgoing();
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the current outgoing packet from the queue upon completion.
|
||||
*/
|
||||
_finishCurrentOutgoing() {
|
||||
if (this._currentOutgoing) {
|
||||
this._currentOutgoing.destroy();
|
||||
this._outgoing.shift();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the entire outgoing queue.
|
||||
*/
|
||||
_destroyAllOutgoing() {
|
||||
for (const packet of this._outgoing) {
|
||||
packet.destroy();
|
||||
}
|
||||
this._outgoing = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the input stream for reading. Once this method has been
|
||||
* called, we watch for packets on the input stream, and pass them to
|
||||
* the appropriate handlers via this.hooks.
|
||||
*/
|
||||
ready() {
|
||||
this.active = true;
|
||||
this._waitForIncoming();
|
||||
},
|
||||
|
||||
/**
|
||||
* Asks the input stream to notify us (via onInputStreamReady) when it is
|
||||
* ready for reading.
|
||||
*/
|
||||
_waitForIncoming() {
|
||||
if (this._incomingEnabled) {
|
||||
const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
|
||||
this._input.asyncWait(this, 0, 0, threadManager.currentThread);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Pause this transport's attempts to read from the input stream.
|
||||
* This is used when we've temporarily handed off our input stream for
|
||||
* reading bulk data.
|
||||
*/
|
||||
pauseIncoming() {
|
||||
this._incomingEnabled = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Resume this transport's attempts to read from the input stream.
|
||||
*/
|
||||
resumeIncoming() {
|
||||
this._incomingEnabled = true;
|
||||
this._flushIncoming();
|
||||
this._waitForIncoming();
|
||||
},
|
||||
|
||||
// nsIInputStreamCallback
|
||||
/**
|
||||
* Called when the stream is either readable or closed.
|
||||
*/
|
||||
onInputStreamReady(stream) {
|
||||
try {
|
||||
while (stream.available() && this._incomingEnabled &&
|
||||
this._processIncoming(stream, stream.available())) {
|
||||
// Loop until there is nothing more to process
|
||||
}
|
||||
this._waitForIncoming();
|
||||
} catch (e) {
|
||||
if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
|
||||
this.close(e.result);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Process the incoming data. Will create a new currently incoming
|
||||
* Packet if needed. Tells the incoming Packet to read as much data
|
||||
* as it can, but reading may not complete. The Packet signals that
|
||||
* its data is ready for delivery by calling one of this transport's
|
||||
* _on*Ready methods (see ./packets.js and the _on*Ready methods below).
|
||||
*
|
||||
* @return {boolean}
|
||||
* Whether incoming stream processing should continue for any
|
||||
* remaining data.
|
||||
*/
|
||||
_processIncoming(stream, count) {
|
||||
dumpv("Data available: " + count);
|
||||
|
||||
if (!count) {
|
||||
dumpv("Nothing to read, skipping");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this._incoming) {
|
||||
dumpv("Creating a new packet from incoming");
|
||||
|
||||
if (!this._readHeader(stream)) {
|
||||
// Not enough data to read packet type
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attempt to create a new Packet by trying to parse each possible
|
||||
// header pattern.
|
||||
this._incoming = Packet.fromHeader(this._incomingHeader, this);
|
||||
if (!this._incoming) {
|
||||
throw new Error("No packet types for header: " +
|
||||
this._incomingHeader);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._incoming.done) {
|
||||
// We have an incomplete packet, keep reading it.
|
||||
dumpv("Existing packet incomplete, keep reading");
|
||||
this._incoming.read(stream, this._scriptableInput);
|
||||
}
|
||||
} catch (e) {
|
||||
dump(`Error reading incoming packet: (${e} - ${e.stack})\n`);
|
||||
|
||||
// Now in an invalid state, shut down the transport.
|
||||
this.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this._incoming.done) {
|
||||
// Still not complete, we'll wait for more data.
|
||||
dumpv("Packet not done, wait for more");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ready for next packet
|
||||
this._flushIncoming();
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Read as far as we can into the incoming data, attempting to build
|
||||
* up a complete packet header (which terminates with ":"). We'll only
|
||||
* read up to PACKET_HEADER_MAX characters.
|
||||
*
|
||||
* @return {boolean}
|
||||
* True if we now have a complete header.
|
||||
*/
|
||||
_readHeader() {
|
||||
const amountToRead = PACKET_HEADER_MAX - this._incomingHeader.length;
|
||||
this._incomingHeader +=
|
||||
Stream.delimitedRead(this._scriptableInput, ":", amountToRead);
|
||||
if (flags.wantVerbose) {
|
||||
dumpv("Header read: " + this._incomingHeader);
|
||||
}
|
||||
|
||||
if (this._incomingHeader.endsWith(":")) {
|
||||
if (flags.wantVerbose) {
|
||||
dumpv("Found packet header successfully: " + this._incomingHeader);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this._incomingHeader.length >= PACKET_HEADER_MAX) {
|
||||
throw new Error("Failed to parse packet header!");
|
||||
}
|
||||
|
||||
// Not enough data yet.
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* If the incoming packet is done, log it as needed and clear the buffer.
|
||||
*/
|
||||
_flushIncoming() {
|
||||
if (!this._incoming.done) {
|
||||
return;
|
||||
}
|
||||
if (flags.wantLogging) {
|
||||
dumpv("Got: " + this._incoming);
|
||||
}
|
||||
this._destroyIncoming();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler triggered by an incoming JSONPacket completing it's |read|
|
||||
* method. Delivers the packet to this.hooks.onPacket.
|
||||
*/
|
||||
_onJSONObjectReady(object) {
|
||||
executeSoon(() => {
|
||||
// Ensure the transport is still alive by the time this runs.
|
||||
if (this.active) {
|
||||
this.emit("packet", object);
|
||||
this.hooks.onPacket(object);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler triggered by an incoming BulkPacket entering the |read|
|
||||
* phase for the stream portion of the packet. Delivers info about the
|
||||
* incoming streaming data to this.hooks.onBulkPacket. See the main
|
||||
* comment on the transport at the top of this file for more details.
|
||||
*/
|
||||
_onBulkReadReady(...args) {
|
||||
executeSoon(() => {
|
||||
// Ensure the transport is still alive by the time this runs.
|
||||
if (this.active) {
|
||||
this.emit("bulkpacket", ...args);
|
||||
this.hooks.onBulkPacket(...args);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove all handlers and references related to the current incoming
|
||||
* packet, either because it is now complete or because the transport
|
||||
* is closing.
|
||||
*/
|
||||
_destroyIncoming() {
|
||||
if (this._incoming) {
|
||||
this._incoming.destroy();
|
||||
}
|
||||
this._incomingHeader = "";
|
||||
this._incoming = null;
|
||||
},
|
||||
};
|
||||
238
remote/server/WebSocket.jsm
Normal file
238
remote/server/WebSocket.jsm
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["WebSocketServer"];
|
||||
|
||||
// This file is an XPCOM service-ified copy of ../devtools/server/socket/websocket-server.js.
|
||||
|
||||
const CC = Components.Constructor;
|
||||
|
||||
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const {Stream} = ChromeUtils.import("chrome://remote/content/server/Stream.jsm");
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "WebSocket", () => {
|
||||
return Services.appShell.hiddenDOMWindow.WebSocket;
|
||||
});
|
||||
|
||||
const CryptoHash = CC("@mozilla.org/security/hash;1", "nsICryptoHash", "initWithString");
|
||||
const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
|
||||
|
||||
// limit the header size to put an upper bound on allocated memory
|
||||
const HEADER_MAX_LEN = 8000;
|
||||
|
||||
// TODO(ato): Merge this with httpd.js so that we can respond to both HTTP/1.1
|
||||
// as well as WebSocket requests on the same server.
|
||||
|
||||
/**
|
||||
* Read a line from async input stream
|
||||
* and return promise that resolves to the line once it has been read.
|
||||
* If the line is longer than HEADER_MAX_LEN, will throw error.
|
||||
*/
|
||||
function readLine(input) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let line = "";
|
||||
const wait = () => {
|
||||
input.asyncWait(stream => {
|
||||
try {
|
||||
const amountToRead = HEADER_MAX_LEN - line.length;
|
||||
line += Stream.delimitedRead(input, "\n", amountToRead);
|
||||
|
||||
if (line.endsWith("\n")) {
|
||||
resolve(line.trimRight());
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.length >= HEADER_MAX_LEN) {
|
||||
throw new Error(
|
||||
`Failed to read HTTP header longer than ${HEADER_MAX_LEN} bytes`);
|
||||
}
|
||||
|
||||
wait();
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
}, 0, 0, threadManager.currentThread);
|
||||
};
|
||||
|
||||
wait();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a string of bytes to async output stream
|
||||
* and return promise that resolves once all data has been written.
|
||||
* Doesn't do any UTF-16/UTF-8 conversion.
|
||||
* The string is treated as an array of bytes.
|
||||
*/
|
||||
function writeString(output, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const wait = () => {
|
||||
if (data.length === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
output.asyncWait(stream => {
|
||||
try {
|
||||
const written = output.write(data, data.length);
|
||||
data = data.slice(written);
|
||||
wait();
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
}, 0, 0, threadManager.currentThread);
|
||||
};
|
||||
|
||||
wait();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read HTTP request from async input stream.
|
||||
*
|
||||
* @return Request line (string) and Map of header names and values.
|
||||
*/
|
||||
const readHttpRequest = async function(input) {
|
||||
let requestLine = "";
|
||||
const headers = new Map();
|
||||
|
||||
while (true) {
|
||||
const line = await readLine(input);
|
||||
if (line.length == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!requestLine) {
|
||||
requestLine = line;
|
||||
} else {
|
||||
const colon = line.indexOf(":");
|
||||
if (colon == -1) {
|
||||
throw new Error(`Malformed HTTP header: ${line}`);
|
||||
}
|
||||
|
||||
const name = line.slice(0, colon).toLowerCase();
|
||||
const value = line.slice(colon + 1).trim();
|
||||
headers.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
return {requestLine, headers};
|
||||
};
|
||||
|
||||
/** Write HTTP response (array of strings) to async output stream. */
|
||||
function writeHttpResponse(output, response) {
|
||||
const s = response.join("\r\n") + "\r\n\r\n";
|
||||
return writeString(output, s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the WebSocket handshake headers
|
||||
* and return the key to be sent in Sec-WebSocket-Accept response header.
|
||||
*/
|
||||
function processRequest({ requestLine, headers }) {
|
||||
const [method, path] = requestLine.split(" ");
|
||||
if (method !== "GET") {
|
||||
throw new Error("The handshake request must use GET method");
|
||||
}
|
||||
|
||||
if (path !== "/") {
|
||||
throw new Error("The handshake request has unknown path");
|
||||
}
|
||||
|
||||
const upgrade = headers.get("upgrade");
|
||||
if (!upgrade || upgrade !== "websocket") {
|
||||
throw new Error("The handshake request has incorrect Upgrade header");
|
||||
}
|
||||
|
||||
const connection = headers.get("connection");
|
||||
if (!connection || !connection.split(",").map(t => t.trim()).includes("Upgrade")) {
|
||||
throw new Error("The handshake request has incorrect Connection header");
|
||||
}
|
||||
|
||||
const version = headers.get("sec-websocket-version");
|
||||
if (!version || version !== "13") {
|
||||
throw new Error("The handshake request must have Sec-WebSocket-Version: 13");
|
||||
}
|
||||
|
||||
// Compute the accept key
|
||||
const key = headers.get("sec-websocket-key");
|
||||
if (!key) {
|
||||
throw new Error("The handshake request must have a Sec-WebSocket-Key header");
|
||||
}
|
||||
|
||||
const acceptKey = computeKey(key);
|
||||
return {acceptKey};
|
||||
}
|
||||
|
||||
function computeKey(key) {
|
||||
const str = `${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`;
|
||||
const data = Array.from(str, ch => ch.charCodeAt(0));
|
||||
const hash = new CryptoHash("sha1");
|
||||
hash.update(data, data.length);
|
||||
return hash.finish(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the server part of a WebSocket opening handshake on an incoming connection.
|
||||
*/
|
||||
const serverHandshake = async function(input, output) {
|
||||
// Read the request
|
||||
const request = await readHttpRequest(input);
|
||||
|
||||
try {
|
||||
// Check and extract info from the request
|
||||
const { acceptKey } = processRequest(request);
|
||||
|
||||
// Send response headers
|
||||
await writeHttpResponse(output, [
|
||||
"HTTP/1.1 101 Switching Protocols",
|
||||
"Upgrade: websocket",
|
||||
"Connection: Upgrade",
|
||||
`Sec-WebSocket-Accept: ${acceptKey}`,
|
||||
]);
|
||||
} catch (error) {
|
||||
// Send error response in case of error
|
||||
await writeHttpResponse(output, [ "HTTP/1.1 400 Bad Request" ]);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Accept an incoming WebSocket server connection.
|
||||
* Takes an established nsISocketTransport in the parameters.
|
||||
* Performs the WebSocket handshake and waits for the WebSocket to open.
|
||||
* Returns Promise with a WebSocket ready to send and receive messages.
|
||||
*/
|
||||
const accept = async function(transport, rx, tx) {
|
||||
await serverHandshake(rx, tx);
|
||||
|
||||
const transportProvider = {
|
||||
setListener(upgradeListener) {
|
||||
// onTransportAvailable callback shouldn't be called synchronously
|
||||
executeSoon(() => {
|
||||
upgradeListener.onTransportAvailable(transport, rx, tx);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const so = WebSocket.createServerWebSocket(null, [], transportProvider, "");
|
||||
so.addEventListener("close", () => {
|
||||
rx.close();
|
||||
tx.close();
|
||||
});
|
||||
|
||||
so.onopen = () => resolve(so);
|
||||
so.onerror = err => reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
const executeSoon = function(func) {
|
||||
Services.tm.dispatchToMainThread(func);
|
||||
};
|
||||
|
||||
const WebSocketServer = {accept};
|
||||
81
remote/server/WebSocketTransport.jsm
Normal file
81
remote/server/WebSocketTransport.jsm
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
/* 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/. */
|
||||
|
||||
// This is an XPCOM service-ified copy of ../devtools/shared/transport/websocket-transport.js.
|
||||
|
||||
"use strict";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["WebSocketDebuggerTransport"];
|
||||
|
||||
const {EventEmitter} = ChromeUtils.import("chrome://remote/content/EventEmitter.jsm");
|
||||
|
||||
function WebSocketDebuggerTransport(socket) {
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
this.active = false;
|
||||
this.hooks = null;
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
WebSocketDebuggerTransport.prototype = {
|
||||
ready() {
|
||||
if (this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.addEventListener("message", this);
|
||||
this.socket.addEventListener("close", this);
|
||||
|
||||
this.active = true;
|
||||
},
|
||||
|
||||
send(object) {
|
||||
this.emit("send", object);
|
||||
if (this.socket) {
|
||||
this.socket.send(JSON.stringify(object));
|
||||
}
|
||||
},
|
||||
|
||||
startBulkSend() {
|
||||
throw new Error("Bulk send is not supported by WebSocket transport");
|
||||
},
|
||||
|
||||
close() {
|
||||
this.emit("close");
|
||||
this.active = false;
|
||||
|
||||
this.socket.removeEventListener("message", this);
|
||||
this.socket.removeEventListener("close", this);
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
|
||||
if (this.hooks) {
|
||||
this.hooks.onClosed();
|
||||
this.hooks = null;
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "message":
|
||||
this.onMessage(event);
|
||||
break;
|
||||
case "close":
|
||||
this.close();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onMessage({ data }) {
|
||||
if (typeof data !== "string") {
|
||||
throw new Error("Binary messages are not supported by WebSocket transport");
|
||||
}
|
||||
|
||||
const object = JSON.parse(data);
|
||||
this.emit("packet", object);
|
||||
if (this.hooks) {
|
||||
this.hooks.onPacket(object);
|
||||
}
|
||||
},
|
||||
};
|
||||
38
remote/test/demo.js
Normal file
38
remote/test/demo.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
"use strict";
|
||||
|
||||
const CDP = require("chrome-remote-interface");
|
||||
|
||||
async function demo() {
|
||||
let client;
|
||||
try {
|
||||
client = await CDP();
|
||||
const {Log, Network, Page} = client;
|
||||
|
||||
// receive console.log messages and print them
|
||||
Log.enable();
|
||||
Log.entryAdded(({entry}) => {
|
||||
const {timestamp, level, text, args} = entry;
|
||||
const msg = text || args.join(" ");
|
||||
console.log(`${timestamp}\t${level.toUpperCase()}\t${msg}`);
|
||||
});
|
||||
|
||||
// turn on network stack logging
|
||||
Network.requestWillBeSent((params) => {
|
||||
console.log(params.request.url);
|
||||
});
|
||||
|
||||
// turn on navigation related events, such as DOMContentLoaded et al.
|
||||
await Page.enable();
|
||||
|
||||
await Page.navigate({url: "https://sny.no/e/consoledemo.html"});
|
||||
await Page.loadEventFired();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
demo();
|
||||
5
remote/test/moz.build
Normal file
5
remote/test/moz.build
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# 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/.
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"]
|
||||
9
remote/test/unit/.eslintrc.js
Normal file
9
remote/test/unit/.eslintrc.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/* 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";
|
||||
|
||||
module.exports = {
|
||||
"extends": ["plugin:mozilla/xpcshell-test"],
|
||||
};
|
||||
19
remote/test/unit/test_Session.js
Normal file
19
remote/test/unit/test_Session.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/* 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 {Session} = ChromeUtils.import("chrome://remote/content/Session.jsm");
|
||||
|
||||
const connection = {onmessage: () => {}};
|
||||
|
||||
add_test(function test_Session_destructor() {
|
||||
const session = new Session(connection);
|
||||
session.domains.get("Log");
|
||||
equal(session.domains.size, 1);
|
||||
session.destructor();
|
||||
equal(session.domains.size, 0);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
8
remote/test/unit/xpcshell.ini
Normal file
8
remote/test/unit/xpcshell.ini
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# 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/.
|
||||
|
||||
[DEFAULT]
|
||||
skip-if = appname == "thunderbird"
|
||||
|
||||
[test_Session.js]
|
||||
|
|
@ -903,6 +903,22 @@ add_old_configure_assignment('FT2_LIBS',
|
|||
add_old_configure_assignment('FT2_CFLAGS',
|
||||
ft2_info.cflags)
|
||||
|
||||
|
||||
# CDP remote agent
|
||||
# ==============================================================
|
||||
#
|
||||
# The source code lives under ../remote.
|
||||
|
||||
option('--enable-cdp', help='Enable CDP-based remote agent')
|
||||
|
||||
@depends('--enable-cdp')
|
||||
def remote(value):
|
||||
if value:
|
||||
return True
|
||||
|
||||
set_config('ENABLE_REMOTE_AGENT', remote)
|
||||
|
||||
|
||||
# Marionette remote protocol
|
||||
# ==============================================================
|
||||
#
|
||||
|
|
@ -938,6 +954,7 @@ def marionette(value):
|
|||
|
||||
set_config('ENABLE_MARIONETTE', marionette)
|
||||
|
||||
|
||||
# geckodriver WebDriver implementation
|
||||
# ==============================================================
|
||||
#
|
||||
|
|
@ -967,6 +984,7 @@ def geckodriver(enabled):
|
|||
|
||||
set_config('ENABLE_GECKODRIVER', geckodriver)
|
||||
|
||||
|
||||
# WebRTC
|
||||
# ========================================================
|
||||
@depends(target)
|
||||
|
|
|
|||
|
|
@ -161,6 +161,9 @@ if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']:
|
|||
'/toolkit/system/gnome',
|
||||
]
|
||||
|
||||
if CONFIG['ENABLE_REMOTE_AGENT']:
|
||||
DIRS += ['/remote']
|
||||
|
||||
if CONFIG['ENABLE_MARIONETTE']:
|
||||
DIRS += [
|
||||
'/testing/firefox-ui',
|
||||
|
|
|
|||
Loading…
Reference in a new issue