Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
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/. */
5 "use strict";
7 const { Cu } = require("chrome");
8 const { DebuggerServer } = require("devtools/server/main");
9 const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
11 Cu.import("resource://gre/modules/jsdebugger.jsm");
12 addDebuggerToGlobal(this);
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 }
32 return this._depth;
33 }
34 });
35 }
37 const { setTimeout } = require("sdk/timers");
39 /**
40 * The number of milliseconds we should buffer frame enter/exit packets before
41 * sending.
42 */
43 const BUFFER_SEND_DELAY = 50;
45 /**
46 * The maximum number of arguments we will send for any single function call.
47 */
48 const MAX_ARGUMENTS = 3;
50 /**
51 * The maximum number of an object's properties we will serialize.
52 */
53 const MAX_PROPERTIES = 3;
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 ]);
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;
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 }
90 this._sequence = 0;
91 this._bufferSendTimer = null;
92 this._buffer = [];
93 this.onExitFrame = this.onExitFrame.bind(this);
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 }
104 TraceActor.prototype = {
105 actorPrefix: "trace",
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; },
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 },
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 },
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 },
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 },
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 },
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 },
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 }
210 if (!this.dbg) {
211 this._initDebugger();
212 this.globalManager.findGlobals.call(this);
213 }
215 this._attached = true;
217 return {
218 type: "attached",
219 traceTypes: Object.keys(this._requestsForTraceType)
220 .filter(k => !!this._requestsForTraceType[k])
221 };
222 },
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 }
235 this.dbg = null;
237 this._attached = false;
238 return { type: "detached" };
239 },
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 }
257 if (this.idle) {
258 this.dbg.enabled = true;
259 this._sequence = 0;
260 this._startTime = Date.now();
261 }
263 // Start recording all requested trace types.
264 for (let traceType of aRequest.trace) {
265 this._requestsForTraceType[traceType]++;
266 }
268 this._totalTraces++;
269 let name = aRequest.name || "Trace " + this._totalTraces;
270 this._activeTraces.push(name, aRequest.trace);
272 return { type: "startedTrace", why: "requested", name: name };
273 },
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 }
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 }
304 for (let traceType of stoppedTraceTypes) {
305 this._requestsForTraceType[traceType]--;
306 }
308 if (this.idle) {
309 this.dbg.enabled = false;
310 }
312 return { type: "stoppedTrace", why: "requested", name: name };
313 },
315 // JS Debugger API hooks.
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 }
329 let packet = {
330 type: "enteredFrame",
331 sequence: this._sequence++
332 };
334 if (this._requestsForTraceType.name) {
335 packet.name = aFrame.callee
336 ? aFrame.callee.displayName || "(anonymous function)"
337 : "(" + aFrame.type + ")";
338 }
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 }
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 }
363 if (this._requestsForTraceType.time) {
364 packet.time = Date.now() - this._startTime;
365 }
367 if (this._requestsForTraceType.parameterNames && aFrame.callee) {
368 packet.parameterNames = aFrame.callee.parameterNames;
369 }
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 }
382 if (this._requestsForTraceType.depth) {
383 packet.depth = aFrame.depth;
384 }
386 const onExitFrame = this.onExitFrame;
387 aFrame.onPop = function (aCompletion) {
388 onExitFrame(this, aCompletion);
389 };
391 this._send(packet);
392 },
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 };
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 }
419 if (this._requestsForTraceType.time) {
420 packet.time = Date.now() - this._startTime;
421 }
423 if (this._requestsForTraceType.depth) {
424 packet.depth = aFrame.depth;
425 }
427 if (aCompletion) {
428 if (this._requestsForTraceType.return && "return" in aCompletion) {
429 packet.return = createValueSnapshot(aCompletion.return, true);
430 }
432 else if (this._requestsForTraceType.throw && "throw" in aCompletion) {
433 packet.throw = createValueSnapshot(aCompletion.throw, true);
434 }
436 else if (this._requestsForTraceType.yield && "yield" in aCompletion) {
437 packet.yield = createValueSnapshot(aCompletion.yield, true);
438 }
439 }
441 this._send(packet);
442 }
443 };
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 };
455 exports.register = function(handle) {
456 handle.addTabActor(TraceActor, "traceActor");
457 };
459 exports.unregister = function(handle) {
460 handle.removeTabActor(TraceActor, "traceActor");
461 };
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 }
479 MapStack.prototype = {
480 get size() { return this._stack.length; },
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 },
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 },
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 },
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 },
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 },
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 };
559 // TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when
560 // it is implemented.
561 function getOffsetColumn(aOffset, aScript) {
562 return 0;
563 }
565 // Serialization helper functions. Largely copied from script.js and modified
566 // for use in serialization rather than object actor requests.
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 }
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 }
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);
644 if (aObject.class == "DeadObject") {
645 return desc;
646 }
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 }
659 return desc;
660 }
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 }
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 }
698 return {
699 configurable: desc.configurable,
700 enumerable: desc.enumerable,
701 writable: desc.writable,
702 value: createValueSnapshot(desc.value)
703 };
704 }