--- a/.eslintignore
+++ b/.eslintignore
@@ -137,16 +137,17 @@ devtools/shared/apps/**
devtools/shared/client/**
devtools/shared/discovery/**
devtools/shared/gcli/**
!devtools/shared/gcli/templater.js
devtools/shared/heapsnapshot/**
devtools/shared/layout/**
devtools/shared/locales/**
devtools/shared/performance/**
+!devtools/shared/platform/**
devtools/shared/qrcode/**
devtools/shared/security/**
devtools/shared/shims/**
devtools/shared/tests/**
!devtools/shared/tests/unit/test_csslexer.js
devtools/shared/touch/**
devtools/shared/transport/**
!devtools/shared/transport/transport.js
--- a/devtools/.eslintrc
+++ b/devtools/.eslintrc
@@ -34,16 +34,20 @@
// Rules from the mozilla plugin
"mozilla/mark-test-function-used": 1,
"mozilla/no-aArgs": 1,
"mozilla/no-cpows-in-tests": 2,
"mozilla/no-single-arg-cu-import": 2,
// See bug 1224289.
"mozilla/reject-importGlobalProperties": 2,
+ // devtools/shared/platform is special; see the README.md in that
+ // directory for details. We reject requires using explicit
+ // subdirectories of this directory.
+ "mozilla/reject-some-requires": [2, "^devtools/shared/platform/(chome|content)/"],
"mozilla/var-only-at-top-level": 1,
// Rules from the React plugin
"react/display-name": 2,
"react/no-danger": 2,
"react/no-did-mount-set-state": 2,
"react/no-did-update-set-state": 2,
"react/no-direct-mutation-state": 2,
--- a/devtools/client/inspector/.eslintrc
+++ b/devtools/client/inspector/.eslintrc
@@ -2,11 +2,11 @@
// Extend from the devtools eslintrc.
"extends": "../../.eslintrc",
"rules": {
// The inspector is being migrated to HTML and cleaned of
// chrome-privileged code, so this rule disallows requiring chrome
// code. Some files in the inspector disable this rule still. The
// goal is to enable the rule globally on all files.
- "mozilla/reject-some-requires": [2, "^(chrome|chrome:.*|resource:.*|devtools/server/.*|.*\\.jsm)$"],
+ "mozilla/reject-some-requires": [2, "^(chrome|chrome:.*|resource:.*|devtools/server/.*|.*\\.jsm|devtools/shared/platform/(chome|content)/.*)$"],
},
}
--- a/devtools/shared/DevToolsUtils.js
+++ b/devtools/shared/DevToolsUtils.js
@@ -6,16 +6,17 @@
/* General utilities used throughout devtools. */
var { Ci, Cu, Cc, components } = require("chrome");
var Services = require("Services");
var promise = require("promise");
var defer = require("devtools/shared/defer");
var flags = require("./flags");
+var {getStack, callFunctionWithAsyncStack} = require("devtools/shared/platform/stack");
loader.lazyRequireGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm", true);
// Re-export the thread-safe utils.
const ThreadSafeDevToolsUtils = require("./ThreadSafeDevToolsUtils.js");
for (let key of Object.keys(ThreadSafeDevToolsUtils)) {
exports[key] = ThreadSafeDevToolsUtils[key];
@@ -27,19 +28,19 @@ for (let key of Object.keys(ThreadSafeDe
exports.executeSoon = function executeSoon(aFn) {
if (isWorker) {
setImmediate(aFn);
} else {
let executor;
// Only enable async stack reporting when DEBUG_JS_MODULES is set
// (customized local builds) to avoid a performance penalty.
if (AppConstants.DEBUG_JS_MODULES || flags.testing) {
- let stack = components.stack;
+ let stack = getStack();
executor = () => {
- Cu.callFunctionWithAsyncStack(aFn, stack, "DevToolsUtils.executeSoon");
+ callFunctionWithAsyncStack(aFn, stack, "DevToolsUtils.executeSoon");
};
} else {
executor = aFn;
}
Services.tm.mainThread.dispatch({
run: exports.makeInfallible(executor)
}, Ci.nsIThread.DISPATCH_NORMAL);
}
--- a/devtools/shared/Loader.jsm
+++ b/devtools/shared/Loader.jsm
@@ -28,16 +28,23 @@ var sharedGlobalBlocklist = ["sdk/indexe
*/
function BuiltinProvider() {}
BuiltinProvider.prototype = {
load: function () {
const paths = {
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
"": "resource://gre/modules/commonjs/",
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ // Modules here are intended to have one implementation for
+ // chrome, and a separate implementation for content. Here we
+ // map the directory to the chrome subdirectory, but the content
+ // loader will map to the content subdirectory. See the
+ // README.md in devtools/shared/platform.
+ "devtools/shared/platform": "resource://devtools/shared/platform/chrome",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
"devtools": "resource://devtools",
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
"gcli": "resource://devtools/shared/gcli/source/lib/gcli",
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
"acorn": "resource://devtools/acorn",
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
"acorn/util/walk": "resource://devtools/acorn/walk.js",
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
--- a/devtools/shared/client/main.js
+++ b/devtools/shared/client/main.js
@@ -1,19 +1,20 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-const { Ci, Cu, components } = require("chrome");
+const { Ci, Cu } = require("chrome");
const Services = require("Services");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { getStack, callFunctionWithAsyncStack } = require("devtools/shared/platform/stack");
const promise = Cu.import("resource://devtools/shared/deprecated-sync-thenables.js", {}).Promise;
loader.lazyRequireGetter(this, "events", "sdk/event/core");
loader.lazyRequireGetter(this, "WebConsoleClient", "devtools/shared/webconsole/client", true);
loader.lazyRequireGetter(this, "DebuggerSocket", "devtools/shared/security/socket", true);
loader.lazyRequireGetter(this, "Authentication", "devtools/shared/security/auth");
@@ -698,17 +699,17 @@ DebuggerClient.prototype = {
if (aOnResponse) {
aOnResponse(resp);
}
return promise.reject(resp);
}
let request = new Request(aRequest);
request.format = "json";
- request.stack = components.stack;
+ request.stack = getStack();
if (aOnResponse) {
request.on("json-reply", aOnResponse);
}
this._sendOrQueueRequest(request);
// Implement a Promise like API on the returned object
// that resolves/rejects on request response
@@ -1004,18 +1005,18 @@ DebuggerClient.prototype = {
// that lack a packet type.
if (aPacket.type) {
this.emit(aPacket.type, aPacket);
}
if (activeRequest) {
let emitReply = () => activeRequest.emit("json-reply", aPacket);
if (activeRequest.stack) {
- Cu.callFunctionWithAsyncStack(emitReply, activeRequest.stack,
- "DevTools RDP");
+ callFunctionWithAsyncStack(emitReply, activeRequest.stack,
+ "DevTools RDP");
} else {
emitReply();
}
}
},
/**
* Called by the DebuggerTransport to dispatch incoming bulk packets as
--- a/devtools/shared/event-emitter.js
+++ b/devtools/shared/event-emitter.js
@@ -40,38 +40,38 @@
// but it doesn't depends on any real module. We can save a few cycles
// and bytes by not loading Loader.jsm.
let require = function (module) {
switch (module) {
case "devtools/shared/defer":
return Cu.import("resource://gre/modules/Promise.jsm", {}).Promise.defer;
case "Services":
return Cu.import("resource://gre/modules/Services.jsm", {}).Services;
- case "chrome":
- return {
- Cu,
- components: Components
- };
+ case "devtools/shared/platform/stack": {
+ let obj = {};
+ Cu.import("resource://devtools/shared/platform/chrome/stack.js", obj);
+ return obj;
+ }
}
return null;
};
factory.call(this, require, this, { exports: this }, console);
this.EXPORTED_SYMBOLS = ["EventEmitter"];
}
}).call(this, function (require, exports, module, console) {
// ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠
// After this point the code may not use Cu.import, and should only
// require() modules that are "clean-for-content".
let EventEmitter = this.EventEmitter = function () {};
module.exports = EventEmitter;
// See comment in JSM module boilerplate when adding a new dependency.
- const { components } = require("chrome");
const Services = require("Services");
const defer = require("devtools/shared/defer");
+ const { describeNthCaller } = require("devtools/shared/platform/stack");
let loggingEnabled = true;
if (!isWorker) {
loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
Services.prefs.addObserver("devtools.dump.emit", {
observe: () => {
loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
}
@@ -199,26 +199,17 @@
}
},
logEvent(event, args) {
if (!loggingEnabled) {
return;
}
- let caller, func, path;
- if (!isWorker) {
- caller = components.stack.caller.caller;
- func = caller.name;
- let file = caller.filename;
- if (file.includes(" -> ")) {
- file = caller.filename.split(/ -> /)[1];
- }
- path = file + ":" + caller.lineNumber;
- }
+ let description = describeNthCaller(2);
let argOut = "(";
if (args.length === 1) {
argOut += event;
}
let out = "EMITTING: ";
@@ -246,14 +237,14 @@
}
}
} catch (e) {
// Object is dead so the toolbox is most likely shutting down,
// do nothing.
}
argOut += ")";
- out += "emit" + argOut + " from " + func + "() -> " + path + "\n";
+ out += "emit" + argOut + " from " + description + "\n";
dump(out);
},
};
});
--- a/devtools/shared/moz.build
+++ b/devtools/shared/moz.build
@@ -14,16 +14,17 @@ DIRS += [
'fronts',
'gcli',
'heapsnapshot',
'inspector',
'jsbeautify',
'layout',
'locales',
'performance',
+ 'platform',
'pretty-fast',
'qrcode',
'security',
'sourcemap',
'shims',
'specs',
'touch',
'transport',
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/README.md
@@ -0,0 +1,13 @@
+This directory is treated specially by the loaders.
+
+In particular, when running in chrome, a resource like
+"devtools/shared/platform/mumble" will be found in the chrome
+subdirectory; and when running in content, it will be found in the
+content subdirectory.
+
+Outside of tests, it's not ok to require a specific version of a file;
+and there is an eslint test to check for that. That is,
+require("devtools/shared/platform/client/mumble") is an error.
+
+When adding a new file, you must add two copies, one to chrome and one
+to content. Otherwise, one case or the other will fail to work.
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/chrome/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'stack.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/chrome/stack.js
@@ -0,0 +1,75 @@
+/* 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/. */
+
+// A few wrappers for stack-manipulation. This version of the module
+// is used in chrome code.
+
+"use strict";
+
+(function (factory) {
+ // This file might be require()d, but might also be loaded via
+ // Cu.import. Account for the differences here.
+ if (this.module && module.id.indexOf("stack") >= 0) {
+ // require.
+ const {components, Cu} = require("chrome");
+ factory.call(this, components, Cu, exports);
+ } else {
+ // Cu.import.
+ this.isWorker = false;
+ factory.call(this, Components, Components.utils, this);
+ this.EXPORTED_SYMBOLS = ["callFunctionWithAsyncStack", "describeNthCaller",
+ "getStack"];
+ }
+}).call(this, function (components, Cu, exports) {
+ /**
+ * Return a description of the Nth caller, suitable for logging.
+ *
+ * @param {Number} n the caller to describe
+ * @return {String} a description of the nth caller.
+ */
+ function describeNthCaller(n) {
+ if (isWorker) {
+ return "";
+ }
+
+ let caller = components.stack;
+ // Do one extra iteration to skip this function.
+ while (n >= 0) {
+ --n;
+ caller = caller.caller;
+ }
+
+ let func = caller.name;
+ let file = caller.filename;
+ if (file.includes(" -> ")) {
+ file = caller.filename.split(/ -> /)[1];
+ }
+ let path = file + ":" + caller.lineNumber;
+
+ return func + "() -> " + path;
+ }
+
+ /**
+ * Return a stack object that can be serialized and, when
+ * deserialized, passed to callFunctionWithAsyncStack.
+ */
+ function getStack() {
+ return components.stack.caller;
+ }
+
+ /**
+ * Like Cu.callFunctionWithAsyncStack but handles the isWorker case
+ * -- |Cu| isn't defined in workers.
+ */
+ function callFunctionWithAsyncStack(callee, stack, id) {
+ if (isWorker) {
+ return callee();
+ }
+ return Cu.callFunctionWithAsyncStack(callee, stack, id);
+ }
+
+ exports.callFunctionWithAsyncStack = callFunctionWithAsyncStack;
+ exports.describeNthCaller = describeNthCaller;
+ exports.getStack = getStack;
+});
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/content/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'stack.js',
+)
+
+XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/content/stack.js
@@ -0,0 +1,49 @@
+/* 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/. */
+
+// A few wrappers for stack-manipulation. This version of the module
+// is used in content code. Note that this particular copy of the
+// file can only be loaded via require(), because Cu.import doesn't
+// exist in the content case. So, we don't need the code to handle
+// both require and import here.
+
+"use strict";
+
+/**
+ * Looks like Cu.callFunctionWithAsyncStack, but just calls the callee.
+ */
+function callFunctionWithAsyncStack(callee, stack, id) {
+ return callee();
+}
+
+/**
+ * Return a description of the Nth caller, suitable for logging.
+ *
+ * @param {Number} n the caller to describe
+ * @return {String} a description of the nth caller.
+ */
+function describeNthCaller(n) {
+ if (isWorker) {
+ return "";
+ }
+
+ let stack = new Error().stack.split("\n");
+ // Add one here to skip this function.
+ return stack[n + 1];
+}
+
+/**
+ * Return a stack object that can be serialized and, when
+ * deserialized, passed to callFunctionWithAsyncStack.
+ */
+function getStack() {
+ // There's no reason for this to do anything fancy, since it's only
+ // used to pass back into callFunctionWithAsyncStack, which we can't
+ // implement.
+ return null;
+}
+
+exports.callFunctionWithAsyncStack = callFunctionWithAsyncStack;
+exports.describeNthCaller = describeNthCaller;
+exports.getStack = getStack;
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/content/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+ // Extend from the common devtools xpcshell eslintrc config.
+ "extends": "../../../../.eslintrc.xpcshell"
+}
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/content/test/test_stack.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There isn't really very much about the content stack.js that we can
+// test, but we'll do what we can.
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+// Make sure to explicitly require the content version of this module.
+// We have to use the ".." trick due to the way the loader remaps
+// devtools/shared/platform.
+const {
+ callFunctionWithAsyncStack,
+ getStack,
+ describeNthCaller
+} = require("devtools/shared/platform/../content/stack");
+
+function f3() {
+ return describeNthCaller(2);
+}
+
+function f2() {
+ return f3();
+}
+
+function f1() {
+ return f2();
+}
+
+function run_test() {
+ let value = 7;
+
+ const changeValue = () => {
+ value = 9;
+ };
+
+ callFunctionWithAsyncStack(changeValue, getStack(), "test_stack");
+ equal(value, 9, "callFunctionWithAsyncStack worked");
+
+ let stack = getStack();
+ equal(JSON.parse(JSON.stringify(stack)), stack, "stack is serializable");
+
+ let desc = f1();
+ ok(desc.includes("f1"), "stack description includes f1");
+}
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/content/test/xpcshell.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = devtools
+head =
+tail =
+firefox-appdir = browser
+
+[test_stack.js]
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ 'chrome',
+ 'content',
+]
--- a/devtools/shared/protocol.js
+++ b/devtools/shared/protocol.js
@@ -1,22 +1,21 @@
/* 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 { Cu, components } = require("chrome");
-var Services = require("Services");
var promise = require("promise");
var defer = require("devtools/shared/defer");
var {Class} = require("sdk/core/heritage");
var {EventTarget} = require("sdk/event/target");
var events = require("sdk/event/core");
var object = require("sdk/util/object");
+var {getStack, callFunctionWithAsyncStack} = require("devtools/shared/platform/stack");
exports.emit = events.emit;
/**
* Types: named marshallers/demarshallers.
*
* Types provide a 'write' function that takes a js representation and
* returns a protocol representation, and a "read" function that
@@ -1201,17 +1200,17 @@ var Front = Class({
request: function (packet) {
let deferred = defer();
// Save packet basics for debugging
let { to, type } = packet;
this._requests.push({
deferred,
to: to || this.actorID,
type,
- stack: components.stack,
+ stack: getStack(),
});
this.send(packet);
return deferred.promise;
},
/**
* Handler for incoming packets from the client's actor.
*/
@@ -1247,17 +1246,17 @@ var Front = Class({
if (this._requests.length === 0) {
let msg = "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
let err = Error(msg);
console.error(err);
throw err;
}
let { deferred, stack } = this._requests.shift();
- Cu.callFunctionWithAsyncStack(() => {
+ callFunctionWithAsyncStack(() => {
if (packet.error) {
// "Protocol error" is here to avoid TBPL heuristics. See also
// https://mxr.mozilla.org/webtools-central/source/tbpl/php/inc/GeneralErrorFilter.php
let message;
if (packet.error && packet.message) {
message = "Protocol error (" + packet.error + "): " + packet.message;
} else {
message = packet.error;
--- a/devtools/shared/worker/loader.js
+++ b/devtools/shared/worker/loader.js
@@ -493,16 +493,23 @@ this.worker = new WorkerDebuggerLoader({
"Services": Object.create(null),
"chrome": chrome,
"xpcInspector": xpcInspector
},
paths: {
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
"": "resource://gre/modules/commonjs/",
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ // Modules here are intended to have one implementation for
+ // chrome, and a separate implementation for content. Here we
+ // map the directory to the chrome subdirectory, but the content
+ // loader will map to the content subdirectory. See the
+ // README.md in devtools/shared/platform.
+ "devtools/shared/platform": "resource://devtools/shared/platform/chrome",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
"devtools": "resource://devtools",
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
"promise": "resource://gre/modules/Promise-backend.js",
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
"source-map": "resource://devtools/shared/sourcemap/source-map.js",
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
"xpcshell-test": "resource://test"
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠