802acd35b0c8bf999000b31766590ad61922e9e1
[aai/esr-gui.git] /
1 "use strict";
2 module.exports = function() {
3 var async = require("./async.js");
4 var util = require("./util.js");
5 var bluebirdFramePattern =
6     /[\\\/]bluebird[\\\/]js[\\\/](main|debug|zalgo|instrumented)/;
7 var stackFramePattern = null;
8 var formatStack = null;
9 var indentStackFrames = false;
10 var warn;
11
12 function CapturedTrace(parent) {
13     this._parent = parent;
14     var length = this._length = 1 + (parent === undefined ? 0 : parent._length);
15     captureStackTrace(this, CapturedTrace);
16     if (length > 32) this.uncycle();
17 }
18 util.inherits(CapturedTrace, Error);
19
20 CapturedTrace.prototype.uncycle = function() {
21     var length = this._length;
22     if (length < 2) return;
23     var nodes = [];
24     var stackToIndex = {};
25
26     for (var i = 0, node = this; node !== undefined; ++i) {
27         nodes.push(node);
28         node = node._parent;
29     }
30     length = this._length = i;
31     for (var i = length - 1; i >= 0; --i) {
32         var stack = nodes[i].stack;
33         if (stackToIndex[stack] === undefined) {
34             stackToIndex[stack] = i;
35         }
36     }
37     for (var i = 0; i < length; ++i) {
38         var currentStack = nodes[i].stack;
39         var index = stackToIndex[currentStack];
40         if (index !== undefined && index !== i) {
41             if (index > 0) {
42                 nodes[index - 1]._parent = undefined;
43                 nodes[index - 1]._length = 1;
44             }
45             nodes[i]._parent = undefined;
46             nodes[i]._length = 1;
47             var cycleEdgeNode = i > 0 ? nodes[i - 1] : this;
48
49             if (index < length - 1) {
50                 cycleEdgeNode._parent = nodes[index + 1];
51                 cycleEdgeNode._parent.uncycle();
52                 cycleEdgeNode._length =
53                     cycleEdgeNode._parent._length + 1;
54             } else {
55                 cycleEdgeNode._parent = undefined;
56                 cycleEdgeNode._length = 1;
57             }
58             var currentChildLength = cycleEdgeNode._length + 1;
59             for (var j = i - 2; j >= 0; --j) {
60                 nodes[j]._length = currentChildLength;
61                 currentChildLength++;
62             }
63             return;
64         }
65     }
66 };
67
68 CapturedTrace.prototype.parent = function() {
69     return this._parent;
70 };
71
72 CapturedTrace.prototype.hasParent = function() {
73     return this._parent !== undefined;
74 };
75
76 CapturedTrace.prototype.attachExtraTrace = function(error) {
77     if (error.__stackCleaned__) return;
78     this.uncycle();
79     var parsed = CapturedTrace.parseStackAndMessage(error);
80     var message = parsed.message;
81     var stacks = [parsed.stack];
82
83     var trace = this;
84     while (trace !== undefined) {
85         stacks.push(cleanStack(trace.stack.split("\n")));
86         trace = trace._parent;
87     }
88     removeCommonRoots(stacks);
89     removeDuplicateOrEmptyJumps(stacks);
90     util.notEnumerableProp(error, "stack", reconstructStack(message, stacks));
91     util.notEnumerableProp(error, "__stackCleaned__", true);
92 };
93
94 function reconstructStack(message, stacks) {
95     for (var i = 0; i < stacks.length - 1; ++i) {
96         stacks[i].push("From previous event:");
97         stacks[i] = stacks[i].join("\n");
98     }
99     if (i < stacks.length) {
100         stacks[i] = stacks[i].join("\n");
101     }
102     return message + "\n" + stacks.join("\n");
103 }
104
105 function removeDuplicateOrEmptyJumps(stacks) {
106     for (var i = 0; i < stacks.length; ++i) {
107         if (stacks[i].length === 0 ||
108             ((i + 1 < stacks.length) && stacks[i][0] === stacks[i+1][0])) {
109             stacks.splice(i, 1);
110             i--;
111         }
112     }
113 }
114
115 function removeCommonRoots(stacks) {
116     var current = stacks[0];
117     for (var i = 1; i < stacks.length; ++i) {
118         var prev = stacks[i];
119         var currentLastIndex = current.length - 1;
120         var currentLastLine = current[currentLastIndex];
121         var commonRootMeetPoint = -1;
122
123         for (var j = prev.length - 1; j >= 0; --j) {
124             if (prev[j] === currentLastLine) {
125                 commonRootMeetPoint = j;
126                 break;
127             }
128         }
129
130         for (var j = commonRootMeetPoint; j >= 0; --j) {
131             var line = prev[j];
132             if (current[currentLastIndex] === line) {
133                 current.pop();
134                 currentLastIndex--;
135             } else {
136                 break;
137             }
138         }
139         current = prev;
140     }
141 }
142
143 function cleanStack(stack) {
144     var ret = [];
145     for (var i = 0; i < stack.length; ++i) {
146         var line = stack[i];
147         var isTraceLine = stackFramePattern.test(line) ||
148             "    (No stack trace)" === line;
149         var isInternalFrame = isTraceLine && shouldIgnore(line);
150         if (isTraceLine && !isInternalFrame) {
151             if (indentStackFrames && line.charAt(0) !== " ") {
152                 line = "    " + line;
153             }
154             ret.push(line);
155         }
156     }
157     return ret;
158 }
159
160 function stackFramesAsArray(error) {
161     var stack = error.stack.replace(/\s+$/g, "").split("\n");
162     for (var i = 0; i < stack.length; ++i) {
163         var line = stack[i];
164         if ("    (No stack trace)" === line || stackFramePattern.test(line)) {
165             break;
166         }
167     }
168     if (i > 0) {
169         stack = stack.slice(i);
170     }
171     return stack;
172 }
173
174 CapturedTrace.parseStackAndMessage = function(error) {
175     var stack = error.stack;
176     var message = error.toString();
177     stack = typeof stack === "string" && stack.length > 0
178                 ? stackFramesAsArray(error) : ["    (No stack trace)"];
179     return {
180         message: message,
181         stack: cleanStack(stack)
182     };
183 };
184
185 CapturedTrace.formatAndLogError = function(error, title) {
186     if (typeof console !== "undefined") {
187         var message;
188         if (typeof error === "object" || typeof error === "function") {
189             var stack = error.stack;
190             message = title + formatStack(stack, error);
191         } else {
192             message = title + String(error);
193         }
194         if (typeof warn === "function") {
195             warn(message);
196         } else if (typeof console.log === "function" ||
197             typeof console.log === "object") {
198             console.log(message);
199         }
200     }
201 };
202
203 CapturedTrace.unhandledRejection = function (reason) {
204     CapturedTrace.formatAndLogError(reason, "^--- With additional stack trace: ");
205 };
206
207 CapturedTrace.isSupported = function () {
208     return typeof captureStackTrace === "function";
209 };
210
211 CapturedTrace.fireRejectionEvent =
212 function(name, localHandler, reason, promise) {
213     var localEventFired = false;
214     try {
215         if (typeof localHandler === "function") {
216             localEventFired = true;
217             if (name === "rejectionHandled") {
218                 localHandler(promise);
219             } else {
220                 localHandler(reason, promise);
221             }
222         }
223     } catch (e) {
224         async.throwLater(e);
225     }
226
227     var globalEventFired = false;
228     try {
229         globalEventFired = fireGlobalEvent(name, reason, promise);
230     } catch (e) {
231         globalEventFired = true;
232         async.throwLater(e);
233     }
234
235     var domEventFired = false;
236     if (fireDomEvent) {
237         try {
238             domEventFired = fireDomEvent(name.toLowerCase(), {
239                 reason: reason,
240                 promise: promise
241             });
242         } catch (e) {
243             domEventFired = true;
244             async.throwLater(e);
245         }
246     }
247
248     if (!globalEventFired && !localEventFired && !domEventFired &&
249         name === "unhandledRejection") {
250         CapturedTrace.formatAndLogError(reason, "Unhandled rejection ");
251     }
252 };
253
254 function formatNonError(obj) {
255     var str;
256     if (typeof obj === "function") {
257         str = "[function " +
258             (obj.name || "anonymous") +
259             "]";
260     } else {
261         str = obj.toString();
262         var ruselessToString = /\[object [a-zA-Z0-9$_]+\]/;
263         if (ruselessToString.test(str)) {
264             try {
265                 var newStr = JSON.stringify(obj);
266                 str = newStr;
267             }
268             catch(e) {
269
270             }
271         }
272         if (str.length === 0) {
273             str = "(empty array)";
274         }
275     }
276     return ("(<" + snip(str) + ">, no stack trace)");
277 }
278
279 function snip(str) {
280     var maxChars = 41;
281     if (str.length < maxChars) {
282         return str;
283     }
284     return str.substr(0, maxChars - 3) + "...";
285 }
286
287 var shouldIgnore = function() { return false; };
288 var parseLineInfoRegex = /[\/<\(]([^:\/]+):(\d+):(?:\d+)\)?\s*$/;
289 function parseLineInfo(line) {
290     var matches = line.match(parseLineInfoRegex);
291     if (matches) {
292         return {
293             fileName: matches[1],
294             line: parseInt(matches[2], 10)
295         };
296     }
297 }
298 CapturedTrace.setBounds = function(firstLineError, lastLineError) {
299     if (!CapturedTrace.isSupported()) return;
300     var firstStackLines = firstLineError.stack.split("\n");
301     var lastStackLines = lastLineError.stack.split("\n");
302     var firstIndex = -1;
303     var lastIndex = -1;
304     var firstFileName;
305     var lastFileName;
306     for (var i = 0; i < firstStackLines.length; ++i) {
307         var result = parseLineInfo(firstStackLines[i]);
308         if (result) {
309             firstFileName = result.fileName;
310             firstIndex = result.line;
311             break;
312         }
313     }
314     for (var i = 0; i < lastStackLines.length; ++i) {
315         var result = parseLineInfo(lastStackLines[i]);
316         if (result) {
317             lastFileName = result.fileName;
318             lastIndex = result.line;
319             break;
320         }
321     }
322     if (firstIndex < 0 || lastIndex < 0 || !firstFileName || !lastFileName ||
323         firstFileName !== lastFileName || firstIndex >= lastIndex) {
324         return;
325     }
326
327     shouldIgnore = function(line) {
328         if (bluebirdFramePattern.test(line)) return true;
329         var info = parseLineInfo(line);
330         if (info) {
331             if (info.fileName === firstFileName &&
332                 (firstIndex <= info.line && info.line <= lastIndex)) {
333                 return true;
334             }
335         }
336         return false;
337     };
338 };
339
340 var captureStackTrace = (function stackDetection() {
341     var v8stackFramePattern = /^\s*at\s*/;
342     var v8stackFormatter = function(stack, error) {
343         if (typeof stack === "string") return stack;
344
345         if (error.name !== undefined &&
346             error.message !== undefined) {
347             return error.toString();
348         }
349         return formatNonError(error);
350     };
351
352     if (typeof Error.stackTraceLimit === "number" &&
353         typeof Error.captureStackTrace === "function") {
354         Error.stackTraceLimit = Error.stackTraceLimit + 6;
355         stackFramePattern = v8stackFramePattern;
356         formatStack = v8stackFormatter;
357         var captureStackTrace = Error.captureStackTrace;
358
359         shouldIgnore = function(line) {
360             return bluebirdFramePattern.test(line);
361         };
362         return function(receiver, ignoreUntil) {
363             Error.stackTraceLimit = Error.stackTraceLimit + 6;
364             captureStackTrace(receiver, ignoreUntil);
365             Error.stackTraceLimit = Error.stackTraceLimit - 6;
366         };
367     }
368     var err = new Error();
369
370     if (typeof err.stack === "string" &&
371         err.stack.split("\n")[0].indexOf("stackDetection@") >= 0) {
372         stackFramePattern = /@/;
373         formatStack = v8stackFormatter;
374         indentStackFrames = true;
375         return function captureStackTrace(o) {
376             o.stack = new Error().stack;
377         };
378     }
379
380     var hasStackAfterThrow;
381     try { throw new Error(); }
382     catch(e) {
383         hasStackAfterThrow = ("stack" in e);
384     }
385     if (!("stack" in err) && hasStackAfterThrow &&
386         typeof Error.stackTraceLimit === "number") {
387         stackFramePattern = v8stackFramePattern;
388         formatStack = v8stackFormatter;
389         return function captureStackTrace(o) {
390             Error.stackTraceLimit = Error.stackTraceLimit + 6;
391             try { throw new Error(); }
392             catch(e) { o.stack = e.stack; }
393             Error.stackTraceLimit = Error.stackTraceLimit - 6;
394         };
395     }
396
397     formatStack = function(stack, error) {
398         if (typeof stack === "string") return stack;
399
400         if ((typeof error === "object" ||
401             typeof error === "function") &&
402             error.name !== undefined &&
403             error.message !== undefined) {
404             return error.toString();
405         }
406         return formatNonError(error);
407     };
408
409     return null;
410
411 })([]);
412
413 var fireDomEvent;
414 var fireGlobalEvent = (function() {
415     if (util.isNode) {
416         return function(name, reason, promise) {
417             if (name === "rejectionHandled") {
418                 return process.emit(name, promise);
419             } else {
420                 return process.emit(name, reason, promise);
421             }
422         };
423     } else {
424         var customEventWorks = false;
425         var anyEventWorks = true;
426         try {
427             var ev = new self.CustomEvent("test");
428             customEventWorks = ev instanceof CustomEvent;
429         } catch (e) {}
430         if (!customEventWorks) {
431             try {
432                 var event = document.createEvent("CustomEvent");
433                 event.initCustomEvent("testingtheevent", false, true, {});
434                 self.dispatchEvent(event);
435             } catch (e) {
436                 anyEventWorks = false;
437             }
438         }
439         if (anyEventWorks) {
440             fireDomEvent = function(type, detail) {
441                 var event;
442                 if (customEventWorks) {
443                     event = new self.CustomEvent(type, {
444                         detail: detail,
445                         bubbles: false,
446                         cancelable: true
447                     });
448                 } else if (self.dispatchEvent) {
449                     event = document.createEvent("CustomEvent");
450                     event.initCustomEvent(type, false, true, detail);
451                 }
452
453                 return event ? !self.dispatchEvent(event) : false;
454             };
455         }
456
457         var toWindowMethodNameMap = {};
458         toWindowMethodNameMap["unhandledRejection"] = ("on" +
459             "unhandledRejection").toLowerCase();
460         toWindowMethodNameMap["rejectionHandled"] = ("on" +
461             "rejectionHandled").toLowerCase();
462
463         return function(name, reason, promise) {
464             var methodName = toWindowMethodNameMap[name];
465             var method = self[methodName];
466             if (!method) return false;
467             if (name === "rejectionHandled") {
468                 method.call(self, promise);
469             } else {
470                 method.call(self, reason, promise);
471             }
472             return true;
473         };
474     }
475 })();
476
477 if (typeof console !== "undefined" && typeof console.warn !== "undefined") {
478     warn = function (message) {
479         console.warn(message);
480     };
481     if (util.isNode && process.stderr.isTTY) {
482         warn = function(message) {
483             process.stderr.write("\u001b[31m" + message + "\u001b[39m\n");
484         };
485     } else if (!util.isNode && typeof (new Error().stack) === "string") {
486         warn = function(message) {
487             console.warn("%c" + message, "color: red");
488         };
489     }
490 }
491
492 return CapturedTrace;
493 };