Wed, 31 Dec 2014 06:09:35 +0100
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 | }; |