toolkit/devtools/server/actors/script.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; js-indent-level: 2; -*- */
michael@0 2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7 "use strict";
michael@0 8
michael@0 9 let B2G_ID = "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}";
michael@0 10
michael@0 11 let TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array",
michael@0 12 "Uint32Array", "Int8Array", "Int16Array", "Int32Array", "Float32Array",
michael@0 13 "Float64Array"];
michael@0 14
michael@0 15 // Number of items to preview in objects, arrays, maps, sets, lists,
michael@0 16 // collections, etc.
michael@0 17 let OBJECT_PREVIEW_MAX_ITEMS = 10;
michael@0 18
michael@0 19 let addonManager = null;
michael@0 20
michael@0 21 /**
michael@0 22 * This is a wrapper around amIAddonManager.mapURIToAddonID which always returns
michael@0 23 * false on B2G to avoid loading the add-on manager there and reports any
michael@0 24 * exceptions rather than throwing so that the caller doesn't have to worry
michael@0 25 * about them.
michael@0 26 */
michael@0 27 function mapURIToAddonID(uri, id) {
michael@0 28 if (Services.appinfo.ID == B2G_ID) {
michael@0 29 return false;
michael@0 30 }
michael@0 31
michael@0 32 if (!addonManager) {
michael@0 33 addonManager = Cc["@mozilla.org/addons/integration;1"].
michael@0 34 getService(Ci.amIAddonManager);
michael@0 35 }
michael@0 36
michael@0 37 try {
michael@0 38 return addonManager.mapURIToAddonID(uri, id);
michael@0 39 }
michael@0 40 catch (e) {
michael@0 41 DevtoolsUtils.reportException("mapURIToAddonID", e);
michael@0 42 return false;
michael@0 43 }
michael@0 44 }
michael@0 45
michael@0 46 /**
michael@0 47 * BreakpointStore objects keep track of all breakpoints that get set so that we
michael@0 48 * can reset them when the same script is introduced to the thread again (such
michael@0 49 * as after a refresh).
michael@0 50 */
michael@0 51 function BreakpointStore() {
michael@0 52 this._size = 0;
michael@0 53
michael@0 54 // If we have a whole-line breakpoint set at LINE in URL, then
michael@0 55 //
michael@0 56 // this._wholeLineBreakpoints[URL][LINE]
michael@0 57 //
michael@0 58 // is an object
michael@0 59 //
michael@0 60 // { url, line[, actor] }
michael@0 61 //
michael@0 62 // where the `actor` property is optional.
michael@0 63 this._wholeLineBreakpoints = Object.create(null);
michael@0 64
michael@0 65 // If we have a breakpoint set at LINE, COLUMN in URL, then
michael@0 66 //
michael@0 67 // this._breakpoints[URL][LINE][COLUMN]
michael@0 68 //
michael@0 69 // is an object
michael@0 70 //
michael@0 71 // { url, line, column[, actor] }
michael@0 72 //
michael@0 73 // where the `actor` property is optional.
michael@0 74 this._breakpoints = Object.create(null);
michael@0 75 }
michael@0 76
michael@0 77 BreakpointStore.prototype = {
michael@0 78 _size: null,
michael@0 79 get size() { return this._size; },
michael@0 80
michael@0 81 /**
michael@0 82 * Add a breakpoint to the breakpoint store.
michael@0 83 *
michael@0 84 * @param Object aBreakpoint
michael@0 85 * The breakpoint to be added (not copied). It is an object with the
michael@0 86 * following properties:
michael@0 87 * - url
michael@0 88 * - line
michael@0 89 * - column (optional; omission implies that the breakpoint is for
michael@0 90 * the whole line)
michael@0 91 * - condition (optional)
michael@0 92 * - actor (optional)
michael@0 93 */
michael@0 94 addBreakpoint: function (aBreakpoint) {
michael@0 95 let { url, line, column } = aBreakpoint;
michael@0 96 let updating = false;
michael@0 97
michael@0 98 if (column != null) {
michael@0 99 if (!this._breakpoints[url]) {
michael@0 100 this._breakpoints[url] = [];
michael@0 101 }
michael@0 102 if (!this._breakpoints[url][line]) {
michael@0 103 this._breakpoints[url][line] = [];
michael@0 104 }
michael@0 105 this._breakpoints[url][line][column] = aBreakpoint;
michael@0 106 } else {
michael@0 107 // Add a breakpoint that breaks on the whole line.
michael@0 108 if (!this._wholeLineBreakpoints[url]) {
michael@0 109 this._wholeLineBreakpoints[url] = [];
michael@0 110 }
michael@0 111 this._wholeLineBreakpoints[url][line] = aBreakpoint;
michael@0 112 }
michael@0 113
michael@0 114 this._size++;
michael@0 115 },
michael@0 116
michael@0 117 /**
michael@0 118 * Remove a breakpoint from the breakpoint store.
michael@0 119 *
michael@0 120 * @param Object aBreakpoint
michael@0 121 * The breakpoint to be removed. It is an object with the following
michael@0 122 * properties:
michael@0 123 * - url
michael@0 124 * - line
michael@0 125 * - column (optional)
michael@0 126 */
michael@0 127 removeBreakpoint: function ({ url, line, column }) {
michael@0 128 if (column != null) {
michael@0 129 if (this._breakpoints[url]) {
michael@0 130 if (this._breakpoints[url][line]) {
michael@0 131 if (this._breakpoints[url][line][column]) {
michael@0 132 delete this._breakpoints[url][line][column];
michael@0 133 this._size--;
michael@0 134
michael@0 135 // If this was the last breakpoint on this line, delete the line from
michael@0 136 // `this._breakpoints[url]` as well. Otherwise `_iterLines` will yield
michael@0 137 // this line even though we no longer have breakpoints on
michael@0 138 // it. Furthermore, we use Object.keys() instead of just checking
michael@0 139 // `this._breakpoints[url].length` directly, because deleting
michael@0 140 // properties from sparse arrays doesn't update the `length` property
michael@0 141 // like adding them does.
michael@0 142 if (Object.keys(this._breakpoints[url][line]).length === 0) {
michael@0 143 delete this._breakpoints[url][line];
michael@0 144 }
michael@0 145 }
michael@0 146 }
michael@0 147 }
michael@0 148 } else {
michael@0 149 if (this._wholeLineBreakpoints[url]) {
michael@0 150 if (this._wholeLineBreakpoints[url][line]) {
michael@0 151 delete this._wholeLineBreakpoints[url][line];
michael@0 152 this._size--;
michael@0 153 }
michael@0 154 }
michael@0 155 }
michael@0 156 },
michael@0 157
michael@0 158 /**
michael@0 159 * Get a breakpoint from the breakpoint store. Will throw an error if the
michael@0 160 * breakpoint is not found.
michael@0 161 *
michael@0 162 * @param Object aLocation
michael@0 163 * The location of the breakpoint you are retrieving. It is an object
michael@0 164 * with the following properties:
michael@0 165 * - url
michael@0 166 * - line
michael@0 167 * - column (optional)
michael@0 168 */
michael@0 169 getBreakpoint: function (aLocation) {
michael@0 170 let { url, line, column } = aLocation;
michael@0 171 dbg_assert(url != null);
michael@0 172 dbg_assert(line != null);
michael@0 173
michael@0 174 var foundBreakpoint = this.hasBreakpoint(aLocation);
michael@0 175 if (foundBreakpoint == null) {
michael@0 176 throw new Error("No breakpoint at url = " + url
michael@0 177 + ", line = " + line
michael@0 178 + ", column = " + column);
michael@0 179 }
michael@0 180
michael@0 181 return foundBreakpoint;
michael@0 182 },
michael@0 183
michael@0 184 /**
michael@0 185 * Checks if the breakpoint store has a requested breakpoint.
michael@0 186 *
michael@0 187 * @param Object aLocation
michael@0 188 * The location of the breakpoint you are retrieving. It is an object
michael@0 189 * with the following properties:
michael@0 190 * - url
michael@0 191 * - line
michael@0 192 * - column (optional)
michael@0 193 * @returns The stored breakpoint if it exists, null otherwise.
michael@0 194 */
michael@0 195 hasBreakpoint: function (aLocation) {
michael@0 196 let { url, line, column } = aLocation;
michael@0 197 dbg_assert(url != null);
michael@0 198 dbg_assert(line != null);
michael@0 199 for (let bp of this.findBreakpoints(aLocation)) {
michael@0 200 // We will get whole line breakpoints before individual columns, so just
michael@0 201 // return the first one and if they didn't specify a column then they will
michael@0 202 // get the whole line breakpoint, and otherwise we will find the correct
michael@0 203 // one.
michael@0 204 return bp;
michael@0 205 }
michael@0 206
michael@0 207 return null;
michael@0 208 },
michael@0 209
michael@0 210 /**
michael@0 211 * Iterate over the breakpoints in this breakpoint store. You can optionally
michael@0 212 * provide search parameters to filter the set of breakpoints down to those
michael@0 213 * that match your parameters.
michael@0 214 *
michael@0 215 * @param Object aSearchParams
michael@0 216 * Optional. An object with the following properties:
michael@0 217 * - url
michael@0 218 * - line (optional; requires the url property)
michael@0 219 * - column (optional; requires the line property)
michael@0 220 */
michael@0 221 findBreakpoints: function* (aSearchParams={}) {
michael@0 222 if (aSearchParams.column != null) {
michael@0 223 dbg_assert(aSearchParams.line != null);
michael@0 224 }
michael@0 225 if (aSearchParams.line != null) {
michael@0 226 dbg_assert(aSearchParams.url != null);
michael@0 227 }
michael@0 228
michael@0 229 for (let url of this._iterUrls(aSearchParams.url)) {
michael@0 230 for (let line of this._iterLines(url, aSearchParams.line)) {
michael@0 231 // Always yield whole line breakpoints first. See comment in
michael@0 232 // |BreakpointStore.prototype.hasBreakpoint|.
michael@0 233 if (aSearchParams.column == null
michael@0 234 && this._wholeLineBreakpoints[url]
michael@0 235 && this._wholeLineBreakpoints[url][line]) {
michael@0 236 yield this._wholeLineBreakpoints[url][line];
michael@0 237 }
michael@0 238 for (let column of this._iterColumns(url, line, aSearchParams.column)) {
michael@0 239 yield this._breakpoints[url][line][column];
michael@0 240 }
michael@0 241 }
michael@0 242 }
michael@0 243 },
michael@0 244
michael@0 245 _iterUrls: function* (aUrl) {
michael@0 246 if (aUrl) {
michael@0 247 if (this._breakpoints[aUrl] || this._wholeLineBreakpoints[aUrl]) {
michael@0 248 yield aUrl;
michael@0 249 }
michael@0 250 } else {
michael@0 251 for (let url of Object.keys(this._wholeLineBreakpoints)) {
michael@0 252 yield url;
michael@0 253 }
michael@0 254 for (let url of Object.keys(this._breakpoints)) {
michael@0 255 if (url in this._wholeLineBreakpoints) {
michael@0 256 continue;
michael@0 257 }
michael@0 258 yield url;
michael@0 259 }
michael@0 260 }
michael@0 261 },
michael@0 262
michael@0 263 _iterLines: function* (aUrl, aLine) {
michael@0 264 if (aLine != null) {
michael@0 265 if ((this._wholeLineBreakpoints[aUrl]
michael@0 266 && this._wholeLineBreakpoints[aUrl][aLine])
michael@0 267 || (this._breakpoints[aUrl] && this._breakpoints[aUrl][aLine])) {
michael@0 268 yield aLine;
michael@0 269 }
michael@0 270 } else {
michael@0 271 const wholeLines = this._wholeLineBreakpoints[aUrl]
michael@0 272 ? Object.keys(this._wholeLineBreakpoints[aUrl])
michael@0 273 : [];
michael@0 274 const columnLines = this._breakpoints[aUrl]
michael@0 275 ? Object.keys(this._breakpoints[aUrl])
michael@0 276 : [];
michael@0 277
michael@0 278 const lines = wholeLines.concat(columnLines).sort();
michael@0 279
michael@0 280 let lastLine;
michael@0 281 for (let line of lines) {
michael@0 282 if (line === lastLine) {
michael@0 283 continue;
michael@0 284 }
michael@0 285 yield line;
michael@0 286 lastLine = line;
michael@0 287 }
michael@0 288 }
michael@0 289 },
michael@0 290
michael@0 291 _iterColumns: function* (aUrl, aLine, aColumn) {
michael@0 292 if (!this._breakpoints[aUrl] || !this._breakpoints[aUrl][aLine]) {
michael@0 293 return;
michael@0 294 }
michael@0 295
michael@0 296 if (aColumn != null) {
michael@0 297 if (this._breakpoints[aUrl][aLine][aColumn]) {
michael@0 298 yield aColumn;
michael@0 299 }
michael@0 300 } else {
michael@0 301 for (let column in this._breakpoints[aUrl][aLine]) {
michael@0 302 yield column;
michael@0 303 }
michael@0 304 }
michael@0 305 },
michael@0 306 };
michael@0 307
michael@0 308 /**
michael@0 309 * Manages pushing event loops and automatically pops and exits them in the
michael@0 310 * correct order as they are resolved.
michael@0 311 *
michael@0 312 * @param nsIJSInspector inspector
michael@0 313 * The underlying JS inspector we use to enter and exit nested event
michael@0 314 * loops.
michael@0 315 * @param ThreadActor thread
michael@0 316 * The thread actor instance that owns this EventLoopStack.
michael@0 317 * @param DebuggerServerConnection connection
michael@0 318 * The remote protocol connection associated with this event loop stack.
michael@0 319 * @param Object hooks
michael@0 320 * An object with the following properties:
michael@0 321 * - url: The URL string of the debuggee we are spinning an event loop
michael@0 322 * for.
michael@0 323 * - preNest: function called before entering a nested event loop
michael@0 324 * - postNest: function called after exiting a nested event loop
michael@0 325 */
michael@0 326 function EventLoopStack({ inspector, thread, connection, hooks }) {
michael@0 327 this._inspector = inspector;
michael@0 328 this._hooks = hooks;
michael@0 329 this._thread = thread;
michael@0 330 this._connection = connection;
michael@0 331 }
michael@0 332
michael@0 333 EventLoopStack.prototype = {
michael@0 334 /**
michael@0 335 * The number of nested event loops on the stack.
michael@0 336 */
michael@0 337 get size() {
michael@0 338 return this._inspector.eventLoopNestLevel;
michael@0 339 },
michael@0 340
michael@0 341 /**
michael@0 342 * The URL of the debuggee who pushed the event loop on top of the stack.
michael@0 343 */
michael@0 344 get lastPausedUrl() {
michael@0 345 let url = null;
michael@0 346 if (this.size > 0) {
michael@0 347 try {
michael@0 348 url = this._inspector.lastNestRequestor.url
michael@0 349 } catch (e) {
michael@0 350 // The tab's URL getter may throw if the tab is destroyed by the time
michael@0 351 // this code runs, but we don't really care at this point.
michael@0 352 dumpn(e);
michael@0 353 }
michael@0 354 }
michael@0 355 return url;
michael@0 356 },
michael@0 357
michael@0 358 /**
michael@0 359 * The DebuggerServerConnection of the debugger who pushed the event loop on
michael@0 360 * top of the stack
michael@0 361 */
michael@0 362 get lastConnection() {
michael@0 363 return this._inspector.lastNestRequestor._connection;
michael@0 364 },
michael@0 365
michael@0 366 /**
michael@0 367 * Push a new nested event loop onto the stack.
michael@0 368 *
michael@0 369 * @returns EventLoop
michael@0 370 */
michael@0 371 push: function () {
michael@0 372 return new EventLoop({
michael@0 373 inspector: this._inspector,
michael@0 374 thread: this._thread,
michael@0 375 connection: this._connection,
michael@0 376 hooks: this._hooks
michael@0 377 });
michael@0 378 }
michael@0 379 };
michael@0 380
michael@0 381 /**
michael@0 382 * An object that represents a nested event loop. It is used as the nest
michael@0 383 * requestor with nsIJSInspector instances.
michael@0 384 *
michael@0 385 * @param nsIJSInspector inspector
michael@0 386 * The JS Inspector that runs nested event loops.
michael@0 387 * @param ThreadActor thread
michael@0 388 * The thread actor that is creating this nested event loop.
michael@0 389 * @param DebuggerServerConnection connection
michael@0 390 * The remote protocol connection associated with this event loop.
michael@0 391 * @param Object hooks
michael@0 392 * The same hooks object passed into EventLoopStack during its
michael@0 393 * initialization.
michael@0 394 */
michael@0 395 function EventLoop({ inspector, thread, connection, hooks }) {
michael@0 396 this._inspector = inspector;
michael@0 397 this._thread = thread;
michael@0 398 this._hooks = hooks;
michael@0 399 this._connection = connection;
michael@0 400
michael@0 401 this.enter = this.enter.bind(this);
michael@0 402 this.resolve = this.resolve.bind(this);
michael@0 403 }
michael@0 404
michael@0 405 EventLoop.prototype = {
michael@0 406 entered: false,
michael@0 407 resolved: false,
michael@0 408 get url() { return this._hooks.url; },
michael@0 409
michael@0 410 /**
michael@0 411 * Enter this nested event loop.
michael@0 412 */
michael@0 413 enter: function () {
michael@0 414 let nestData = this._hooks.preNest
michael@0 415 ? this._hooks.preNest()
michael@0 416 : null;
michael@0 417
michael@0 418 this.entered = true;
michael@0 419 this._inspector.enterNestedEventLoop(this);
michael@0 420
michael@0 421 // Keep exiting nested event loops while the last requestor is resolved.
michael@0 422 if (this._inspector.eventLoopNestLevel > 0) {
michael@0 423 const { resolved } = this._inspector.lastNestRequestor;
michael@0 424 if (resolved) {
michael@0 425 this._inspector.exitNestedEventLoop();
michael@0 426 }
michael@0 427 }
michael@0 428
michael@0 429 dbg_assert(this._thread.state === "running",
michael@0 430 "Should be in the running state");
michael@0 431
michael@0 432 if (this._hooks.postNest) {
michael@0 433 this._hooks.postNest(nestData);
michael@0 434 }
michael@0 435 },
michael@0 436
michael@0 437 /**
michael@0 438 * Resolve this nested event loop.
michael@0 439 *
michael@0 440 * @returns boolean
michael@0 441 * True if we exited this nested event loop because it was on top of
michael@0 442 * the stack, false if there is another nested event loop above this
michael@0 443 * one that hasn't resolved yet.
michael@0 444 */
michael@0 445 resolve: function () {
michael@0 446 if (!this.entered) {
michael@0 447 throw new Error("Can't resolve an event loop before it has been entered!");
michael@0 448 }
michael@0 449 if (this.resolved) {
michael@0 450 throw new Error("Already resolved this nested event loop!");
michael@0 451 }
michael@0 452 this.resolved = true;
michael@0 453 if (this === this._inspector.lastNestRequestor) {
michael@0 454 this._inspector.exitNestedEventLoop();
michael@0 455 return true;
michael@0 456 }
michael@0 457 return false;
michael@0 458 },
michael@0 459 };
michael@0 460
michael@0 461 /**
michael@0 462 * JSD2 actors.
michael@0 463 */
michael@0 464 /**
michael@0 465 * Creates a ThreadActor.
michael@0 466 *
michael@0 467 * ThreadActors manage a JSInspector object and manage execution/inspection
michael@0 468 * of debuggees.
michael@0 469 *
michael@0 470 * @param aHooks object
michael@0 471 * An object with preNest and postNest methods for calling when entering
michael@0 472 * and exiting a nested event loop.
michael@0 473 * @param aGlobal object [optional]
michael@0 474 * An optional (for content debugging only) reference to the content
michael@0 475 * window.
michael@0 476 */
michael@0 477 function ThreadActor(aHooks, aGlobal)
michael@0 478 {
michael@0 479 this._state = "detached";
michael@0 480 this._frameActors = [];
michael@0 481 this._hooks = aHooks;
michael@0 482 this.global = aGlobal;
michael@0 483 // A map of actorID -> actor for breakpoints created and managed by the server.
michael@0 484 this._hiddenBreakpoints = new Map();
michael@0 485
michael@0 486 this.findGlobals = this.globalManager.findGlobals.bind(this);
michael@0 487 this.onNewGlobal = this.globalManager.onNewGlobal.bind(this);
michael@0 488 this.onNewSource = this.onNewSource.bind(this);
michael@0 489 this._allEventsListener = this._allEventsListener.bind(this);
michael@0 490
michael@0 491 this._options = {
michael@0 492 useSourceMaps: false
michael@0 493 };
michael@0 494
michael@0 495 this._gripDepth = 0;
michael@0 496 }
michael@0 497
michael@0 498 /**
michael@0 499 * The breakpoint store must be shared across instances of ThreadActor so that
michael@0 500 * page reloads don't blow away all of our breakpoints.
michael@0 501 */
michael@0 502 ThreadActor.breakpointStore = new BreakpointStore();
michael@0 503
michael@0 504 ThreadActor.prototype = {
michael@0 505 // Used by the ObjectActor to keep track of the depth of grip() calls.
michael@0 506 _gripDepth: null,
michael@0 507
michael@0 508 actorPrefix: "context",
michael@0 509
michael@0 510 get state() { return this._state; },
michael@0 511 get attached() this.state == "attached" ||
michael@0 512 this.state == "running" ||
michael@0 513 this.state == "paused",
michael@0 514
michael@0 515 get breakpointStore() { return ThreadActor.breakpointStore; },
michael@0 516
michael@0 517 get threadLifetimePool() {
michael@0 518 if (!this._threadLifetimePool) {
michael@0 519 this._threadLifetimePool = new ActorPool(this.conn);
michael@0 520 this.conn.addActorPool(this._threadLifetimePool);
michael@0 521 this._threadLifetimePool.objectActors = new WeakMap();
michael@0 522 }
michael@0 523 return this._threadLifetimePool;
michael@0 524 },
michael@0 525
michael@0 526 get sources() {
michael@0 527 if (!this._sources) {
michael@0 528 this._sources = new ThreadSources(this, this._options.useSourceMaps,
michael@0 529 this._allowSource, this.onNewSource);
michael@0 530 }
michael@0 531 return this._sources;
michael@0 532 },
michael@0 533
michael@0 534 get youngestFrame() {
michael@0 535 if (this.state != "paused") {
michael@0 536 return null;
michael@0 537 }
michael@0 538 return this.dbg.getNewestFrame();
michael@0 539 },
michael@0 540
michael@0 541 _prettyPrintWorker: null,
michael@0 542 get prettyPrintWorker() {
michael@0 543 if (!this._prettyPrintWorker) {
michael@0 544 this._prettyPrintWorker = new ChromeWorker(
michael@0 545 "resource://gre/modules/devtools/server/actors/pretty-print-worker.js");
michael@0 546
michael@0 547 this._prettyPrintWorker.addEventListener(
michael@0 548 "error", this._onPrettyPrintError, false);
michael@0 549
michael@0 550 if (dumpn.wantLogging) {
michael@0 551 this._prettyPrintWorker.addEventListener("message", this._onPrettyPrintMsg, false);
michael@0 552
michael@0 553 const postMsg = this._prettyPrintWorker.postMessage;
michael@0 554 this._prettyPrintWorker.postMessage = data => {
michael@0 555 dumpn("Sending message to prettyPrintWorker: "
michael@0 556 + JSON.stringify(data, null, 2) + "\n");
michael@0 557 return postMsg.call(this._prettyPrintWorker, data);
michael@0 558 };
michael@0 559 }
michael@0 560 }
michael@0 561 return this._prettyPrintWorker;
michael@0 562 },
michael@0 563
michael@0 564 _onPrettyPrintError: function ({ message, filename, lineno }) {
michael@0 565 reportError(new Error(message + " @ " + filename + ":" + lineno));
michael@0 566 },
michael@0 567
michael@0 568 _onPrettyPrintMsg: function ({ data }) {
michael@0 569 dumpn("Received message from prettyPrintWorker: "
michael@0 570 + JSON.stringify(data, null, 2) + "\n");
michael@0 571 },
michael@0 572
michael@0 573 /**
michael@0 574 * Keep track of all of the nested event loops we use to pause the debuggee
michael@0 575 * when we hit a breakpoint/debugger statement/etc in one place so we can
michael@0 576 * resolve them when we get resume packets. We have more than one (and keep
michael@0 577 * them in a stack) because we can pause within client evals.
michael@0 578 */
michael@0 579 _threadPauseEventLoops: null,
michael@0 580 _pushThreadPause: function () {
michael@0 581 if (!this._threadPauseEventLoops) {
michael@0 582 this._threadPauseEventLoops = [];
michael@0 583 }
michael@0 584 const eventLoop = this._nestedEventLoops.push();
michael@0 585 this._threadPauseEventLoops.push(eventLoop);
michael@0 586 eventLoop.enter();
michael@0 587 },
michael@0 588 _popThreadPause: function () {
michael@0 589 const eventLoop = this._threadPauseEventLoops.pop();
michael@0 590 dbg_assert(eventLoop, "Should have an event loop.");
michael@0 591 eventLoop.resolve();
michael@0 592 },
michael@0 593
michael@0 594 /**
michael@0 595 * Remove all debuggees and clear out the thread's sources.
michael@0 596 */
michael@0 597 clearDebuggees: function () {
michael@0 598 if (this.dbg) {
michael@0 599 this.dbg.removeAllDebuggees();
michael@0 600 }
michael@0 601 this._sources = null;
michael@0 602 },
michael@0 603
michael@0 604 /**
michael@0 605 * Add a debuggee global to the Debugger object.
michael@0 606 *
michael@0 607 * @returns the Debugger.Object that corresponds to the global.
michael@0 608 */
michael@0 609 addDebuggee: function (aGlobal) {
michael@0 610 let globalDebugObject;
michael@0 611 try {
michael@0 612 globalDebugObject = this.dbg.addDebuggee(aGlobal);
michael@0 613 } catch (e) {
michael@0 614 // Ignore attempts to add the debugger's compartment as a debuggee.
michael@0 615 dumpn("Ignoring request to add the debugger's compartment as a debuggee");
michael@0 616 }
michael@0 617 return globalDebugObject;
michael@0 618 },
michael@0 619
michael@0 620 /**
michael@0 621 * Initialize the Debugger.
michael@0 622 */
michael@0 623 _initDebugger: function () {
michael@0 624 this.dbg = new Debugger();
michael@0 625 this.dbg.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this);
michael@0 626 this.dbg.onDebuggerStatement = this.onDebuggerStatement.bind(this);
michael@0 627 this.dbg.onNewScript = this.onNewScript.bind(this);
michael@0 628 this.dbg.onNewGlobalObject = this.globalManager.onNewGlobal.bind(this);
michael@0 629 // Keep the debugger disabled until a client attaches.
michael@0 630 this.dbg.enabled = this._state != "detached";
michael@0 631 },
michael@0 632
michael@0 633 /**
michael@0 634 * Remove a debuggee global from the JSInspector.
michael@0 635 */
michael@0 636 removeDebugee: function (aGlobal) {
michael@0 637 try {
michael@0 638 this.dbg.removeDebuggee(aGlobal);
michael@0 639 } catch(ex) {
michael@0 640 // XXX: This debuggee has code currently executing on the stack,
michael@0 641 // we need to save this for later.
michael@0 642 }
michael@0 643 },
michael@0 644
michael@0 645 /**
michael@0 646 * Add the provided window and all windows in its frame tree as debuggees.
michael@0 647 *
michael@0 648 * @returns the Debugger.Object that corresponds to the window.
michael@0 649 */
michael@0 650 _addDebuggees: function (aWindow) {
michael@0 651 let globalDebugObject = this.addDebuggee(aWindow);
michael@0 652 let frames = aWindow.frames;
michael@0 653 if (frames) {
michael@0 654 for (let i = 0; i < frames.length; i++) {
michael@0 655 this._addDebuggees(frames[i]);
michael@0 656 }
michael@0 657 }
michael@0 658 return globalDebugObject;
michael@0 659 },
michael@0 660
michael@0 661 /**
michael@0 662 * An object that will be used by ThreadActors to tailor their behavior
michael@0 663 * depending on the debugging context being required (chrome or content).
michael@0 664 */
michael@0 665 globalManager: {
michael@0 666 findGlobals: function () {
michael@0 667 const { gDevToolsExtensions: {
michael@0 668 getContentGlobals
michael@0 669 } } = Cu.import("resource://gre/modules/devtools/DevToolsExtensions.jsm", {});
michael@0 670
michael@0 671 this.globalDebugObject = this._addDebuggees(this.global);
michael@0 672
michael@0 673 // global may not be a window
michael@0 674 try {
michael@0 675 getContentGlobals({
michael@0 676 'inner-window-id': getInnerId(this.global)
michael@0 677 }).forEach(this.addDebuggee.bind(this));
michael@0 678 }
michael@0 679 catch(e) {}
michael@0 680 },
michael@0 681
michael@0 682 /**
michael@0 683 * A function that the engine calls when a new global object
michael@0 684 * (for example a sandbox) has been created.
michael@0 685 *
michael@0 686 * @param aGlobal Debugger.Object
michael@0 687 * The new global object that was created.
michael@0 688 */
michael@0 689 onNewGlobal: function (aGlobal) {
michael@0 690 let useGlobal = (aGlobal.hostAnnotations &&
michael@0 691 aGlobal.hostAnnotations.type == "document" &&
michael@0 692 aGlobal.hostAnnotations.element === this.global);
michael@0 693
michael@0 694 // check if the global is a sdk page-mod sandbox
michael@0 695 if (!useGlobal) {
michael@0 696 let metadata = {};
michael@0 697 let id = "";
michael@0 698 try {
michael@0 699 id = getInnerId(this.global);
michael@0 700 metadata = Cu.getSandboxMetadata(aGlobal.unsafeDereference());
michael@0 701 }
michael@0 702 catch (e) {}
michael@0 703
michael@0 704 useGlobal = (metadata['inner-window-id'] && metadata['inner-window-id'] == id);
michael@0 705 }
michael@0 706
michael@0 707 // Content debugging only cares about new globals in the contant window,
michael@0 708 // like iframe children.
michael@0 709 if (useGlobal) {
michael@0 710 this.addDebuggee(aGlobal);
michael@0 711 // Notify the client.
michael@0 712 this.conn.send({
michael@0 713 from: this.actorID,
michael@0 714 type: "newGlobal",
michael@0 715 // TODO: after bug 801084 lands see if we need to JSONify this.
michael@0 716 hostAnnotations: aGlobal.hostAnnotations
michael@0 717 });
michael@0 718 }
michael@0 719 }
michael@0 720 },
michael@0 721
michael@0 722 disconnect: function () {
michael@0 723 dumpn("in ThreadActor.prototype.disconnect");
michael@0 724 if (this._state == "paused") {
michael@0 725 this.onResume();
michael@0 726 }
michael@0 727
michael@0 728 this.clearDebuggees();
michael@0 729 this.conn.removeActorPool(this._threadLifetimePool);
michael@0 730 this._threadLifetimePool = null;
michael@0 731
michael@0 732 if (this._prettyPrintWorker) {
michael@0 733 this._prettyPrintWorker.removeEventListener(
michael@0 734 "error", this._onPrettyPrintError, false);
michael@0 735 this._prettyPrintWorker.removeEventListener(
michael@0 736 "message", this._onPrettyPrintMsg, false);
michael@0 737 this._prettyPrintWorker.terminate();
michael@0 738 this._prettyPrintWorker = null;
michael@0 739 }
michael@0 740
michael@0 741 if (!this.dbg) {
michael@0 742 return;
michael@0 743 }
michael@0 744 this.dbg.enabled = false;
michael@0 745 this.dbg = null;
michael@0 746 },
michael@0 747
michael@0 748 /**
michael@0 749 * Disconnect the debugger and put the actor in the exited state.
michael@0 750 */
michael@0 751 exit: function () {
michael@0 752 this.disconnect();
michael@0 753 this._state = "exited";
michael@0 754 },
michael@0 755
michael@0 756 // Request handlers
michael@0 757 onAttach: function (aRequest) {
michael@0 758 if (this.state === "exited") {
michael@0 759 return { type: "exited" };
michael@0 760 }
michael@0 761
michael@0 762 if (this.state !== "detached") {
michael@0 763 return { error: "wrongState",
michael@0 764 message: "Current state is " + this.state };
michael@0 765 }
michael@0 766
michael@0 767 this._state = "attached";
michael@0 768
michael@0 769 update(this._options, aRequest.options || {});
michael@0 770
michael@0 771 // Initialize an event loop stack. This can't be done in the constructor,
michael@0 772 // because this.conn is not yet initialized by the actor pool at that time.
michael@0 773 this._nestedEventLoops = new EventLoopStack({
michael@0 774 inspector: DebuggerServer.xpcInspector,
michael@0 775 hooks: this._hooks,
michael@0 776 connection: this.conn,
michael@0 777 thread: this
michael@0 778 });
michael@0 779
michael@0 780 if (!this.dbg) {
michael@0 781 this._initDebugger();
michael@0 782 }
michael@0 783 this.findGlobals();
michael@0 784 this.dbg.enabled = true;
michael@0 785 try {
michael@0 786 // Put ourselves in the paused state.
michael@0 787 let packet = this._paused();
michael@0 788 if (!packet) {
michael@0 789 return { error: "notAttached" };
michael@0 790 }
michael@0 791 packet.why = { type: "attached" };
michael@0 792
michael@0 793 this._restoreBreakpoints();
michael@0 794
michael@0 795 // Send the response to the attach request now (rather than
michael@0 796 // returning it), because we're going to start a nested event loop
michael@0 797 // here.
michael@0 798 this.conn.send(packet);
michael@0 799
michael@0 800 // Start a nested event loop.
michael@0 801 this._pushThreadPause();
michael@0 802
michael@0 803 // We already sent a response to this request, don't send one
michael@0 804 // now.
michael@0 805 return null;
michael@0 806 } catch (e) {
michael@0 807 reportError(e);
michael@0 808 return { error: "notAttached", message: e.toString() };
michael@0 809 }
michael@0 810 },
michael@0 811
michael@0 812 onDetach: function (aRequest) {
michael@0 813 this.disconnect();
michael@0 814 this._state = "detached";
michael@0 815
michael@0 816 dumpn("ThreadActor.prototype.onDetach: returning 'detached' packet");
michael@0 817 return {
michael@0 818 type: "detached"
michael@0 819 };
michael@0 820 },
michael@0 821
michael@0 822 onReconfigure: function (aRequest) {
michael@0 823 if (this.state == "exited") {
michael@0 824 return { error: "wrongState" };
michael@0 825 }
michael@0 826
michael@0 827 update(this._options, aRequest.options || {});
michael@0 828 // Clear existing sources, so they can be recreated on next access.
michael@0 829 this._sources = null;
michael@0 830
michael@0 831 return {};
michael@0 832 },
michael@0 833
michael@0 834 /**
michael@0 835 * Pause the debuggee, by entering a nested event loop, and return a 'paused'
michael@0 836 * packet to the client.
michael@0 837 *
michael@0 838 * @param Debugger.Frame aFrame
michael@0 839 * The newest debuggee frame in the stack.
michael@0 840 * @param object aReason
michael@0 841 * An object with a 'type' property containing the reason for the pause.
michael@0 842 * @param function onPacket
michael@0 843 * Hook to modify the packet before it is sent. Feel free to return a
michael@0 844 * promise.
michael@0 845 */
michael@0 846 _pauseAndRespond: function (aFrame, aReason, onPacket=function (k) { return k; }) {
michael@0 847 try {
michael@0 848 let packet = this._paused(aFrame);
michael@0 849 if (!packet) {
michael@0 850 return undefined;
michael@0 851 }
michael@0 852 packet.why = aReason;
michael@0 853
michael@0 854 this.sources.getOriginalLocation(packet.frame.where).then(aOrigPosition => {
michael@0 855 packet.frame.where = aOrigPosition;
michael@0 856 resolve(onPacket(packet))
michael@0 857 .then(null, error => {
michael@0 858 reportError(error);
michael@0 859 return {
michael@0 860 error: "unknownError",
michael@0 861 message: error.message + "\n" + error.stack
michael@0 862 };
michael@0 863 })
michael@0 864 .then(packet => {
michael@0 865 this.conn.send(packet);
michael@0 866 });
michael@0 867 });
michael@0 868
michael@0 869 this._pushThreadPause();
michael@0 870 } catch(e) {
michael@0 871 reportError(e, "Got an exception during TA__pauseAndRespond: ");
michael@0 872 }
michael@0 873
michael@0 874 return undefined;
michael@0 875 },
michael@0 876
michael@0 877 /**
michael@0 878 * Handle resume requests that include a forceCompletion request.
michael@0 879 *
michael@0 880 * @param Object aRequest
michael@0 881 * The request packet received over the RDP.
michael@0 882 * @returns A response packet.
michael@0 883 */
michael@0 884 _forceCompletion: function (aRequest) {
michael@0 885 // TODO: remove this when Debugger.Frame.prototype.pop is implemented in
michael@0 886 // bug 736733.
michael@0 887 return {
michael@0 888 error: "notImplemented",
michael@0 889 message: "forced completion is not yet implemented."
michael@0 890 };
michael@0 891 },
michael@0 892
michael@0 893 _makeOnEnterFrame: function ({ pauseAndRespond }) {
michael@0 894 return aFrame => {
michael@0 895 const generatedLocation = getFrameLocation(aFrame);
michael@0 896 let { url } = this.synchronize(this.sources.getOriginalLocation(
michael@0 897 generatedLocation));
michael@0 898
michael@0 899 return this.sources.isBlackBoxed(url)
michael@0 900 ? undefined
michael@0 901 : pauseAndRespond(aFrame);
michael@0 902 };
michael@0 903 },
michael@0 904
michael@0 905 _makeOnPop: function ({ thread, pauseAndRespond, createValueGrip }) {
michael@0 906 return function (aCompletion) {
michael@0 907 // onPop is called with 'this' set to the current frame.
michael@0 908
michael@0 909 const generatedLocation = getFrameLocation(this);
michael@0 910 const { url } = thread.synchronize(thread.sources.getOriginalLocation(
michael@0 911 generatedLocation));
michael@0 912
michael@0 913 if (thread.sources.isBlackBoxed(url)) {
michael@0 914 return undefined;
michael@0 915 }
michael@0 916
michael@0 917 // Note that we're popping this frame; we need to watch for
michael@0 918 // subsequent step events on its caller.
michael@0 919 this.reportedPop = true;
michael@0 920
michael@0 921 return pauseAndRespond(this, aPacket => {
michael@0 922 aPacket.why.frameFinished = {};
michael@0 923 if (!aCompletion) {
michael@0 924 aPacket.why.frameFinished.terminated = true;
michael@0 925 } else if (aCompletion.hasOwnProperty("return")) {
michael@0 926 aPacket.why.frameFinished.return = createValueGrip(aCompletion.return);
michael@0 927 } else if (aCompletion.hasOwnProperty("yield")) {
michael@0 928 aPacket.why.frameFinished.return = createValueGrip(aCompletion.yield);
michael@0 929 } else {
michael@0 930 aPacket.why.frameFinished.throw = createValueGrip(aCompletion.throw);
michael@0 931 }
michael@0 932 return aPacket;
michael@0 933 });
michael@0 934 };
michael@0 935 },
michael@0 936
michael@0 937 _makeOnStep: function ({ thread, pauseAndRespond, startFrame,
michael@0 938 startLocation }) {
michael@0 939 return function () {
michael@0 940 // onStep is called with 'this' set to the current frame.
michael@0 941
michael@0 942 const generatedLocation = getFrameLocation(this);
michael@0 943 const newLocation = thread.synchronize(thread.sources.getOriginalLocation(
michael@0 944 generatedLocation));
michael@0 945
michael@0 946 // Cases when we should pause because we have executed enough to consider
michael@0 947 // a "step" to have occured:
michael@0 948 //
michael@0 949 // 1.1. We change frames.
michael@0 950 // 1.2. We change URLs (can happen without changing frames thanks to
michael@0 951 // source mapping).
michael@0 952 // 1.3. We change lines.
michael@0 953 //
michael@0 954 // Cases when we should always continue execution, even if one of the
michael@0 955 // above cases is true:
michael@0 956 //
michael@0 957 // 2.1. We are in a source mapped region, but inside a null mapping
michael@0 958 // (doesn't correlate to any region of original source)
michael@0 959 // 2.2. The source we are in is black boxed.
michael@0 960
michael@0 961 // Cases 2.1 and 2.2
michael@0 962 if (newLocation.url == null
michael@0 963 || thread.sources.isBlackBoxed(newLocation.url)) {
michael@0 964 return undefined;
michael@0 965 }
michael@0 966
michael@0 967 // Cases 1.1, 1.2 and 1.3
michael@0 968 if (this !== startFrame
michael@0 969 || startLocation.url !== newLocation.url
michael@0 970 || startLocation.line !== newLocation.line) {
michael@0 971 return pauseAndRespond(this);
michael@0 972 }
michael@0 973
michael@0 974 // Otherwise, let execution continue (we haven't executed enough code to
michael@0 975 // consider this a "step" yet).
michael@0 976 return undefined;
michael@0 977 };
michael@0 978 },
michael@0 979
michael@0 980 /**
michael@0 981 * Define the JS hook functions for stepping.
michael@0 982 */
michael@0 983 _makeSteppingHooks: function (aStartLocation) {
michael@0 984 // Bind these methods and state because some of the hooks are called
michael@0 985 // with 'this' set to the current frame. Rather than repeating the
michael@0 986 // binding in each _makeOnX method, just do it once here and pass it
michael@0 987 // in to each function.
michael@0 988 const steppingHookState = {
michael@0 989 pauseAndRespond: (aFrame, onPacket=(k)=>k) => {
michael@0 990 this._pauseAndRespond(aFrame, { type: "resumeLimit" }, onPacket);
michael@0 991 },
michael@0 992 createValueGrip: this.createValueGrip.bind(this),
michael@0 993 thread: this,
michael@0 994 startFrame: this.youngestFrame,
michael@0 995 startLocation: aStartLocation
michael@0 996 };
michael@0 997
michael@0 998 return {
michael@0 999 onEnterFrame: this._makeOnEnterFrame(steppingHookState),
michael@0 1000 onPop: this._makeOnPop(steppingHookState),
michael@0 1001 onStep: this._makeOnStep(steppingHookState)
michael@0 1002 };
michael@0 1003 },
michael@0 1004
michael@0 1005 /**
michael@0 1006 * Handle attaching the various stepping hooks we need to attach when we
michael@0 1007 * receive a resume request with a resumeLimit property.
michael@0 1008 *
michael@0 1009 * @param Object aRequest
michael@0 1010 * The request packet received over the RDP.
michael@0 1011 * @returns A promise that resolves to true once the hooks are attached, or is
michael@0 1012 * rejected with an error packet.
michael@0 1013 */
michael@0 1014 _handleResumeLimit: function (aRequest) {
michael@0 1015 let steppingType = aRequest.resumeLimit.type;
michael@0 1016 if (["step", "next", "finish"].indexOf(steppingType) == -1) {
michael@0 1017 return reject({ error: "badParameterType",
michael@0 1018 message: "Unknown resumeLimit type" });
michael@0 1019 }
michael@0 1020
michael@0 1021 const generatedLocation = getFrameLocation(this.youngestFrame);
michael@0 1022 return this.sources.getOriginalLocation(generatedLocation)
michael@0 1023 .then(originalLocation => {
michael@0 1024 const { onEnterFrame, onPop, onStep } = this._makeSteppingHooks(originalLocation);
michael@0 1025
michael@0 1026 // Make sure there is still a frame on the stack if we are to continue
michael@0 1027 // stepping.
michael@0 1028 let stepFrame = this._getNextStepFrame(this.youngestFrame);
michael@0 1029 if (stepFrame) {
michael@0 1030 switch (steppingType) {
michael@0 1031 case "step":
michael@0 1032 this.dbg.onEnterFrame = onEnterFrame;
michael@0 1033 // Fall through.
michael@0 1034 case "next":
michael@0 1035 if (stepFrame.script) {
michael@0 1036 stepFrame.onStep = onStep;
michael@0 1037 }
michael@0 1038 stepFrame.onPop = onPop;
michael@0 1039 break;
michael@0 1040 case "finish":
michael@0 1041 stepFrame.onPop = onPop;
michael@0 1042 }
michael@0 1043 }
michael@0 1044
michael@0 1045 return true;
michael@0 1046 });
michael@0 1047 },
michael@0 1048
michael@0 1049 /**
michael@0 1050 * Clear the onStep and onPop hooks from the given frame and all of the frames
michael@0 1051 * below it.
michael@0 1052 *
michael@0 1053 * @param Debugger.Frame aFrame
michael@0 1054 * The frame we want to clear the stepping hooks from.
michael@0 1055 */
michael@0 1056 _clearSteppingHooks: function (aFrame) {
michael@0 1057 while (aFrame) {
michael@0 1058 aFrame.onStep = undefined;
michael@0 1059 aFrame.onPop = undefined;
michael@0 1060 aFrame = aFrame.older;
michael@0 1061 }
michael@0 1062 },
michael@0 1063
michael@0 1064 /**
michael@0 1065 * Listen to the debuggee's DOM events if we received a request to do so.
michael@0 1066 *
michael@0 1067 * @param Object aRequest
michael@0 1068 * The resume request packet received over the RDP.
michael@0 1069 */
michael@0 1070 _maybeListenToEvents: function (aRequest) {
michael@0 1071 // Break-on-DOMEvents is only supported in content debugging.
michael@0 1072 let events = aRequest.pauseOnDOMEvents;
michael@0 1073 if (this.global && events &&
michael@0 1074 (events == "*" ||
michael@0 1075 (Array.isArray(events) && events.length))) {
michael@0 1076 this._pauseOnDOMEvents = events;
michael@0 1077 let els = Cc["@mozilla.org/eventlistenerservice;1"]
michael@0 1078 .getService(Ci.nsIEventListenerService);
michael@0 1079 els.addListenerForAllEvents(this.global, this._allEventsListener, true);
michael@0 1080 }
michael@0 1081 },
michael@0 1082
michael@0 1083 /**
michael@0 1084 * Handle a protocol request to resume execution of the debuggee.
michael@0 1085 */
michael@0 1086 onResume: function (aRequest) {
michael@0 1087 if (this._state !== "paused") {
michael@0 1088 return {
michael@0 1089 error: "wrongState",
michael@0 1090 message: "Can't resume when debuggee isn't paused. Current state is '"
michael@0 1091 + this._state + "'"
michael@0 1092 };
michael@0 1093 }
michael@0 1094
michael@0 1095 // In case of multiple nested event loops (due to multiple debuggers open in
michael@0 1096 // different tabs or multiple debugger clients connected to the same tab)
michael@0 1097 // only allow resumption in a LIFO order.
michael@0 1098 if (this._nestedEventLoops.size && this._nestedEventLoops.lastPausedUrl
michael@0 1099 && (this._nestedEventLoops.lastPausedUrl !== this._hooks.url
michael@0 1100 || this._nestedEventLoops.lastConnection !== this.conn)) {
michael@0 1101 return {
michael@0 1102 error: "wrongOrder",
michael@0 1103 message: "trying to resume in the wrong order.",
michael@0 1104 lastPausedUrl: this._nestedEventLoops.lastPausedUrl
michael@0 1105 };
michael@0 1106 }
michael@0 1107
michael@0 1108 if (aRequest && aRequest.forceCompletion) {
michael@0 1109 return this._forceCompletion(aRequest);
michael@0 1110 }
michael@0 1111
michael@0 1112 let resumeLimitHandled;
michael@0 1113 if (aRequest && aRequest.resumeLimit) {
michael@0 1114 resumeLimitHandled = this._handleResumeLimit(aRequest)
michael@0 1115 } else {
michael@0 1116 this._clearSteppingHooks(this.youngestFrame);
michael@0 1117 resumeLimitHandled = resolve(true);
michael@0 1118 }
michael@0 1119
michael@0 1120 return resumeLimitHandled.then(() => {
michael@0 1121 if (aRequest) {
michael@0 1122 this._options.pauseOnExceptions = aRequest.pauseOnExceptions;
michael@0 1123 this._options.ignoreCaughtExceptions = aRequest.ignoreCaughtExceptions;
michael@0 1124 this.maybePauseOnExceptions();
michael@0 1125 this._maybeListenToEvents(aRequest);
michael@0 1126 }
michael@0 1127
michael@0 1128 let packet = this._resumed();
michael@0 1129 this._popThreadPause();
michael@0 1130 return packet;
michael@0 1131 }, error => {
michael@0 1132 return error instanceof Error
michael@0 1133 ? { error: "unknownError",
michael@0 1134 message: DevToolsUtils.safeErrorString(error) }
michael@0 1135 // It is a known error, and the promise was rejected with an error
michael@0 1136 // packet.
michael@0 1137 : error;
michael@0 1138 });
michael@0 1139 },
michael@0 1140
michael@0 1141 /**
michael@0 1142 * Spin up a nested event loop so we can synchronously resolve a promise.
michael@0 1143 *
michael@0 1144 * @param aPromise
michael@0 1145 * The promise we want to resolve.
michael@0 1146 * @returns The promise's resolution.
michael@0 1147 */
michael@0 1148 synchronize: function(aPromise) {
michael@0 1149 let needNest = true;
michael@0 1150 let eventLoop;
michael@0 1151 let returnVal;
michael@0 1152
michael@0 1153 aPromise
michael@0 1154 .then((aResolvedVal) => {
michael@0 1155 needNest = false;
michael@0 1156 returnVal = aResolvedVal;
michael@0 1157 })
michael@0 1158 .then(null, (aError) => {
michael@0 1159 reportError(aError, "Error inside synchronize:");
michael@0 1160 })
michael@0 1161 .then(() => {
michael@0 1162 if (eventLoop) {
michael@0 1163 eventLoop.resolve();
michael@0 1164 }
michael@0 1165 });
michael@0 1166
michael@0 1167 if (needNest) {
michael@0 1168 eventLoop = this._nestedEventLoops.push();
michael@0 1169 eventLoop.enter();
michael@0 1170 }
michael@0 1171
michael@0 1172 return returnVal;
michael@0 1173 },
michael@0 1174
michael@0 1175 /**
michael@0 1176 * Set the debugging hook to pause on exceptions if configured to do so.
michael@0 1177 */
michael@0 1178 maybePauseOnExceptions: function() {
michael@0 1179 if (this._options.pauseOnExceptions) {
michael@0 1180 this.dbg.onExceptionUnwind = this.onExceptionUnwind.bind(this);
michael@0 1181 }
michael@0 1182 },
michael@0 1183
michael@0 1184 /**
michael@0 1185 * A listener that gets called for every event fired on the page, when a list
michael@0 1186 * of interesting events was provided with the pauseOnDOMEvents property. It
michael@0 1187 * is used to set server-managed breakpoints on any existing event listeners
michael@0 1188 * for those events.
michael@0 1189 *
michael@0 1190 * @param Event event
michael@0 1191 * The event that was fired.
michael@0 1192 */
michael@0 1193 _allEventsListener: function(event) {
michael@0 1194 if (this._pauseOnDOMEvents == "*" ||
michael@0 1195 this._pauseOnDOMEvents.indexOf(event.type) != -1) {
michael@0 1196 for (let listener of this._getAllEventListeners(event.target)) {
michael@0 1197 if (event.type == listener.type || this._pauseOnDOMEvents == "*") {
michael@0 1198 this._breakOnEnter(listener.script);
michael@0 1199 }
michael@0 1200 }
michael@0 1201 }
michael@0 1202 },
michael@0 1203
michael@0 1204 /**
michael@0 1205 * Return an array containing all the event listeners attached to the
michael@0 1206 * specified event target and its ancestors in the event target chain.
michael@0 1207 *
michael@0 1208 * @param EventTarget eventTarget
michael@0 1209 * The target the event was dispatched on.
michael@0 1210 * @returns Array
michael@0 1211 */
michael@0 1212 _getAllEventListeners: function(eventTarget) {
michael@0 1213 let els = Cc["@mozilla.org/eventlistenerservice;1"]
michael@0 1214 .getService(Ci.nsIEventListenerService);
michael@0 1215
michael@0 1216 let targets = els.getEventTargetChainFor(eventTarget);
michael@0 1217 let listeners = [];
michael@0 1218
michael@0 1219 for (let target of targets) {
michael@0 1220 let handlers = els.getListenerInfoFor(target);
michael@0 1221 for (let handler of handlers) {
michael@0 1222 // Null is returned for all-events handlers, and native event listeners
michael@0 1223 // don't provide any listenerObject, which makes them not that useful to
michael@0 1224 // a JS debugger.
michael@0 1225 if (!handler || !handler.listenerObject || !handler.type)
michael@0 1226 continue;
michael@0 1227 // Create a listener-like object suitable for our purposes.
michael@0 1228 let l = Object.create(null);
michael@0 1229 l.type = handler.type;
michael@0 1230 let listener = handler.listenerObject;
michael@0 1231 l.script = this.globalDebugObject.makeDebuggeeValue(listener).script;
michael@0 1232 // Chrome listeners won't be converted to debuggee values, since their
michael@0 1233 // compartment is not added as a debuggee.
michael@0 1234 if (!l.script)
michael@0 1235 continue;
michael@0 1236 listeners.push(l);
michael@0 1237 }
michael@0 1238 }
michael@0 1239 return listeners;
michael@0 1240 },
michael@0 1241
michael@0 1242 /**
michael@0 1243 * Set a breakpoint on the first bytecode offset in the provided script.
michael@0 1244 */
michael@0 1245 _breakOnEnter: function(script) {
michael@0 1246 let offsets = script.getAllOffsets();
michael@0 1247 for (let line = 0, n = offsets.length; line < n; line++) {
michael@0 1248 if (offsets[line]) {
michael@0 1249 let location = { url: script.url, line: line };
michael@0 1250 let resp = this._createAndStoreBreakpoint(location);
michael@0 1251 dbg_assert(!resp.actualLocation, "No actualLocation should be returned");
michael@0 1252 if (resp.error) {
michael@0 1253 reportError(new Error("Unable to set breakpoint on event listener"));
michael@0 1254 return;
michael@0 1255 }
michael@0 1256 let bp = this.breakpointStore.getBreakpoint(location);
michael@0 1257 let bpActor = bp.actor;
michael@0 1258 dbg_assert(bp, "Breakpoint must exist");
michael@0 1259 dbg_assert(bpActor, "Breakpoint actor must be created");
michael@0 1260 this._hiddenBreakpoints.set(bpActor.actorID, bpActor);
michael@0 1261 break;
michael@0 1262 }
michael@0 1263 }
michael@0 1264 },
michael@0 1265
michael@0 1266 /**
michael@0 1267 * Helper method that returns the next frame when stepping.
michael@0 1268 */
michael@0 1269 _getNextStepFrame: function (aFrame) {
michael@0 1270 let stepFrame = aFrame.reportedPop ? aFrame.older : aFrame;
michael@0 1271 if (!stepFrame || !stepFrame.script) {
michael@0 1272 stepFrame = null;
michael@0 1273 }
michael@0 1274 return stepFrame;
michael@0 1275 },
michael@0 1276
michael@0 1277 onClientEvaluate: function (aRequest) {
michael@0 1278 if (this.state !== "paused") {
michael@0 1279 return { error: "wrongState",
michael@0 1280 message: "Debuggee must be paused to evaluate code." };
michael@0 1281 }
michael@0 1282
michael@0 1283 let frame = this._requestFrame(aRequest.frame);
michael@0 1284 if (!frame) {
michael@0 1285 return { error: "unknownFrame",
michael@0 1286 message: "Evaluation frame not found" };
michael@0 1287 }
michael@0 1288
michael@0 1289 if (!frame.environment) {
michael@0 1290 return { error: "notDebuggee",
michael@0 1291 message: "cannot access the environment of this frame." };
michael@0 1292 }
michael@0 1293
michael@0 1294 let youngest = this.youngestFrame;
michael@0 1295
michael@0 1296 // Put ourselves back in the running state and inform the client.
michael@0 1297 let resumedPacket = this._resumed();
michael@0 1298 this.conn.send(resumedPacket);
michael@0 1299
michael@0 1300 // Run the expression.
michael@0 1301 // XXX: test syntax errors
michael@0 1302 let completion = frame.eval(aRequest.expression);
michael@0 1303
michael@0 1304 // Put ourselves back in the pause state.
michael@0 1305 let packet = this._paused(youngest);
michael@0 1306 packet.why = { type: "clientEvaluated",
michael@0 1307 frameFinished: this.createProtocolCompletionValue(completion) };
michael@0 1308
michael@0 1309 // Return back to our previous pause's event loop.
michael@0 1310 return packet;
michael@0 1311 },
michael@0 1312
michael@0 1313 onFrames: function (aRequest) {
michael@0 1314 if (this.state !== "paused") {
michael@0 1315 return { error: "wrongState",
michael@0 1316 message: "Stack frames are only available while the debuggee is paused."};
michael@0 1317 }
michael@0 1318
michael@0 1319 let start = aRequest.start ? aRequest.start : 0;
michael@0 1320 let count = aRequest.count;
michael@0 1321
michael@0 1322 // Find the starting frame...
michael@0 1323 let frame = this.youngestFrame;
michael@0 1324 let i = 0;
michael@0 1325 while (frame && (i < start)) {
michael@0 1326 frame = frame.older;
michael@0 1327 i++;
michael@0 1328 }
michael@0 1329
michael@0 1330 // Return request.count frames, or all remaining
michael@0 1331 // frames if count is not defined.
michael@0 1332 let frames = [];
michael@0 1333 let promises = [];
michael@0 1334 for (; frame && (!count || i < (start + count)); i++, frame=frame.older) {
michael@0 1335 let form = this._createFrameActor(frame).form();
michael@0 1336 form.depth = i;
michael@0 1337 frames.push(form);
michael@0 1338
michael@0 1339 let promise = this.sources.getOriginalLocation(form.where)
michael@0 1340 .then((aOrigLocation) => {
michael@0 1341 form.where = aOrigLocation;
michael@0 1342 let source = this.sources.source({ url: form.where.url });
michael@0 1343 if (source) {
michael@0 1344 form.source = source.form();
michael@0 1345 }
michael@0 1346 });
michael@0 1347 promises.push(promise);
michael@0 1348 }
michael@0 1349
michael@0 1350 return all(promises).then(function () {
michael@0 1351 return { frames: frames };
michael@0 1352 });
michael@0 1353 },
michael@0 1354
michael@0 1355 onReleaseMany: function (aRequest) {
michael@0 1356 if (!aRequest.actors) {
michael@0 1357 return { error: "missingParameter",
michael@0 1358 message: "no actors were specified" };
michael@0 1359 }
michael@0 1360
michael@0 1361 let res;
michael@0 1362 for each (let actorID in aRequest.actors) {
michael@0 1363 let actor = this.threadLifetimePool.get(actorID);
michael@0 1364 if (!actor) {
michael@0 1365 if (!res) {
michael@0 1366 res = { error: "notReleasable",
michael@0 1367 message: "Only thread-lifetime actors can be released." };
michael@0 1368 }
michael@0 1369 continue;
michael@0 1370 }
michael@0 1371 actor.onRelease();
michael@0 1372 }
michael@0 1373 return res ? res : {};
michael@0 1374 },
michael@0 1375
michael@0 1376 /**
michael@0 1377 * Handle a protocol request to set a breakpoint.
michael@0 1378 */
michael@0 1379 onSetBreakpoint: function (aRequest) {
michael@0 1380 if (this.state !== "paused") {
michael@0 1381 return { error: "wrongState",
michael@0 1382 message: "Breakpoints can only be set while the debuggee is paused."};
michael@0 1383 }
michael@0 1384
michael@0 1385 let { url: originalSource,
michael@0 1386 line: originalLine,
michael@0 1387 column: originalColumn } = aRequest.location;
michael@0 1388
michael@0 1389 let locationPromise = this.sources.getGeneratedLocation(aRequest.location);
michael@0 1390 return locationPromise.then(({url, line, column}) => {
michael@0 1391 if (line == null ||
michael@0 1392 line < 0 ||
michael@0 1393 this.dbg.findScripts({ url: url }).length == 0) {
michael@0 1394 return {
michael@0 1395 error: "noScript",
michael@0 1396 message: "Requested setting a breakpoint on "
michael@0 1397 + url + ":" + line
michael@0 1398 + (column != null ? ":" + column : "")
michael@0 1399 + " but there is no Debugger.Script at that location"
michael@0 1400 };
michael@0 1401 }
michael@0 1402
michael@0 1403 let response = this._createAndStoreBreakpoint({
michael@0 1404 url: url,
michael@0 1405 line: line,
michael@0 1406 column: column,
michael@0 1407 condition: aRequest.condition
michael@0 1408 });
michael@0 1409 // If the original location of our generated location is different from
michael@0 1410 // the original location we attempted to set the breakpoint on, we will
michael@0 1411 // need to know so that we can set actualLocation on the response.
michael@0 1412 let originalLocation = this.sources.getOriginalLocation({
michael@0 1413 url: url,
michael@0 1414 line: line,
michael@0 1415 column: column
michael@0 1416 });
michael@0 1417
michael@0 1418 return all([response, originalLocation])
michael@0 1419 .then(([aResponse, {url, line}]) => {
michael@0 1420 if (aResponse.actualLocation) {
michael@0 1421 let actualOrigLocation = this.sources.getOriginalLocation(aResponse.actualLocation);
michael@0 1422 return actualOrigLocation.then(({ url, line, column }) => {
michael@0 1423 if (url !== originalSource
michael@0 1424 || line !== originalLine
michael@0 1425 || column !== originalColumn) {
michael@0 1426 aResponse.actualLocation = {
michael@0 1427 url: url,
michael@0 1428 line: line,
michael@0 1429 column: column
michael@0 1430 };
michael@0 1431 }
michael@0 1432 return aResponse;
michael@0 1433 });
michael@0 1434 }
michael@0 1435
michael@0 1436 if (url !== originalSource || line !== originalLine) {
michael@0 1437 aResponse.actualLocation = { url: url, line: line };
michael@0 1438 }
michael@0 1439
michael@0 1440 return aResponse;
michael@0 1441 });
michael@0 1442 });
michael@0 1443 },
michael@0 1444
michael@0 1445 /**
michael@0 1446 * Create a breakpoint at the specified location and store it in the
michael@0 1447 * cache. Takes ownership of `aLocation`.
michael@0 1448 *
michael@0 1449 * @param Object aLocation
michael@0 1450 * An object of the form { url, line[, column] }
michael@0 1451 */
michael@0 1452 _createAndStoreBreakpoint: function (aLocation) {
michael@0 1453 // Add the breakpoint to the store for later reuse, in case it belongs to a
michael@0 1454 // script that hasn't appeared yet.
michael@0 1455 this.breakpointStore.addBreakpoint(aLocation);
michael@0 1456 return this._setBreakpoint(aLocation);
michael@0 1457 },
michael@0 1458
michael@0 1459 /**
michael@0 1460 * Set a breakpoint using the jsdbg2 API. If the line on which the breakpoint
michael@0 1461 * is being set contains no code, then the breakpoint will slide down to the
michael@0 1462 * next line that has runnable code. In this case the server breakpoint cache
michael@0 1463 * will be updated, so callers that iterate over the breakpoint cache should
michael@0 1464 * take that into account.
michael@0 1465 *
michael@0 1466 * @param object aLocation
michael@0 1467 * The location of the breakpoint (in the generated source, if source
michael@0 1468 * mapping).
michael@0 1469 */
michael@0 1470 _setBreakpoint: function (aLocation) {
michael@0 1471 let actor;
michael@0 1472 let storedBp = this.breakpointStore.getBreakpoint(aLocation);
michael@0 1473 if (storedBp.actor) {
michael@0 1474 actor = storedBp.actor;
michael@0 1475 actor.condition = aLocation.condition;
michael@0 1476 } else {
michael@0 1477 storedBp.actor = actor = new BreakpointActor(this, {
michael@0 1478 url: aLocation.url,
michael@0 1479 line: aLocation.line,
michael@0 1480 column: aLocation.column,
michael@0 1481 condition: aLocation.condition
michael@0 1482 });
michael@0 1483 this.threadLifetimePool.addActor(actor);
michael@0 1484 }
michael@0 1485
michael@0 1486 // Find all scripts matching the given location
michael@0 1487 let scripts = this.dbg.findScripts(aLocation);
michael@0 1488 if (scripts.length == 0) {
michael@0 1489 return {
michael@0 1490 error: "noScript",
michael@0 1491 message: "Requested setting a breakpoint on "
michael@0 1492 + aLocation.url + ":" + aLocation.line
michael@0 1493 + (aLocation.column != null ? ":" + aLocation.column : "")
michael@0 1494 + " but there is no Debugger.Script at that location",
michael@0 1495 actor: actor.actorID
michael@0 1496 };
michael@0 1497 }
michael@0 1498
michael@0 1499 /**
michael@0 1500 * For each script, if the given line has at least one entry point, set a
michael@0 1501 * breakpoint on the bytecode offets for each of them.
michael@0 1502 */
michael@0 1503
michael@0 1504 // Debugger.Script -> array of offset mappings
michael@0 1505 let scriptsAndOffsetMappings = new Map();
michael@0 1506
michael@0 1507 for (let script of scripts) {
michael@0 1508 this._findClosestOffsetMappings(aLocation,
michael@0 1509 script,
michael@0 1510 scriptsAndOffsetMappings);
michael@0 1511 }
michael@0 1512
michael@0 1513 if (scriptsAndOffsetMappings.size > 0) {
michael@0 1514 for (let [script, mappings] of scriptsAndOffsetMappings) {
michael@0 1515 for (let offsetMapping of mappings) {
michael@0 1516 script.setBreakpoint(offsetMapping.offset, actor);
michael@0 1517 }
michael@0 1518 actor.addScript(script, this);
michael@0 1519 }
michael@0 1520
michael@0 1521 return {
michael@0 1522 actor: actor.actorID
michael@0 1523 };
michael@0 1524 }
michael@0 1525
michael@0 1526 /**
michael@0 1527 * If we get here, no breakpoint was set. This is because the given line
michael@0 1528 * has no entry points, for example because it is empty. As a fallback
michael@0 1529 * strategy, we try to set the breakpoint on the smallest line greater
michael@0 1530 * than or equal to the given line that as at least one entry point.
michael@0 1531 */
michael@0 1532
michael@0 1533 // Find all innermost scripts matching the given location
michael@0 1534 let scripts = this.dbg.findScripts({
michael@0 1535 url: aLocation.url,
michael@0 1536 line: aLocation.line,
michael@0 1537 innermost: true
michael@0 1538 });
michael@0 1539
michael@0 1540 /**
michael@0 1541 * For each innermost script, look for the smallest line greater than or
michael@0 1542 * equal to the given line that has one or more entry points. If found, set
michael@0 1543 * a breakpoint on the bytecode offset for each of its entry points.
michael@0 1544 */
michael@0 1545 let actualLocation;
michael@0 1546 let found = false;
michael@0 1547 for (let script of scripts) {
michael@0 1548 let offsets = script.getAllOffsets();
michael@0 1549 for (let line = aLocation.line; line < offsets.length; ++line) {
michael@0 1550 if (offsets[line]) {
michael@0 1551 for (let offset of offsets[line]) {
michael@0 1552 script.setBreakpoint(offset, actor);
michael@0 1553 }
michael@0 1554 actor.addScript(script, this);
michael@0 1555 if (!actualLocation) {
michael@0 1556 actualLocation = {
michael@0 1557 url: aLocation.url,
michael@0 1558 line: line
michael@0 1559 };
michael@0 1560 }
michael@0 1561 found = true;
michael@0 1562 break;
michael@0 1563 }
michael@0 1564 }
michael@0 1565 }
michael@0 1566 if (found) {
michael@0 1567 let existingBp = this.breakpointStore.hasBreakpoint(actualLocation);
michael@0 1568
michael@0 1569 if (existingBp && existingBp.actor) {
michael@0 1570 /**
michael@0 1571 * We already have a breakpoint actor for the actual location, so
michael@0 1572 * actor we created earlier is now redundant. Delete it, update the
michael@0 1573 * breakpoint store, and return the actor for the actual location.
michael@0 1574 */
michael@0 1575 actor.onDelete();
michael@0 1576 this.breakpointStore.removeBreakpoint(aLocation);
michael@0 1577 return {
michael@0 1578 actor: existingBp.actor.actorID,
michael@0 1579 actualLocation: actualLocation
michael@0 1580 };
michael@0 1581 } else {
michael@0 1582 /**
michael@0 1583 * We don't have a breakpoint actor for the actual location yet.
michael@0 1584 * Instead or creating a new actor, reuse the actor we created earlier,
michael@0 1585 * and update the breakpoint store.
michael@0 1586 */
michael@0 1587 actor.location = actualLocation;
michael@0 1588 this.breakpointStore.addBreakpoint({
michael@0 1589 actor: actor,
michael@0 1590 url: actualLocation.url,
michael@0 1591 line: actualLocation.line,
michael@0 1592 column: actualLocation.column
michael@0 1593 });
michael@0 1594 this.breakpointStore.removeBreakpoint(aLocation);
michael@0 1595 return {
michael@0 1596 actor: actor.actorID,
michael@0 1597 actualLocation: actualLocation
michael@0 1598 };
michael@0 1599 }
michael@0 1600 }
michael@0 1601
michael@0 1602 /**
michael@0 1603 * If we get here, no line matching the given line was found, so just
michael@0 1604 * fail epically.
michael@0 1605 */
michael@0 1606 return {
michael@0 1607 error: "noCodeAtLineColumn",
michael@0 1608 actor: actor.actorID
michael@0 1609 };
michael@0 1610 },
michael@0 1611
michael@0 1612 /**
michael@0 1613 * Find all of the offset mappings associated with `aScript` that are closest
michael@0 1614 * to `aTargetLocation`. If new offset mappings are found that are closer to
michael@0 1615 * `aTargetOffset` than the existing offset mappings inside
michael@0 1616 * `aScriptsAndOffsetMappings`, we empty that map and only consider the
michael@0 1617 * closest offset mappings. If there is no column in `aTargetLocation`, we add
michael@0 1618 * all offset mappings that are on the given line.
michael@0 1619 *
michael@0 1620 * @param Object aTargetLocation
michael@0 1621 * An object of the form { url, line[, column] }.
michael@0 1622 * @param Debugger.Script aScript
michael@0 1623 * The script in which we are searching for offsets.
michael@0 1624 * @param Map aScriptsAndOffsetMappings
michael@0 1625 * A Map object which maps Debugger.Script instances to arrays of
michael@0 1626 * offset mappings. This is an out param.
michael@0 1627 */
michael@0 1628 _findClosestOffsetMappings: function (aTargetLocation,
michael@0 1629 aScript,
michael@0 1630 aScriptsAndOffsetMappings) {
michael@0 1631 // If we are given a column, we will try and break only at that location,
michael@0 1632 // otherwise we will break anytime we get on that line.
michael@0 1633
michael@0 1634 if (aTargetLocation.column == null) {
michael@0 1635 let offsetMappings = aScript.getLineOffsets(aTargetLocation.line)
michael@0 1636 .map(o => ({
michael@0 1637 line: aTargetLocation.line,
michael@0 1638 offset: o
michael@0 1639 }));
michael@0 1640 if (offsetMappings.length) {
michael@0 1641 aScriptsAndOffsetMappings.set(aScript, offsetMappings);
michael@0 1642 }
michael@0 1643 return;
michael@0 1644 }
michael@0 1645
michael@0 1646 let offsetMappings = aScript.getAllColumnOffsets()
michael@0 1647 .filter(({ lineNumber }) => lineNumber === aTargetLocation.line);
michael@0 1648
michael@0 1649 // Attempt to find the current closest offset distance from the target
michael@0 1650 // location by grabbing any offset mapping in the map by doing one iteration
michael@0 1651 // and then breaking (they all have the same distance from the target
michael@0 1652 // location).
michael@0 1653 let closestDistance = Infinity;
michael@0 1654 if (aScriptsAndOffsetMappings.size) {
michael@0 1655 for (let mappings of aScriptsAndOffsetMappings.values()) {
michael@0 1656 closestDistance = Math.abs(aTargetLocation.column - mappings[0].columnNumber);
michael@0 1657 break;
michael@0 1658 }
michael@0 1659 }
michael@0 1660
michael@0 1661 for (let mapping of offsetMappings) {
michael@0 1662 let currentDistance = Math.abs(aTargetLocation.column - mapping.columnNumber);
michael@0 1663
michael@0 1664 if (currentDistance > closestDistance) {
michael@0 1665 continue;
michael@0 1666 } else if (currentDistance < closestDistance) {
michael@0 1667 closestDistance = currentDistance;
michael@0 1668 aScriptsAndOffsetMappings.clear();
michael@0 1669 aScriptsAndOffsetMappings.set(aScript, [mapping]);
michael@0 1670 } else {
michael@0 1671 if (!aScriptsAndOffsetMappings.has(aScript)) {
michael@0 1672 aScriptsAndOffsetMappings.set(aScript, []);
michael@0 1673 }
michael@0 1674 aScriptsAndOffsetMappings.get(aScript).push(mapping);
michael@0 1675 }
michael@0 1676 }
michael@0 1677 },
michael@0 1678
michael@0 1679 /**
michael@0 1680 * Get the script and source lists from the debugger.
michael@0 1681 */
michael@0 1682 _discoverSources: function () {
michael@0 1683 // Only get one script per url.
michael@0 1684 const sourcesToScripts = new Map();
michael@0 1685 for (let s of this.dbg.findScripts()) {
michael@0 1686 if (s.source) {
michael@0 1687 sourcesToScripts.set(s.source, s);
michael@0 1688 }
michael@0 1689 }
michael@0 1690
michael@0 1691 return all([this.sources.sourcesForScript(script)
michael@0 1692 for (script of sourcesToScripts.values())]);
michael@0 1693 },
michael@0 1694
michael@0 1695 onSources: function (aRequest) {
michael@0 1696 return this._discoverSources().then(() => {
michael@0 1697 return {
michael@0 1698 sources: [s.form() for (s of this.sources.iter())]
michael@0 1699 };
michael@0 1700 });
michael@0 1701 },
michael@0 1702
michael@0 1703 /**
michael@0 1704 * Disassociate all breakpoint actors from their scripts and clear the
michael@0 1705 * breakpoint handlers. This method can be used when the thread actor intends
michael@0 1706 * to keep the breakpoint store, but needs to clear any actual breakpoints,
michael@0 1707 * e.g. due to a page navigation. This way the breakpoint actors' script
michael@0 1708 * caches won't hold on to the Debugger.Script objects leaking memory.
michael@0 1709 */
michael@0 1710 disableAllBreakpoints: function () {
michael@0 1711 for (let bp of this.breakpointStore.findBreakpoints()) {
michael@0 1712 if (bp.actor) {
michael@0 1713 bp.actor.removeScripts();
michael@0 1714 }
michael@0 1715 }
michael@0 1716 },
michael@0 1717
michael@0 1718 /**
michael@0 1719 * Handle a protocol request to pause the debuggee.
michael@0 1720 */
michael@0 1721 onInterrupt: function (aRequest) {
michael@0 1722 if (this.state == "exited") {
michael@0 1723 return { type: "exited" };
michael@0 1724 } else if (this.state == "paused") {
michael@0 1725 // TODO: return the actual reason for the existing pause.
michael@0 1726 return { type: "paused", why: { type: "alreadyPaused" } };
michael@0 1727 } else if (this.state != "running") {
michael@0 1728 return { error: "wrongState",
michael@0 1729 message: "Received interrupt request in " + this.state +
michael@0 1730 " state." };
michael@0 1731 }
michael@0 1732
michael@0 1733 try {
michael@0 1734 // Put ourselves in the paused state.
michael@0 1735 let packet = this._paused();
michael@0 1736 if (!packet) {
michael@0 1737 return { error: "notInterrupted" };
michael@0 1738 }
michael@0 1739 packet.why = { type: "interrupted" };
michael@0 1740
michael@0 1741 // Send the response to the interrupt request now (rather than
michael@0 1742 // returning it), because we're going to start a nested event loop
michael@0 1743 // here.
michael@0 1744 this.conn.send(packet);
michael@0 1745
michael@0 1746 // Start a nested event loop.
michael@0 1747 this._pushThreadPause();
michael@0 1748
michael@0 1749 // We already sent a response to this request, don't send one
michael@0 1750 // now.
michael@0 1751 return null;
michael@0 1752 } catch (e) {
michael@0 1753 reportError(e);
michael@0 1754 return { error: "notInterrupted", message: e.toString() };
michael@0 1755 }
michael@0 1756 },
michael@0 1757
michael@0 1758 /**
michael@0 1759 * Handle a protocol request to retrieve all the event listeners on the page.
michael@0 1760 */
michael@0 1761 onEventListeners: function (aRequest) {
michael@0 1762 // This request is only supported in content debugging.
michael@0 1763 if (!this.global) {
michael@0 1764 return {
michael@0 1765 error: "notImplemented",
michael@0 1766 message: "eventListeners request is only supported in content debugging"
michael@0 1767 };
michael@0 1768 }
michael@0 1769
michael@0 1770 let els = Cc["@mozilla.org/eventlistenerservice;1"]
michael@0 1771 .getService(Ci.nsIEventListenerService);
michael@0 1772
michael@0 1773 let nodes = this.global.document.getElementsByTagName("*");
michael@0 1774 nodes = [this.global].concat([].slice.call(nodes));
michael@0 1775 let listeners = [];
michael@0 1776
michael@0 1777 for (let node of nodes) {
michael@0 1778 let handlers = els.getListenerInfoFor(node);
michael@0 1779
michael@0 1780 for (let handler of handlers) {
michael@0 1781 // Create a form object for serializing the listener via the protocol.
michael@0 1782 let listenerForm = Object.create(null);
michael@0 1783 let listener = handler.listenerObject;
michael@0 1784 // Native event listeners don't provide any listenerObject or type and
michael@0 1785 // are not that useful to a JS debugger.
michael@0 1786 if (!listener || !handler.type) {
michael@0 1787 continue;
michael@0 1788 }
michael@0 1789
michael@0 1790 // There will be no tagName if the event listener is set on the window.
michael@0 1791 let selector = node.tagName ? findCssSelector(node) : "window";
michael@0 1792 let nodeDO = this.globalDebugObject.makeDebuggeeValue(node);
michael@0 1793 listenerForm.node = {
michael@0 1794 selector: selector,
michael@0 1795 object: this.createValueGrip(nodeDO)
michael@0 1796 };
michael@0 1797 listenerForm.type = handler.type;
michael@0 1798 listenerForm.capturing = handler.capturing;
michael@0 1799 listenerForm.allowsUntrusted = handler.allowsUntrusted;
michael@0 1800 listenerForm.inSystemEventGroup = handler.inSystemEventGroup;
michael@0 1801 listenerForm.isEventHandler = !!node["on" + listenerForm.type];
michael@0 1802 // Get the Debugger.Object for the listener object.
michael@0 1803 let listenerDO = this.globalDebugObject.makeDebuggeeValue(listener);
michael@0 1804 listenerForm.function = this.createValueGrip(listenerDO);
michael@0 1805 listeners.push(listenerForm);
michael@0 1806 }
michael@0 1807 }
michael@0 1808 return { listeners: listeners };
michael@0 1809 },
michael@0 1810
michael@0 1811 /**
michael@0 1812 * Return the Debug.Frame for a frame mentioned by the protocol.
michael@0 1813 */
michael@0 1814 _requestFrame: function (aFrameID) {
michael@0 1815 if (!aFrameID) {
michael@0 1816 return this.youngestFrame;
michael@0 1817 }
michael@0 1818
michael@0 1819 if (this._framePool.has(aFrameID)) {
michael@0 1820 return this._framePool.get(aFrameID).frame;
michael@0 1821 }
michael@0 1822
michael@0 1823 return undefined;
michael@0 1824 },
michael@0 1825
michael@0 1826 _paused: function (aFrame) {
michael@0 1827 // We don't handle nested pauses correctly. Don't try - if we're
michael@0 1828 // paused, just continue running whatever code triggered the pause.
michael@0 1829 // We don't want to actually have nested pauses (although we
michael@0 1830 // have nested event loops). If code runs in the debuggee during
michael@0 1831 // a pause, it should cause the actor to resume (dropping
michael@0 1832 // pause-lifetime actors etc) and then repause when complete.
michael@0 1833
michael@0 1834 if (this.state === "paused") {
michael@0 1835 return undefined;
michael@0 1836 }
michael@0 1837
michael@0 1838 // Clear stepping hooks.
michael@0 1839 this.dbg.onEnterFrame = undefined;
michael@0 1840 this.dbg.onExceptionUnwind = undefined;
michael@0 1841 if (aFrame) {
michael@0 1842 aFrame.onStep = undefined;
michael@0 1843 aFrame.onPop = undefined;
michael@0 1844 }
michael@0 1845 // Clear DOM event breakpoints.
michael@0 1846 // XPCShell tests don't use actual DOM windows for globals and cause
michael@0 1847 // removeListenerForAllEvents to throw.
michael@0 1848 if (this.global && !this.global.toString().contains("Sandbox")) {
michael@0 1849 let els = Cc["@mozilla.org/eventlistenerservice;1"]
michael@0 1850 .getService(Ci.nsIEventListenerService);
michael@0 1851 els.removeListenerForAllEvents(this.global, this._allEventsListener, true);
michael@0 1852 for (let [,bp] of this._hiddenBreakpoints) {
michael@0 1853 bp.onDelete();
michael@0 1854 }
michael@0 1855 this._hiddenBreakpoints.clear();
michael@0 1856 }
michael@0 1857
michael@0 1858 this._state = "paused";
michael@0 1859
michael@0 1860 // Create the actor pool that will hold the pause actor and its
michael@0 1861 // children.
michael@0 1862 dbg_assert(!this._pausePool, "No pause pool should exist yet");
michael@0 1863 this._pausePool = new ActorPool(this.conn);
michael@0 1864 this.conn.addActorPool(this._pausePool);
michael@0 1865
michael@0 1866 // Give children of the pause pool a quick link back to the
michael@0 1867 // thread...
michael@0 1868 this._pausePool.threadActor = this;
michael@0 1869
michael@0 1870 // Create the pause actor itself...
michael@0 1871 dbg_assert(!this._pauseActor, "No pause actor should exist yet");
michael@0 1872 this._pauseActor = new PauseActor(this._pausePool);
michael@0 1873 this._pausePool.addActor(this._pauseActor);
michael@0 1874
michael@0 1875 // Update the list of frames.
michael@0 1876 let poppedFrames = this._updateFrames();
michael@0 1877
michael@0 1878 // Send off the paused packet and spin an event loop.
michael@0 1879 let packet = { from: this.actorID,
michael@0 1880 type: "paused",
michael@0 1881 actor: this._pauseActor.actorID };
michael@0 1882 if (aFrame) {
michael@0 1883 packet.frame = this._createFrameActor(aFrame).form();
michael@0 1884 }
michael@0 1885
michael@0 1886 if (poppedFrames) {
michael@0 1887 packet.poppedFrames = poppedFrames;
michael@0 1888 }
michael@0 1889
michael@0 1890 return packet;
michael@0 1891 },
michael@0 1892
michael@0 1893 _resumed: function () {
michael@0 1894 this._state = "running";
michael@0 1895
michael@0 1896 // Drop the actors in the pause actor pool.
michael@0 1897 this.conn.removeActorPool(this._pausePool);
michael@0 1898
michael@0 1899 this._pausePool = null;
michael@0 1900 this._pauseActor = null;
michael@0 1901
michael@0 1902 return { from: this.actorID, type: "resumed" };
michael@0 1903 },
michael@0 1904
michael@0 1905 /**
michael@0 1906 * Expire frame actors for frames that have been popped.
michael@0 1907 *
michael@0 1908 * @returns A list of actor IDs whose frames have been popped.
michael@0 1909 */
michael@0 1910 _updateFrames: function () {
michael@0 1911 let popped = [];
michael@0 1912
michael@0 1913 // Create the actor pool that will hold the still-living frames.
michael@0 1914 let framePool = new ActorPool(this.conn);
michael@0 1915 let frameList = [];
michael@0 1916
michael@0 1917 for each (let frameActor in this._frameActors) {
michael@0 1918 if (frameActor.frame.live) {
michael@0 1919 framePool.addActor(frameActor);
michael@0 1920 frameList.push(frameActor);
michael@0 1921 } else {
michael@0 1922 popped.push(frameActor.actorID);
michael@0 1923 }
michael@0 1924 }
michael@0 1925
michael@0 1926 // Remove the old frame actor pool, this will expire
michael@0 1927 // any actors that weren't added to the new pool.
michael@0 1928 if (this._framePool) {
michael@0 1929 this.conn.removeActorPool(this._framePool);
michael@0 1930 }
michael@0 1931
michael@0 1932 this._frameActors = frameList;
michael@0 1933 this._framePool = framePool;
michael@0 1934 this.conn.addActorPool(framePool);
michael@0 1935
michael@0 1936 return popped;
michael@0 1937 },
michael@0 1938
michael@0 1939 _createFrameActor: function (aFrame) {
michael@0 1940 if (aFrame.actor) {
michael@0 1941 return aFrame.actor;
michael@0 1942 }
michael@0 1943
michael@0 1944 let actor = new FrameActor(aFrame, this);
michael@0 1945 this._frameActors.push(actor);
michael@0 1946 this._framePool.addActor(actor);
michael@0 1947 aFrame.actor = actor;
michael@0 1948
michael@0 1949 return actor;
michael@0 1950 },
michael@0 1951
michael@0 1952 /**
michael@0 1953 * Create and return an environment actor that corresponds to the provided
michael@0 1954 * Debugger.Environment.
michael@0 1955 * @param Debugger.Environment aEnvironment
michael@0 1956 * The lexical environment we want to extract.
michael@0 1957 * @param object aPool
michael@0 1958 * The pool where the newly-created actor will be placed.
michael@0 1959 * @return The EnvironmentActor for aEnvironment or undefined for host
michael@0 1960 * functions or functions scoped to a non-debuggee global.
michael@0 1961 */
michael@0 1962 createEnvironmentActor: function (aEnvironment, aPool) {
michael@0 1963 if (!aEnvironment) {
michael@0 1964 return undefined;
michael@0 1965 }
michael@0 1966
michael@0 1967 if (aEnvironment.actor) {
michael@0 1968 return aEnvironment.actor;
michael@0 1969 }
michael@0 1970
michael@0 1971 let actor = new EnvironmentActor(aEnvironment, this);
michael@0 1972 aPool.addActor(actor);
michael@0 1973 aEnvironment.actor = actor;
michael@0 1974
michael@0 1975 return actor;
michael@0 1976 },
michael@0 1977
michael@0 1978 /**
michael@0 1979 * Create a grip for the given debuggee value. If the value is an
michael@0 1980 * object, will create an actor with the given lifetime.
michael@0 1981 */
michael@0 1982 createValueGrip: function (aValue, aPool=false) {
michael@0 1983 if (!aPool) {
michael@0 1984 aPool = this._pausePool;
michael@0 1985 }
michael@0 1986
michael@0 1987 switch (typeof aValue) {
michael@0 1988 case "boolean":
michael@0 1989 return aValue;
michael@0 1990 case "string":
michael@0 1991 if (this._stringIsLong(aValue)) {
michael@0 1992 return this.longStringGrip(aValue, aPool);
michael@0 1993 }
michael@0 1994 return aValue;
michael@0 1995 case "number":
michael@0 1996 if (aValue === Infinity) {
michael@0 1997 return { type: "Infinity" };
michael@0 1998 } else if (aValue === -Infinity) {
michael@0 1999 return { type: "-Infinity" };
michael@0 2000 } else if (Number.isNaN(aValue)) {
michael@0 2001 return { type: "NaN" };
michael@0 2002 } else if (!aValue && 1 / aValue === -Infinity) {
michael@0 2003 return { type: "-0" };
michael@0 2004 }
michael@0 2005 return aValue;
michael@0 2006 case "undefined":
michael@0 2007 return { type: "undefined" };
michael@0 2008 case "object":
michael@0 2009 if (aValue === null) {
michael@0 2010 return { type: "null" };
michael@0 2011 }
michael@0 2012 return this.objectGrip(aValue, aPool);
michael@0 2013 default:
michael@0 2014 dbg_assert(false, "Failed to provide a grip for: " + aValue);
michael@0 2015 return null;
michael@0 2016 }
michael@0 2017 },
michael@0 2018
michael@0 2019 /**
michael@0 2020 * Return a protocol completion value representing the given
michael@0 2021 * Debugger-provided completion value.
michael@0 2022 */
michael@0 2023 createProtocolCompletionValue: function (aCompletion) {
michael@0 2024 let protoValue = {};
michael@0 2025 if ("return" in aCompletion) {
michael@0 2026 protoValue.return = this.createValueGrip(aCompletion.return);
michael@0 2027 } else if ("yield" in aCompletion) {
michael@0 2028 protoValue.return = this.createValueGrip(aCompletion.yield);
michael@0 2029 } else if ("throw" in aCompletion) {
michael@0 2030 protoValue.throw = this.createValueGrip(aCompletion.throw);
michael@0 2031 } else {
michael@0 2032 protoValue.terminated = true;
michael@0 2033 }
michael@0 2034 return protoValue;
michael@0 2035 },
michael@0 2036
michael@0 2037 /**
michael@0 2038 * Create a grip for the given debuggee object.
michael@0 2039 *
michael@0 2040 * @param aValue Debugger.Object
michael@0 2041 * The debuggee object value.
michael@0 2042 * @param aPool ActorPool
michael@0 2043 * The actor pool where the new object actor will be added.
michael@0 2044 */
michael@0 2045 objectGrip: function (aValue, aPool) {
michael@0 2046 if (!aPool.objectActors) {
michael@0 2047 aPool.objectActors = new WeakMap();
michael@0 2048 }
michael@0 2049
michael@0 2050 if (aPool.objectActors.has(aValue)) {
michael@0 2051 return aPool.objectActors.get(aValue).grip();
michael@0 2052 } else if (this.threadLifetimePool.objectActors.has(aValue)) {
michael@0 2053 return this.threadLifetimePool.objectActors.get(aValue).grip();
michael@0 2054 }
michael@0 2055
michael@0 2056 let actor = new PauseScopedObjectActor(aValue, this);
michael@0 2057 aPool.addActor(actor);
michael@0 2058 aPool.objectActors.set(aValue, actor);
michael@0 2059 return actor.grip();
michael@0 2060 },
michael@0 2061
michael@0 2062 /**
michael@0 2063 * Create a grip for the given debuggee object with a pause lifetime.
michael@0 2064 *
michael@0 2065 * @param aValue Debugger.Object
michael@0 2066 * The debuggee object value.
michael@0 2067 */
michael@0 2068 pauseObjectGrip: function (aValue) {
michael@0 2069 if (!this._pausePool) {
michael@0 2070 throw "Object grip requested while not paused.";
michael@0 2071 }
michael@0 2072
michael@0 2073 return this.objectGrip(aValue, this._pausePool);
michael@0 2074 },
michael@0 2075
michael@0 2076 /**
michael@0 2077 * Extend the lifetime of the provided object actor to thread lifetime.
michael@0 2078 *
michael@0 2079 * @param aActor object
michael@0 2080 * The object actor.
michael@0 2081 */
michael@0 2082 threadObjectGrip: function (aActor) {
michael@0 2083 // We want to reuse the existing actor ID, so we just remove it from the
michael@0 2084 // current pool's weak map and then let pool.addActor do the rest.
michael@0 2085 aActor.registeredPool.objectActors.delete(aActor.obj);
michael@0 2086 this.threadLifetimePool.addActor(aActor);
michael@0 2087 this.threadLifetimePool.objectActors.set(aActor.obj, aActor);
michael@0 2088 },
michael@0 2089
michael@0 2090 /**
michael@0 2091 * Handle a protocol request to promote multiple pause-lifetime grips to
michael@0 2092 * thread-lifetime grips.
michael@0 2093 *
michael@0 2094 * @param aRequest object
michael@0 2095 * The protocol request object.
michael@0 2096 */
michael@0 2097 onThreadGrips: function (aRequest) {
michael@0 2098 if (this.state != "paused") {
michael@0 2099 return { error: "wrongState" };
michael@0 2100 }
michael@0 2101
michael@0 2102 if (!aRequest.actors) {
michael@0 2103 return { error: "missingParameter",
michael@0 2104 message: "no actors were specified" };
michael@0 2105 }
michael@0 2106
michael@0 2107 for (let actorID of aRequest.actors) {
michael@0 2108 let actor = this._pausePool.get(actorID);
michael@0 2109 if (actor) {
michael@0 2110 this.threadObjectGrip(actor);
michael@0 2111 }
michael@0 2112 }
michael@0 2113 return {};
michael@0 2114 },
michael@0 2115
michael@0 2116 /**
michael@0 2117 * Create a grip for the given string.
michael@0 2118 *
michael@0 2119 * @param aString String
michael@0 2120 * The string we are creating a grip for.
michael@0 2121 * @param aPool ActorPool
michael@0 2122 * The actor pool where the new actor will be added.
michael@0 2123 */
michael@0 2124 longStringGrip: function (aString, aPool) {
michael@0 2125 if (!aPool.longStringActors) {
michael@0 2126 aPool.longStringActors = {};
michael@0 2127 }
michael@0 2128
michael@0 2129 if (aPool.longStringActors.hasOwnProperty(aString)) {
michael@0 2130 return aPool.longStringActors[aString].grip();
michael@0 2131 }
michael@0 2132
michael@0 2133 let actor = new LongStringActor(aString, this);
michael@0 2134 aPool.addActor(actor);
michael@0 2135 aPool.longStringActors[aString] = actor;
michael@0 2136 return actor.grip();
michael@0 2137 },
michael@0 2138
michael@0 2139 /**
michael@0 2140 * Create a long string grip that is scoped to a pause.
michael@0 2141 *
michael@0 2142 * @param aString String
michael@0 2143 * The string we are creating a grip for.
michael@0 2144 */
michael@0 2145 pauseLongStringGrip: function (aString) {
michael@0 2146 return this.longStringGrip(aString, this._pausePool);
michael@0 2147 },
michael@0 2148
michael@0 2149 /**
michael@0 2150 * Create a long string grip that is scoped to a thread.
michael@0 2151 *
michael@0 2152 * @param aString String
michael@0 2153 * The string we are creating a grip for.
michael@0 2154 */
michael@0 2155 threadLongStringGrip: function (aString) {
michael@0 2156 return this.longStringGrip(aString, this._threadLifetimePool);
michael@0 2157 },
michael@0 2158
michael@0 2159 /**
michael@0 2160 * Returns true if the string is long enough to use a LongStringActor instead
michael@0 2161 * of passing the value directly over the protocol.
michael@0 2162 *
michael@0 2163 * @param aString String
michael@0 2164 * The string we are checking the length of.
michael@0 2165 */
michael@0 2166 _stringIsLong: function (aString) {
michael@0 2167 return aString.length >= DebuggerServer.LONG_STRING_LENGTH;
michael@0 2168 },
michael@0 2169
michael@0 2170 // JS Debugger API hooks.
michael@0 2171
michael@0 2172 /**
michael@0 2173 * A function that the engine calls when a call to a debug event hook,
michael@0 2174 * breakpoint handler, watchpoint handler, or similar function throws some
michael@0 2175 * exception.
michael@0 2176 *
michael@0 2177 * @param aException exception
michael@0 2178 * The exception that was thrown in the debugger code.
michael@0 2179 */
michael@0 2180 uncaughtExceptionHook: function (aException) {
michael@0 2181 dumpn("Got an exception: " + aException.message + "\n" + aException.stack);
michael@0 2182 },
michael@0 2183
michael@0 2184 /**
michael@0 2185 * A function that the engine calls when a debugger statement has been
michael@0 2186 * executed in the specified frame.
michael@0 2187 *
michael@0 2188 * @param aFrame Debugger.Frame
michael@0 2189 * The stack frame that contained the debugger statement.
michael@0 2190 */
michael@0 2191 onDebuggerStatement: function (aFrame) {
michael@0 2192 // Don't pause if we are currently stepping (in or over) or the frame is
michael@0 2193 // black-boxed.
michael@0 2194 const generatedLocation = getFrameLocation(aFrame);
michael@0 2195 const { url } = this.synchronize(this.sources.getOriginalLocation(
michael@0 2196 generatedLocation));
michael@0 2197
michael@0 2198 return this.sources.isBlackBoxed(url) || aFrame.onStep
michael@0 2199 ? undefined
michael@0 2200 : this._pauseAndRespond(aFrame, { type: "debuggerStatement" });
michael@0 2201 },
michael@0 2202
michael@0 2203 /**
michael@0 2204 * A function that the engine calls when an exception has been thrown and has
michael@0 2205 * propagated to the specified frame.
michael@0 2206 *
michael@0 2207 * @param aFrame Debugger.Frame
michael@0 2208 * The youngest remaining stack frame.
michael@0 2209 * @param aValue object
michael@0 2210 * The exception that was thrown.
michael@0 2211 */
michael@0 2212 onExceptionUnwind: function (aFrame, aValue) {
michael@0 2213 let willBeCaught = false;
michael@0 2214 for (let frame = aFrame; frame != null; frame = frame.older) {
michael@0 2215 if (frame.script.isInCatchScope(frame.offset)) {
michael@0 2216 willBeCaught = true;
michael@0 2217 break;
michael@0 2218 }
michael@0 2219 }
michael@0 2220
michael@0 2221 if (willBeCaught && this._options.ignoreCaughtExceptions) {
michael@0 2222 return undefined;
michael@0 2223 }
michael@0 2224
michael@0 2225 const generatedLocation = getFrameLocation(aFrame);
michael@0 2226 const { url } = this.synchronize(this.sources.getOriginalLocation(
michael@0 2227 generatedLocation));
michael@0 2228
michael@0 2229 if (this.sources.isBlackBoxed(url)) {
michael@0 2230 return undefined;
michael@0 2231 }
michael@0 2232
michael@0 2233 try {
michael@0 2234 let packet = this._paused(aFrame);
michael@0 2235 if (!packet) {
michael@0 2236 return undefined;
michael@0 2237 }
michael@0 2238
michael@0 2239 packet.why = { type: "exception",
michael@0 2240 exception: this.createValueGrip(aValue) };
michael@0 2241 this.conn.send(packet);
michael@0 2242
michael@0 2243 this._pushThreadPause();
michael@0 2244 } catch(e) {
michael@0 2245 reportError(e, "Got an exception during TA_onExceptionUnwind: ");
michael@0 2246 }
michael@0 2247
michael@0 2248 return undefined;
michael@0 2249 },
michael@0 2250
michael@0 2251 /**
michael@0 2252 * A function that the engine calls when a new script has been loaded into the
michael@0 2253 * scope of the specified debuggee global.
michael@0 2254 *
michael@0 2255 * @param aScript Debugger.Script
michael@0 2256 * The source script that has been loaded into a debuggee compartment.
michael@0 2257 * @param aGlobal Debugger.Object
michael@0 2258 * A Debugger.Object instance whose referent is the global object.
michael@0 2259 */
michael@0 2260 onNewScript: function (aScript, aGlobal) {
michael@0 2261 this._addScript(aScript);
michael@0 2262
michael@0 2263 // |onNewScript| is only fired for top level scripts (AKA staticLevel == 0),
michael@0 2264 // so we have to make sure to call |_addScript| on every child script as
michael@0 2265 // well to restore breakpoints in those scripts.
michael@0 2266 for (let s of aScript.getChildScripts()) {
michael@0 2267 this._addScript(s);
michael@0 2268 }
michael@0 2269
michael@0 2270 this.sources.sourcesForScript(aScript);
michael@0 2271 },
michael@0 2272
michael@0 2273 onNewSource: function (aSource) {
michael@0 2274 this.conn.send({
michael@0 2275 from: this.actorID,
michael@0 2276 type: "newSource",
michael@0 2277 source: aSource.form()
michael@0 2278 });
michael@0 2279 },
michael@0 2280
michael@0 2281 /**
michael@0 2282 * Check if scripts from the provided source URL are allowed to be stored in
michael@0 2283 * the cache.
michael@0 2284 *
michael@0 2285 * @param aSourceUrl String
michael@0 2286 * The url of the script's source that will be stored.
michael@0 2287 * @returns true, if the script can be added, false otherwise.
michael@0 2288 */
michael@0 2289 _allowSource: function (aSourceUrl) {
michael@0 2290 // Ignore anything we don't have a URL for (eval scripts, for example).
michael@0 2291 if (!aSourceUrl)
michael@0 2292 return false;
michael@0 2293 // Ignore XBL bindings for content debugging.
michael@0 2294 if (aSourceUrl.indexOf("chrome://") == 0) {
michael@0 2295 return false;
michael@0 2296 }
michael@0 2297 // Ignore about:* pages for content debugging.
michael@0 2298 if (aSourceUrl.indexOf("about:") == 0) {
michael@0 2299 return false;
michael@0 2300 }
michael@0 2301 return true;
michael@0 2302 },
michael@0 2303
michael@0 2304 /**
michael@0 2305 * Restore any pre-existing breakpoints to the scripts that we have access to.
michael@0 2306 */
michael@0 2307 _restoreBreakpoints: function () {
michael@0 2308 if (this.breakpointStore.size === 0) {
michael@0 2309 return;
michael@0 2310 }
michael@0 2311
michael@0 2312 for (let s of this.dbg.findScripts()) {
michael@0 2313 this._addScript(s);
michael@0 2314 }
michael@0 2315 },
michael@0 2316
michael@0 2317 /**
michael@0 2318 * Add the provided script to the server cache.
michael@0 2319 *
michael@0 2320 * @param aScript Debugger.Script
michael@0 2321 * The source script that will be stored.
michael@0 2322 * @returns true, if the script was added; false otherwise.
michael@0 2323 */
michael@0 2324 _addScript: function (aScript) {
michael@0 2325 if (!this._allowSource(aScript.url)) {
michael@0 2326 return false;
michael@0 2327 }
michael@0 2328
michael@0 2329 // Set any stored breakpoints.
michael@0 2330
michael@0 2331 let endLine = aScript.startLine + aScript.lineCount - 1;
michael@0 2332 for (let bp of this.breakpointStore.findBreakpoints({ url: aScript.url })) {
michael@0 2333 // Only consider breakpoints that are not already associated with
michael@0 2334 // scripts, and limit search to the line numbers contained in the new
michael@0 2335 // script.
michael@0 2336 if (!bp.actor.scripts.length
michael@0 2337 && bp.line >= aScript.startLine
michael@0 2338 && bp.line <= endLine) {
michael@0 2339 this._setBreakpoint(bp);
michael@0 2340 }
michael@0 2341 }
michael@0 2342
michael@0 2343 return true;
michael@0 2344 },
michael@0 2345
michael@0 2346
michael@0 2347 /**
michael@0 2348 * Get prototypes and properties of multiple objects.
michael@0 2349 */
michael@0 2350 onPrototypesAndProperties: function (aRequest) {
michael@0 2351 let result = {};
michael@0 2352 for (let actorID of aRequest.actors) {
michael@0 2353 // This code assumes that there are no lazily loaded actors returned
michael@0 2354 // by this call.
michael@0 2355 let actor = this.conn.getActor(actorID);
michael@0 2356 if (!actor) {
michael@0 2357 return { from: this.actorID,
michael@0 2358 error: "noSuchActor" };
michael@0 2359 }
michael@0 2360 let handler = actor.onPrototypeAndProperties;
michael@0 2361 if (!handler) {
michael@0 2362 return { from: this.actorID,
michael@0 2363 error: "unrecognizedPacketType",
michael@0 2364 message: ('Actor "' + actorID +
michael@0 2365 '" does not recognize the packet type ' +
michael@0 2366 '"prototypeAndProperties"') };
michael@0 2367 }
michael@0 2368 result[actorID] = handler.call(actor, {});
michael@0 2369 }
michael@0 2370 return { from: this.actorID,
michael@0 2371 actors: result };
michael@0 2372 }
michael@0 2373
michael@0 2374 };
michael@0 2375
michael@0 2376 ThreadActor.prototype.requestTypes = {
michael@0 2377 "attach": ThreadActor.prototype.onAttach,
michael@0 2378 "detach": ThreadActor.prototype.onDetach,
michael@0 2379 "reconfigure": ThreadActor.prototype.onReconfigure,
michael@0 2380 "resume": ThreadActor.prototype.onResume,
michael@0 2381 "clientEvaluate": ThreadActor.prototype.onClientEvaluate,
michael@0 2382 "frames": ThreadActor.prototype.onFrames,
michael@0 2383 "interrupt": ThreadActor.prototype.onInterrupt,
michael@0 2384 "eventListeners": ThreadActor.prototype.onEventListeners,
michael@0 2385 "releaseMany": ThreadActor.prototype.onReleaseMany,
michael@0 2386 "setBreakpoint": ThreadActor.prototype.onSetBreakpoint,
michael@0 2387 "sources": ThreadActor.prototype.onSources,
michael@0 2388 "threadGrips": ThreadActor.prototype.onThreadGrips,
michael@0 2389 "prototypesAndProperties": ThreadActor.prototype.onPrototypesAndProperties
michael@0 2390 };
michael@0 2391
michael@0 2392
michael@0 2393 /**
michael@0 2394 * Creates a PauseActor.
michael@0 2395 *
michael@0 2396 * PauseActors exist for the lifetime of a given debuggee pause. Used to
michael@0 2397 * scope pause-lifetime grips.
michael@0 2398 *
michael@0 2399 * @param ActorPool aPool
michael@0 2400 * The actor pool created for this pause.
michael@0 2401 */
michael@0 2402 function PauseActor(aPool)
michael@0 2403 {
michael@0 2404 this.pool = aPool;
michael@0 2405 }
michael@0 2406
michael@0 2407 PauseActor.prototype = {
michael@0 2408 actorPrefix: "pause"
michael@0 2409 };
michael@0 2410
michael@0 2411
michael@0 2412 /**
michael@0 2413 * A base actor for any actors that should only respond receive messages in the
michael@0 2414 * paused state. Subclasses may expose a `threadActor` which is used to help
michael@0 2415 * determine when we are in a paused state. Subclasses should set their own
michael@0 2416 * "constructor" property if they want better error messages. You should never
michael@0 2417 * instantiate a PauseScopedActor directly, only through subclasses.
michael@0 2418 */
michael@0 2419 function PauseScopedActor()
michael@0 2420 {
michael@0 2421 }
michael@0 2422
michael@0 2423 /**
michael@0 2424 * A function decorator for creating methods to handle protocol messages that
michael@0 2425 * should only be received while in the paused state.
michael@0 2426 *
michael@0 2427 * @param aMethod Function
michael@0 2428 * The function we are decorating.
michael@0 2429 */
michael@0 2430 PauseScopedActor.withPaused = function (aMethod) {
michael@0 2431 return function () {
michael@0 2432 if (this.isPaused()) {
michael@0 2433 return aMethod.apply(this, arguments);
michael@0 2434 } else {
michael@0 2435 return this._wrongState();
michael@0 2436 }
michael@0 2437 };
michael@0 2438 };
michael@0 2439
michael@0 2440 PauseScopedActor.prototype = {
michael@0 2441
michael@0 2442 /**
michael@0 2443 * Returns true if we are in the paused state.
michael@0 2444 */
michael@0 2445 isPaused: function () {
michael@0 2446 // When there is not a ThreadActor available (like in the webconsole) we
michael@0 2447 // have to be optimistic and assume that we are paused so that we can
michael@0 2448 // respond to requests.
michael@0 2449 return this.threadActor ? this.threadActor.state === "paused" : true;
michael@0 2450 },
michael@0 2451
michael@0 2452 /**
michael@0 2453 * Returns the wrongState response packet for this actor.
michael@0 2454 */
michael@0 2455 _wrongState: function () {
michael@0 2456 return {
michael@0 2457 error: "wrongState",
michael@0 2458 message: this.constructor.name +
michael@0 2459 " actors can only be accessed while the thread is paused."
michael@0 2460 };
michael@0 2461 }
michael@0 2462 };
michael@0 2463
michael@0 2464 /**
michael@0 2465 * Resolve a URI back to physical file.
michael@0 2466 *
michael@0 2467 * Of course, this works only for URIs pointing to local resources.
michael@0 2468 *
michael@0 2469 * @param aURI
michael@0 2470 * URI to resolve
michael@0 2471 * @return
michael@0 2472 * resolved nsIURI
michael@0 2473 */
michael@0 2474 function resolveURIToLocalPath(aURI) {
michael@0 2475 switch (aURI.scheme) {
michael@0 2476 case "jar":
michael@0 2477 case "file":
michael@0 2478 return aURI;
michael@0 2479
michael@0 2480 case "chrome":
michael@0 2481 let resolved = Cc["@mozilla.org/chrome/chrome-registry;1"].
michael@0 2482 getService(Ci.nsIChromeRegistry).convertChromeURL(aURI);
michael@0 2483 return resolveURIToLocalPath(resolved);
michael@0 2484
michael@0 2485 case "resource":
michael@0 2486 resolved = Cc["@mozilla.org/network/protocol;1?name=resource"].
michael@0 2487 getService(Ci.nsIResProtocolHandler).resolveURI(aURI);
michael@0 2488 aURI = Services.io.newURI(resolved, null, null);
michael@0 2489 return resolveURIToLocalPath(aURI);
michael@0 2490
michael@0 2491 default:
michael@0 2492 return null;
michael@0 2493 }
michael@0 2494 }
michael@0 2495
michael@0 2496 /**
michael@0 2497 * A SourceActor provides information about the source of a script.
michael@0 2498 *
michael@0 2499 * @param String url
michael@0 2500 * The url of the source we are representing.
michael@0 2501 * @param ThreadActor thread
michael@0 2502 * The current thread actor.
michael@0 2503 * @param SourceMapConsumer sourceMap
michael@0 2504 * Optional. The source map that introduced this source, if available.
michael@0 2505 * @param String generatedSource
michael@0 2506 * Optional, passed in when aSourceMap is also passed in. The generated
michael@0 2507 * source url that introduced this source.
michael@0 2508 * @param String text
michael@0 2509 * Optional. The content text of this source, if immediately available.
michael@0 2510 * @param String contentType
michael@0 2511 * Optional. The content type of this source, if immediately available.
michael@0 2512 */
michael@0 2513 function SourceActor({ url, thread, sourceMap, generatedSource, text,
michael@0 2514 contentType }) {
michael@0 2515 this._threadActor = thread;
michael@0 2516 this._url = url;
michael@0 2517 this._sourceMap = sourceMap;
michael@0 2518 this._generatedSource = generatedSource;
michael@0 2519 this._text = text;
michael@0 2520 this._contentType = contentType;
michael@0 2521
michael@0 2522 this.onSource = this.onSource.bind(this);
michael@0 2523 this._invertSourceMap = this._invertSourceMap.bind(this);
michael@0 2524 this._saveMap = this._saveMap.bind(this);
michael@0 2525 this._getSourceText = this._getSourceText.bind(this);
michael@0 2526
michael@0 2527 this._mapSourceToAddon();
michael@0 2528
michael@0 2529 if (this.threadActor.sources.isPrettyPrinted(this.url)) {
michael@0 2530 this._init = this.onPrettyPrint({
michael@0 2531 indent: this.threadActor.sources.prettyPrintIndent(this.url)
michael@0 2532 }).then(null, error => {
michael@0 2533 DevToolsUtils.reportException("SourceActor", error);
michael@0 2534 });
michael@0 2535 } else {
michael@0 2536 this._init = null;
michael@0 2537 }
michael@0 2538 }
michael@0 2539
michael@0 2540 SourceActor.prototype = {
michael@0 2541 constructor: SourceActor,
michael@0 2542 actorPrefix: "source",
michael@0 2543
michael@0 2544 _oldSourceMap: null,
michael@0 2545 _init: null,
michael@0 2546 _addonID: null,
michael@0 2547 _addonPath: null,
michael@0 2548
michael@0 2549 get threadActor() this._threadActor,
michael@0 2550 get url() this._url,
michael@0 2551 get addonID() this._addonID,
michael@0 2552 get addonPath() this._addonPath,
michael@0 2553
michael@0 2554 get prettyPrintWorker() {
michael@0 2555 return this.threadActor.prettyPrintWorker;
michael@0 2556 },
michael@0 2557
michael@0 2558 form: function () {
michael@0 2559 return {
michael@0 2560 actor: this.actorID,
michael@0 2561 url: this._url,
michael@0 2562 addonID: this._addonID,
michael@0 2563 addonPath: this._addonPath,
michael@0 2564 isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url),
michael@0 2565 isPrettyPrinted: this.threadActor.sources.isPrettyPrinted(this.url)
michael@0 2566 // TODO bug 637572: introductionScript
michael@0 2567 };
michael@0 2568 },
michael@0 2569
michael@0 2570 disconnect: function () {
michael@0 2571 if (this.registeredPool && this.registeredPool.sourceActors) {
michael@0 2572 delete this.registeredPool.sourceActors[this.actorID];
michael@0 2573 }
michael@0 2574 },
michael@0 2575
michael@0 2576 _mapSourceToAddon: function() {
michael@0 2577 try {
michael@0 2578 var nsuri = Services.io.newURI(this._url.split(" -> ").pop(), null, null);
michael@0 2579 }
michael@0 2580 catch (e) {
michael@0 2581 // We can't do anything with an invalid URI
michael@0 2582 return;
michael@0 2583 }
michael@0 2584
michael@0 2585 let localURI = resolveURIToLocalPath(nsuri);
michael@0 2586
michael@0 2587 let id = {};
michael@0 2588 if (localURI && mapURIToAddonID(localURI, id)) {
michael@0 2589 this._addonID = id.value;
michael@0 2590
michael@0 2591 if (localURI instanceof Ci.nsIJARURI) {
michael@0 2592 // The path in the add-on is easy for jar: uris
michael@0 2593 this._addonPath = localURI.JAREntry;
michael@0 2594 }
michael@0 2595 else if (localURI instanceof Ci.nsIFileURL) {
michael@0 2596 // For file: uris walk up to find the last directory that is part of the
michael@0 2597 // add-on
michael@0 2598 let target = localURI.file;
michael@0 2599 let path = target.leafName;
michael@0 2600
michael@0 2601 // We can assume that the directory containing the source file is part
michael@0 2602 // of the add-on
michael@0 2603 let root = target.parent;
michael@0 2604 let file = root.parent;
michael@0 2605 while (file && mapURIToAddonID(Services.io.newFileURI(file), {})) {
michael@0 2606 path = root.leafName + "/" + path;
michael@0 2607 root = file;
michael@0 2608 file = file.parent;
michael@0 2609 }
michael@0 2610
michael@0 2611 if (!file) {
michael@0 2612 const error = new Error("Could not find the root of the add-on for " + this._url);
michael@0 2613 DevToolsUtils.reportException("SourceActor.prototype._mapSourceToAddon", error)
michael@0 2614 return;
michael@0 2615 }
michael@0 2616
michael@0 2617 this._addonPath = path;
michael@0 2618 }
michael@0 2619 }
michael@0 2620 },
michael@0 2621
michael@0 2622 _getSourceText: function () {
michael@0 2623 const toResolvedContent = t => resolve({
michael@0 2624 content: t,
michael@0 2625 contentType: this._contentType
michael@0 2626 });
michael@0 2627
michael@0 2628 let sc;
michael@0 2629 if (this._sourceMap && (sc = this._sourceMap.sourceContentFor(this._url))) {
michael@0 2630 return toResolvedContent(sc);
michael@0 2631 }
michael@0 2632
michael@0 2633 if (this._text) {
michael@0 2634 return toResolvedContent(this._text);
michael@0 2635 }
michael@0 2636
michael@0 2637 // XXX bug 865252: Don't load from the cache if this is a source mapped
michael@0 2638 // source because we can't guarantee that the cache has the most up to date
michael@0 2639 // content for this source like we can if it isn't source mapped.
michael@0 2640 let sourceFetched = fetch(this._url, { loadFromCache: !this._sourceMap });
michael@0 2641
michael@0 2642 // Record the contentType we just learned during fetching
michael@0 2643 sourceFetched.then(({ contentType }) => {
michael@0 2644 this._contentType = contentType;
michael@0 2645 });
michael@0 2646
michael@0 2647 return sourceFetched;
michael@0 2648 },
michael@0 2649
michael@0 2650 /**
michael@0 2651 * Handler for the "source" packet.
michael@0 2652 */
michael@0 2653 onSource: function () {
michael@0 2654 return resolve(this._init)
michael@0 2655 .then(this._getSourceText)
michael@0 2656 .then(({ content, contentType }) => {
michael@0 2657 return {
michael@0 2658 from: this.actorID,
michael@0 2659 source: this.threadActor.createValueGrip(
michael@0 2660 content, this.threadActor.threadLifetimePool),
michael@0 2661 contentType: contentType
michael@0 2662 };
michael@0 2663 })
michael@0 2664 .then(null, aError => {
michael@0 2665 reportError(aError, "Got an exception during SA_onSource: ");
michael@0 2666 return {
michael@0 2667 "from": this.actorID,
michael@0 2668 "error": "loadSourceError",
michael@0 2669 "message": "Could not load the source for " + this._url + ".\n"
michael@0 2670 + DevToolsUtils.safeErrorString(aError)
michael@0 2671 };
michael@0 2672 });
michael@0 2673 },
michael@0 2674
michael@0 2675 /**
michael@0 2676 * Handler for the "prettyPrint" packet.
michael@0 2677 */
michael@0 2678 onPrettyPrint: function ({ indent }) {
michael@0 2679 this.threadActor.sources.prettyPrint(this._url, indent);
michael@0 2680 return this._getSourceText()
michael@0 2681 .then(this._sendToPrettyPrintWorker(indent))
michael@0 2682 .then(this._invertSourceMap)
michael@0 2683 .then(this._saveMap)
michael@0 2684 .then(() => {
michael@0 2685 // We need to reset `_init` now because we have already done the work of
michael@0 2686 // pretty printing, and don't want onSource to wait forever for
michael@0 2687 // initialization to complete.
michael@0 2688 this._init = null;
michael@0 2689 })
michael@0 2690 .then(this.onSource)
michael@0 2691 .then(null, error => {
michael@0 2692 this.onDisablePrettyPrint();
michael@0 2693 return {
michael@0 2694 from: this.actorID,
michael@0 2695 error: "prettyPrintError",
michael@0 2696 message: DevToolsUtils.safeErrorString(error)
michael@0 2697 };
michael@0 2698 });
michael@0 2699 },
michael@0 2700
michael@0 2701 /**
michael@0 2702 * Return a function that sends a request to the pretty print worker, waits on
michael@0 2703 * the worker's response, and then returns the pretty printed code.
michael@0 2704 *
michael@0 2705 * @param Number aIndent
michael@0 2706 * The number of spaces to indent by the code by, when we send the
michael@0 2707 * request to the pretty print worker.
michael@0 2708 * @returns Function
michael@0 2709 * Returns a function which takes an AST, and returns a promise that
michael@0 2710 * is resolved with `{ code, mappings }` where `code` is the pretty
michael@0 2711 * printed code, and `mappings` is an array of source mappings.
michael@0 2712 */
michael@0 2713 _sendToPrettyPrintWorker: function (aIndent) {
michael@0 2714 return ({ content }) => {
michael@0 2715 const deferred = promise.defer();
michael@0 2716 const id = Math.random();
michael@0 2717
michael@0 2718 const onReply = ({ data }) => {
michael@0 2719 if (data.id !== id) {
michael@0 2720 return;
michael@0 2721 }
michael@0 2722 this.prettyPrintWorker.removeEventListener("message", onReply, false);
michael@0 2723
michael@0 2724 if (data.error) {
michael@0 2725 deferred.reject(new Error(data.error));
michael@0 2726 } else {
michael@0 2727 deferred.resolve(data);
michael@0 2728 }
michael@0 2729 };
michael@0 2730
michael@0 2731 this.prettyPrintWorker.addEventListener("message", onReply, false);
michael@0 2732 this.prettyPrintWorker.postMessage({
michael@0 2733 id: id,
michael@0 2734 url: this._url,
michael@0 2735 indent: aIndent,
michael@0 2736 source: content
michael@0 2737 });
michael@0 2738
michael@0 2739 return deferred.promise;
michael@0 2740 };
michael@0 2741 },
michael@0 2742
michael@0 2743 /**
michael@0 2744 * Invert a source map. So if a source map maps from a to b, return a new
michael@0 2745 * source map from b to a. We need to do this because the source map we get
michael@0 2746 * from _generatePrettyCodeAndMap goes the opposite way we want it to for
michael@0 2747 * debugging.
michael@0 2748 *
michael@0 2749 * Note that the source map is modified in place.
michael@0 2750 */
michael@0 2751 _invertSourceMap: function ({ code, mappings }) {
michael@0 2752 const generator = new SourceMapGenerator({ file: this._url });
michael@0 2753 return DevToolsUtils.yieldingEach(mappings, m => {
michael@0 2754 let mapping = {
michael@0 2755 generated: {
michael@0 2756 line: m.generatedLine,
michael@0 2757 column: m.generatedColumn
michael@0 2758 }
michael@0 2759 };
michael@0 2760 if (m.source) {
michael@0 2761 mapping.source = m.source;
michael@0 2762 mapping.original = {
michael@0 2763 line: m.originalLine,
michael@0 2764 column: m.originalColumn
michael@0 2765 };
michael@0 2766 mapping.name = m.name;
michael@0 2767 }
michael@0 2768 generator.addMapping(mapping);
michael@0 2769 }).then(() => {
michael@0 2770 generator.setSourceContent(this._url, code);
michael@0 2771 const consumer = SourceMapConsumer.fromSourceMap(generator);
michael@0 2772
michael@0 2773 // XXX bug 918802: Monkey punch the source map consumer, because iterating
michael@0 2774 // over all mappings and inverting each of them, and then creating a new
michael@0 2775 // SourceMapConsumer is slow.
michael@0 2776
michael@0 2777 const getOrigPos = consumer.originalPositionFor.bind(consumer);
michael@0 2778 const getGenPos = consumer.generatedPositionFor.bind(consumer);
michael@0 2779
michael@0 2780 consumer.originalPositionFor = ({ line, column }) => {
michael@0 2781 const location = getGenPos({
michael@0 2782 line: line,
michael@0 2783 column: column,
michael@0 2784 source: this._url
michael@0 2785 });
michael@0 2786 location.source = this._url;
michael@0 2787 return location;
michael@0 2788 };
michael@0 2789
michael@0 2790 consumer.generatedPositionFor = ({ line, column }) => getOrigPos({
michael@0 2791 line: line,
michael@0 2792 column: column
michael@0 2793 });
michael@0 2794
michael@0 2795 return {
michael@0 2796 code: code,
michael@0 2797 map: consumer
michael@0 2798 };
michael@0 2799 });
michael@0 2800 },
michael@0 2801
michael@0 2802 /**
michael@0 2803 * Save the source map back to our thread's ThreadSources object so that
michael@0 2804 * stepping, breakpoints, debugger statements, etc can use it. If we are
michael@0 2805 * pretty printing a source mapped source, we need to compose the existing
michael@0 2806 * source map with our new one.
michael@0 2807 */
michael@0 2808 _saveMap: function ({ map }) {
michael@0 2809 if (this._sourceMap) {
michael@0 2810 // Compose the source maps
michael@0 2811 this._oldSourceMap = this._sourceMap;
michael@0 2812 this._sourceMap = SourceMapGenerator.fromSourceMap(this._sourceMap);
michael@0 2813 this._sourceMap.applySourceMap(map, this._url);
michael@0 2814 this._sourceMap = SourceMapConsumer.fromSourceMap(this._sourceMap);
michael@0 2815 this._threadActor.sources.saveSourceMap(this._sourceMap,
michael@0 2816 this._generatedSource);
michael@0 2817 } else {
michael@0 2818 this._sourceMap = map;
michael@0 2819 this._threadActor.sources.saveSourceMap(this._sourceMap, this._url);
michael@0 2820 }
michael@0 2821 },
michael@0 2822
michael@0 2823 /**
michael@0 2824 * Handler for the "disablePrettyPrint" packet.
michael@0 2825 */
michael@0 2826 onDisablePrettyPrint: function () {
michael@0 2827 this._sourceMap = this._oldSourceMap;
michael@0 2828 this.threadActor.sources.saveSourceMap(this._sourceMap,
michael@0 2829 this._generatedSource || this._url);
michael@0 2830 this.threadActor.sources.disablePrettyPrint(this._url);
michael@0 2831 return this.onSource();
michael@0 2832 },
michael@0 2833
michael@0 2834 /**
michael@0 2835 * Handler for the "blackbox" packet.
michael@0 2836 */
michael@0 2837 onBlackBox: function (aRequest) {
michael@0 2838 this.threadActor.sources.blackBox(this.url);
michael@0 2839 let packet = {
michael@0 2840 from: this.actorID
michael@0 2841 };
michael@0 2842 if (this.threadActor.state == "paused"
michael@0 2843 && this.threadActor.youngestFrame
michael@0 2844 && this.threadActor.youngestFrame.script.url == this.url) {
michael@0 2845 packet.pausedInSource = true;
michael@0 2846 }
michael@0 2847 return packet;
michael@0 2848 },
michael@0 2849
michael@0 2850 /**
michael@0 2851 * Handler for the "unblackbox" packet.
michael@0 2852 */
michael@0 2853 onUnblackBox: function (aRequest) {
michael@0 2854 this.threadActor.sources.unblackBox(this.url);
michael@0 2855 return {
michael@0 2856 from: this.actorID
michael@0 2857 };
michael@0 2858 }
michael@0 2859 };
michael@0 2860
michael@0 2861 SourceActor.prototype.requestTypes = {
michael@0 2862 "source": SourceActor.prototype.onSource,
michael@0 2863 "blackbox": SourceActor.prototype.onBlackBox,
michael@0 2864 "unblackbox": SourceActor.prototype.onUnblackBox,
michael@0 2865 "prettyPrint": SourceActor.prototype.onPrettyPrint,
michael@0 2866 "disablePrettyPrint": SourceActor.prototype.onDisablePrettyPrint
michael@0 2867 };
michael@0 2868
michael@0 2869
michael@0 2870 /**
michael@0 2871 * Determine if a given value is non-primitive.
michael@0 2872 *
michael@0 2873 * @param Any aValue
michael@0 2874 * The value to test.
michael@0 2875 * @return Boolean
michael@0 2876 * Whether the value is non-primitive.
michael@0 2877 */
michael@0 2878 function isObject(aValue) {
michael@0 2879 const type = typeof aValue;
michael@0 2880 return type == "object" ? aValue !== null : type == "function";
michael@0 2881 }
michael@0 2882
michael@0 2883 /**
michael@0 2884 * Create a function that can safely stringify Debugger.Objects of a given
michael@0 2885 * builtin type.
michael@0 2886 *
michael@0 2887 * @param Function aCtor
michael@0 2888 * The builtin class constructor.
michael@0 2889 * @return Function
michael@0 2890 * The stringifier for the class.
michael@0 2891 */
michael@0 2892 function createBuiltinStringifier(aCtor) {
michael@0 2893 return aObj => aCtor.prototype.toString.call(aObj.unsafeDereference());
michael@0 2894 }
michael@0 2895
michael@0 2896 /**
michael@0 2897 * Stringify a Debugger.Object-wrapped Error instance.
michael@0 2898 *
michael@0 2899 * @param Debugger.Object aObj
michael@0 2900 * The object to stringify.
michael@0 2901 * @return String
michael@0 2902 * The stringification of the object.
michael@0 2903 */
michael@0 2904 function errorStringify(aObj) {
michael@0 2905 let name = DevToolsUtils.getProperty(aObj, "name");
michael@0 2906 if (name === "" || name === undefined) {
michael@0 2907 name = aObj.class;
michael@0 2908 } else if (isObject(name)) {
michael@0 2909 name = stringify(name);
michael@0 2910 }
michael@0 2911
michael@0 2912 let message = DevToolsUtils.getProperty(aObj, "message");
michael@0 2913 if (isObject(message)) {
michael@0 2914 message = stringify(message);
michael@0 2915 }
michael@0 2916
michael@0 2917 if (message === "" || message === undefined) {
michael@0 2918 return name;
michael@0 2919 }
michael@0 2920 return name + ": " + message;
michael@0 2921 }
michael@0 2922
michael@0 2923 /**
michael@0 2924 * Stringify a Debugger.Object based on its class.
michael@0 2925 *
michael@0 2926 * @param Debugger.Object aObj
michael@0 2927 * The object to stringify.
michael@0 2928 * @return String
michael@0 2929 * The stringification for the object.
michael@0 2930 */
michael@0 2931 function stringify(aObj) {
michael@0 2932 if (aObj.class == "DeadObject") {
michael@0 2933 const error = new Error("Dead object encountered.");
michael@0 2934 DevToolsUtils.reportException("stringify", error);
michael@0 2935 return "<dead object>";
michael@0 2936 }
michael@0 2937 const stringifier = stringifiers[aObj.class] || stringifiers.Object;
michael@0 2938 return stringifier(aObj);
michael@0 2939 }
michael@0 2940
michael@0 2941 // Used to prevent infinite recursion when an array is found inside itself.
michael@0 2942 let seen = null;
michael@0 2943
michael@0 2944 let stringifiers = {
michael@0 2945 Error: errorStringify,
michael@0 2946 EvalError: errorStringify,
michael@0 2947 RangeError: errorStringify,
michael@0 2948 ReferenceError: errorStringify,
michael@0 2949 SyntaxError: errorStringify,
michael@0 2950 TypeError: errorStringify,
michael@0 2951 URIError: errorStringify,
michael@0 2952 Boolean: createBuiltinStringifier(Boolean),
michael@0 2953 Function: createBuiltinStringifier(Function),
michael@0 2954 Number: createBuiltinStringifier(Number),
michael@0 2955 RegExp: createBuiltinStringifier(RegExp),
michael@0 2956 String: createBuiltinStringifier(String),
michael@0 2957 Object: obj => "[object " + obj.class + "]",
michael@0 2958 Array: obj => {
michael@0 2959 // If we're at the top level then we need to create the Set for tracking
michael@0 2960 // previously stringified arrays.
michael@0 2961 const topLevel = !seen;
michael@0 2962 if (topLevel) {
michael@0 2963 seen = new Set();
michael@0 2964 } else if (seen.has(obj)) {
michael@0 2965 return "";
michael@0 2966 }
michael@0 2967
michael@0 2968 seen.add(obj);
michael@0 2969
michael@0 2970 const len = DevToolsUtils.getProperty(obj, "length");
michael@0 2971 let string = "";
michael@0 2972
michael@0 2973 // The following check is only required because the debuggee could possibly
michael@0 2974 // be a Proxy and return any value. For normal objects, array.length is
michael@0 2975 // always a non-negative integer.
michael@0 2976 if (typeof len == "number" && len > 0) {
michael@0 2977 for (let i = 0; i < len; i++) {
michael@0 2978 const desc = obj.getOwnPropertyDescriptor(i);
michael@0 2979 if (desc) {
michael@0 2980 const { value } = desc;
michael@0 2981 if (value != null) {
michael@0 2982 string += isObject(value) ? stringify(value) : value;
michael@0 2983 }
michael@0 2984 }
michael@0 2985
michael@0 2986 if (i < len - 1) {
michael@0 2987 string += ",";
michael@0 2988 }
michael@0 2989 }
michael@0 2990 }
michael@0 2991
michael@0 2992 if (topLevel) {
michael@0 2993 seen = null;
michael@0 2994 }
michael@0 2995
michael@0 2996 return string;
michael@0 2997 },
michael@0 2998 DOMException: obj => {
michael@0 2999 const message = DevToolsUtils.getProperty(obj, "message") || "<no message>";
michael@0 3000 const result = (+DevToolsUtils.getProperty(obj, "result")).toString(16);
michael@0 3001 const code = DevToolsUtils.getProperty(obj, "code");
michael@0 3002 const name = DevToolsUtils.getProperty(obj, "name") || "<unknown>";
michael@0 3003
michael@0 3004 return '[Exception... "' + message + '" ' +
michael@0 3005 'code: "' + code +'" ' +
michael@0 3006 'nsresult: "0x' + result + ' (' + name + ')"]';
michael@0 3007 }
michael@0 3008 };
michael@0 3009
michael@0 3010 /**
michael@0 3011 * Creates an actor for the specified object.
michael@0 3012 *
michael@0 3013 * @param aObj Debugger.Object
michael@0 3014 * The debuggee object.
michael@0 3015 * @param aThreadActor ThreadActor
michael@0 3016 * The parent thread actor for this object.
michael@0 3017 */
michael@0 3018 function ObjectActor(aObj, aThreadActor)
michael@0 3019 {
michael@0 3020 dbg_assert(!aObj.optimizedOut, "Should not create object actors for optimized out values!");
michael@0 3021 this.obj = aObj;
michael@0 3022 this.threadActor = aThreadActor;
michael@0 3023 }
michael@0 3024
michael@0 3025 ObjectActor.prototype = {
michael@0 3026 actorPrefix: "obj",
michael@0 3027
michael@0 3028 /**
michael@0 3029 * Returns a grip for this actor for returning in a protocol message.
michael@0 3030 */
michael@0 3031 grip: function () {
michael@0 3032 this.threadActor._gripDepth++;
michael@0 3033
michael@0 3034 let g = {
michael@0 3035 "type": "object",
michael@0 3036 "class": this.obj.class,
michael@0 3037 "actor": this.actorID,
michael@0 3038 "extensible": this.obj.isExtensible(),
michael@0 3039 "frozen": this.obj.isFrozen(),
michael@0 3040 "sealed": this.obj.isSealed()
michael@0 3041 };
michael@0 3042
michael@0 3043 if (this.obj.class != "DeadObject") {
michael@0 3044 let raw = Cu.unwaiveXrays(this.obj.unsafeDereference());
michael@0 3045 if (!DevToolsUtils.isSafeJSObject(raw)) {
michael@0 3046 raw = null;
michael@0 3047 }
michael@0 3048
michael@0 3049 let previewers = DebuggerServer.ObjectActorPreviewers[this.obj.class] ||
michael@0 3050 DebuggerServer.ObjectActorPreviewers.Object;
michael@0 3051 for (let fn of previewers) {
michael@0 3052 try {
michael@0 3053 if (fn(this, g, raw)) {
michael@0 3054 break;
michael@0 3055 }
michael@0 3056 } catch (e) {
michael@0 3057 DevToolsUtils.reportException("ObjectActor.prototype.grip previewer function", e);
michael@0 3058 }
michael@0 3059 }
michael@0 3060 }
michael@0 3061
michael@0 3062 this.threadActor._gripDepth--;
michael@0 3063 return g;
michael@0 3064 },
michael@0 3065
michael@0 3066 /**
michael@0 3067 * Releases this actor from the pool.
michael@0 3068 */
michael@0 3069 release: function () {
michael@0 3070 if (this.registeredPool.objectActors) {
michael@0 3071 this.registeredPool.objectActors.delete(this.obj);
michael@0 3072 }
michael@0 3073 this.registeredPool.removeActor(this);
michael@0 3074 },
michael@0 3075
michael@0 3076 /**
michael@0 3077 * Handle a protocol request to provide the definition site of this function
michael@0 3078 * object.
michael@0 3079 *
michael@0 3080 * @param aRequest object
michael@0 3081 * The protocol request object.
michael@0 3082 */
michael@0 3083 onDefinitionSite: function OA_onDefinitionSite(aRequest) {
michael@0 3084 if (this.obj.class != "Function") {
michael@0 3085 return {
michael@0 3086 from: this.actorID,
michael@0 3087 error: "objectNotFunction",
michael@0 3088 message: this.actorID + " is not a function."
michael@0 3089 };
michael@0 3090 }
michael@0 3091
michael@0 3092 if (!this.obj.script) {
michael@0 3093 return {
michael@0 3094 from: this.actorID,
michael@0 3095 error: "noScript",
michael@0 3096 message: this.actorID + " has no Debugger.Script"
michael@0 3097 };
michael@0 3098 }
michael@0 3099
michael@0 3100 const generatedLocation = {
michael@0 3101 url: this.obj.script.url,
michael@0 3102 line: this.obj.script.startLine,
michael@0 3103 // TODO bug 901138: use Debugger.Script.prototype.startColumn.
michael@0 3104 column: 0
michael@0 3105 };
michael@0 3106
michael@0 3107 return this.threadActor.sources.getOriginalLocation(generatedLocation)
michael@0 3108 .then(({ url, line, column }) => {
michael@0 3109 return {
michael@0 3110 from: this.actorID,
michael@0 3111 url: url,
michael@0 3112 line: line,
michael@0 3113 column: column
michael@0 3114 };
michael@0 3115 });
michael@0 3116 },
michael@0 3117
michael@0 3118 /**
michael@0 3119 * Handle a protocol request to provide the names of the properties defined on
michael@0 3120 * the object and not its prototype.
michael@0 3121 *
michael@0 3122 * @param aRequest object
michael@0 3123 * The protocol request object.
michael@0 3124 */
michael@0 3125 onOwnPropertyNames: function (aRequest) {
michael@0 3126 return { from: this.actorID,
michael@0 3127 ownPropertyNames: this.obj.getOwnPropertyNames() };
michael@0 3128 },
michael@0 3129
michael@0 3130 /**
michael@0 3131 * Handle a protocol request to provide the prototype and own properties of
michael@0 3132 * the object.
michael@0 3133 *
michael@0 3134 * @param aRequest object
michael@0 3135 * The protocol request object.
michael@0 3136 */
michael@0 3137 onPrototypeAndProperties: function (aRequest) {
michael@0 3138 let ownProperties = Object.create(null);
michael@0 3139 let names;
michael@0 3140 try {
michael@0 3141 names = this.obj.getOwnPropertyNames();
michael@0 3142 } catch (ex) {
michael@0 3143 // The above can throw if this.obj points to a dead object.
michael@0 3144 // TODO: we should use Cu.isDeadWrapper() - see bug 885800.
michael@0 3145 return { from: this.actorID,
michael@0 3146 prototype: this.threadActor.createValueGrip(null),
michael@0 3147 ownProperties: ownProperties,
michael@0 3148 safeGetterValues: Object.create(null) };
michael@0 3149 }
michael@0 3150 for (let name of names) {
michael@0 3151 ownProperties[name] = this._propertyDescriptor(name);
michael@0 3152 }
michael@0 3153 return { from: this.actorID,
michael@0 3154 prototype: this.threadActor.createValueGrip(this.obj.proto),
michael@0 3155 ownProperties: ownProperties,
michael@0 3156 safeGetterValues: this._findSafeGetterValues(ownProperties) };
michael@0 3157 },
michael@0 3158
michael@0 3159 /**
michael@0 3160 * Find the safe getter values for the current Debugger.Object, |this.obj|.
michael@0 3161 *
michael@0 3162 * @private
michael@0 3163 * @param object aOwnProperties
michael@0 3164 * The object that holds the list of known ownProperties for
michael@0 3165 * |this.obj|.
michael@0 3166 * @param number [aLimit=0]
michael@0 3167 * Optional limit of getter values to find.
michael@0 3168 * @return object
michael@0 3169 * An object that maps property names to safe getter descriptors as
michael@0 3170 * defined by the remote debugging protocol.
michael@0 3171 */
michael@0 3172 _findSafeGetterValues: function (aOwnProperties, aLimit = 0)
michael@0 3173 {
michael@0 3174 let safeGetterValues = Object.create(null);
michael@0 3175 let obj = this.obj;
michael@0 3176 let level = 0, i = 0;
michael@0 3177
michael@0 3178 while (obj) {
michael@0 3179 let getters = this._findSafeGetters(obj);
michael@0 3180 for (let name of getters) {
michael@0 3181 // Avoid overwriting properties from prototypes closer to this.obj. Also
michael@0 3182 // avoid providing safeGetterValues from prototypes if property |name|
michael@0 3183 // is already defined as an own property.
michael@0 3184 if (name in safeGetterValues ||
michael@0 3185 (obj != this.obj && name in aOwnProperties)) {
michael@0 3186 continue;
michael@0 3187 }
michael@0 3188
michael@0 3189 let desc = null, getter = null;
michael@0 3190 try {
michael@0 3191 desc = obj.getOwnPropertyDescriptor(name);
michael@0 3192 getter = desc.get;
michael@0 3193 } catch (ex) {
michael@0 3194 // The above can throw if the cache becomes stale.
michael@0 3195 }
michael@0 3196 if (!getter) {
michael@0 3197 obj._safeGetters = null;
michael@0 3198 continue;
michael@0 3199 }
michael@0 3200
michael@0 3201 let result = getter.call(this.obj);
michael@0 3202 if (result && !("throw" in result)) {
michael@0 3203 let getterValue = undefined;
michael@0 3204 if ("return" in result) {
michael@0 3205 getterValue = result.return;
michael@0 3206 } else if ("yield" in result) {
michael@0 3207 getterValue = result.yield;
michael@0 3208 }
michael@0 3209 // WebIDL attributes specified with the LenientThis extended attribute
michael@0 3210 // return undefined and should be ignored.
michael@0 3211 if (getterValue !== undefined) {
michael@0 3212 safeGetterValues[name] = {
michael@0 3213 getterValue: this.threadActor.createValueGrip(getterValue),
michael@0 3214 getterPrototypeLevel: level,
michael@0 3215 enumerable: desc.enumerable,
michael@0 3216 writable: level == 0 ? desc.writable : true,
michael@0 3217 };
michael@0 3218 if (aLimit && ++i == aLimit) {
michael@0 3219 break;
michael@0 3220 }
michael@0 3221 }
michael@0 3222 }
michael@0 3223 }
michael@0 3224 if (aLimit && i == aLimit) {
michael@0 3225 break;
michael@0 3226 }
michael@0 3227
michael@0 3228 obj = obj.proto;
michael@0 3229 level++;
michael@0 3230 }
michael@0 3231
michael@0 3232 return safeGetterValues;
michael@0 3233 },
michael@0 3234
michael@0 3235 /**
michael@0 3236 * Find the safe getters for a given Debugger.Object. Safe getters are native
michael@0 3237 * getters which are safe to execute.
michael@0 3238 *
michael@0 3239 * @private
michael@0 3240 * @param Debugger.Object aObject
michael@0 3241 * The Debugger.Object where you want to find safe getters.
michael@0 3242 * @return Set
michael@0 3243 * A Set of names of safe getters. This result is cached for each
michael@0 3244 * Debugger.Object.
michael@0 3245 */
michael@0 3246 _findSafeGetters: function (aObject)
michael@0 3247 {
michael@0 3248 if (aObject._safeGetters) {
michael@0 3249 return aObject._safeGetters;
michael@0 3250 }
michael@0 3251
michael@0 3252 let getters = new Set();
michael@0 3253 let names = [];
michael@0 3254 try {
michael@0 3255 names = aObject.getOwnPropertyNames()
michael@0 3256 } catch (ex) {
michael@0 3257 // Calling getOwnPropertyNames() on some wrapped native prototypes is not
michael@0 3258 // allowed: "cannot modify properties of a WrappedNative". See bug 952093.
michael@0 3259 }
michael@0 3260
michael@0 3261 for (let name of names) {
michael@0 3262 let desc = null;
michael@0 3263 try {
michael@0 3264 desc = aObject.getOwnPropertyDescriptor(name);
michael@0 3265 } catch (e) {
michael@0 3266 // Calling getOwnPropertyDescriptor on wrapped native prototypes is not
michael@0 3267 // allowed (bug 560072).
michael@0 3268 }
michael@0 3269 if (!desc || desc.value !== undefined || !("get" in desc)) {
michael@0 3270 continue;
michael@0 3271 }
michael@0 3272
michael@0 3273 if (DevToolsUtils.hasSafeGetter(desc)) {
michael@0 3274 getters.add(name);
michael@0 3275 }
michael@0 3276 }
michael@0 3277
michael@0 3278 aObject._safeGetters = getters;
michael@0 3279 return getters;
michael@0 3280 },
michael@0 3281
michael@0 3282 /**
michael@0 3283 * Handle a protocol request to provide the prototype of the object.
michael@0 3284 *
michael@0 3285 * @param aRequest object
michael@0 3286 * The protocol request object.
michael@0 3287 */
michael@0 3288 onPrototype: function (aRequest) {
michael@0 3289 return { from: this.actorID,
michael@0 3290 prototype: this.threadActor.createValueGrip(this.obj.proto) };
michael@0 3291 },
michael@0 3292
michael@0 3293 /**
michael@0 3294 * Handle a protocol request to provide the property descriptor of the
michael@0 3295 * object's specified property.
michael@0 3296 *
michael@0 3297 * @param aRequest object
michael@0 3298 * The protocol request object.
michael@0 3299 */
michael@0 3300 onProperty: function (aRequest) {
michael@0 3301 if (!aRequest.name) {
michael@0 3302 return { error: "missingParameter",
michael@0 3303 message: "no property name was specified" };
michael@0 3304 }
michael@0 3305
michael@0 3306 return { from: this.actorID,
michael@0 3307 descriptor: this._propertyDescriptor(aRequest.name) };
michael@0 3308 },
michael@0 3309
michael@0 3310 /**
michael@0 3311 * Handle a protocol request to provide the display string for the object.
michael@0 3312 *
michael@0 3313 * @param aRequest object
michael@0 3314 * The protocol request object.
michael@0 3315 */
michael@0 3316 onDisplayString: function (aRequest) {
michael@0 3317 const string = stringify(this.obj);
michael@0 3318 return { from: this.actorID,
michael@0 3319 displayString: this.threadActor.createValueGrip(string) };
michael@0 3320 },
michael@0 3321
michael@0 3322 /**
michael@0 3323 * A helper method that creates a property descriptor for the provided object,
michael@0 3324 * properly formatted for sending in a protocol response.
michael@0 3325 *
michael@0 3326 * @private
michael@0 3327 * @param string aName
michael@0 3328 * The property that the descriptor is generated for.
michael@0 3329 * @param boolean [aOnlyEnumerable]
michael@0 3330 * Optional: true if you want a descriptor only for an enumerable
michael@0 3331 * property, false otherwise.
michael@0 3332 * @return object|undefined
michael@0 3333 * The property descriptor, or undefined if this is not an enumerable
michael@0 3334 * property and aOnlyEnumerable=true.
michael@0 3335 */
michael@0 3336 _propertyDescriptor: function (aName, aOnlyEnumerable) {
michael@0 3337 let desc;
michael@0 3338 try {
michael@0 3339 desc = this.obj.getOwnPropertyDescriptor(aName);
michael@0 3340 } catch (e) {
michael@0 3341 // Calling getOwnPropertyDescriptor on wrapped native prototypes is not
michael@0 3342 // allowed (bug 560072). Inform the user with a bogus, but hopefully
michael@0 3343 // explanatory, descriptor.
michael@0 3344 return {
michael@0 3345 configurable: false,
michael@0 3346 writable: false,
michael@0 3347 enumerable: false,
michael@0 3348 value: e.name
michael@0 3349 };
michael@0 3350 }
michael@0 3351
michael@0 3352 if (!desc || aOnlyEnumerable && !desc.enumerable) {
michael@0 3353 return undefined;
michael@0 3354 }
michael@0 3355
michael@0 3356 let retval = {
michael@0 3357 configurable: desc.configurable,
michael@0 3358 enumerable: desc.enumerable
michael@0 3359 };
michael@0 3360
michael@0 3361 if ("value" in desc) {
michael@0 3362 retval.writable = desc.writable;
michael@0 3363 retval.value = this.threadActor.createValueGrip(desc.value);
michael@0 3364 } else {
michael@0 3365 if ("get" in desc) {
michael@0 3366 retval.get = this.threadActor.createValueGrip(desc.get);
michael@0 3367 }
michael@0 3368 if ("set" in desc) {
michael@0 3369 retval.set = this.threadActor.createValueGrip(desc.set);
michael@0 3370 }
michael@0 3371 }
michael@0 3372 return retval;
michael@0 3373 },
michael@0 3374
michael@0 3375 /**
michael@0 3376 * Handle a protocol request to provide the source code of a function.
michael@0 3377 *
michael@0 3378 * @param aRequest object
michael@0 3379 * The protocol request object.
michael@0 3380 */
michael@0 3381 onDecompile: function (aRequest) {
michael@0 3382 if (this.obj.class !== "Function") {
michael@0 3383 return { error: "objectNotFunction",
michael@0 3384 message: "decompile request is only valid for object grips " +
michael@0 3385 "with a 'Function' class." };
michael@0 3386 }
michael@0 3387
michael@0 3388 return { from: this.actorID,
michael@0 3389 decompiledCode: this.obj.decompile(!!aRequest.pretty) };
michael@0 3390 },
michael@0 3391
michael@0 3392 /**
michael@0 3393 * Handle a protocol request to provide the parameters of a function.
michael@0 3394 *
michael@0 3395 * @param aRequest object
michael@0 3396 * The protocol request object.
michael@0 3397 */
michael@0 3398 onParameterNames: function (aRequest) {
michael@0 3399 if (this.obj.class !== "Function") {
michael@0 3400 return { error: "objectNotFunction",
michael@0 3401 message: "'parameterNames' request is only valid for object " +
michael@0 3402 "grips with a 'Function' class." };
michael@0 3403 }
michael@0 3404
michael@0 3405 return { parameterNames: this.obj.parameterNames };
michael@0 3406 },
michael@0 3407
michael@0 3408 /**
michael@0 3409 * Handle a protocol request to release a thread-lifetime grip.
michael@0 3410 *
michael@0 3411 * @param aRequest object
michael@0 3412 * The protocol request object.
michael@0 3413 */
michael@0 3414 onRelease: function (aRequest) {
michael@0 3415 this.release();
michael@0 3416 return {};
michael@0 3417 },
michael@0 3418
michael@0 3419 /**
michael@0 3420 * Handle a protocol request to provide the lexical scope of a function.
michael@0 3421 *
michael@0 3422 * @param aRequest object
michael@0 3423 * The protocol request object.
michael@0 3424 */
michael@0 3425 onScope: function (aRequest) {
michael@0 3426 if (this.obj.class !== "Function") {
michael@0 3427 return { error: "objectNotFunction",
michael@0 3428 message: "scope request is only valid for object grips with a" +
michael@0 3429 " 'Function' class." };
michael@0 3430 }
michael@0 3431
michael@0 3432 let envActor = this.threadActor.createEnvironmentActor(this.obj.environment,
michael@0 3433 this.registeredPool);
michael@0 3434 if (!envActor) {
michael@0 3435 return { error: "notDebuggee",
michael@0 3436 message: "cannot access the environment of this function." };
michael@0 3437 }
michael@0 3438
michael@0 3439 return { from: this.actorID, scope: envActor.form() };
michael@0 3440 }
michael@0 3441 };
michael@0 3442
michael@0 3443 ObjectActor.prototype.requestTypes = {
michael@0 3444 "definitionSite": ObjectActor.prototype.onDefinitionSite,
michael@0 3445 "parameterNames": ObjectActor.prototype.onParameterNames,
michael@0 3446 "prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties,
michael@0 3447 "prototype": ObjectActor.prototype.onPrototype,
michael@0 3448 "property": ObjectActor.prototype.onProperty,
michael@0 3449 "displayString": ObjectActor.prototype.onDisplayString,
michael@0 3450 "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames,
michael@0 3451 "decompile": ObjectActor.prototype.onDecompile,
michael@0 3452 "release": ObjectActor.prototype.onRelease,
michael@0 3453 "scope": ObjectActor.prototype.onScope,
michael@0 3454 };
michael@0 3455
michael@0 3456
michael@0 3457 /**
michael@0 3458 * Functions for adding information to ObjectActor grips for the purpose of
michael@0 3459 * having customized output. This object holds arrays mapped by
michael@0 3460 * Debugger.Object.prototype.class.
michael@0 3461 *
michael@0 3462 * In each array you can add functions that take two
michael@0 3463 * arguments:
michael@0 3464 * - the ObjectActor instance to make a preview for,
michael@0 3465 * - the grip object being prepared for the client,
michael@0 3466 * - the raw JS object after calling Debugger.Object.unsafeDereference(). This
michael@0 3467 * argument is only provided if the object is safe for reading properties and
michael@0 3468 * executing methods. See DevToolsUtils.isSafeJSObject().
michael@0 3469 *
michael@0 3470 * Functions must return false if they cannot provide preview
michael@0 3471 * information for the debugger object, or true otherwise.
michael@0 3472 */
michael@0 3473 DebuggerServer.ObjectActorPreviewers = {
michael@0 3474 String: [function({obj, threadActor}, aGrip) {
michael@0 3475 let result = genericObjectPreviewer("String", String, obj, threadActor);
michael@0 3476 if (result) {
michael@0 3477 let length = DevToolsUtils.getProperty(obj, "length");
michael@0 3478 if (typeof length != "number") {
michael@0 3479 return false;
michael@0 3480 }
michael@0 3481
michael@0 3482 aGrip.displayString = result.value;
michael@0 3483 return true;
michael@0 3484 }
michael@0 3485
michael@0 3486 return true;
michael@0 3487 }],
michael@0 3488
michael@0 3489 Boolean: [function({obj, threadActor}, aGrip) {
michael@0 3490 let result = genericObjectPreviewer("Boolean", Boolean, obj, threadActor);
michael@0 3491 if (result) {
michael@0 3492 aGrip.preview = result;
michael@0 3493 return true;
michael@0 3494 }
michael@0 3495
michael@0 3496 return false;
michael@0 3497 }],
michael@0 3498
michael@0 3499 Number: [function({obj, threadActor}, aGrip) {
michael@0 3500 let result = genericObjectPreviewer("Number", Number, obj, threadActor);
michael@0 3501 if (result) {
michael@0 3502 aGrip.preview = result;
michael@0 3503 return true;
michael@0 3504 }
michael@0 3505
michael@0 3506 return false;
michael@0 3507 }],
michael@0 3508
michael@0 3509 Function: [function({obj, threadActor}, aGrip) {
michael@0 3510 if (obj.name) {
michael@0 3511 aGrip.name = obj.name;
michael@0 3512 }
michael@0 3513
michael@0 3514 if (obj.displayName) {
michael@0 3515 aGrip.displayName = obj.displayName.substr(0, 500);
michael@0 3516 }
michael@0 3517
michael@0 3518 if (obj.parameterNames) {
michael@0 3519 aGrip.parameterNames = obj.parameterNames;
michael@0 3520 }
michael@0 3521
michael@0 3522 // Check if the developer has added a de-facto standard displayName
michael@0 3523 // property for us to use.
michael@0 3524 let userDisplayName;
michael@0 3525 try {
michael@0 3526 userDisplayName = obj.getOwnPropertyDescriptor("displayName");
michael@0 3527 } catch (e) {
michael@0 3528 // Calling getOwnPropertyDescriptor with displayName might throw
michael@0 3529 // with "permission denied" errors for some functions.
michael@0 3530 dumpn(e);
michael@0 3531 }
michael@0 3532
michael@0 3533 if (userDisplayName && typeof userDisplayName.value == "string" &&
michael@0 3534 userDisplayName.value) {
michael@0 3535 aGrip.userDisplayName = threadActor.createValueGrip(userDisplayName.value);
michael@0 3536 }
michael@0 3537
michael@0 3538 return true;
michael@0 3539 }],
michael@0 3540
michael@0 3541 RegExp: [function({obj, threadActor}, aGrip) {
michael@0 3542 // Avoid having any special preview for the RegExp.prototype itself.
michael@0 3543 if (!obj.proto || obj.proto.class != "RegExp") {
michael@0 3544 return false;
michael@0 3545 }
michael@0 3546
michael@0 3547 let str = RegExp.prototype.toString.call(obj.unsafeDereference());
michael@0 3548 aGrip.displayString = threadActor.createValueGrip(str);
michael@0 3549 return true;
michael@0 3550 }],
michael@0 3551
michael@0 3552 Date: [function({obj, threadActor}, aGrip) {
michael@0 3553 if (!obj.proto || obj.proto.class != "Date") {
michael@0 3554 return false;
michael@0 3555 }
michael@0 3556
michael@0 3557 let time = Date.prototype.getTime.call(obj.unsafeDereference());
michael@0 3558
michael@0 3559 aGrip.preview = {
michael@0 3560 timestamp: threadActor.createValueGrip(time),
michael@0 3561 };
michael@0 3562 return true;
michael@0 3563 }],
michael@0 3564
michael@0 3565 Array: [function({obj, threadActor}, aGrip) {
michael@0 3566 let length = DevToolsUtils.getProperty(obj, "length");
michael@0 3567 if (typeof length != "number") {
michael@0 3568 return false;
michael@0 3569 }
michael@0 3570
michael@0 3571 aGrip.preview = {
michael@0 3572 kind: "ArrayLike",
michael@0 3573 length: length,
michael@0 3574 };
michael@0 3575
michael@0 3576 if (threadActor._gripDepth > 1) {
michael@0 3577 return true;
michael@0 3578 }
michael@0 3579
michael@0 3580 let raw = obj.unsafeDereference();
michael@0 3581 let items = aGrip.preview.items = [];
michael@0 3582
michael@0 3583 for (let [i, value] of Array.prototype.entries.call(raw)) {
michael@0 3584 if (Object.hasOwnProperty.call(raw, i)) {
michael@0 3585 value = makeDebuggeeValueIfNeeded(obj, value);
michael@0 3586 items.push(threadActor.createValueGrip(value));
michael@0 3587 } else {
michael@0 3588 items.push(null);
michael@0 3589 }
michael@0 3590
michael@0 3591 if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
michael@0 3592 break;
michael@0 3593 }
michael@0 3594 }
michael@0 3595
michael@0 3596 return true;
michael@0 3597 }], // Array
michael@0 3598
michael@0 3599 Set: [function({obj, threadActor}, aGrip) {
michael@0 3600 let size = DevToolsUtils.getProperty(obj, "size");
michael@0 3601 if (typeof size != "number") {
michael@0 3602 return false;
michael@0 3603 }
michael@0 3604
michael@0 3605 aGrip.preview = {
michael@0 3606 kind: "ArrayLike",
michael@0 3607 length: size,
michael@0 3608 };
michael@0 3609
michael@0 3610 // Avoid recursive object grips.
michael@0 3611 if (threadActor._gripDepth > 1) {
michael@0 3612 return true;
michael@0 3613 }
michael@0 3614
michael@0 3615 let raw = obj.unsafeDereference();
michael@0 3616 let items = aGrip.preview.items = [];
michael@0 3617 for (let item of Set.prototype.values.call(raw)) {
michael@0 3618 item = makeDebuggeeValueIfNeeded(obj, item);
michael@0 3619 items.push(threadActor.createValueGrip(item));
michael@0 3620 if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
michael@0 3621 break;
michael@0 3622 }
michael@0 3623 }
michael@0 3624
michael@0 3625 return true;
michael@0 3626 }], // Set
michael@0 3627
michael@0 3628 Map: [function({obj, threadActor}, aGrip) {
michael@0 3629 let size = DevToolsUtils.getProperty(obj, "size");
michael@0 3630 if (typeof size != "number") {
michael@0 3631 return false;
michael@0 3632 }
michael@0 3633
michael@0 3634 aGrip.preview = {
michael@0 3635 kind: "MapLike",
michael@0 3636 size: size,
michael@0 3637 };
michael@0 3638
michael@0 3639 if (threadActor._gripDepth > 1) {
michael@0 3640 return true;
michael@0 3641 }
michael@0 3642
michael@0 3643 let raw = obj.unsafeDereference();
michael@0 3644 let entries = aGrip.preview.entries = [];
michael@0 3645 for (let [key, value] of Map.prototype.entries.call(raw)) {
michael@0 3646 key = makeDebuggeeValueIfNeeded(obj, key);
michael@0 3647 value = makeDebuggeeValueIfNeeded(obj, value);
michael@0 3648 entries.push([threadActor.createValueGrip(key),
michael@0 3649 threadActor.createValueGrip(value)]);
michael@0 3650 if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
michael@0 3651 break;
michael@0 3652 }
michael@0 3653 }
michael@0 3654
michael@0 3655 return true;
michael@0 3656 }], // Map
michael@0 3657
michael@0 3658 DOMStringMap: [function({obj, threadActor}, aGrip, aRawObj) {
michael@0 3659 if (!aRawObj) {
michael@0 3660 return false;
michael@0 3661 }
michael@0 3662
michael@0 3663 let keys = obj.getOwnPropertyNames();
michael@0 3664 aGrip.preview = {
michael@0 3665 kind: "MapLike",
michael@0 3666 size: keys.length,
michael@0 3667 };
michael@0 3668
michael@0 3669 if (threadActor._gripDepth > 1) {
michael@0 3670 return true;
michael@0 3671 }
michael@0 3672
michael@0 3673 let entries = aGrip.preview.entries = [];
michael@0 3674 for (let key of keys) {
michael@0 3675 let value = makeDebuggeeValueIfNeeded(obj, aRawObj[key]);
michael@0 3676 entries.push([key, threadActor.createValueGrip(value)]);
michael@0 3677 if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
michael@0 3678 break;
michael@0 3679 }
michael@0 3680 }
michael@0 3681
michael@0 3682 return true;
michael@0 3683 }], // DOMStringMap
michael@0 3684 }; // DebuggerServer.ObjectActorPreviewers
michael@0 3685
michael@0 3686 /**
michael@0 3687 * Generic previewer for "simple" classes like String, Number and Boolean.
michael@0 3688 *
michael@0 3689 * @param string aClassName
michael@0 3690 * Class name to expect.
michael@0 3691 * @param object aClass
michael@0 3692 * The class to expect, eg. String. The valueOf() method of the class is
michael@0 3693 * invoked on the given object.
michael@0 3694 * @param Debugger.Object aObj
michael@0 3695 * The debugger object we need to preview.
michael@0 3696 * @param object aThreadActor
michael@0 3697 * The thread actor to use to create a value grip.
michael@0 3698 * @return object|null
michael@0 3699 * An object with one property, "value", which holds the value grip that
michael@0 3700 * represents the given object. Null is returned if we cant preview the
michael@0 3701 * object.
michael@0 3702 */
michael@0 3703 function genericObjectPreviewer(aClassName, aClass, aObj, aThreadActor) {
michael@0 3704 if (!aObj.proto || aObj.proto.class != aClassName) {
michael@0 3705 return null;
michael@0 3706 }
michael@0 3707
michael@0 3708 let raw = aObj.unsafeDereference();
michael@0 3709 let v = null;
michael@0 3710 try {
michael@0 3711 v = aClass.prototype.valueOf.call(raw);
michael@0 3712 } catch (ex) {
michael@0 3713 // valueOf() can throw if the raw JS object is "misbehaved".
michael@0 3714 return null;
michael@0 3715 }
michael@0 3716
michael@0 3717 if (v !== null) {
michael@0 3718 v = aThreadActor.createValueGrip(makeDebuggeeValueIfNeeded(aObj, v));
michael@0 3719 return { value: v };
michael@0 3720 }
michael@0 3721
michael@0 3722 return null;
michael@0 3723 }
michael@0 3724
michael@0 3725 // Preview functions that do not rely on the object class.
michael@0 3726 DebuggerServer.ObjectActorPreviewers.Object = [
michael@0 3727 function TypedArray({obj, threadActor}, aGrip) {
michael@0 3728 if (TYPED_ARRAY_CLASSES.indexOf(obj.class) == -1) {
michael@0 3729 return false;
michael@0 3730 }
michael@0 3731
michael@0 3732 let length = DevToolsUtils.getProperty(obj, "length");
michael@0 3733 if (typeof length != "number") {
michael@0 3734 return false;
michael@0 3735 }
michael@0 3736
michael@0 3737 aGrip.preview = {
michael@0 3738 kind: "ArrayLike",
michael@0 3739 length: length,
michael@0 3740 };
michael@0 3741
michael@0 3742 if (threadActor._gripDepth > 1) {
michael@0 3743 return true;
michael@0 3744 }
michael@0 3745
michael@0 3746 let raw = obj.unsafeDereference();
michael@0 3747 let global = Cu.getGlobalForObject(DebuggerServer);
michael@0 3748 let classProto = global[obj.class].prototype;
michael@0 3749 let safeView = classProto.subarray.call(raw, 0, OBJECT_PREVIEW_MAX_ITEMS);
michael@0 3750 let items = aGrip.preview.items = [];
michael@0 3751 for (let i = 0; i < safeView.length; i++) {
michael@0 3752 items.push(safeView[i]);
michael@0 3753 }
michael@0 3754
michael@0 3755 return true;
michael@0 3756 },
michael@0 3757
michael@0 3758 function Error({obj, threadActor}, aGrip) {
michael@0 3759 switch (obj.class) {
michael@0 3760 case "Error":
michael@0 3761 case "EvalError":
michael@0 3762 case "RangeError":
michael@0 3763 case "ReferenceError":
michael@0 3764 case "SyntaxError":
michael@0 3765 case "TypeError":
michael@0 3766 case "URIError":
michael@0 3767 let name = DevToolsUtils.getProperty(obj, "name");
michael@0 3768 let msg = DevToolsUtils.getProperty(obj, "message");
michael@0 3769 let stack = DevToolsUtils.getProperty(obj, "stack");
michael@0 3770 let fileName = DevToolsUtils.getProperty(obj, "fileName");
michael@0 3771 let lineNumber = DevToolsUtils.getProperty(obj, "lineNumber");
michael@0 3772 let columnNumber = DevToolsUtils.getProperty(obj, "columnNumber");
michael@0 3773 aGrip.preview = {
michael@0 3774 kind: "Error",
michael@0 3775 name: threadActor.createValueGrip(name),
michael@0 3776 message: threadActor.createValueGrip(msg),
michael@0 3777 stack: threadActor.createValueGrip(stack),
michael@0 3778 fileName: threadActor.createValueGrip(fileName),
michael@0 3779 lineNumber: threadActor.createValueGrip(lineNumber),
michael@0 3780 columnNumber: threadActor.createValueGrip(columnNumber),
michael@0 3781 };
michael@0 3782 return true;
michael@0 3783 default:
michael@0 3784 return false;
michael@0 3785 }
michael@0 3786 },
michael@0 3787
michael@0 3788 function CSSMediaRule({obj, threadActor}, aGrip, aRawObj) {
michael@0 3789 if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSMediaRule)) {
michael@0 3790 return false;
michael@0 3791 }
michael@0 3792 aGrip.preview = {
michael@0 3793 kind: "ObjectWithText",
michael@0 3794 text: threadActor.createValueGrip(aRawObj.conditionText),
michael@0 3795 };
michael@0 3796 return true;
michael@0 3797 },
michael@0 3798
michael@0 3799 function CSSStyleRule({obj, threadActor}, aGrip, aRawObj) {
michael@0 3800 if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSStyleRule)) {
michael@0 3801 return false;
michael@0 3802 }
michael@0 3803 aGrip.preview = {
michael@0 3804 kind: "ObjectWithText",
michael@0 3805 text: threadActor.createValueGrip(aRawObj.selectorText),
michael@0 3806 };
michael@0 3807 return true;
michael@0 3808 },
michael@0 3809
michael@0 3810 function ObjectWithURL({obj, threadActor}, aGrip, aRawObj) {
michael@0 3811 if (!aRawObj ||
michael@0 3812 !(aRawObj instanceof Ci.nsIDOMCSSImportRule ||
michael@0 3813 aRawObj instanceof Ci.nsIDOMCSSStyleSheet ||
michael@0 3814 aRawObj instanceof Ci.nsIDOMLocation ||
michael@0 3815 aRawObj instanceof Ci.nsIDOMWindow)) {
michael@0 3816 return false;
michael@0 3817 }
michael@0 3818
michael@0 3819 let url;
michael@0 3820 if (aRawObj instanceof Ci.nsIDOMWindow && aRawObj.location) {
michael@0 3821 url = aRawObj.location.href;
michael@0 3822 } else if (aRawObj.href) {
michael@0 3823 url = aRawObj.href;
michael@0 3824 } else {
michael@0 3825 return false;
michael@0 3826 }
michael@0 3827
michael@0 3828 aGrip.preview = {
michael@0 3829 kind: "ObjectWithURL",
michael@0 3830 url: threadActor.createValueGrip(url),
michael@0 3831 };
michael@0 3832
michael@0 3833 return true;
michael@0 3834 },
michael@0 3835
michael@0 3836 function ArrayLike({obj, threadActor}, aGrip, aRawObj) {
michael@0 3837 if (!aRawObj ||
michael@0 3838 obj.class != "DOMStringList" &&
michael@0 3839 obj.class != "DOMTokenList" &&
michael@0 3840 !(aRawObj instanceof Ci.nsIDOMMozNamedAttrMap ||
michael@0 3841 aRawObj instanceof Ci.nsIDOMCSSRuleList ||
michael@0 3842 aRawObj instanceof Ci.nsIDOMCSSValueList ||
michael@0 3843 aRawObj instanceof Ci.nsIDOMFileList ||
michael@0 3844 aRawObj instanceof Ci.nsIDOMFontFaceList ||
michael@0 3845 aRawObj instanceof Ci.nsIDOMMediaList ||
michael@0 3846 aRawObj instanceof Ci.nsIDOMNodeList ||
michael@0 3847 aRawObj instanceof Ci.nsIDOMStyleSheetList)) {
michael@0 3848 return false;
michael@0 3849 }
michael@0 3850
michael@0 3851 if (typeof aRawObj.length != "number") {
michael@0 3852 return false;
michael@0 3853 }
michael@0 3854
michael@0 3855 aGrip.preview = {
michael@0 3856 kind: "ArrayLike",
michael@0 3857 length: aRawObj.length,
michael@0 3858 };
michael@0 3859
michael@0 3860 if (threadActor._gripDepth > 1) {
michael@0 3861 return true;
michael@0 3862 }
michael@0 3863
michael@0 3864 let items = aGrip.preview.items = [];
michael@0 3865
michael@0 3866 for (let i = 0; i < aRawObj.length &&
michael@0 3867 items.length < OBJECT_PREVIEW_MAX_ITEMS; i++) {
michael@0 3868 let value = makeDebuggeeValueIfNeeded(obj, aRawObj[i]);
michael@0 3869 items.push(threadActor.createValueGrip(value));
michael@0 3870 }
michael@0 3871
michael@0 3872 return true;
michael@0 3873 }, // ArrayLike
michael@0 3874
michael@0 3875 function CSSStyleDeclaration({obj, threadActor}, aGrip, aRawObj) {
michael@0 3876 if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSStyleDeclaration)) {
michael@0 3877 return false;
michael@0 3878 }
michael@0 3879
michael@0 3880 aGrip.preview = {
michael@0 3881 kind: "MapLike",
michael@0 3882 size: aRawObj.length,
michael@0 3883 };
michael@0 3884
michael@0 3885 let entries = aGrip.preview.entries = [];
michael@0 3886
michael@0 3887 for (let i = 0; i < OBJECT_PREVIEW_MAX_ITEMS &&
michael@0 3888 i < aRawObj.length; i++) {
michael@0 3889 let prop = aRawObj[i];
michael@0 3890 let value = aRawObj.getPropertyValue(prop);
michael@0 3891 entries.push([prop, threadActor.createValueGrip(value)]);
michael@0 3892 }
michael@0 3893
michael@0 3894 return true;
michael@0 3895 },
michael@0 3896
michael@0 3897 function DOMNode({obj, threadActor}, aGrip, aRawObj) {
michael@0 3898 if (obj.class == "Object" || !aRawObj || !(aRawObj instanceof Ci.nsIDOMNode)) {
michael@0 3899 return false;
michael@0 3900 }
michael@0 3901
michael@0 3902 let preview = aGrip.preview = {
michael@0 3903 kind: "DOMNode",
michael@0 3904 nodeType: aRawObj.nodeType,
michael@0 3905 nodeName: aRawObj.nodeName,
michael@0 3906 };
michael@0 3907
michael@0 3908 if (aRawObj instanceof Ci.nsIDOMDocument && aRawObj.location) {
michael@0 3909 preview.location = threadActor.createValueGrip(aRawObj.location.href);
michael@0 3910 } else if (aRawObj instanceof Ci.nsIDOMDocumentFragment) {
michael@0 3911 preview.childNodesLength = aRawObj.childNodes.length;
michael@0 3912
michael@0 3913 if (threadActor._gripDepth < 2) {
michael@0 3914 preview.childNodes = [];
michael@0 3915 for (let node of aRawObj.childNodes) {
michael@0 3916 let actor = threadActor.createValueGrip(obj.makeDebuggeeValue(node));
michael@0 3917 preview.childNodes.push(actor);
michael@0 3918 if (preview.childNodes.length == OBJECT_PREVIEW_MAX_ITEMS) {
michael@0 3919 break;
michael@0 3920 }
michael@0 3921 }
michael@0 3922 }
michael@0 3923 } else if (aRawObj instanceof Ci.nsIDOMElement) {
michael@0 3924 // Add preview for DOM element attributes.
michael@0 3925 if (aRawObj instanceof Ci.nsIDOMHTMLElement) {
michael@0 3926 preview.nodeName = preview.nodeName.toLowerCase();
michael@0 3927 }
michael@0 3928
michael@0 3929 let i = 0;
michael@0 3930 preview.attributes = {};
michael@0 3931 preview.attributesLength = aRawObj.attributes.length;
michael@0 3932 for (let attr of aRawObj.attributes) {
michael@0 3933 preview.attributes[attr.nodeName] = threadActor.createValueGrip(attr.value);
michael@0 3934 if (++i == OBJECT_PREVIEW_MAX_ITEMS) {
michael@0 3935 break;
michael@0 3936 }
michael@0 3937 }
michael@0 3938 } else if (aRawObj instanceof Ci.nsIDOMAttr) {
michael@0 3939 preview.value = threadActor.createValueGrip(aRawObj.value);
michael@0 3940 } else if (aRawObj instanceof Ci.nsIDOMText ||
michael@0 3941 aRawObj instanceof Ci.nsIDOMComment) {
michael@0 3942 preview.textContent = threadActor.createValueGrip(aRawObj.textContent);
michael@0 3943 }
michael@0 3944
michael@0 3945 return true;
michael@0 3946 }, // DOMNode
michael@0 3947
michael@0 3948 function DOMEvent({obj, threadActor}, aGrip, aRawObj) {
michael@0 3949 if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMEvent)) {
michael@0 3950 return false;
michael@0 3951 }
michael@0 3952
michael@0 3953 let preview = aGrip.preview = {
michael@0 3954 kind: "DOMEvent",
michael@0 3955 type: aRawObj.type,
michael@0 3956 properties: Object.create(null),
michael@0 3957 };
michael@0 3958
michael@0 3959 if (threadActor._gripDepth < 2) {
michael@0 3960 let target = obj.makeDebuggeeValue(aRawObj.target);
michael@0 3961 preview.target = threadActor.createValueGrip(target);
michael@0 3962 }
michael@0 3963
michael@0 3964 let props = [];
michael@0 3965 if (aRawObj instanceof Ci.nsIDOMMouseEvent) {
michael@0 3966 props.push("buttons", "clientX", "clientY", "layerX", "layerY");
michael@0 3967 } else if (aRawObj instanceof Ci.nsIDOMKeyEvent) {
michael@0 3968 let modifiers = [];
michael@0 3969 if (aRawObj.altKey) {
michael@0 3970 modifiers.push("Alt");
michael@0 3971 }
michael@0 3972 if (aRawObj.ctrlKey) {
michael@0 3973 modifiers.push("Control");
michael@0 3974 }
michael@0 3975 if (aRawObj.metaKey) {
michael@0 3976 modifiers.push("Meta");
michael@0 3977 }
michael@0 3978 if (aRawObj.shiftKey) {
michael@0 3979 modifiers.push("Shift");
michael@0 3980 }
michael@0 3981 preview.eventKind = "key";
michael@0 3982 preview.modifiers = modifiers;
michael@0 3983
michael@0 3984 props.push("key", "charCode", "keyCode");
michael@0 3985 } else if (aRawObj instanceof Ci.nsIDOMTransitionEvent ||
michael@0 3986 aRawObj instanceof Ci.nsIDOMAnimationEvent) {
michael@0 3987 props.push("animationName", "pseudoElement");
michael@0 3988 } else if (aRawObj instanceof Ci.nsIDOMClipboardEvent) {
michael@0 3989 props.push("clipboardData");
michael@0 3990 }
michael@0 3991
michael@0 3992 // Add event-specific properties.
michael@0 3993 for (let prop of props) {
michael@0 3994 let value = aRawObj[prop];
michael@0 3995 if (value && (typeof value == "object" || typeof value == "function")) {
michael@0 3996 // Skip properties pointing to objects.
michael@0 3997 if (threadActor._gripDepth > 1) {
michael@0 3998 continue;
michael@0 3999 }
michael@0 4000 value = obj.makeDebuggeeValue(value);
michael@0 4001 }
michael@0 4002 preview.properties[prop] = threadActor.createValueGrip(value);
michael@0 4003 }
michael@0 4004
michael@0 4005 // Add any properties we find on the event object.
michael@0 4006 if (!props.length) {
michael@0 4007 let i = 0;
michael@0 4008 for (let prop in aRawObj) {
michael@0 4009 let value = aRawObj[prop];
michael@0 4010 if (prop == "target" || prop == "type" || value === null ||
michael@0 4011 typeof value == "function") {
michael@0 4012 continue;
michael@0 4013 }
michael@0 4014 if (value && typeof value == "object") {
michael@0 4015 if (threadActor._gripDepth > 1) {
michael@0 4016 continue;
michael@0 4017 }
michael@0 4018 value = obj.makeDebuggeeValue(value);
michael@0 4019 }
michael@0 4020 preview.properties[prop] = threadActor.createValueGrip(value);
michael@0 4021 if (++i == OBJECT_PREVIEW_MAX_ITEMS) {
michael@0 4022 break;
michael@0 4023 }
michael@0 4024 }
michael@0 4025 }
michael@0 4026
michael@0 4027 return true;
michael@0 4028 }, // DOMEvent
michael@0 4029
michael@0 4030 function DOMException({obj, threadActor}, aGrip, aRawObj) {
michael@0 4031 if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMDOMException)) {
michael@0 4032 return false;
michael@0 4033 }
michael@0 4034
michael@0 4035 aGrip.preview = {
michael@0 4036 kind: "DOMException",
michael@0 4037 name: threadActor.createValueGrip(aRawObj.name),
michael@0 4038 message: threadActor.createValueGrip(aRawObj.message),
michael@0 4039 code: threadActor.createValueGrip(aRawObj.code),
michael@0 4040 result: threadActor.createValueGrip(aRawObj.result),
michael@0 4041 filename: threadActor.createValueGrip(aRawObj.filename),
michael@0 4042 lineNumber: threadActor.createValueGrip(aRawObj.lineNumber),
michael@0 4043 columnNumber: threadActor.createValueGrip(aRawObj.columnNumber),
michael@0 4044 };
michael@0 4045
michael@0 4046 return true;
michael@0 4047 },
michael@0 4048
michael@0 4049 function GenericObject(aObjectActor, aGrip) {
michael@0 4050 let {obj, threadActor} = aObjectActor;
michael@0 4051 if (aGrip.preview || aGrip.displayString || threadActor._gripDepth > 1) {
michael@0 4052 return false;
michael@0 4053 }
michael@0 4054
michael@0 4055 let i = 0, names = [];
michael@0 4056 let preview = aGrip.preview = {
michael@0 4057 kind: "Object",
michael@0 4058 ownProperties: Object.create(null),
michael@0 4059 };
michael@0 4060
michael@0 4061 try {
michael@0 4062 names = obj.getOwnPropertyNames();
michael@0 4063 } catch (ex) {
michael@0 4064 // Calling getOwnPropertyNames() on some wrapped native prototypes is not
michael@0 4065 // allowed: "cannot modify properties of a WrappedNative". See bug 952093.
michael@0 4066 }
michael@0 4067
michael@0 4068 preview.ownPropertiesLength = names.length;
michael@0 4069
michael@0 4070 for (let name of names) {
michael@0 4071 let desc = aObjectActor._propertyDescriptor(name, true);
michael@0 4072 if (!desc) {
michael@0 4073 continue;
michael@0 4074 }
michael@0 4075
michael@0 4076 preview.ownProperties[name] = desc;
michael@0 4077 if (++i == OBJECT_PREVIEW_MAX_ITEMS) {
michael@0 4078 break;
michael@0 4079 }
michael@0 4080 }
michael@0 4081
michael@0 4082 if (i < OBJECT_PREVIEW_MAX_ITEMS) {
michael@0 4083 preview.safeGetterValues = aObjectActor.
michael@0 4084 _findSafeGetterValues(preview.ownProperties,
michael@0 4085 OBJECT_PREVIEW_MAX_ITEMS - i);
michael@0 4086 }
michael@0 4087
michael@0 4088 return true;
michael@0 4089 }, // GenericObject
michael@0 4090 ]; // DebuggerServer.ObjectActorPreviewers.Object
michael@0 4091
michael@0 4092 /**
michael@0 4093 * Creates a pause-scoped actor for the specified object.
michael@0 4094 * @see ObjectActor
michael@0 4095 */
michael@0 4096 function PauseScopedObjectActor()
michael@0 4097 {
michael@0 4098 ObjectActor.apply(this, arguments);
michael@0 4099 }
michael@0 4100
michael@0 4101 PauseScopedObjectActor.prototype = Object.create(PauseScopedActor.prototype);
michael@0 4102
michael@0 4103 update(PauseScopedObjectActor.prototype, ObjectActor.prototype);
michael@0 4104
michael@0 4105 update(PauseScopedObjectActor.prototype, {
michael@0 4106 constructor: PauseScopedObjectActor,
michael@0 4107 actorPrefix: "pausedobj",
michael@0 4108
michael@0 4109 onOwnPropertyNames:
michael@0 4110 PauseScopedActor.withPaused(ObjectActor.prototype.onOwnPropertyNames),
michael@0 4111
michael@0 4112 onPrototypeAndProperties:
michael@0 4113 PauseScopedActor.withPaused(ObjectActor.prototype.onPrototypeAndProperties),
michael@0 4114
michael@0 4115 onPrototype: PauseScopedActor.withPaused(ObjectActor.prototype.onPrototype),
michael@0 4116 onProperty: PauseScopedActor.withPaused(ObjectActor.prototype.onProperty),
michael@0 4117 onDecompile: PauseScopedActor.withPaused(ObjectActor.prototype.onDecompile),
michael@0 4118
michael@0 4119 onDisplayString:
michael@0 4120 PauseScopedActor.withPaused(ObjectActor.prototype.onDisplayString),
michael@0 4121
michael@0 4122 onParameterNames:
michael@0 4123 PauseScopedActor.withPaused(ObjectActor.prototype.onParameterNames),
michael@0 4124
michael@0 4125 /**
michael@0 4126 * Handle a protocol request to promote a pause-lifetime grip to a
michael@0 4127 * thread-lifetime grip.
michael@0 4128 *
michael@0 4129 * @param aRequest object
michael@0 4130 * The protocol request object.
michael@0 4131 */
michael@0 4132 onThreadGrip: PauseScopedActor.withPaused(function (aRequest) {
michael@0 4133 this.threadActor.threadObjectGrip(this);
michael@0 4134 return {};
michael@0 4135 }),
michael@0 4136
michael@0 4137 /**
michael@0 4138 * Handle a protocol request to release a thread-lifetime grip.
michael@0 4139 *
michael@0 4140 * @param aRequest object
michael@0 4141 * The protocol request object.
michael@0 4142 */
michael@0 4143 onRelease: PauseScopedActor.withPaused(function (aRequest) {
michael@0 4144 if (this.registeredPool !== this.threadActor.threadLifetimePool) {
michael@0 4145 return { error: "notReleasable",
michael@0 4146 message: "Only thread-lifetime actors can be released." };
michael@0 4147 }
michael@0 4148
michael@0 4149 this.release();
michael@0 4150 return {};
michael@0 4151 }),
michael@0 4152 });
michael@0 4153
michael@0 4154 update(PauseScopedObjectActor.prototype.requestTypes, {
michael@0 4155 "threadGrip": PauseScopedObjectActor.prototype.onThreadGrip,
michael@0 4156 });
michael@0 4157
michael@0 4158
michael@0 4159 /**
michael@0 4160 * Creates an actor for the specied "very long" string. "Very long" is specified
michael@0 4161 * at the server's discretion.
michael@0 4162 *
michael@0 4163 * @param aString String
michael@0 4164 * The string.
michael@0 4165 */
michael@0 4166 function LongStringActor(aString)
michael@0 4167 {
michael@0 4168 this.string = aString;
michael@0 4169 this.stringLength = aString.length;
michael@0 4170 }
michael@0 4171
michael@0 4172 LongStringActor.prototype = {
michael@0 4173
michael@0 4174 actorPrefix: "longString",
michael@0 4175
michael@0 4176 disconnect: function () {
michael@0 4177 // Because longStringActors is not a weak map, we won't automatically leave
michael@0 4178 // it so we need to manually leave on disconnect so that we don't leak
michael@0 4179 // memory.
michael@0 4180 if (this.registeredPool && this.registeredPool.longStringActors) {
michael@0 4181 delete this.registeredPool.longStringActors[this.actorID];
michael@0 4182 }
michael@0 4183 },
michael@0 4184
michael@0 4185 /**
michael@0 4186 * Returns a grip for this actor for returning in a protocol message.
michael@0 4187 */
michael@0 4188 grip: function () {
michael@0 4189 return {
michael@0 4190 "type": "longString",
michael@0 4191 "initial": this.string.substring(
michael@0 4192 0, DebuggerServer.LONG_STRING_INITIAL_LENGTH),
michael@0 4193 "length": this.stringLength,
michael@0 4194 "actor": this.actorID
michael@0 4195 };
michael@0 4196 },
michael@0 4197
michael@0 4198 /**
michael@0 4199 * Handle a request to extract part of this actor's string.
michael@0 4200 *
michael@0 4201 * @param aRequest object
michael@0 4202 * The protocol request object.
michael@0 4203 */
michael@0 4204 onSubstring: function (aRequest) {
michael@0 4205 return {
michael@0 4206 "from": this.actorID,
michael@0 4207 "substring": this.string.substring(aRequest.start, aRequest.end)
michael@0 4208 };
michael@0 4209 },
michael@0 4210
michael@0 4211 /**
michael@0 4212 * Handle a request to release this LongStringActor instance.
michael@0 4213 */
michael@0 4214 onRelease: function () {
michael@0 4215 // TODO: also check if registeredPool === threadActor.threadLifetimePool
michael@0 4216 // when the web console moves aray from manually releasing pause-scoped
michael@0 4217 // actors.
michael@0 4218 if (this.registeredPool.longStringActors) {
michael@0 4219 delete this.registeredPool.longStringActors[this.actorID];
michael@0 4220 }
michael@0 4221 this.registeredPool.removeActor(this);
michael@0 4222 return {};
michael@0 4223 },
michael@0 4224 };
michael@0 4225
michael@0 4226 LongStringActor.prototype.requestTypes = {
michael@0 4227 "substring": LongStringActor.prototype.onSubstring,
michael@0 4228 "release": LongStringActor.prototype.onRelease
michael@0 4229 };
michael@0 4230
michael@0 4231
michael@0 4232 /**
michael@0 4233 * Creates an actor for the specified stack frame.
michael@0 4234 *
michael@0 4235 * @param aFrame Debugger.Frame
michael@0 4236 * The debuggee frame.
michael@0 4237 * @param aThreadActor ThreadActor
michael@0 4238 * The parent thread actor for this frame.
michael@0 4239 */
michael@0 4240 function FrameActor(aFrame, aThreadActor)
michael@0 4241 {
michael@0 4242 this.frame = aFrame;
michael@0 4243 this.threadActor = aThreadActor;
michael@0 4244 }
michael@0 4245
michael@0 4246 FrameActor.prototype = {
michael@0 4247 actorPrefix: "frame",
michael@0 4248
michael@0 4249 /**
michael@0 4250 * A pool that contains frame-lifetime objects, like the environment.
michael@0 4251 */
michael@0 4252 _frameLifetimePool: null,
michael@0 4253 get frameLifetimePool() {
michael@0 4254 if (!this._frameLifetimePool) {
michael@0 4255 this._frameLifetimePool = new ActorPool(this.conn);
michael@0 4256 this.conn.addActorPool(this._frameLifetimePool);
michael@0 4257 }
michael@0 4258 return this._frameLifetimePool;
michael@0 4259 },
michael@0 4260
michael@0 4261 /**
michael@0 4262 * Finalization handler that is called when the actor is being evicted from
michael@0 4263 * the pool.
michael@0 4264 */
michael@0 4265 disconnect: function () {
michael@0 4266 this.conn.removeActorPool(this._frameLifetimePool);
michael@0 4267 this._frameLifetimePool = null;
michael@0 4268 },
michael@0 4269
michael@0 4270 /**
michael@0 4271 * Returns a frame form for use in a protocol message.
michael@0 4272 */
michael@0 4273 form: function () {
michael@0 4274 let form = { actor: this.actorID,
michael@0 4275 type: this.frame.type };
michael@0 4276 if (this.frame.type === "call") {
michael@0 4277 form.callee = this.threadActor.createValueGrip(this.frame.callee);
michael@0 4278 }
michael@0 4279
michael@0 4280 if (this.frame.environment) {
michael@0 4281 let envActor = this.threadActor
michael@0 4282 .createEnvironmentActor(this.frame.environment,
michael@0 4283 this.frameLifetimePool);
michael@0 4284 form.environment = envActor.form();
michael@0 4285 }
michael@0 4286 form.this = this.threadActor.createValueGrip(this.frame.this);
michael@0 4287 form.arguments = this._args();
michael@0 4288 if (this.frame.script) {
michael@0 4289 form.where = getFrameLocation(this.frame);
michael@0 4290 }
michael@0 4291
michael@0 4292 if (!this.frame.older) {
michael@0 4293 form.oldest = true;
michael@0 4294 }
michael@0 4295
michael@0 4296 return form;
michael@0 4297 },
michael@0 4298
michael@0 4299 _args: function () {
michael@0 4300 if (!this.frame.arguments) {
michael@0 4301 return [];
michael@0 4302 }
michael@0 4303
michael@0 4304 return [this.threadActor.createValueGrip(arg)
michael@0 4305 for each (arg in this.frame.arguments)];
michael@0 4306 },
michael@0 4307
michael@0 4308 /**
michael@0 4309 * Handle a protocol request to pop this frame from the stack.
michael@0 4310 *
michael@0 4311 * @param aRequest object
michael@0 4312 * The protocol request object.
michael@0 4313 */
michael@0 4314 onPop: function (aRequest) {
michael@0 4315 // TODO: remove this when Debugger.Frame.prototype.pop is implemented
michael@0 4316 if (typeof this.frame.pop != "function") {
michael@0 4317 return { error: "notImplemented",
michael@0 4318 message: "Popping frames is not yet implemented." };
michael@0 4319 }
michael@0 4320
michael@0 4321 while (this.frame != this.threadActor.dbg.getNewestFrame()) {
michael@0 4322 this.threadActor.dbg.getNewestFrame().pop();
michael@0 4323 }
michael@0 4324 this.frame.pop(aRequest.completionValue);
michael@0 4325
michael@0 4326 // TODO: return the watches property when frame pop watch actors are
michael@0 4327 // implemented.
michael@0 4328 return { from: this.actorID };
michael@0 4329 }
michael@0 4330 };
michael@0 4331
michael@0 4332 FrameActor.prototype.requestTypes = {
michael@0 4333 "pop": FrameActor.prototype.onPop,
michael@0 4334 };
michael@0 4335
michael@0 4336
michael@0 4337 /**
michael@0 4338 * Creates a BreakpointActor. BreakpointActors exist for the lifetime of their
michael@0 4339 * containing thread and are responsible for deleting breakpoints, handling
michael@0 4340 * breakpoint hits and associating breakpoints with scripts.
michael@0 4341 *
michael@0 4342 * @param ThreadActor aThreadActor
michael@0 4343 * The parent thread actor that contains this breakpoint.
michael@0 4344 * @param object aLocation
michael@0 4345 * The location of the breakpoint as specified in the protocol.
michael@0 4346 */
michael@0 4347 function BreakpointActor(aThreadActor, { url, line, column, condition })
michael@0 4348 {
michael@0 4349 this.scripts = [];
michael@0 4350 this.threadActor = aThreadActor;
michael@0 4351 this.location = { url: url, line: line, column: column };
michael@0 4352 this.condition = condition;
michael@0 4353 }
michael@0 4354
michael@0 4355 BreakpointActor.prototype = {
michael@0 4356 actorPrefix: "breakpoint",
michael@0 4357 condition: null,
michael@0 4358
michael@0 4359 /**
michael@0 4360 * Called when this same breakpoint is added to another Debugger.Script
michael@0 4361 * instance, in the case of a page reload.
michael@0 4362 *
michael@0 4363 * @param aScript Debugger.Script
michael@0 4364 * The new source script on which the breakpoint has been set.
michael@0 4365 * @param ThreadActor aThreadActor
michael@0 4366 * The parent thread actor that contains this breakpoint.
michael@0 4367 */
michael@0 4368 addScript: function (aScript, aThreadActor) {
michael@0 4369 this.threadActor = aThreadActor;
michael@0 4370 this.scripts.push(aScript);
michael@0 4371 },
michael@0 4372
michael@0 4373 /**
michael@0 4374 * Remove the breakpoints from associated scripts and clear the script cache.
michael@0 4375 */
michael@0 4376 removeScripts: function () {
michael@0 4377 for (let script of this.scripts) {
michael@0 4378 script.clearBreakpoint(this);
michael@0 4379 }
michael@0 4380 this.scripts = [];
michael@0 4381 },
michael@0 4382
michael@0 4383 /**
michael@0 4384 * Check if this breakpoint has a condition that doesn't error and
michael@0 4385 * evaluates to true in aFrame
michael@0 4386 *
michael@0 4387 * @param aFrame Debugger.Frame
michael@0 4388 * The frame to evaluate the condition in
michael@0 4389 */
michael@0 4390 isValidCondition: function(aFrame) {
michael@0 4391 if(!this.condition) {
michael@0 4392 return true;
michael@0 4393 }
michael@0 4394 var res = aFrame.eval(this.condition);
michael@0 4395 return res.return;
michael@0 4396 },
michael@0 4397
michael@0 4398 /**
michael@0 4399 * A function that the engine calls when a breakpoint has been hit.
michael@0 4400 *
michael@0 4401 * @param aFrame Debugger.Frame
michael@0 4402 * The stack frame that contained the breakpoint.
michael@0 4403 */
michael@0 4404 hit: function (aFrame) {
michael@0 4405 // Don't pause if we are currently stepping (in or over) or the frame is
michael@0 4406 // black-boxed.
michael@0 4407 let { url } = this.threadActor.synchronize(
michael@0 4408 this.threadActor.sources.getOriginalLocation({
michael@0 4409 url: this.location.url,
michael@0 4410 line: this.location.line,
michael@0 4411 column: this.location.column
michael@0 4412 }));
michael@0 4413
michael@0 4414 if (this.threadActor.sources.isBlackBoxed(url)
michael@0 4415 || aFrame.onStep
michael@0 4416 || !this.isValidCondition(aFrame)) {
michael@0 4417 return undefined;
michael@0 4418 }
michael@0 4419
michael@0 4420 let reason = {};
michael@0 4421 if (this.threadActor._hiddenBreakpoints.has(this.actorID)) {
michael@0 4422 reason.type = "pauseOnDOMEvents";
michael@0 4423 } else {
michael@0 4424 reason.type = "breakpoint";
michael@0 4425 // TODO: add the rest of the breakpoints on that line (bug 676602).
michael@0 4426 reason.actors = [ this.actorID ];
michael@0 4427 }
michael@0 4428 return this.threadActor._pauseAndRespond(aFrame, reason);
michael@0 4429 },
michael@0 4430
michael@0 4431 /**
michael@0 4432 * Handle a protocol request to remove this breakpoint.
michael@0 4433 *
michael@0 4434 * @param aRequest object
michael@0 4435 * The protocol request object.
michael@0 4436 */
michael@0 4437 onDelete: function (aRequest) {
michael@0 4438 // Remove from the breakpoint store.
michael@0 4439 this.threadActor.breakpointStore.removeBreakpoint(this.location);
michael@0 4440 this.threadActor.threadLifetimePool.removeActor(this);
michael@0 4441 // Remove the actual breakpoint from the associated scripts.
michael@0 4442 this.removeScripts();
michael@0 4443 return { from: this.actorID };
michael@0 4444 }
michael@0 4445 };
michael@0 4446
michael@0 4447 BreakpointActor.prototype.requestTypes = {
michael@0 4448 "delete": BreakpointActor.prototype.onDelete
michael@0 4449 };
michael@0 4450
michael@0 4451
michael@0 4452 /**
michael@0 4453 * Creates an EnvironmentActor. EnvironmentActors are responsible for listing
michael@0 4454 * the bindings introduced by a lexical environment and assigning new values to
michael@0 4455 * those identifier bindings.
michael@0 4456 *
michael@0 4457 * @param Debugger.Environment aEnvironment
michael@0 4458 * The lexical environment that will be used to create the actor.
michael@0 4459 * @param ThreadActor aThreadActor
michael@0 4460 * The parent thread actor that contains this environment.
michael@0 4461 */
michael@0 4462 function EnvironmentActor(aEnvironment, aThreadActor)
michael@0 4463 {
michael@0 4464 this.obj = aEnvironment;
michael@0 4465 this.threadActor = aThreadActor;
michael@0 4466 }
michael@0 4467
michael@0 4468 EnvironmentActor.prototype = {
michael@0 4469 actorPrefix: "environment",
michael@0 4470
michael@0 4471 /**
michael@0 4472 * Return an environment form for use in a protocol message.
michael@0 4473 */
michael@0 4474 form: function () {
michael@0 4475 let form = { actor: this.actorID };
michael@0 4476
michael@0 4477 // What is this environment's type?
michael@0 4478 if (this.obj.type == "declarative") {
michael@0 4479 form.type = this.obj.callee ? "function" : "block";
michael@0 4480 } else {
michael@0 4481 form.type = this.obj.type;
michael@0 4482 }
michael@0 4483
michael@0 4484 // Does this environment have a parent?
michael@0 4485 if (this.obj.parent) {
michael@0 4486 form.parent = (this.threadActor
michael@0 4487 .createEnvironmentActor(this.obj.parent,
michael@0 4488 this.registeredPool)
michael@0 4489 .form());
michael@0 4490 }
michael@0 4491
michael@0 4492 // Does this environment reflect the properties of an object as variables?
michael@0 4493 if (this.obj.type == "object" || this.obj.type == "with") {
michael@0 4494 form.object = this.threadActor.createValueGrip(this.obj.object);
michael@0 4495 }
michael@0 4496
michael@0 4497 // Is this the environment created for a function call?
michael@0 4498 if (this.obj.callee) {
michael@0 4499 form.function = this.threadActor.createValueGrip(this.obj.callee);
michael@0 4500 }
michael@0 4501
michael@0 4502 // Shall we list this environment's bindings?
michael@0 4503 if (this.obj.type == "declarative") {
michael@0 4504 form.bindings = this._bindings();
michael@0 4505 }
michael@0 4506
michael@0 4507 return form;
michael@0 4508 },
michael@0 4509
michael@0 4510 /**
michael@0 4511 * Return the identifier bindings object as required by the remote protocol
michael@0 4512 * specification.
michael@0 4513 */
michael@0 4514 _bindings: function () {
michael@0 4515 let bindings = { arguments: [], variables: {} };
michael@0 4516
michael@0 4517 // TODO: this part should be removed in favor of the commented-out part
michael@0 4518 // below when getVariableDescriptor lands (bug 725815).
michael@0 4519 if (typeof this.obj.getVariable != "function") {
michael@0 4520 //if (typeof this.obj.getVariableDescriptor != "function") {
michael@0 4521 return bindings;
michael@0 4522 }
michael@0 4523
michael@0 4524 let parameterNames;
michael@0 4525 if (this.obj.callee) {
michael@0 4526 parameterNames = this.obj.callee.parameterNames;
michael@0 4527 }
michael@0 4528 for each (let name in parameterNames) {
michael@0 4529 let arg = {};
michael@0 4530
michael@0 4531 let value = this.obj.getVariable(name);
michael@0 4532 // The slot is optimized out.
michael@0 4533 // FIXME: Need actual UI, bug 941287.
michael@0 4534 if (value && value.optimizedOut) {
michael@0 4535 continue;
michael@0 4536 }
michael@0 4537
michael@0 4538 // TODO: this part should be removed in favor of the commented-out part
michael@0 4539 // below when getVariableDescriptor lands (bug 725815).
michael@0 4540 let desc = {
michael@0 4541 value: value,
michael@0 4542 configurable: false,
michael@0 4543 writable: true,
michael@0 4544 enumerable: true
michael@0 4545 };
michael@0 4546
michael@0 4547 // let desc = this.obj.getVariableDescriptor(name);
michael@0 4548 let descForm = {
michael@0 4549 enumerable: true,
michael@0 4550 configurable: desc.configurable
michael@0 4551 };
michael@0 4552 if ("value" in desc) {
michael@0 4553 descForm.value = this.threadActor.createValueGrip(desc.value);
michael@0 4554 descForm.writable = desc.writable;
michael@0 4555 } else {
michael@0 4556 descForm.get = this.threadActor.createValueGrip(desc.get);
michael@0 4557 descForm.set = this.threadActor.createValueGrip(desc.set);
michael@0 4558 }
michael@0 4559 arg[name] = descForm;
michael@0 4560 bindings.arguments.push(arg);
michael@0 4561 }
michael@0 4562
michael@0 4563 for each (let name in this.obj.names()) {
michael@0 4564 if (bindings.arguments.some(function exists(element) {
michael@0 4565 return !!element[name];
michael@0 4566 })) {
michael@0 4567 continue;
michael@0 4568 }
michael@0 4569
michael@0 4570 let value = this.obj.getVariable(name);
michael@0 4571 // The slot is optimized out or arguments on a dead scope.
michael@0 4572 // FIXME: Need actual UI, bug 941287.
michael@0 4573 if (value && (value.optimizedOut || value.missingArguments)) {
michael@0 4574 continue;
michael@0 4575 }
michael@0 4576
michael@0 4577 // TODO: this part should be removed in favor of the commented-out part
michael@0 4578 // below when getVariableDescriptor lands.
michael@0 4579 let desc = {
michael@0 4580 value: value,
michael@0 4581 configurable: false,
michael@0 4582 writable: true,
michael@0 4583 enumerable: true
michael@0 4584 };
michael@0 4585
michael@0 4586 //let desc = this.obj.getVariableDescriptor(name);
michael@0 4587 let descForm = {
michael@0 4588 enumerable: true,
michael@0 4589 configurable: desc.configurable
michael@0 4590 };
michael@0 4591 if ("value" in desc) {
michael@0 4592 descForm.value = this.threadActor.createValueGrip(desc.value);
michael@0 4593 descForm.writable = desc.writable;
michael@0 4594 } else {
michael@0 4595 descForm.get = this.threadActor.createValueGrip(desc.get);
michael@0 4596 descForm.set = this.threadActor.createValueGrip(desc.set);
michael@0 4597 }
michael@0 4598 bindings.variables[name] = descForm;
michael@0 4599 }
michael@0 4600
michael@0 4601 return bindings;
michael@0 4602 },
michael@0 4603
michael@0 4604 /**
michael@0 4605 * Handle a protocol request to change the value of a variable bound in this
michael@0 4606 * lexical environment.
michael@0 4607 *
michael@0 4608 * @param aRequest object
michael@0 4609 * The protocol request object.
michael@0 4610 */
michael@0 4611 onAssign: function (aRequest) {
michael@0 4612 // TODO: enable the commented-out part when getVariableDescriptor lands
michael@0 4613 // (bug 725815).
michael@0 4614 /*let desc = this.obj.getVariableDescriptor(aRequest.name);
michael@0 4615
michael@0 4616 if (!desc.writable) {
michael@0 4617 return { error: "immutableBinding",
michael@0 4618 message: "Changing the value of an immutable binding is not " +
michael@0 4619 "allowed" };
michael@0 4620 }*/
michael@0 4621
michael@0 4622 try {
michael@0 4623 this.obj.setVariable(aRequest.name, aRequest.value);
michael@0 4624 } catch (e if e instanceof Debugger.DebuggeeWouldRun) {
michael@0 4625 return { error: "threadWouldRun",
michael@0 4626 cause: e.cause ? e.cause : "setter",
michael@0 4627 message: "Assigning a value would cause the debuggee to run" };
michael@0 4628 }
michael@0 4629 return { from: this.actorID };
michael@0 4630 },
michael@0 4631
michael@0 4632 /**
michael@0 4633 * Handle a protocol request to fully enumerate the bindings introduced by the
michael@0 4634 * lexical environment.
michael@0 4635 *
michael@0 4636 * @param aRequest object
michael@0 4637 * The protocol request object.
michael@0 4638 */
michael@0 4639 onBindings: function (aRequest) {
michael@0 4640 return { from: this.actorID,
michael@0 4641 bindings: this._bindings() };
michael@0 4642 }
michael@0 4643 };
michael@0 4644
michael@0 4645 EnvironmentActor.prototype.requestTypes = {
michael@0 4646 "assign": EnvironmentActor.prototype.onAssign,
michael@0 4647 "bindings": EnvironmentActor.prototype.onBindings
michael@0 4648 };
michael@0 4649
michael@0 4650 /**
michael@0 4651 * Override the toString method in order to get more meaningful script output
michael@0 4652 * for debugging the debugger.
michael@0 4653 */
michael@0 4654 Debugger.Script.prototype.toString = function() {
michael@0 4655 let output = "";
michael@0 4656 if (this.url) {
michael@0 4657 output += this.url;
michael@0 4658 }
michael@0 4659 if (typeof this.startLine != "undefined") {
michael@0 4660 output += ":" + this.startLine;
michael@0 4661 if (this.lineCount && this.lineCount > 1) {
michael@0 4662 output += "-" + (this.startLine + this.lineCount - 1);
michael@0 4663 }
michael@0 4664 }
michael@0 4665 if (this.strictMode) {
michael@0 4666 output += ":strict";
michael@0 4667 }
michael@0 4668 return output;
michael@0 4669 };
michael@0 4670
michael@0 4671 /**
michael@0 4672 * Helper property for quickly getting to the line number a stack frame is
michael@0 4673 * currently paused at.
michael@0 4674 */
michael@0 4675 Object.defineProperty(Debugger.Frame.prototype, "line", {
michael@0 4676 configurable: true,
michael@0 4677 get: function() {
michael@0 4678 if (this.script) {
michael@0 4679 return this.script.getOffsetLine(this.offset);
michael@0 4680 } else {
michael@0 4681 return null;
michael@0 4682 }
michael@0 4683 }
michael@0 4684 });
michael@0 4685
michael@0 4686
michael@0 4687 /**
michael@0 4688 * Creates an actor for handling chrome debugging. ChromeDebuggerActor is a
michael@0 4689 * thin wrapper over ThreadActor, slightly changing some of its behavior.
michael@0 4690 *
michael@0 4691 * @param aConnection object
michael@0 4692 * The DebuggerServerConnection with which this ChromeDebuggerActor
michael@0 4693 * is associated. (Currently unused, but required to make this
michael@0 4694 * constructor usable with addGlobalActor.)
michael@0 4695 *
michael@0 4696 * @param aHooks object
michael@0 4697 * An object with preNest and postNest methods for calling when entering
michael@0 4698 * and exiting a nested event loop.
michael@0 4699 */
michael@0 4700 function ChromeDebuggerActor(aConnection, aHooks)
michael@0 4701 {
michael@0 4702 ThreadActor.call(this, aHooks);
michael@0 4703 }
michael@0 4704
michael@0 4705 ChromeDebuggerActor.prototype = Object.create(ThreadActor.prototype);
michael@0 4706
michael@0 4707 update(ChromeDebuggerActor.prototype, {
michael@0 4708 constructor: ChromeDebuggerActor,
michael@0 4709
michael@0 4710 // A constant prefix that will be used to form the actor ID by the server.
michael@0 4711 actorPrefix: "chromeDebugger",
michael@0 4712
michael@0 4713 /**
michael@0 4714 * Override the eligibility check for scripts and sources to make sure every
michael@0 4715 * script and source with a URL is stored when debugging chrome.
michael@0 4716 */
michael@0 4717 _allowSource: function(aSourceURL) !!aSourceURL,
michael@0 4718
michael@0 4719 /**
michael@0 4720 * An object that will be used by ThreadActors to tailor their behavior
michael@0 4721 * depending on the debugging context being required (chrome or content).
michael@0 4722 * The methods that this object provides must be bound to the ThreadActor
michael@0 4723 * before use.
michael@0 4724 */
michael@0 4725 globalManager: {
michael@0 4726 findGlobals: function () {
michael@0 4727 // Add every global known to the debugger as debuggee.
michael@0 4728 this.dbg.addAllGlobalsAsDebuggees();
michael@0 4729 },
michael@0 4730
michael@0 4731 /**
michael@0 4732 * A function that the engine calls when a new global object has been
michael@0 4733 * created.
michael@0 4734 *
michael@0 4735 * @param aGlobal Debugger.Object
michael@0 4736 * The new global object that was created.
michael@0 4737 */
michael@0 4738 onNewGlobal: function (aGlobal) {
michael@0 4739 this.addDebuggee(aGlobal);
michael@0 4740 // Notify the client.
michael@0 4741 this.conn.send({
michael@0 4742 from: this.actorID,
michael@0 4743 type: "newGlobal",
michael@0 4744 // TODO: after bug 801084 lands see if we need to JSONify this.
michael@0 4745 hostAnnotations: aGlobal.hostAnnotations
michael@0 4746 });
michael@0 4747 }
michael@0 4748 }
michael@0 4749 });
michael@0 4750
michael@0 4751 /**
michael@0 4752 * Creates an actor for handling add-on debugging. AddonThreadActor is
michael@0 4753 * a thin wrapper over ThreadActor.
michael@0 4754 *
michael@0 4755 * @param aConnection object
michael@0 4756 * The DebuggerServerConnection with which this AddonThreadActor
michael@0 4757 * is associated. (Currently unused, but required to make this
michael@0 4758 * constructor usable with addGlobalActor.)
michael@0 4759 *
michael@0 4760 * @param aHooks object
michael@0 4761 * An object with preNest and postNest methods for calling
michael@0 4762 * when entering and exiting a nested event loops.
michael@0 4763 *
michael@0 4764 * @param aAddonID string
michael@0 4765 * ID of the add-on this actor will debug. It will be used to
michael@0 4766 * filter out globals marked for debugging.
michael@0 4767 */
michael@0 4768
michael@0 4769 function AddonThreadActor(aConnect, aHooks, aAddonID) {
michael@0 4770 this.addonID = aAddonID;
michael@0 4771 ThreadActor.call(this, aHooks);
michael@0 4772 }
michael@0 4773
michael@0 4774 AddonThreadActor.prototype = Object.create(ThreadActor.prototype);
michael@0 4775
michael@0 4776 update(AddonThreadActor.prototype, {
michael@0 4777 constructor: AddonThreadActor,
michael@0 4778
michael@0 4779 // A constant prefix that will be used to form the actor ID by the server.
michael@0 4780 actorPrefix: "addonThread",
michael@0 4781
michael@0 4782 onAttach: function(aRequest) {
michael@0 4783 if (!this.attached) {
michael@0 4784 Services.obs.addObserver(this, "chrome-document-global-created", false);
michael@0 4785 Services.obs.addObserver(this, "content-document-global-created", false);
michael@0 4786 }
michael@0 4787 return ThreadActor.prototype.onAttach.call(this, aRequest);
michael@0 4788 },
michael@0 4789
michael@0 4790 disconnect: function() {
michael@0 4791 if (this.attached) {
michael@0 4792 Services.obs.removeObserver(this, "content-document-global-created");
michael@0 4793 Services.obs.removeObserver(this, "chrome-document-global-created");
michael@0 4794 }
michael@0 4795 return ThreadActor.prototype.disconnect.call(this);
michael@0 4796 },
michael@0 4797
michael@0 4798 /**
michael@0 4799 * Called when a new DOM document global is created. Check if the DOM was
michael@0 4800 * loaded from an add-on and if so make the window a debuggee.
michael@0 4801 */
michael@0 4802 observe: function(aSubject, aTopic, aData) {
michael@0 4803 let id = {};
michael@0 4804 if (mapURIToAddonID(aSubject.location, id) && id.value === this.addonID) {
michael@0 4805 this.dbg.addDebuggee(aSubject.defaultView);
michael@0 4806 }
michael@0 4807 },
michael@0 4808
michael@0 4809 /**
michael@0 4810 * Override the eligibility check for scripts and sources to make
michael@0 4811 * sure every script and source with a URL is stored when debugging
michael@0 4812 * add-ons.
michael@0 4813 */
michael@0 4814 _allowSource: function(aSourceURL) {
michael@0 4815 // Hide eval scripts
michael@0 4816 if (!aSourceURL) {
michael@0 4817 return false;
michael@0 4818 }
michael@0 4819
michael@0 4820 // XPIProvider.jsm evals some code in every add-on's bootstrap.js. Hide it
michael@0 4821 if (aSourceURL == "resource://gre/modules/addons/XPIProvider.jsm") {
michael@0 4822 return false;
michael@0 4823 }
michael@0 4824
michael@0 4825 return true;
michael@0 4826 },
michael@0 4827
michael@0 4828 /**
michael@0 4829 * An object that will be used by ThreadActors to tailor their
michael@0 4830 * behaviour depending on the debugging context being required (chrome,
michael@0 4831 * addon or content). The methods that this object provides must
michael@0 4832 * be bound to the ThreadActor before use.
michael@0 4833 */
michael@0 4834 globalManager: {
michael@0 4835 findGlobals: function ADA_findGlobals() {
michael@0 4836 for (let global of this.dbg.findAllGlobals()) {
michael@0 4837 if (this._checkGlobal(global)) {
michael@0 4838 this.dbg.addDebuggee(global);
michael@0 4839 }
michael@0 4840 }
michael@0 4841 },
michael@0 4842
michael@0 4843 /**
michael@0 4844 * A function that the engine calls when a new global object
michael@0 4845 * has been created.
michael@0 4846 *
michael@0 4847 * @param aGlobal Debugger.Object
michael@0 4848 * The new global object that was created.
michael@0 4849 */
michael@0 4850 onNewGlobal: function ADA_onNewGlobal(aGlobal) {
michael@0 4851 if (this._checkGlobal(aGlobal)) {
michael@0 4852 this.addDebuggee(aGlobal);
michael@0 4853 // Notify the client.
michael@0 4854 this.conn.send({
michael@0 4855 from: this.actorID,
michael@0 4856 type: "newGlobal",
michael@0 4857 // TODO: after bug 801084 lands see if we need to JSONify this.
michael@0 4858 hostAnnotations: aGlobal.hostAnnotations
michael@0 4859 });
michael@0 4860 }
michael@0 4861 }
michael@0 4862 },
michael@0 4863
michael@0 4864 /**
michael@0 4865 * Checks if the provided global belongs to the debugged add-on.
michael@0 4866 *
michael@0 4867 * @param aGlobal Debugger.Object
michael@0 4868 */
michael@0 4869 _checkGlobal: function ADA_checkGlobal(aGlobal) {
michael@0 4870 let obj = null;
michael@0 4871 try {
michael@0 4872 obj = aGlobal.unsafeDereference();
michael@0 4873 }
michael@0 4874 catch (e) {
michael@0 4875 // Because of bug 991399 we sometimes get bad objects here. If we can't
michael@0 4876 // dereference them then they won't be useful to us
michael@0 4877 return false;
michael@0 4878 }
michael@0 4879
michael@0 4880 try {
michael@0 4881 // This will fail for non-Sandbox objects, hence the try-catch block.
michael@0 4882 let metadata = Cu.getSandboxMetadata(obj);
michael@0 4883 if (metadata) {
michael@0 4884 return metadata.addonID === this.addonID;
michael@0 4885 }
michael@0 4886 } catch (e) {
michael@0 4887 }
michael@0 4888
michael@0 4889 if (obj instanceof Ci.nsIDOMWindow) {
michael@0 4890 let id = {};
michael@0 4891 if (mapURIToAddonID(obj.document.documentURIObject, id)) {
michael@0 4892 return id.value === this.addonID;
michael@0 4893 }
michael@0 4894 return false;
michael@0 4895 }
michael@0 4896
michael@0 4897 // Check the global for a __URI__ property and then try to map that to an
michael@0 4898 // add-on
michael@0 4899 let uridescriptor = aGlobal.getOwnPropertyDescriptor("__URI__");
michael@0 4900 if (uridescriptor && "value" in uridescriptor && uridescriptor.value) {
michael@0 4901 let uri;
michael@0 4902 try {
michael@0 4903 uri = Services.io.newURI(uridescriptor.value, null, null);
michael@0 4904 }
michael@0 4905 catch (e) {
michael@0 4906 DevToolsUtils.reportException("AddonThreadActor.prototype._checkGlobal",
michael@0 4907 new Error("Invalid URI: " + uridescriptor.value));
michael@0 4908 return false;
michael@0 4909 }
michael@0 4910
michael@0 4911 let id = {};
michael@0 4912 if (mapURIToAddonID(uri, id)) {
michael@0 4913 return id.value === this.addonID;
michael@0 4914 }
michael@0 4915 }
michael@0 4916
michael@0 4917 return false;
michael@0 4918 }
michael@0 4919 });
michael@0 4920
michael@0 4921 AddonThreadActor.prototype.requestTypes = Object.create(ThreadActor.prototype.requestTypes);
michael@0 4922 update(AddonThreadActor.prototype.requestTypes, {
michael@0 4923 "attach": AddonThreadActor.prototype.onAttach
michael@0 4924 });
michael@0 4925
michael@0 4926 /**
michael@0 4927 * Manages the sources for a thread. Handles source maps, locations in the
michael@0 4928 * sources, etc for ThreadActors.
michael@0 4929 */
michael@0 4930 function ThreadSources(aThreadActor, aUseSourceMaps, aAllowPredicate,
michael@0 4931 aOnNewSource) {
michael@0 4932 this._thread = aThreadActor;
michael@0 4933 this._useSourceMaps = aUseSourceMaps;
michael@0 4934 this._allow = aAllowPredicate;
michael@0 4935 this._onNewSource = aOnNewSource;
michael@0 4936
michael@0 4937 // generated source url --> promise of SourceMapConsumer
michael@0 4938 this._sourceMapsByGeneratedSource = Object.create(null);
michael@0 4939 // original source url --> promise of SourceMapConsumer
michael@0 4940 this._sourceMapsByOriginalSource = Object.create(null);
michael@0 4941 // source url --> SourceActor
michael@0 4942 this._sourceActors = Object.create(null);
michael@0 4943 // original url --> generated url
michael@0 4944 this._generatedUrlsByOriginalUrl = Object.create(null);
michael@0 4945 }
michael@0 4946
michael@0 4947 /**
michael@0 4948 * Must be a class property because it needs to persist across reloads, same as
michael@0 4949 * the breakpoint store.
michael@0 4950 */
michael@0 4951 ThreadSources._blackBoxedSources = new Set(["self-hosted"]);
michael@0 4952 ThreadSources._prettyPrintedSources = new Map();
michael@0 4953
michael@0 4954 ThreadSources.prototype = {
michael@0 4955 /**
michael@0 4956 * Return the source actor representing |url|, creating one if none
michael@0 4957 * exists already. Returns null if |url| is not allowed by the 'allow'
michael@0 4958 * predicate.
michael@0 4959 *
michael@0 4960 * Right now this takes a URL, but in the future it should
michael@0 4961 * take a Debugger.Source. See bug 637572.
michael@0 4962 *
michael@0 4963 * @param String url
michael@0 4964 * The source URL.
michael@0 4965 * @param optional SourceMapConsumer sourceMap
michael@0 4966 * The source map that introduced this source, if any.
michael@0 4967 * @param optional String generatedSource
michael@0 4968 * The generated source url that introduced this source via source map,
michael@0 4969 * if any.
michael@0 4970 * @param optional String text
michael@0 4971 * The text content of the source, if immediately available.
michael@0 4972 * @param optional String contentType
michael@0 4973 * The content type of the source, if immediately available.
michael@0 4974 * @returns a SourceActor representing the source at aURL or null.
michael@0 4975 */
michael@0 4976 source: function ({ url, sourceMap, generatedSource, text, contentType }) {
michael@0 4977 if (!this._allow(url)) {
michael@0 4978 return null;
michael@0 4979 }
michael@0 4980
michael@0 4981 if (url in this._sourceActors) {
michael@0 4982 return this._sourceActors[url];
michael@0 4983 }
michael@0 4984
michael@0 4985 let actor = new SourceActor({
michael@0 4986 url: url,
michael@0 4987 thread: this._thread,
michael@0 4988 sourceMap: sourceMap,
michael@0 4989 generatedSource: generatedSource,
michael@0 4990 text: text,
michael@0 4991 contentType: contentType
michael@0 4992 });
michael@0 4993 this._thread.threadLifetimePool.addActor(actor);
michael@0 4994 this._sourceActors[url] = actor;
michael@0 4995 try {
michael@0 4996 this._onNewSource(actor);
michael@0 4997 } catch (e) {
michael@0 4998 reportError(e);
michael@0 4999 }
michael@0 5000 return actor;
michael@0 5001 },
michael@0 5002
michael@0 5003 /**
michael@0 5004 * Only to be used when we aren't source mapping.
michael@0 5005 */
michael@0 5006 _sourceForScript: function (aScript) {
michael@0 5007 const spec = {
michael@0 5008 url: aScript.url
michael@0 5009 };
michael@0 5010
michael@0 5011 // XXX bug 915433: We can't rely on Debugger.Source.prototype.text if the
michael@0 5012 // source is an HTML-embedded <script> tag. Since we don't have an API
michael@0 5013 // implemented to detect whether this is the case, we need to be
michael@0 5014 // conservative and only use Debugger.Source.prototype.text if we get a
michael@0 5015 // normal .js file.
michael@0 5016 if (aScript.url) {
michael@0 5017 try {
michael@0 5018 const url = Services.io.newURI(aScript.url, null, null)
michael@0 5019 .QueryInterface(Ci.nsIURL);
michael@0 5020 if (url.fileExtension === "js") {
michael@0 5021 spec.contentType = "text/javascript";
michael@0 5022 spec.text = aScript.source.text;
michael@0 5023 }
michael@0 5024 } catch(ex) {
michael@0 5025 // Not a valid URI.
michael@0 5026 }
michael@0 5027 }
michael@0 5028
michael@0 5029 return this.source(spec);
michael@0 5030 },
michael@0 5031
michael@0 5032 /**
michael@0 5033 * Return a promise of an array of source actors representing all the
michael@0 5034 * sources of |aScript|.
michael@0 5035 *
michael@0 5036 * If source map handling is enabled and |aScript| has a source map, then
michael@0 5037 * use it to find all of |aScript|'s *original* sources; return a promise
michael@0 5038 * of an array of source actors for those.
michael@0 5039 */
michael@0 5040 sourcesForScript: function (aScript) {
michael@0 5041 if (!this._useSourceMaps || !aScript.sourceMapURL) {
michael@0 5042 return resolve([this._sourceForScript(aScript)].filter(isNotNull));
michael@0 5043 }
michael@0 5044
michael@0 5045 return this.sourceMap(aScript)
michael@0 5046 .then((aSourceMap) => {
michael@0 5047 return [
michael@0 5048 this.source({ url: s,
michael@0 5049 sourceMap: aSourceMap,
michael@0 5050 generatedSource: aScript.url })
michael@0 5051 for (s of aSourceMap.sources)
michael@0 5052 ];
michael@0 5053 })
michael@0 5054 .then(null, (e) => {
michael@0 5055 reportError(e);
michael@0 5056 delete this._sourceMapsByGeneratedSource[aScript.url];
michael@0 5057 return [this._sourceForScript(aScript)];
michael@0 5058 })
michael@0 5059 .then(ss => ss.filter(isNotNull));
michael@0 5060 },
michael@0 5061
michael@0 5062 /**
michael@0 5063 * Return a promise of a SourceMapConsumer for the source map for
michael@0 5064 * |aScript|; if we already have such a promise extant, return that.
michael@0 5065 * |aScript| must have a non-null sourceMapURL.
michael@0 5066 */
michael@0 5067 sourceMap: function (aScript) {
michael@0 5068 dbg_assert(aScript.sourceMapURL, "Script should have a sourceMapURL");
michael@0 5069 let sourceMapURL = this._normalize(aScript.sourceMapURL, aScript.url);
michael@0 5070 let map = this._fetchSourceMap(sourceMapURL, aScript.url)
michael@0 5071 .then(aSourceMap => this.saveSourceMap(aSourceMap, aScript.url));
michael@0 5072 this._sourceMapsByGeneratedSource[aScript.url] = map;
michael@0 5073 return map;
michael@0 5074 },
michael@0 5075
michael@0 5076 /**
michael@0 5077 * Save the given source map so that we can use it to query source locations
michael@0 5078 * down the line.
michael@0 5079 */
michael@0 5080 saveSourceMap: function (aSourceMap, aGeneratedSource) {
michael@0 5081 if (!aSourceMap) {
michael@0 5082 delete this._sourceMapsByGeneratedSource[aGeneratedSource];
michael@0 5083 return null;
michael@0 5084 }
michael@0 5085 this._sourceMapsByGeneratedSource[aGeneratedSource] = resolve(aSourceMap);
michael@0 5086 for (let s of aSourceMap.sources) {
michael@0 5087 this._generatedUrlsByOriginalUrl[s] = aGeneratedSource;
michael@0 5088 this._sourceMapsByOriginalSource[s] = resolve(aSourceMap);
michael@0 5089 }
michael@0 5090 return aSourceMap;
michael@0 5091 },
michael@0 5092
michael@0 5093 /**
michael@0 5094 * Return a promise of a SourceMapConsumer for the source map located at
michael@0 5095 * |aAbsSourceMapURL|, which must be absolute. If there is already such a
michael@0 5096 * promise extant, return it.
michael@0 5097 *
michael@0 5098 * @param string aAbsSourceMapURL
michael@0 5099 * The source map URL, in absolute form, not relative.
michael@0 5100 * @param string aScriptURL
michael@0 5101 * When the source map URL is a data URI, there is no sourceRoot on the
michael@0 5102 * source map, and the source map's sources are relative, we resolve
michael@0 5103 * them from aScriptURL.
michael@0 5104 */
michael@0 5105 _fetchSourceMap: function (aAbsSourceMapURL, aScriptURL) {
michael@0 5106 return fetch(aAbsSourceMapURL, { loadFromCache: false })
michael@0 5107 .then(({ content }) => {
michael@0 5108 let map = new SourceMapConsumer(content);
michael@0 5109 this._setSourceMapRoot(map, aAbsSourceMapURL, aScriptURL);
michael@0 5110 return map;
michael@0 5111 });
michael@0 5112 },
michael@0 5113
michael@0 5114 /**
michael@0 5115 * Sets the source map's sourceRoot to be relative to the source map url.
michael@0 5116 */
michael@0 5117 _setSourceMapRoot: function (aSourceMap, aAbsSourceMapURL, aScriptURL) {
michael@0 5118 const base = this._dirname(
michael@0 5119 aAbsSourceMapURL.indexOf("data:") === 0
michael@0 5120 ? aScriptURL
michael@0 5121 : aAbsSourceMapURL);
michael@0 5122 aSourceMap.sourceRoot = aSourceMap.sourceRoot
michael@0 5123 ? this._normalize(aSourceMap.sourceRoot, base)
michael@0 5124 : base;
michael@0 5125 },
michael@0 5126
michael@0 5127 _dirname: function (aPath) {
michael@0 5128 return Services.io.newURI(
michael@0 5129 ".", null, Services.io.newURI(aPath, null, null)).spec;
michael@0 5130 },
michael@0 5131
michael@0 5132 /**
michael@0 5133 * Returns a promise of the location in the original source if the source is
michael@0 5134 * source mapped, otherwise a promise of the same location.
michael@0 5135 */
michael@0 5136 getOriginalLocation: function ({ url, line, column }) {
michael@0 5137 if (url in this._sourceMapsByGeneratedSource) {
michael@0 5138 column = column || 0;
michael@0 5139
michael@0 5140 return this._sourceMapsByGeneratedSource[url]
michael@0 5141 .then((aSourceMap) => {
michael@0 5142 let { source: aSourceURL, line: aLine, column: aColumn } = aSourceMap.originalPositionFor({
michael@0 5143 line: line,
michael@0 5144 column: column
michael@0 5145 });
michael@0 5146 return {
michael@0 5147 url: aSourceURL,
michael@0 5148 line: aLine,
michael@0 5149 column: aColumn
michael@0 5150 };
michael@0 5151 })
michael@0 5152 .then(null, error => {
michael@0 5153 if (!DevToolsUtils.reportingDisabled) {
michael@0 5154 DevToolsUtils.reportException("ThreadSources.prototype.getOriginalLocation", error);
michael@0 5155 }
michael@0 5156 return { url: null, line: null, column: null };
michael@0 5157 });
michael@0 5158 }
michael@0 5159
michael@0 5160 // No source map
michael@0 5161 return resolve({
michael@0 5162 url: url,
michael@0 5163 line: line,
michael@0 5164 column: column
michael@0 5165 });
michael@0 5166 },
michael@0 5167
michael@0 5168 /**
michael@0 5169 * Returns a promise of the location in the generated source corresponding to
michael@0 5170 * the original source and line given.
michael@0 5171 *
michael@0 5172 * When we pass a script S representing generated code to |sourceMap|,
michael@0 5173 * above, that returns a promise P. The process of resolving P populates
michael@0 5174 * the tables this function uses; thus, it won't know that S's original
michael@0 5175 * source URLs map to S until P is resolved.
michael@0 5176 */
michael@0 5177 getGeneratedLocation: function ({ url, line, column }) {
michael@0 5178 if (url in this._sourceMapsByOriginalSource) {
michael@0 5179 return this._sourceMapsByOriginalSource[url]
michael@0 5180 .then((aSourceMap) => {
michael@0 5181 let { line: aLine, column: aColumn } = aSourceMap.generatedPositionFor({
michael@0 5182 source: url,
michael@0 5183 line: line,
michael@0 5184 column: column == null ? Infinity : column
michael@0 5185 });
michael@0 5186 return {
michael@0 5187 url: this._generatedUrlsByOriginalUrl[url],
michael@0 5188 line: aLine,
michael@0 5189 column: aColumn
michael@0 5190 };
michael@0 5191 });
michael@0 5192 }
michael@0 5193
michael@0 5194 // No source map
michael@0 5195 return resolve({
michael@0 5196 url: url,
michael@0 5197 line: line,
michael@0 5198 column: column
michael@0 5199 });
michael@0 5200 },
michael@0 5201
michael@0 5202 /**
michael@0 5203 * Returns true if URL for the given source is black boxed.
michael@0 5204 *
michael@0 5205 * @param aURL String
michael@0 5206 * The URL of the source which we are checking whether it is black
michael@0 5207 * boxed or not.
michael@0 5208 */
michael@0 5209 isBlackBoxed: function (aURL) {
michael@0 5210 return ThreadSources._blackBoxedSources.has(aURL);
michael@0 5211 },
michael@0 5212
michael@0 5213 /**
michael@0 5214 * Add the given source URL to the set of sources that are black boxed.
michael@0 5215 *
michael@0 5216 * @param aURL String
michael@0 5217 * The URL of the source which we are black boxing.
michael@0 5218 */
michael@0 5219 blackBox: function (aURL) {
michael@0 5220 ThreadSources._blackBoxedSources.add(aURL);
michael@0 5221 },
michael@0 5222
michael@0 5223 /**
michael@0 5224 * Remove the given source URL to the set of sources that are black boxed.
michael@0 5225 *
michael@0 5226 * @param aURL String
michael@0 5227 * The URL of the source which we are no longer black boxing.
michael@0 5228 */
michael@0 5229 unblackBox: function (aURL) {
michael@0 5230 ThreadSources._blackBoxedSources.delete(aURL);
michael@0 5231 },
michael@0 5232
michael@0 5233 /**
michael@0 5234 * Returns true if the given URL is pretty printed.
michael@0 5235 *
michael@0 5236 * @param aURL String
michael@0 5237 * The URL of the source that might be pretty printed.
michael@0 5238 */
michael@0 5239 isPrettyPrinted: function (aURL) {
michael@0 5240 return ThreadSources._prettyPrintedSources.has(aURL);
michael@0 5241 },
michael@0 5242
michael@0 5243 /**
michael@0 5244 * Add the given URL to the set of sources that are pretty printed.
michael@0 5245 *
michael@0 5246 * @param aURL String
michael@0 5247 * The URL of the source to be pretty printed.
michael@0 5248 */
michael@0 5249 prettyPrint: function (aURL, aIndent) {
michael@0 5250 ThreadSources._prettyPrintedSources.set(aURL, aIndent);
michael@0 5251 },
michael@0 5252
michael@0 5253 /**
michael@0 5254 * Return the indent the given URL was pretty printed by.
michael@0 5255 */
michael@0 5256 prettyPrintIndent: function (aURL) {
michael@0 5257 return ThreadSources._prettyPrintedSources.get(aURL);
michael@0 5258 },
michael@0 5259
michael@0 5260 /**
michael@0 5261 * Remove the given URL from the set of sources that are pretty printed.
michael@0 5262 *
michael@0 5263 * @param aURL String
michael@0 5264 * The URL of the source that is no longer pretty printed.
michael@0 5265 */
michael@0 5266 disablePrettyPrint: function (aURL) {
michael@0 5267 ThreadSources._prettyPrintedSources.delete(aURL);
michael@0 5268 },
michael@0 5269
michael@0 5270 /**
michael@0 5271 * Normalize multiple relative paths towards the base paths on the right.
michael@0 5272 */
michael@0 5273 _normalize: function (...aURLs) {
michael@0 5274 dbg_assert(aURLs.length > 1, "Should have more than 1 URL");
michael@0 5275 let base = Services.io.newURI(aURLs.pop(), null, null);
michael@0 5276 let url;
michael@0 5277 while ((url = aURLs.pop())) {
michael@0 5278 base = Services.io.newURI(url, null, base);
michael@0 5279 }
michael@0 5280 return base.spec;
michael@0 5281 },
michael@0 5282
michael@0 5283 iter: function* () {
michael@0 5284 for (let url in this._sourceActors) {
michael@0 5285 yield this._sourceActors[url];
michael@0 5286 }
michael@0 5287 }
michael@0 5288 };
michael@0 5289
michael@0 5290 // Utility functions.
michael@0 5291
michael@0 5292 // TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when it is
michael@0 5293 // implemented.
michael@0 5294 function getOffsetColumn(aOffset, aScript) {
michael@0 5295 let bestOffsetMapping = null;
michael@0 5296 for (let offsetMapping of aScript.getAllColumnOffsets()) {
michael@0 5297 if (!bestOffsetMapping ||
michael@0 5298 (offsetMapping.offset <= aOffset &&
michael@0 5299 offsetMapping.offset > bestOffsetMapping.offset)) {
michael@0 5300 bestOffsetMapping = offsetMapping;
michael@0 5301 }
michael@0 5302 }
michael@0 5303
michael@0 5304 if (!bestOffsetMapping) {
michael@0 5305 // XXX: Try not to completely break the experience of using the debugger for
michael@0 5306 // the user by assuming column 0. Simultaneously, report the error so that
michael@0 5307 // there is a paper trail if the assumption is bad and the debugging
michael@0 5308 // experience becomes wonky.
michael@0 5309 reportError(new Error("Could not find a column for offset " + aOffset
michael@0 5310 + " in the script " + aScript));
michael@0 5311 return 0;
michael@0 5312 }
michael@0 5313
michael@0 5314 return bestOffsetMapping.columnNumber;
michael@0 5315 }
michael@0 5316
michael@0 5317 /**
michael@0 5318 * Return the non-source-mapped location of the given Debugger.Frame. If the
michael@0 5319 * frame does not have a script, the location's properties are all null.
michael@0 5320 *
michael@0 5321 * @param Debugger.Frame aFrame
michael@0 5322 * The frame whose location we are getting.
michael@0 5323 * @returns Object
michael@0 5324 * Returns an object of the form { url, line, column }
michael@0 5325 */
michael@0 5326 function getFrameLocation(aFrame) {
michael@0 5327 if (!aFrame || !aFrame.script) {
michael@0 5328 return { url: null, line: null, column: null };
michael@0 5329 }
michael@0 5330 return {
michael@0 5331 url: aFrame.script.url,
michael@0 5332 line: aFrame.script.getOffsetLine(aFrame.offset),
michael@0 5333 column: getOffsetColumn(aFrame.offset, aFrame.script)
michael@0 5334 }
michael@0 5335 }
michael@0 5336
michael@0 5337 /**
michael@0 5338 * Utility function for updating an object with the properties of another
michael@0 5339 * object.
michael@0 5340 *
michael@0 5341 * @param aTarget Object
michael@0 5342 * The object being updated.
michael@0 5343 * @param aNewAttrs Object
michael@0 5344 * The new attributes being set on the target.
michael@0 5345 */
michael@0 5346 function update(aTarget, aNewAttrs) {
michael@0 5347 for (let key in aNewAttrs) {
michael@0 5348 let desc = Object.getOwnPropertyDescriptor(aNewAttrs, key);
michael@0 5349
michael@0 5350 if (desc) {
michael@0 5351 Object.defineProperty(aTarget, key, desc);
michael@0 5352 }
michael@0 5353 }
michael@0 5354 }
michael@0 5355
michael@0 5356 /**
michael@0 5357 * Returns true if its argument is not null.
michael@0 5358 */
michael@0 5359 function isNotNull(aThing) {
michael@0 5360 return aThing !== null;
michael@0 5361 }
michael@0 5362
michael@0 5363 /**
michael@0 5364 * Performs a request to load the desired URL and returns a promise.
michael@0 5365 *
michael@0 5366 * @param aURL String
michael@0 5367 * The URL we will request.
michael@0 5368 * @returns Promise
michael@0 5369 * A promise of the document at that URL, as a string.
michael@0 5370 *
michael@0 5371 * XXX: It may be better to use nsITraceableChannel to get to the sources
michael@0 5372 * without relying on caching when we can (not for eval, etc.):
michael@0 5373 * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
michael@0 5374 */
michael@0 5375 function fetch(aURL, aOptions={ loadFromCache: true }) {
michael@0 5376 let deferred = defer();
michael@0 5377 let scheme;
michael@0 5378 let url = aURL.split(" -> ").pop();
michael@0 5379 let charset;
michael@0 5380 let contentType;
michael@0 5381
michael@0 5382 try {
michael@0 5383 scheme = Services.io.extractScheme(url);
michael@0 5384 } catch (e) {
michael@0 5385 // In the xpcshell tests, the script url is the absolute path of the test
michael@0 5386 // file, which will make a malformed URI error be thrown. Add the file
michael@0 5387 // scheme prefix ourselves.
michael@0 5388 url = "file://" + url;
michael@0 5389 scheme = Services.io.extractScheme(url);
michael@0 5390 }
michael@0 5391
michael@0 5392 switch (scheme) {
michael@0 5393 case "file":
michael@0 5394 case "chrome":
michael@0 5395 case "resource":
michael@0 5396 try {
michael@0 5397 NetUtil.asyncFetch(url, function onFetch(aStream, aStatus, aRequest) {
michael@0 5398 if (!Components.isSuccessCode(aStatus)) {
michael@0 5399 deferred.reject(new Error("Request failed with status code = "
michael@0 5400 + aStatus
michael@0 5401 + " after NetUtil.asyncFetch for url = "
michael@0 5402 + url));
michael@0 5403 return;
michael@0 5404 }
michael@0 5405
michael@0 5406 let source = NetUtil.readInputStreamToString(aStream, aStream.available());
michael@0 5407 contentType = aRequest.contentType;
michael@0 5408 deferred.resolve(source);
michael@0 5409 aStream.close();
michael@0 5410 });
michael@0 5411 } catch (ex) {
michael@0 5412 deferred.reject(ex);
michael@0 5413 }
michael@0 5414 break;
michael@0 5415
michael@0 5416 default:
michael@0 5417 let channel;
michael@0 5418 try {
michael@0 5419 channel = Services.io.newChannel(url, null, null);
michael@0 5420 } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
michael@0 5421 // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but
michael@0 5422 // newChannel won't be able to handle it.
michael@0 5423 url = "file:///" + url;
michael@0 5424 channel = Services.io.newChannel(url, null, null);
michael@0 5425 }
michael@0 5426 let chunks = [];
michael@0 5427 let streamListener = {
michael@0 5428 onStartRequest: function(aRequest, aContext, aStatusCode) {
michael@0 5429 if (!Components.isSuccessCode(aStatusCode)) {
michael@0 5430 deferred.reject(new Error("Request failed with status code = "
michael@0 5431 + aStatusCode
michael@0 5432 + " in onStartRequest handler for url = "
michael@0 5433 + url));
michael@0 5434 }
michael@0 5435 },
michael@0 5436 onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
michael@0 5437 chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
michael@0 5438 },
michael@0 5439 onStopRequest: function(aRequest, aContext, aStatusCode) {
michael@0 5440 if (!Components.isSuccessCode(aStatusCode)) {
michael@0 5441 deferred.reject(new Error("Request failed with status code = "
michael@0 5442 + aStatusCode
michael@0 5443 + " in onStopRequest handler for url = "
michael@0 5444 + url));
michael@0 5445 return;
michael@0 5446 }
michael@0 5447
michael@0 5448 charset = channel.contentCharset;
michael@0 5449 contentType = channel.contentType;
michael@0 5450 deferred.resolve(chunks.join(""));
michael@0 5451 }
michael@0 5452 };
michael@0 5453
michael@0 5454 channel.loadFlags = aOptions.loadFromCache
michael@0 5455 ? channel.LOAD_FROM_CACHE
michael@0 5456 : channel.LOAD_BYPASS_CACHE;
michael@0 5457 channel.asyncOpen(streamListener, null);
michael@0 5458 break;
michael@0 5459 }
michael@0 5460
michael@0 5461 return deferred.promise.then(source => {
michael@0 5462 return {
michael@0 5463 content: convertToUnicode(source, charset),
michael@0 5464 contentType: contentType
michael@0 5465 };
michael@0 5466 });
michael@0 5467 }
michael@0 5468
michael@0 5469 /**
michael@0 5470 * Convert a given string, encoded in a given character set, to unicode.
michael@0 5471 *
michael@0 5472 * @param string aString
michael@0 5473 * A string.
michael@0 5474 * @param string aCharset
michael@0 5475 * A character set.
michael@0 5476 */
michael@0 5477 function convertToUnicode(aString, aCharset=null) {
michael@0 5478 // Decoding primitives.
michael@0 5479 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
michael@0 5480 .createInstance(Ci.nsIScriptableUnicodeConverter);
michael@0 5481 try {
michael@0 5482 converter.charset = aCharset || "UTF-8";
michael@0 5483 return converter.ConvertToUnicode(aString);
michael@0 5484 } catch(e) {
michael@0 5485 return aString;
michael@0 5486 }
michael@0 5487 }
michael@0 5488
michael@0 5489 /**
michael@0 5490 * Report the given error in the error console and to stdout.
michael@0 5491 *
michael@0 5492 * @param Error aError
michael@0 5493 * The error object you wish to report.
michael@0 5494 * @param String aPrefix
michael@0 5495 * An optional prefix for the reported error message.
michael@0 5496 */
michael@0 5497 function reportError(aError, aPrefix="") {
michael@0 5498 dbg_assert(aError instanceof Error, "Must pass Error objects to reportError");
michael@0 5499 let msg = aPrefix + aError.message + ":\n" + aError.stack;
michael@0 5500 Cu.reportError(msg);
michael@0 5501 dumpn(msg);
michael@0 5502 }
michael@0 5503
michael@0 5504 // The following are copied here verbatim from css-logic.js, until we create a
michael@0 5505 // server-friendly helper module.
michael@0 5506
michael@0 5507 /**
michael@0 5508 * Find a unique CSS selector for a given element
michael@0 5509 * @returns a string such that ele.ownerDocument.querySelector(reply) === ele
michael@0 5510 * and ele.ownerDocument.querySelectorAll(reply).length === 1
michael@0 5511 */
michael@0 5512 function findCssSelector(ele) {
michael@0 5513 var document = ele.ownerDocument;
michael@0 5514 if (ele.id && document.getElementById(ele.id) === ele) {
michael@0 5515 return '#' + ele.id;
michael@0 5516 }
michael@0 5517
michael@0 5518 // Inherently unique by tag name
michael@0 5519 var tagName = ele.tagName.toLowerCase();
michael@0 5520 if (tagName === 'html') {
michael@0 5521 return 'html';
michael@0 5522 }
michael@0 5523 if (tagName === 'head') {
michael@0 5524 return 'head';
michael@0 5525 }
michael@0 5526 if (tagName === 'body') {
michael@0 5527 return 'body';
michael@0 5528 }
michael@0 5529
michael@0 5530 if (ele.parentNode == null) {
michael@0 5531 console.log('danger: ' + tagName);
michael@0 5532 }
michael@0 5533
michael@0 5534 // We might be able to find a unique class name
michael@0 5535 var selector, index, matches;
michael@0 5536 if (ele.classList.length > 0) {
michael@0 5537 for (var i = 0; i < ele.classList.length; i++) {
michael@0 5538 // Is this className unique by itself?
michael@0 5539 selector = '.' + ele.classList.item(i);
michael@0 5540 matches = document.querySelectorAll(selector);
michael@0 5541 if (matches.length === 1) {
michael@0 5542 return selector;
michael@0 5543 }
michael@0 5544 // Maybe it's unique with a tag name?
michael@0 5545 selector = tagName + selector;
michael@0 5546 matches = document.querySelectorAll(selector);
michael@0 5547 if (matches.length === 1) {
michael@0 5548 return selector;
michael@0 5549 }
michael@0 5550 // Maybe it's unique using a tag name and nth-child
michael@0 5551 index = positionInNodeList(ele, ele.parentNode.children) + 1;
michael@0 5552 selector = selector + ':nth-child(' + index + ')';
michael@0 5553 matches = document.querySelectorAll(selector);
michael@0 5554 if (matches.length === 1) {
michael@0 5555 return selector;
michael@0 5556 }
michael@0 5557 }
michael@0 5558 }
michael@0 5559
michael@0 5560 // So we can be unique w.r.t. our parent, and use recursion
michael@0 5561 index = positionInNodeList(ele, ele.parentNode.children) + 1;
michael@0 5562 selector = findCssSelector(ele.parentNode) + ' > ' +
michael@0 5563 tagName + ':nth-child(' + index + ')';
michael@0 5564
michael@0 5565 return selector;
michael@0 5566 };
michael@0 5567
michael@0 5568 /**
michael@0 5569 * Find the position of [element] in [nodeList].
michael@0 5570 * @returns an index of the match, or -1 if there is no match
michael@0 5571 */
michael@0 5572 function positionInNodeList(element, nodeList) {
michael@0 5573 for (var i = 0; i < nodeList.length; i++) {
michael@0 5574 if (element === nodeList[i]) {
michael@0 5575 return i;
michael@0 5576 }
michael@0 5577 }
michael@0 5578 return -1;
michael@0 5579 }
michael@0 5580
michael@0 5581 /**
michael@0 5582 * Make a debuggee value for the given object, if needed. Primitive values
michael@0 5583 * are left the same.
michael@0 5584 *
michael@0 5585 * Use case: you have a raw JS object (after unsafe dereference) and you want to
michael@0 5586 * send it to the client. In that case you need to use an ObjectActor which
michael@0 5587 * requires a debuggee value. The Debugger.Object.prototype.makeDebuggeeValue()
michael@0 5588 * method works only for JS objects and functions.
michael@0 5589 *
michael@0 5590 * @param Debugger.Object obj
michael@0 5591 * @param any value
michael@0 5592 * @return object
michael@0 5593 */
michael@0 5594 function makeDebuggeeValueIfNeeded(obj, value) {
michael@0 5595 if (value && (typeof value == "object" || typeof value == "function")) {
michael@0 5596 return obj.makeDebuggeeValue(value);
michael@0 5597 }
michael@0 5598 return value;
michael@0 5599 }
michael@0 5600
michael@0 5601 function getInnerId(window) {
michael@0 5602 return window.QueryInterface(Ci.nsIInterfaceRequestor).
michael@0 5603 getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
michael@0 5604 };

mercurial