michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: /* General utilities used throughout devtools. */ michael@0: michael@0: // hasChrome is provided as a global by the loader. It is true if we are running michael@0: // on the main thread, and false if we are running on a worker thread. michael@0: var { Ci, Cu } = require("chrome"); michael@0: var Services = require("Services"); michael@0: var { setTimeout } = require("Timer"); michael@0: michael@0: /** michael@0: * Turn the error |aError| into a string, without fail. michael@0: */ michael@0: exports.safeErrorString = function safeErrorString(aError) { michael@0: try { michael@0: let errorString = aError.toString(); michael@0: if (typeof errorString == "string") { michael@0: // Attempt to attach a stack to |errorString|. If it throws an error, or michael@0: // isn't a string, don't use it. michael@0: try { michael@0: if (aError.stack) { michael@0: let stack = aError.stack.toString(); michael@0: if (typeof stack == "string") { michael@0: errorString += "\nStack: " + stack; michael@0: } michael@0: } michael@0: } catch (ee) { } michael@0: michael@0: // Append additional line and column number information to the output, michael@0: // since it might not be part of the stringified error. michael@0: if (typeof aError.lineNumber == "number" && typeof aError.columnNumber == "number") { michael@0: errorString += "Line: " + aError.lineNumber + ", column: " + aError.columnNumber; michael@0: } michael@0: michael@0: return errorString; michael@0: } michael@0: } catch (ee) { } michael@0: michael@0: return ""; michael@0: } michael@0: michael@0: /** michael@0: * Report that |aWho| threw an exception, |aException|. michael@0: */ michael@0: exports.reportException = function reportException(aWho, aException) { michael@0: let msg = aWho + " threw an exception: " + exports.safeErrorString(aException); michael@0: michael@0: dump(msg + "\n"); michael@0: michael@0: if (Cu.reportError) { michael@0: /* michael@0: * Note that the xpcshell test harness registers an observer for michael@0: * console messages, so when we're running tests, this will cause michael@0: * the test to quit. michael@0: */ michael@0: Cu.reportError(msg); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Given a handler function that may throw, return an infallible handler michael@0: * function that calls the fallible handler, and logs any exceptions it michael@0: * throws. michael@0: * michael@0: * @param aHandler function michael@0: * A handler function, which may throw. michael@0: * @param aName string michael@0: * A name for aHandler, for use in error messages. If omitted, we use michael@0: * aHandler.name. michael@0: * michael@0: * (SpiderMonkey does generate good names for anonymous functions, but we michael@0: * don't have a way to get at them from JavaScript at the moment.) michael@0: */ michael@0: exports.makeInfallible = function makeInfallible(aHandler, aName) { michael@0: if (!aName) michael@0: aName = aHandler.name; michael@0: michael@0: return function (/* arguments */) { michael@0: try { michael@0: return aHandler.apply(this, arguments); michael@0: } catch (ex) { michael@0: let who = "Handler function"; michael@0: if (aName) { michael@0: who += " " + aName; michael@0: } michael@0: exports.reportException(who, ex); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Interleaves two arrays element by element, returning the combined array, like michael@0: * a zip. In the case of arrays with different sizes, undefined values will be michael@0: * interleaved at the end along with the extra values of the larger array. michael@0: * michael@0: * @param Array a michael@0: * @param Array b michael@0: * @returns Array michael@0: * The combined array, in the form [a1, b1, a2, b2, ...] michael@0: */ michael@0: exports.zip = function zip(a, b) { michael@0: if (!b) { michael@0: return a; michael@0: } michael@0: if (!a) { michael@0: return b; michael@0: } michael@0: const pairs = []; michael@0: for (let i = 0, aLength = a.length, bLength = b.length; michael@0: i < aLength || i < bLength; michael@0: i++) { michael@0: pairs.push([a[i], b[i]]); michael@0: } michael@0: return pairs; michael@0: }; michael@0: michael@0: /** michael@0: * Waits for the next tick in the event loop to execute a callback. michael@0: */ michael@0: exports.executeSoon = function executeSoon(aFn) { michael@0: Services.tm.mainThread.dispatch({ michael@0: run: exports.makeInfallible(aFn) michael@0: }, Ci.nsIThread.DISPATCH_NORMAL); michael@0: }; michael@0: michael@0: /** michael@0: * Waits for the next tick in the event loop. michael@0: * michael@0: * @return Promise michael@0: * A promise that is resolved after the next tick in the event loop. michael@0: */ michael@0: exports.waitForTick = function waitForTick() { michael@0: let deferred = promise.defer(); michael@0: exports.executeSoon(deferred.resolve); michael@0: return deferred.promise; michael@0: }; michael@0: michael@0: /** michael@0: * Waits for the specified amount of time to pass. michael@0: * michael@0: * @param number aDelay michael@0: * The amount of time to wait, in milliseconds. michael@0: * @return Promise michael@0: * A promise that is resolved after the specified amount of time passes. michael@0: */ michael@0: exports.waitForTime = function waitForTime(aDelay) { michael@0: let deferred = promise.defer(); michael@0: setTimeout(deferred.resolve, aDelay); michael@0: return deferred.promise; michael@0: }; michael@0: michael@0: /** michael@0: * Like Array.prototype.forEach, but doesn't cause jankiness when iterating over michael@0: * very large arrays by yielding to the browser and continuing execution on the michael@0: * next tick. michael@0: * michael@0: * @param Array aArray michael@0: * The array being iterated over. michael@0: * @param Function aFn michael@0: * The function called on each item in the array. If a promise is michael@0: * returned by this function, iterating over the array will be paused michael@0: * until the respective promise is resolved. michael@0: * @returns Promise michael@0: * A promise that is resolved once the whole array has been iterated michael@0: * over, and all promises returned by the aFn callback are resolved. michael@0: */ michael@0: exports.yieldingEach = function yieldingEach(aArray, aFn) { michael@0: const deferred = promise.defer(); michael@0: michael@0: let i = 0; michael@0: let len = aArray.length; michael@0: let outstanding = [deferred.promise]; michael@0: michael@0: (function loop() { michael@0: const start = Date.now(); michael@0: michael@0: while (i < len) { michael@0: // Don't block the main thread for longer than 16 ms at a time. To michael@0: // maintain 60fps, you have to render every frame in at least 16ms; we michael@0: // aren't including time spent in non-JS here, but this is Good michael@0: // Enough(tm). michael@0: if (Date.now() - start > 16) { michael@0: exports.executeSoon(loop); michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: outstanding.push(aFn(aArray[i], i++)); michael@0: } catch (e) { michael@0: deferred.reject(e); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: deferred.resolve(); michael@0: }()); michael@0: michael@0: return promise.all(outstanding); michael@0: } michael@0: michael@0: /** michael@0: * Like XPCOMUtils.defineLazyGetter, but with a |this| sensitive getter that michael@0: * allows the lazy getter to be defined on a prototype and work correctly with michael@0: * instances. michael@0: * michael@0: * @param Object aObject michael@0: * The prototype object to define the lazy getter on. michael@0: * @param String aKey michael@0: * The key to define the lazy getter on. michael@0: * @param Function aCallback michael@0: * The callback that will be called to determine the value. Will be michael@0: * called with the |this| value of the current instance. michael@0: */ michael@0: exports.defineLazyPrototypeGetter = michael@0: function defineLazyPrototypeGetter(aObject, aKey, aCallback) { michael@0: Object.defineProperty(aObject, aKey, { michael@0: configurable: true, michael@0: get: function() { michael@0: const value = aCallback.call(this); michael@0: michael@0: Object.defineProperty(this, aKey, { michael@0: configurable: true, michael@0: writable: true, michael@0: value: value michael@0: }); michael@0: michael@0: return value; michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Safely get the property value from a Debugger.Object for a given key. Walks michael@0: * the prototype chain until the property is found. michael@0: * michael@0: * @param Debugger.Object aObject michael@0: * The Debugger.Object to get the value from. michael@0: * @param String aKey michael@0: * The key to look for. michael@0: * @return Any michael@0: */ michael@0: exports.getProperty = function getProperty(aObj, aKey) { michael@0: let root = aObj; michael@0: try { michael@0: do { michael@0: const desc = aObj.getOwnPropertyDescriptor(aKey); michael@0: if (desc) { michael@0: if ("value" in desc) { michael@0: return desc.value; michael@0: } michael@0: // Call the getter if it's safe. michael@0: return exports.hasSafeGetter(desc) ? desc.get.call(root).return : undefined; michael@0: } michael@0: aObj = aObj.proto; michael@0: } while (aObj); michael@0: } catch (e) { michael@0: // If anything goes wrong report the error and return undefined. michael@0: exports.reportException("getProperty", e); michael@0: } michael@0: return undefined; michael@0: }; michael@0: michael@0: /** michael@0: * Determines if a descriptor has a getter which doesn't call into JavaScript. michael@0: * michael@0: * @param Object aDesc michael@0: * The descriptor to check for a safe getter. michael@0: * @return Boolean michael@0: * Whether a safe getter was found. michael@0: */ michael@0: exports.hasSafeGetter = function hasSafeGetter(aDesc) { michael@0: let fn = aDesc.get; michael@0: return fn && fn.callable && fn.class == "Function" && fn.script === undefined; michael@0: }; michael@0: michael@0: /** michael@0: * Check if it is safe to read properties and execute methods from the given JS michael@0: * object. Safety is defined as being protected from unintended code execution michael@0: * from content scripts (or cross-compartment code). michael@0: * michael@0: * See bugs 945920 and 946752 for discussion. michael@0: * michael@0: * @type Object aObj michael@0: * The object to check. michael@0: * @return Boolean michael@0: * True if it is safe to read properties from aObj, or false otherwise. michael@0: */ michael@0: exports.isSafeJSObject = function isSafeJSObject(aObj) { michael@0: if (Cu.getGlobalForObject(aObj) == michael@0: Cu.getGlobalForObject(exports.isSafeJSObject)) { michael@0: return true; // aObj is not a cross-compartment wrapper. michael@0: } michael@0: michael@0: let principal = Cu.getObjectPrincipal(aObj); michael@0: if (Services.scriptSecurityManager.isSystemPrincipal(principal)) { michael@0: return true; // allow chrome objects michael@0: } michael@0: michael@0: return Cu.isXrayWrapper(aObj); michael@0: }; michael@0: michael@0: exports.dumpn = function dumpn(str) { michael@0: if (exports.dumpn.wantLogging) { michael@0: dump("DBG-SERVER: " + str + "\n"); michael@0: } michael@0: } michael@0: michael@0: // We want wantLogging to be writable. The exports object is frozen by the michael@0: // loader, so define it on dumpn instead. michael@0: exports.dumpn.wantLogging = false; michael@0: michael@0: exports.dbg_assert = function dbg_assert(cond, e) { michael@0: if (!cond) { michael@0: return e; michael@0: } michael@0: }