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.
|
# Only contains non-standard test files.
|
||||||
python/**
|
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/ exclusions (pref files).
|
||||||
security/manager/ssl/security-prefs.js
|
security/manager/ssl/security-prefs.js
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,14 @@
|
||||||
|
|
||||||
@RESPATH@/components/Push.manifest
|
@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
|
#ifdef ENABLE_MARIONETTE
|
||||||
@RESPATH@/chrome/marionette@JAREXT@
|
@RESPATH@/chrome/marionette@JAREXT@
|
||||||
@RESPATH@/chrome/marionette.manifest
|
@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',
|
add_old_configure_assignment('FT2_CFLAGS',
|
||||||
ft2_info.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
|
# Marionette remote protocol
|
||||||
# ==============================================================
|
# ==============================================================
|
||||||
#
|
#
|
||||||
|
|
@ -938,6 +954,7 @@ def marionette(value):
|
||||||
|
|
||||||
set_config('ENABLE_MARIONETTE', marionette)
|
set_config('ENABLE_MARIONETTE', marionette)
|
||||||
|
|
||||||
|
|
||||||
# geckodriver WebDriver implementation
|
# geckodriver WebDriver implementation
|
||||||
# ==============================================================
|
# ==============================================================
|
||||||
#
|
#
|
||||||
|
|
@ -967,6 +984,7 @@ def geckodriver(enabled):
|
||||||
|
|
||||||
set_config('ENABLE_GECKODRIVER', geckodriver)
|
set_config('ENABLE_GECKODRIVER', geckodriver)
|
||||||
|
|
||||||
|
|
||||||
# WebRTC
|
# WebRTC
|
||||||
# ========================================================
|
# ========================================================
|
||||||
@depends(target)
|
@depends(target)
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,9 @@ if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']:
|
||||||
'/toolkit/system/gnome',
|
'/toolkit/system/gnome',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if CONFIG['ENABLE_REMOTE_AGENT']:
|
||||||
|
DIRS += ['/remote']
|
||||||
|
|
||||||
if CONFIG['ENABLE_MARIONETTE']:
|
if CONFIG['ENABLE_MARIONETTE']:
|
||||||
DIRS += [
|
DIRS += [
|
||||||
'/testing/firefox-ui',
|
'/testing/firefox-ui',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue