toolkit/devtools/server/actors/tracer.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

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 }

mercurial