|
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 const { Cu } = require("chrome"); |
|
8 const { DebuggerServer } = require("devtools/server/main"); |
|
9 const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {}); |
|
10 |
|
11 Cu.import("resource://gre/modules/jsdebugger.jsm"); |
|
12 addDebuggerToGlobal(this); |
|
13 |
|
14 // TODO bug 943125: remove this polyfill and use Debugger.Frame.prototype.depth |
|
15 // once it is implemented. |
|
16 if (!Object.getOwnPropertyDescriptor(Debugger.Frame.prototype, "depth")) { |
|
17 Debugger.Frame.prototype._depth = null; |
|
18 Object.defineProperty(Debugger.Frame.prototype, "depth", { |
|
19 get: function () { |
|
20 if (this._depth === null) { |
|
21 if (!this.older) { |
|
22 this._depth = 0; |
|
23 } else { |
|
24 // Hide depth from self-hosted frames. |
|
25 const increment = this.script && this.script.url == "self-hosted" |
|
26 ? 0 |
|
27 : 1; |
|
28 this._depth = increment + this.older.depth; |
|
29 } |
|
30 } |
|
31 |
|
32 return this._depth; |
|
33 } |
|
34 }); |
|
35 } |
|
36 |
|
37 const { setTimeout } = require("sdk/timers"); |
|
38 |
|
39 /** |
|
40 * The number of milliseconds we should buffer frame enter/exit packets before |
|
41 * sending. |
|
42 */ |
|
43 const BUFFER_SEND_DELAY = 50; |
|
44 |
|
45 /** |
|
46 * The maximum number of arguments we will send for any single function call. |
|
47 */ |
|
48 const MAX_ARGUMENTS = 3; |
|
49 |
|
50 /** |
|
51 * The maximum number of an object's properties we will serialize. |
|
52 */ |
|
53 const MAX_PROPERTIES = 3; |
|
54 |
|
55 /** |
|
56 * The complete set of trace types supported. |
|
57 */ |
|
58 const TRACE_TYPES = new Set([ |
|
59 "time", |
|
60 "return", |
|
61 "throw", |
|
62 "yield", |
|
63 "name", |
|
64 "location", |
|
65 "callsite", |
|
66 "parameterNames", |
|
67 "arguments", |
|
68 "depth" |
|
69 ]); |
|
70 |
|
71 /** |
|
72 * Creates a TraceActor. TraceActor provides a stream of function |
|
73 * call/return packets to a remote client gathering a full trace. |
|
74 */ |
|
75 function TraceActor(aConn, aParentActor) |
|
76 { |
|
77 this._attached = false; |
|
78 this._activeTraces = new MapStack(); |
|
79 this._totalTraces = 0; |
|
80 this._startTime = 0; |
|
81 |
|
82 // Keep track of how many different trace requests have requested what kind of |
|
83 // tracing info. This way we can minimize the amount of data we are collecting |
|
84 // at any given time. |
|
85 this._requestsForTraceType = Object.create(null); |
|
86 for (let type of TRACE_TYPES) { |
|
87 this._requestsForTraceType[type] = 0; |
|
88 } |
|
89 |
|
90 this._sequence = 0; |
|
91 this._bufferSendTimer = null; |
|
92 this._buffer = []; |
|
93 this.onExitFrame = this.onExitFrame.bind(this); |
|
94 |
|
95 // aParentActor.window might be an Xray for a window, but it might also be a |
|
96 // double-wrapper for a Sandbox. We want to unwrap the latter but not the |
|
97 // former. |
|
98 this.global = aParentActor.window; |
|
99 if (!Cu.isXrayWrapper(this.global)) { |
|
100 this.global = this.global.wrappedJSObject; |
|
101 } |
|
102 } |
|
103 |
|
104 TraceActor.prototype = { |
|
105 actorPrefix: "trace", |
|
106 |
|
107 get attached() { return this._attached; }, |
|
108 get idle() { return this._attached && this._activeTraces.size === 0; }, |
|
109 get tracing() { return this._attached && this._activeTraces.size > 0; }, |
|
110 |
|
111 /** |
|
112 * Buffer traces and only send them every BUFFER_SEND_DELAY milliseconds. |
|
113 */ |
|
114 _send: function(aPacket) { |
|
115 this._buffer.push(aPacket); |
|
116 if (this._bufferSendTimer === null) { |
|
117 this._bufferSendTimer = setTimeout(() => { |
|
118 this.conn.send({ |
|
119 from: this.actorID, |
|
120 type: "traces", |
|
121 traces: this._buffer.splice(0, this._buffer.length) |
|
122 }); |
|
123 this._bufferSendTimer = null; |
|
124 }, BUFFER_SEND_DELAY); |
|
125 } |
|
126 }, |
|
127 |
|
128 /** |
|
129 * Initializes a Debugger instance and adds listeners to it. |
|
130 */ |
|
131 _initDebugger: function() { |
|
132 this.dbg = new Debugger(); |
|
133 this.dbg.onEnterFrame = this.onEnterFrame.bind(this); |
|
134 this.dbg.onNewGlobalObject = this.globalManager.onNewGlobal.bind(this); |
|
135 this.dbg.enabled = false; |
|
136 }, |
|
137 |
|
138 /** |
|
139 * Add a debuggee global to the Debugger object. |
|
140 */ |
|
141 _addDebuggee: function(aGlobal) { |
|
142 try { |
|
143 this.dbg.addDebuggee(aGlobal); |
|
144 } catch (e) { |
|
145 // Ignore attempts to add the debugger's compartment as a debuggee. |
|
146 DevToolsUtils.reportException("TraceActor", |
|
147 new Error("Ignoring request to add the debugger's " |
|
148 + "compartment as a debuggee")); |
|
149 } |
|
150 }, |
|
151 |
|
152 /** |
|
153 * Add the provided window and all windows in its frame tree as debuggees. |
|
154 */ |
|
155 _addDebuggees: function(aWindow) { |
|
156 this._addDebuggee(aWindow); |
|
157 let frames = aWindow.frames; |
|
158 if (frames) { |
|
159 for (let i = 0; i < frames.length; i++) { |
|
160 this._addDebuggees(frames[i]); |
|
161 } |
|
162 } |
|
163 }, |
|
164 |
|
165 /** |
|
166 * An object used by TraceActors to tailor their behavior depending |
|
167 * on the debugging context required (chrome or content). |
|
168 */ |
|
169 globalManager: { |
|
170 /** |
|
171 * Adds all globals in the global object as debuggees. |
|
172 */ |
|
173 findGlobals: function() { |
|
174 this._addDebuggees(this.global); |
|
175 }, |
|
176 |
|
177 /** |
|
178 * A function that the engine calls when a new global object has been |
|
179 * created. Adds the global object as a debuggee if it is in the content |
|
180 * window. |
|
181 * |
|
182 * @param aGlobal Debugger.Object |
|
183 * The new global object that was created. |
|
184 */ |
|
185 onNewGlobal: function(aGlobal) { |
|
186 // Content debugging only cares about new globals in the content |
|
187 // window, like iframe children. |
|
188 if (aGlobal.hostAnnotations && |
|
189 aGlobal.hostAnnotations.type == "document" && |
|
190 aGlobal.hostAnnotations.element === this.global) { |
|
191 this._addDebuggee(aGlobal); |
|
192 } |
|
193 }, |
|
194 }, |
|
195 |
|
196 /** |
|
197 * Handle a protocol request to attach to the trace actor. |
|
198 * |
|
199 * @param aRequest object |
|
200 * The protocol request object. |
|
201 */ |
|
202 onAttach: function(aRequest) { |
|
203 if (this.attached) { |
|
204 return { |
|
205 error: "wrongState", |
|
206 message: "Already attached to a client" |
|
207 }; |
|
208 } |
|
209 |
|
210 if (!this.dbg) { |
|
211 this._initDebugger(); |
|
212 this.globalManager.findGlobals.call(this); |
|
213 } |
|
214 |
|
215 this._attached = true; |
|
216 |
|
217 return { |
|
218 type: "attached", |
|
219 traceTypes: Object.keys(this._requestsForTraceType) |
|
220 .filter(k => !!this._requestsForTraceType[k]) |
|
221 }; |
|
222 }, |
|
223 |
|
224 /** |
|
225 * Handle a protocol request to detach from the trace actor. |
|
226 * |
|
227 * @param aRequest object |
|
228 * The protocol request object. |
|
229 */ |
|
230 onDetach: function() { |
|
231 while (this.tracing) { |
|
232 this.onStopTrace(); |
|
233 } |
|
234 |
|
235 this.dbg = null; |
|
236 |
|
237 this._attached = false; |
|
238 return { type: "detached" }; |
|
239 }, |
|
240 |
|
241 /** |
|
242 * Handle a protocol request to start a new trace. |
|
243 * |
|
244 * @param aRequest object |
|
245 * The protocol request object. |
|
246 */ |
|
247 onStartTrace: function(aRequest) { |
|
248 for (let traceType of aRequest.trace) { |
|
249 if (!TRACE_TYPES.has(traceType)) { |
|
250 return { |
|
251 error: "badParameterType", |
|
252 message: "No such trace type: " + traceType |
|
253 }; |
|
254 } |
|
255 } |
|
256 |
|
257 if (this.idle) { |
|
258 this.dbg.enabled = true; |
|
259 this._sequence = 0; |
|
260 this._startTime = Date.now(); |
|
261 } |
|
262 |
|
263 // Start recording all requested trace types. |
|
264 for (let traceType of aRequest.trace) { |
|
265 this._requestsForTraceType[traceType]++; |
|
266 } |
|
267 |
|
268 this._totalTraces++; |
|
269 let name = aRequest.name || "Trace " + this._totalTraces; |
|
270 this._activeTraces.push(name, aRequest.trace); |
|
271 |
|
272 return { type: "startedTrace", why: "requested", name: name }; |
|
273 }, |
|
274 |
|
275 /** |
|
276 * Handle a protocol request to end a trace. |
|
277 * |
|
278 * @param aRequest object |
|
279 * The protocol request object. |
|
280 */ |
|
281 onStopTrace: function(aRequest) { |
|
282 if (!this.tracing) { |
|
283 return { |
|
284 error: "wrongState", |
|
285 message: "No active traces" |
|
286 }; |
|
287 } |
|
288 |
|
289 let stoppedTraceTypes, name; |
|
290 if (aRequest && aRequest.name) { |
|
291 name = aRequest.name; |
|
292 if (!this._activeTraces.has(name)) { |
|
293 return { |
|
294 error: "noSuchTrace", |
|
295 message: "No active trace with name: " + name |
|
296 }; |
|
297 } |
|
298 stoppedTraceTypes = this._activeTraces.delete(name); |
|
299 } else { |
|
300 name = this._activeTraces.peekKey(); |
|
301 stoppedTraceTypes = this._activeTraces.pop(); |
|
302 } |
|
303 |
|
304 for (let traceType of stoppedTraceTypes) { |
|
305 this._requestsForTraceType[traceType]--; |
|
306 } |
|
307 |
|
308 if (this.idle) { |
|
309 this.dbg.enabled = false; |
|
310 } |
|
311 |
|
312 return { type: "stoppedTrace", why: "requested", name: name }; |
|
313 }, |
|
314 |
|
315 // JS Debugger API hooks. |
|
316 |
|
317 /** |
|
318 * Called by the engine when a frame is entered. Sends an unsolicited packet |
|
319 * to the client carrying requested trace information. |
|
320 * |
|
321 * @param aFrame Debugger.frame |
|
322 * The stack frame that was entered. |
|
323 */ |
|
324 onEnterFrame: function(aFrame) { |
|
325 if (aFrame.script && aFrame.script.url == "self-hosted") { |
|
326 return; |
|
327 } |
|
328 |
|
329 let packet = { |
|
330 type: "enteredFrame", |
|
331 sequence: this._sequence++ |
|
332 }; |
|
333 |
|
334 if (this._requestsForTraceType.name) { |
|
335 packet.name = aFrame.callee |
|
336 ? aFrame.callee.displayName || "(anonymous function)" |
|
337 : "(" + aFrame.type + ")"; |
|
338 } |
|
339 |
|
340 if (this._requestsForTraceType.location && aFrame.script) { |
|
341 // We should return the location of the start of the script, but |
|
342 // Debugger.Script does not provide complete start locations (bug |
|
343 // 901138). Instead, return the current offset (the location of the first |
|
344 // statement in the function). |
|
345 packet.location = { |
|
346 url: aFrame.script.url, |
|
347 line: aFrame.script.getOffsetLine(aFrame.offset), |
|
348 column: getOffsetColumn(aFrame.offset, aFrame.script) |
|
349 }; |
|
350 } |
|
351 |
|
352 if (this._requestsForTraceType.callsite |
|
353 && aFrame.older |
|
354 && aFrame.older.script) { |
|
355 let older = aFrame.older; |
|
356 packet.callsite = { |
|
357 url: older.script.url, |
|
358 line: older.script.getOffsetLine(older.offset), |
|
359 column: getOffsetColumn(older.offset, older.script) |
|
360 }; |
|
361 } |
|
362 |
|
363 if (this._requestsForTraceType.time) { |
|
364 packet.time = Date.now() - this._startTime; |
|
365 } |
|
366 |
|
367 if (this._requestsForTraceType.parameterNames && aFrame.callee) { |
|
368 packet.parameterNames = aFrame.callee.parameterNames; |
|
369 } |
|
370 |
|
371 if (this._requestsForTraceType.arguments && aFrame.arguments) { |
|
372 packet.arguments = []; |
|
373 let i = 0; |
|
374 for (let arg of aFrame.arguments) { |
|
375 if (i++ > MAX_ARGUMENTS) { |
|
376 break; |
|
377 } |
|
378 packet.arguments.push(createValueSnapshot(arg, true)); |
|
379 } |
|
380 } |
|
381 |
|
382 if (this._requestsForTraceType.depth) { |
|
383 packet.depth = aFrame.depth; |
|
384 } |
|
385 |
|
386 const onExitFrame = this.onExitFrame; |
|
387 aFrame.onPop = function (aCompletion) { |
|
388 onExitFrame(this, aCompletion); |
|
389 }; |
|
390 |
|
391 this._send(packet); |
|
392 }, |
|
393 |
|
394 /** |
|
395 * Called by the engine when a frame is exited. Sends an unsolicited packet to |
|
396 * the client carrying requested trace information. |
|
397 * |
|
398 * @param Debugger.Frame aFrame |
|
399 * The Debugger.Frame that was just exited. |
|
400 * @param aCompletion object |
|
401 * The debugger completion value for the frame. |
|
402 */ |
|
403 onExitFrame: function(aFrame, aCompletion) { |
|
404 let packet = { |
|
405 type: "exitedFrame", |
|
406 sequence: this._sequence++, |
|
407 }; |
|
408 |
|
409 if (!aCompletion) { |
|
410 packet.why = "terminated"; |
|
411 } else if (aCompletion.hasOwnProperty("return")) { |
|
412 packet.why = "return"; |
|
413 } else if (aCompletion.hasOwnProperty("yield")) { |
|
414 packet.why = "yield"; |
|
415 } else { |
|
416 packet.why = "throw"; |
|
417 } |
|
418 |
|
419 if (this._requestsForTraceType.time) { |
|
420 packet.time = Date.now() - this._startTime; |
|
421 } |
|
422 |
|
423 if (this._requestsForTraceType.depth) { |
|
424 packet.depth = aFrame.depth; |
|
425 } |
|
426 |
|
427 if (aCompletion) { |
|
428 if (this._requestsForTraceType.return && "return" in aCompletion) { |
|
429 packet.return = createValueSnapshot(aCompletion.return, true); |
|
430 } |
|
431 |
|
432 else if (this._requestsForTraceType.throw && "throw" in aCompletion) { |
|
433 packet.throw = createValueSnapshot(aCompletion.throw, true); |
|
434 } |
|
435 |
|
436 else if (this._requestsForTraceType.yield && "yield" in aCompletion) { |
|
437 packet.yield = createValueSnapshot(aCompletion.yield, true); |
|
438 } |
|
439 } |
|
440 |
|
441 this._send(packet); |
|
442 } |
|
443 }; |
|
444 |
|
445 /** |
|
446 * The request types this actor can handle. |
|
447 */ |
|
448 TraceActor.prototype.requestTypes = { |
|
449 "attach": TraceActor.prototype.onAttach, |
|
450 "detach": TraceActor.prototype.onDetach, |
|
451 "startTrace": TraceActor.prototype.onStartTrace, |
|
452 "stopTrace": TraceActor.prototype.onStopTrace |
|
453 }; |
|
454 |
|
455 exports.register = function(handle) { |
|
456 handle.addTabActor(TraceActor, "traceActor"); |
|
457 }; |
|
458 |
|
459 exports.unregister = function(handle) { |
|
460 handle.removeTabActor(TraceActor, "traceActor"); |
|
461 }; |
|
462 |
|
463 |
|
464 /** |
|
465 * MapStack is a collection of key/value pairs with stack ordering, |
|
466 * where keys are strings and values are any JS value. In addition to |
|
467 * the push and pop stack operations, supports a "delete" operation, |
|
468 * which removes the value associated with a given key from any |
|
469 * location in the stack. |
|
470 */ |
|
471 function MapStack() |
|
472 { |
|
473 // Essentially a MapStack is just sugar-coating around a standard JS |
|
474 // object, plus the _stack array to track ordering. |
|
475 this._stack = []; |
|
476 this._map = Object.create(null); |
|
477 } |
|
478 |
|
479 MapStack.prototype = { |
|
480 get size() { return this._stack.length; }, |
|
481 |
|
482 /** |
|
483 * Return the key for the value on the top of the stack, or |
|
484 * undefined if the stack is empty. |
|
485 */ |
|
486 peekKey: function() { |
|
487 return this._stack[this.size - 1]; |
|
488 }, |
|
489 |
|
490 /** |
|
491 * Return true iff a value has been associated with the given key. |
|
492 * |
|
493 * @param aKey string |
|
494 * The key whose presence is to be tested. |
|
495 */ |
|
496 has: function(aKey) { |
|
497 return Object.prototype.hasOwnProperty.call(this._map, aKey); |
|
498 }, |
|
499 |
|
500 /** |
|
501 * Return the value associated with the given key, or undefined if |
|
502 * no value is associated with the key. |
|
503 * |
|
504 * @param aKey string |
|
505 * The key whose associated value is to be returned. |
|
506 */ |
|
507 get: function(aKey) { |
|
508 return this._map[aKey]; |
|
509 }, |
|
510 |
|
511 /** |
|
512 * Push a new value onto the stack. If another value with the same |
|
513 * key is already on the stack, it will be removed before the new |
|
514 * value is pushed onto the top of the stack. |
|
515 * |
|
516 * @param aKey string |
|
517 * The key of the object to push onto the stack. |
|
518 * |
|
519 * @param aValue |
|
520 * The value to push onto the stack. |
|
521 */ |
|
522 push: function(aKey, aValue) { |
|
523 this.delete(aKey); |
|
524 this._stack.push(aKey); |
|
525 this._map[aKey] = aValue; |
|
526 }, |
|
527 |
|
528 /** |
|
529 * Remove the value from the top of the stack and return it. |
|
530 * Returns undefined if the stack is empty. |
|
531 */ |
|
532 pop: function() { |
|
533 let key = this.peekKey(); |
|
534 let value = this.get(key); |
|
535 this._stack.pop(); |
|
536 delete this._map[key]; |
|
537 return value; |
|
538 }, |
|
539 |
|
540 /** |
|
541 * Remove the value associated with the given key from the stack and |
|
542 * return it. Returns undefined if no value is associated with the |
|
543 * given key. |
|
544 * |
|
545 * @param aKey string |
|
546 * The key for the value to remove from the stack. |
|
547 */ |
|
548 delete: function(aKey) { |
|
549 let value = this.get(aKey); |
|
550 if (this.has(aKey)) { |
|
551 let keyIndex = this._stack.lastIndexOf(aKey); |
|
552 this._stack.splice(keyIndex, 1); |
|
553 delete this._map[aKey]; |
|
554 } |
|
555 return value; |
|
556 } |
|
557 }; |
|
558 |
|
559 // TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when |
|
560 // it is implemented. |
|
561 function getOffsetColumn(aOffset, aScript) { |
|
562 return 0; |
|
563 } |
|
564 |
|
565 // Serialization helper functions. Largely copied from script.js and modified |
|
566 // for use in serialization rather than object actor requests. |
|
567 |
|
568 /** |
|
569 * Create a grip for the given debuggee value. |
|
570 * |
|
571 * @param aValue Debugger.Object|primitive |
|
572 * The value to describe with the created grip. |
|
573 * |
|
574 * @param aDetailed boolean |
|
575 * If true, capture slightly more detailed information, like some |
|
576 * properties on an object. |
|
577 * |
|
578 * @return Object |
|
579 * A primitive value or a snapshot of an object. |
|
580 */ |
|
581 function createValueSnapshot(aValue, aDetailed=false) { |
|
582 switch (typeof aValue) { |
|
583 case "boolean": |
|
584 return aValue; |
|
585 case "string": |
|
586 if (aValue.length >= DebuggerServer.LONG_STRING_LENGTH) { |
|
587 return { |
|
588 type: "longString", |
|
589 initial: aValue.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH), |
|
590 length: aValue.length |
|
591 }; |
|
592 } |
|
593 return aValue; |
|
594 case "number": |
|
595 if (aValue === Infinity) { |
|
596 return { type: "Infinity" }; |
|
597 } else if (aValue === -Infinity) { |
|
598 return { type: "-Infinity" }; |
|
599 } else if (Number.isNaN(aValue)) { |
|
600 return { type: "NaN" }; |
|
601 } else if (!aValue && 1 / aValue === -Infinity) { |
|
602 return { type: "-0" }; |
|
603 } |
|
604 return aValue; |
|
605 case "undefined": |
|
606 return { type: "undefined" }; |
|
607 case "object": |
|
608 if (aValue === null) { |
|
609 return { type: "null" }; |
|
610 } |
|
611 return aDetailed |
|
612 ? detailedObjectSnapshot(aValue) |
|
613 : objectSnapshot(aValue); |
|
614 default: |
|
615 DevToolsUtils.reportException("TraceActor", |
|
616 new Error("Failed to provide a grip for: " + aValue)); |
|
617 return null; |
|
618 } |
|
619 } |
|
620 |
|
621 /** |
|
622 * Create a very minimal snapshot of the given debuggee object. |
|
623 * |
|
624 * @param aObject Debugger.Object |
|
625 * The object to describe with the created grip. |
|
626 */ |
|
627 function objectSnapshot(aObject) { |
|
628 return { |
|
629 "type": "object", |
|
630 "class": aObject.class, |
|
631 }; |
|
632 } |
|
633 |
|
634 /** |
|
635 * Create a (slightly more) detailed snapshot of the given debuggee object. |
|
636 * |
|
637 * @param aObject Debugger.Object |
|
638 * The object to describe with the created descriptor. |
|
639 */ |
|
640 function detailedObjectSnapshot(aObject) { |
|
641 let desc = objectSnapshot(aObject); |
|
642 let ownProperties = desc.ownProperties = Object.create(null); |
|
643 |
|
644 if (aObject.class == "DeadObject") { |
|
645 return desc; |
|
646 } |
|
647 |
|
648 let i = 0; |
|
649 for (let name of aObject.getOwnPropertyNames()) { |
|
650 if (i++ > MAX_PROPERTIES) { |
|
651 break; |
|
652 } |
|
653 let desc = propertySnapshot(name, aObject); |
|
654 if (desc) { |
|
655 ownProperties[name] = desc; |
|
656 } |
|
657 } |
|
658 |
|
659 return desc; |
|
660 } |
|
661 |
|
662 /** |
|
663 * A helper method that creates a snapshot of the object's |aName| property. |
|
664 * |
|
665 * @param aName string |
|
666 * The property of which the snapshot is taken. |
|
667 * |
|
668 * @param aObject Debugger.Object |
|
669 * The object whose property the snapshot is taken of. |
|
670 * |
|
671 * @return Object |
|
672 * The snapshot of the property. |
|
673 */ |
|
674 function propertySnapshot(aName, aObject) { |
|
675 let desc; |
|
676 try { |
|
677 desc = aObject.getOwnPropertyDescriptor(aName); |
|
678 } catch (e) { |
|
679 // Calling getOwnPropertyDescriptor on wrapped native prototypes is not |
|
680 // allowed (bug 560072). Inform the user with a bogus, but hopefully |
|
681 // explanatory, descriptor. |
|
682 return { |
|
683 configurable: false, |
|
684 writable: false, |
|
685 enumerable: false, |
|
686 value: e.name |
|
687 }; |
|
688 } |
|
689 |
|
690 // Only create descriptors for simple values. We skip objects and properties |
|
691 // that have getters and setters; ain't nobody got time for that! |
|
692 if (!desc |
|
693 || typeof desc.value == "object" && desc.value !== null |
|
694 || !("value" in desc)) { |
|
695 return undefined; |
|
696 } |
|
697 |
|
698 return { |
|
699 configurable: desc.configurable, |
|
700 enumerable: desc.enumerable, |
|
701 writable: desc.writable, |
|
702 value: createValueSnapshot(desc.value) |
|
703 }; |
|
704 } |