|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 /* General utilities used throughout devtools. */ |
|
8 |
|
9 // hasChrome is provided as a global by the loader. It is true if we are running |
|
10 // on the main thread, and false if we are running on a worker thread. |
|
11 var { Ci, Cu } = require("chrome"); |
|
12 var Services = require("Services"); |
|
13 var { setTimeout } = require("Timer"); |
|
14 |
|
15 /** |
|
16 * Turn the error |aError| into a string, without fail. |
|
17 */ |
|
18 exports.safeErrorString = function safeErrorString(aError) { |
|
19 try { |
|
20 let errorString = aError.toString(); |
|
21 if (typeof errorString == "string") { |
|
22 // Attempt to attach a stack to |errorString|. If it throws an error, or |
|
23 // isn't a string, don't use it. |
|
24 try { |
|
25 if (aError.stack) { |
|
26 let stack = aError.stack.toString(); |
|
27 if (typeof stack == "string") { |
|
28 errorString += "\nStack: " + stack; |
|
29 } |
|
30 } |
|
31 } catch (ee) { } |
|
32 |
|
33 // Append additional line and column number information to the output, |
|
34 // since it might not be part of the stringified error. |
|
35 if (typeof aError.lineNumber == "number" && typeof aError.columnNumber == "number") { |
|
36 errorString += "Line: " + aError.lineNumber + ", column: " + aError.columnNumber; |
|
37 } |
|
38 |
|
39 return errorString; |
|
40 } |
|
41 } catch (ee) { } |
|
42 |
|
43 return "<failed trying to find error description>"; |
|
44 } |
|
45 |
|
46 /** |
|
47 * Report that |aWho| threw an exception, |aException|. |
|
48 */ |
|
49 exports.reportException = function reportException(aWho, aException) { |
|
50 let msg = aWho + " threw an exception: " + exports.safeErrorString(aException); |
|
51 |
|
52 dump(msg + "\n"); |
|
53 |
|
54 if (Cu.reportError) { |
|
55 /* |
|
56 * Note that the xpcshell test harness registers an observer for |
|
57 * console messages, so when we're running tests, this will cause |
|
58 * the test to quit. |
|
59 */ |
|
60 Cu.reportError(msg); |
|
61 } |
|
62 } |
|
63 |
|
64 /** |
|
65 * Given a handler function that may throw, return an infallible handler |
|
66 * function that calls the fallible handler, and logs any exceptions it |
|
67 * throws. |
|
68 * |
|
69 * @param aHandler function |
|
70 * A handler function, which may throw. |
|
71 * @param aName string |
|
72 * A name for aHandler, for use in error messages. If omitted, we use |
|
73 * aHandler.name. |
|
74 * |
|
75 * (SpiderMonkey does generate good names for anonymous functions, but we |
|
76 * don't have a way to get at them from JavaScript at the moment.) |
|
77 */ |
|
78 exports.makeInfallible = function makeInfallible(aHandler, aName) { |
|
79 if (!aName) |
|
80 aName = aHandler.name; |
|
81 |
|
82 return function (/* arguments */) { |
|
83 try { |
|
84 return aHandler.apply(this, arguments); |
|
85 } catch (ex) { |
|
86 let who = "Handler function"; |
|
87 if (aName) { |
|
88 who += " " + aName; |
|
89 } |
|
90 exports.reportException(who, ex); |
|
91 } |
|
92 } |
|
93 } |
|
94 |
|
95 /** |
|
96 * Interleaves two arrays element by element, returning the combined array, like |
|
97 * a zip. In the case of arrays with different sizes, undefined values will be |
|
98 * interleaved at the end along with the extra values of the larger array. |
|
99 * |
|
100 * @param Array a |
|
101 * @param Array b |
|
102 * @returns Array |
|
103 * The combined array, in the form [a1, b1, a2, b2, ...] |
|
104 */ |
|
105 exports.zip = function zip(a, b) { |
|
106 if (!b) { |
|
107 return a; |
|
108 } |
|
109 if (!a) { |
|
110 return b; |
|
111 } |
|
112 const pairs = []; |
|
113 for (let i = 0, aLength = a.length, bLength = b.length; |
|
114 i < aLength || i < bLength; |
|
115 i++) { |
|
116 pairs.push([a[i], b[i]]); |
|
117 } |
|
118 return pairs; |
|
119 }; |
|
120 |
|
121 /** |
|
122 * Waits for the next tick in the event loop to execute a callback. |
|
123 */ |
|
124 exports.executeSoon = function executeSoon(aFn) { |
|
125 Services.tm.mainThread.dispatch({ |
|
126 run: exports.makeInfallible(aFn) |
|
127 }, Ci.nsIThread.DISPATCH_NORMAL); |
|
128 }; |
|
129 |
|
130 /** |
|
131 * Waits for the next tick in the event loop. |
|
132 * |
|
133 * @return Promise |
|
134 * A promise that is resolved after the next tick in the event loop. |
|
135 */ |
|
136 exports.waitForTick = function waitForTick() { |
|
137 let deferred = promise.defer(); |
|
138 exports.executeSoon(deferred.resolve); |
|
139 return deferred.promise; |
|
140 }; |
|
141 |
|
142 /** |
|
143 * Waits for the specified amount of time to pass. |
|
144 * |
|
145 * @param number aDelay |
|
146 * The amount of time to wait, in milliseconds. |
|
147 * @return Promise |
|
148 * A promise that is resolved after the specified amount of time passes. |
|
149 */ |
|
150 exports.waitForTime = function waitForTime(aDelay) { |
|
151 let deferred = promise.defer(); |
|
152 setTimeout(deferred.resolve, aDelay); |
|
153 return deferred.promise; |
|
154 }; |
|
155 |
|
156 /** |
|
157 * Like Array.prototype.forEach, but doesn't cause jankiness when iterating over |
|
158 * very large arrays by yielding to the browser and continuing execution on the |
|
159 * next tick. |
|
160 * |
|
161 * @param Array aArray |
|
162 * The array being iterated over. |
|
163 * @param Function aFn |
|
164 * The function called on each item in the array. If a promise is |
|
165 * returned by this function, iterating over the array will be paused |
|
166 * until the respective promise is resolved. |
|
167 * @returns Promise |
|
168 * A promise that is resolved once the whole array has been iterated |
|
169 * over, and all promises returned by the aFn callback are resolved. |
|
170 */ |
|
171 exports.yieldingEach = function yieldingEach(aArray, aFn) { |
|
172 const deferred = promise.defer(); |
|
173 |
|
174 let i = 0; |
|
175 let len = aArray.length; |
|
176 let outstanding = [deferred.promise]; |
|
177 |
|
178 (function loop() { |
|
179 const start = Date.now(); |
|
180 |
|
181 while (i < len) { |
|
182 // Don't block the main thread for longer than 16 ms at a time. To |
|
183 // maintain 60fps, you have to render every frame in at least 16ms; we |
|
184 // aren't including time spent in non-JS here, but this is Good |
|
185 // Enough(tm). |
|
186 if (Date.now() - start > 16) { |
|
187 exports.executeSoon(loop); |
|
188 return; |
|
189 } |
|
190 |
|
191 try { |
|
192 outstanding.push(aFn(aArray[i], i++)); |
|
193 } catch (e) { |
|
194 deferred.reject(e); |
|
195 return; |
|
196 } |
|
197 } |
|
198 |
|
199 deferred.resolve(); |
|
200 }()); |
|
201 |
|
202 return promise.all(outstanding); |
|
203 } |
|
204 |
|
205 /** |
|
206 * Like XPCOMUtils.defineLazyGetter, but with a |this| sensitive getter that |
|
207 * allows the lazy getter to be defined on a prototype and work correctly with |
|
208 * instances. |
|
209 * |
|
210 * @param Object aObject |
|
211 * The prototype object to define the lazy getter on. |
|
212 * @param String aKey |
|
213 * The key to define the lazy getter on. |
|
214 * @param Function aCallback |
|
215 * The callback that will be called to determine the value. Will be |
|
216 * called with the |this| value of the current instance. |
|
217 */ |
|
218 exports.defineLazyPrototypeGetter = |
|
219 function defineLazyPrototypeGetter(aObject, aKey, aCallback) { |
|
220 Object.defineProperty(aObject, aKey, { |
|
221 configurable: true, |
|
222 get: function() { |
|
223 const value = aCallback.call(this); |
|
224 |
|
225 Object.defineProperty(this, aKey, { |
|
226 configurable: true, |
|
227 writable: true, |
|
228 value: value |
|
229 }); |
|
230 |
|
231 return value; |
|
232 } |
|
233 }); |
|
234 } |
|
235 |
|
236 /** |
|
237 * Safely get the property value from a Debugger.Object for a given key. Walks |
|
238 * the prototype chain until the property is found. |
|
239 * |
|
240 * @param Debugger.Object aObject |
|
241 * The Debugger.Object to get the value from. |
|
242 * @param String aKey |
|
243 * The key to look for. |
|
244 * @return Any |
|
245 */ |
|
246 exports.getProperty = function getProperty(aObj, aKey) { |
|
247 let root = aObj; |
|
248 try { |
|
249 do { |
|
250 const desc = aObj.getOwnPropertyDescriptor(aKey); |
|
251 if (desc) { |
|
252 if ("value" in desc) { |
|
253 return desc.value; |
|
254 } |
|
255 // Call the getter if it's safe. |
|
256 return exports.hasSafeGetter(desc) ? desc.get.call(root).return : undefined; |
|
257 } |
|
258 aObj = aObj.proto; |
|
259 } while (aObj); |
|
260 } catch (e) { |
|
261 // If anything goes wrong report the error and return undefined. |
|
262 exports.reportException("getProperty", e); |
|
263 } |
|
264 return undefined; |
|
265 }; |
|
266 |
|
267 /** |
|
268 * Determines if a descriptor has a getter which doesn't call into JavaScript. |
|
269 * |
|
270 * @param Object aDesc |
|
271 * The descriptor to check for a safe getter. |
|
272 * @return Boolean |
|
273 * Whether a safe getter was found. |
|
274 */ |
|
275 exports.hasSafeGetter = function hasSafeGetter(aDesc) { |
|
276 let fn = aDesc.get; |
|
277 return fn && fn.callable && fn.class == "Function" && fn.script === undefined; |
|
278 }; |
|
279 |
|
280 /** |
|
281 * Check if it is safe to read properties and execute methods from the given JS |
|
282 * object. Safety is defined as being protected from unintended code execution |
|
283 * from content scripts (or cross-compartment code). |
|
284 * |
|
285 * See bugs 945920 and 946752 for discussion. |
|
286 * |
|
287 * @type Object aObj |
|
288 * The object to check. |
|
289 * @return Boolean |
|
290 * True if it is safe to read properties from aObj, or false otherwise. |
|
291 */ |
|
292 exports.isSafeJSObject = function isSafeJSObject(aObj) { |
|
293 if (Cu.getGlobalForObject(aObj) == |
|
294 Cu.getGlobalForObject(exports.isSafeJSObject)) { |
|
295 return true; // aObj is not a cross-compartment wrapper. |
|
296 } |
|
297 |
|
298 let principal = Cu.getObjectPrincipal(aObj); |
|
299 if (Services.scriptSecurityManager.isSystemPrincipal(principal)) { |
|
300 return true; // allow chrome objects |
|
301 } |
|
302 |
|
303 return Cu.isXrayWrapper(aObj); |
|
304 }; |
|
305 |
|
306 exports.dumpn = function dumpn(str) { |
|
307 if (exports.dumpn.wantLogging) { |
|
308 dump("DBG-SERVER: " + str + "\n"); |
|
309 } |
|
310 } |
|
311 |
|
312 // We want wantLogging to be writable. The exports object is frozen by the |
|
313 // loader, so define it on dumpn instead. |
|
314 exports.dumpn.wantLogging = false; |
|
315 |
|
316 exports.dbg_assert = function dbg_assert(cond, e) { |
|
317 if (!cond) { |
|
318 return e; |
|
319 } |
|
320 } |