1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5"use strict"; 6 7var EXPORTED_SYMBOLS = ["ExecutionContext"]; 8 9const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService( 10 Ci.nsIUUIDGenerator 11); 12 13const TYPED_ARRAY_CLASSES = [ 14 "Uint8Array", 15 "Uint8ClampedArray", 16 "Uint16Array", 17 "Uint32Array", 18 "Int8Array", 19 "Int16Array", 20 "Int32Array", 21 "Float32Array", 22 "Float64Array", 23]; 24 25function uuid() { 26 return uuidGen 27 .generateUUID() 28 .toString() 29 .slice(1, -1); 30} 31 32/** 33 * This class represent a debuggable context onto which we can evaluate Javascript. 34 * This is typically a document, but it could also be a worker, an add-on, ... or 35 * any kind of context involving JS scripts. 36 * 37 * @param {Debugger} dbg 38 * A Debugger instance that we can use to inspect the given global. 39 * @param {GlobalObject} debuggee 40 * The debuggable context's global object. This is typically the document window 41 * object. But it can also be any global object, like a worker global scope object. 42 */ 43class ExecutionContext { 44 constructor(dbg, debuggee, id, isDefault) { 45 this._debugger = dbg; 46 this._debuggee = this._debugger.addDebuggee(debuggee); 47 48 // Here, we assume that debuggee is a window object and we will propably have 49 // to adapt that once we cover workers or contexts that aren't a document. 50 this.window = debuggee; 51 this.windowId = debuggee.windowUtils.currentInnerWindowID; 52 this.id = id; 53 this.frameId = debuggee.docShell.browsingContext.id.toString(); 54 this.isDefault = isDefault; 55 56 // objectId => Debugger.Object 57 this._remoteObjects = new Map(); 58 } 59 60 destructor() { 61 this._debugger.removeDebuggee(this._debuggee); 62 } 63 64 hasRemoteObject(objectId) { 65 return this._remoteObjects.has(objectId); 66 } 67 68 getRemoteObject(objectId) { 69 return this._remoteObjects.get(objectId); 70 } 71 72 getRemoteObjectByNodeId(nodeId) { 73 for (const value of this._remoteObjects.values()) { 74 if (value.nodeId == nodeId) { 75 return value; 76 } 77 } 78 79 return null; 80 } 81 82 releaseObject(objectId) { 83 return this._remoteObjects.delete(objectId); 84 } 85 86 /** 87 * Add a new debuggerObj to the object cache. 88 * 89 * Whenever an object is returned as reference, a new entry is added 90 * to the internal object cache. It means the same underlying object or node 91 * can be represented via multiple references. 92 */ 93 setRemoteObject(debuggerObj) { 94 const objectId = uuid(); 95 96 // TODO: Wrap Symbol into an object, 97 // which would allow us to set the objectId. 98 if (typeof debuggerObj == "object") { 99 debuggerObj.objectId = objectId; 100 } 101 102 // For node objects add an unique identifier. 103 if ( 104 debuggerObj instanceof Debugger.Object && 105 Node.isInstance(debuggerObj.unsafeDereference()) 106 ) { 107 debuggerObj.nodeId = uuid(); 108 // We do not differentiate between backendNodeId and nodeId (yet) 109 debuggerObj.backendNodeId = debuggerObj.nodeId; 110 } 111 112 this._remoteObjects.set(objectId, debuggerObj); 113 114 return objectId; 115 } 116 117 /** 118 * Evaluate a Javascript expression. 119 * 120 * @param {String} expression 121 * The JS expression to evaluate against the JS context. 122 * @param {boolean} options.awaitPromise 123 * Whether execution should `await` for resulting value 124 * and return once awaited promise is resolved. 125 * @param {boolean} returnByValue 126 * Whether the result is expected to be a JSON object 127 * that should be sent by value. 128 * 129 * @return {Object} A multi-form object depending if the execution 130 * succeed or failed. If the expression failed to evaluate, 131 * it will return an object with an `exceptionDetails` attribute 132 * matching the `ExceptionDetails` CDP type. Otherwise it will 133 * return an object with `result` attribute whose type is 134 * `RemoteObject` CDP type. 135 */ 136 async evaluate(expression, awaitPromise, returnByValue) { 137 let rv = this._debuggee.executeInGlobal(expression); 138 if (!rv) { 139 return { 140 exceptionDetails: { 141 text: "Evaluation terminated!", 142 }, 143 }; 144 } 145 146 if (rv.throw) { 147 return this._returnError(rv.throw); 148 } 149 150 let result = rv.return; 151 152 if (result && result.isPromise && awaitPromise) { 153 if (result.promiseState === "fulfilled") { 154 result = result.promiseValue; 155 } else if (result.promiseState === "rejected") { 156 return this._returnError(result.promiseReason); 157 } else { 158 try { 159 const promiseResult = await result.unsafeDereference(); 160 result = this._debuggee.makeDebuggeeValue(promiseResult); 161 } catch (e) { 162 // The promise has been rejected 163 return this._returnError(e); 164 } 165 } 166 } 167 168 if (returnByValue) { 169 result = this._toRemoteObjectByValue(result); 170 } else { 171 result = this._toRemoteObject(result); 172 } 173 174 return { result }; 175 } 176 177 /** 178 * Given a Debugger.Object reference for an Exception, return a JSON object 179 * describing the exception by following CDP ExceptionDetails specification. 180 */ 181 _returnError(exception) { 182 if ( 183 this._debuggee.executeInGlobalWithBindings("exception instanceof Error", { 184 exception, 185 }).return 186 ) { 187 const text = this._debuggee.executeInGlobalWithBindings( 188 "exception.message", 189 { exception } 190 ).return; 191 return { 192 exceptionDetails: { 193 text, 194 }, 195 }; 196 } 197 198 // If that isn't an Error, consider the exception as a JS value 199 return { 200 exceptionDetails: { 201 exception: this._toRemoteObject(exception), 202 }, 203 }; 204 } 205 206 async callFunctionOn( 207 functionDeclaration, 208 callArguments = [], 209 returnByValue = false, 210 awaitPromise = false, 211 objectId = null 212 ) { 213 // Map the given objectId to a JS reference. 214 let thisArg = null; 215 if (objectId) { 216 thisArg = this.getRemoteObject(objectId); 217 if (!thisArg) { 218 throw new Error(`Unable to get target object with id: ${objectId}`); 219 } 220 } 221 222 // First evaluate the function 223 const fun = this._debuggee.executeInGlobal("(" + functionDeclaration + ")"); 224 if (!fun) { 225 return { 226 exceptionDetails: { 227 text: "Evaluation terminated!", 228 }, 229 }; 230 } 231 if (fun.throw) { 232 return this._returnError(fun.throw); 233 } 234 235 // Then map all input arguments, which are matching CDP's CallArguments type, 236 // into JS values 237 const args = callArguments.map(arg => this._fromCallArgument(arg)); 238 239 // Finally, call the function with these arguments 240 const rv = fun.return.apply(thisArg, args); 241 if (rv.throw) { 242 return this._returnError(rv.throw); 243 } 244 245 let result = rv.return; 246 247 if (result && result.isPromise && awaitPromise) { 248 if (result.promiseState === "fulfilled") { 249 result = result.promiseValue; 250 } else if (result.promiseState === "rejected") { 251 return this._returnError(result.promiseReason); 252 } else { 253 try { 254 const promiseResult = await result.unsafeDereference(); 255 result = this._debuggee.makeDebuggeeValue(promiseResult); 256 } catch (e) { 257 // The promise has been rejected 258 return this._returnError(e); 259 } 260 } 261 } 262 263 if (returnByValue) { 264 result = this._toRemoteObjectByValue(result); 265 } else { 266 result = this._toRemoteObject(result); 267 } 268 269 return { result }; 270 } 271 272 getProperties({ objectId, ownProperties }) { 273 let debuggerObj = this.getRemoteObject(objectId); 274 if (!debuggerObj) { 275 throw new Error("Could not find object with given id"); 276 } 277 278 const result = []; 279 const serializeObject = (debuggerObj, isOwn) => { 280 for (const propertyName of debuggerObj.getOwnPropertyNames()) { 281 const descriptor = debuggerObj.getOwnPropertyDescriptor(propertyName); 282 result.push({ 283 name: propertyName, 284 285 configurable: descriptor.configurable, 286 enumerable: descriptor.enumerable, 287 writable: descriptor.writable, 288 value: this._toRemoteObject(descriptor.value), 289 get: descriptor.get 290 ? this._toRemoteObject(descriptor.get) 291 : undefined, 292 set: descriptor.set 293 ? this._toRemoteObject(descriptor.set) 294 : undefined, 295 296 isOwn, 297 }); 298 } 299 }; 300 301 // When `ownProperties` is set to true, we only iterate over own properties. 302 // Otherwise, we also iterate over propreties inherited from the prototype chain. 303 serializeObject(debuggerObj, true); 304 305 if (!ownProperties) { 306 while (true) { 307 debuggerObj = debuggerObj.proto; 308 if (!debuggerObj) { 309 break; 310 } 311 serializeObject(debuggerObj, false); 312 } 313 } 314 315 return { 316 result, 317 }; 318 } 319 320 /** 321 * Given a CDP `CallArgument`, return a JS value that represent this argument. 322 * Note that `CallArgument` is actually very similar to `RemoteObject` 323 */ 324 _fromCallArgument(arg) { 325 if (arg.objectId) { 326 if (!this.hasRemoteObject(arg.objectId)) { 327 throw new Error("Could not find object with given id"); 328 } 329 return this.getRemoteObject(arg.objectId); 330 } 331 332 if (arg.unserializableValue) { 333 switch (arg.unserializableValue) { 334 case "-0": 335 return -0; 336 case "Infinity": 337 return Infinity; 338 case "-Infinity": 339 return -Infinity; 340 case "NaN": 341 return NaN; 342 default: 343 if (/^\d+n$/.test(arg.unserializableValue)) { 344 // eslint-disable-next-line no-undef 345 return BigInt(arg.unserializableValue.slice(0, -1)); 346 } 347 throw new Error("Couldn't parse value object in call argument"); 348 } 349 } 350 351 return this._deserialize(arg.value); 352 } 353 354 /** 355 * Given a JS value, create a copy of it within the debugee compartment. 356 */ 357 _deserialize(obj) { 358 if (typeof obj !== "object") { 359 return obj; 360 } 361 const result = this._debuggee.executeInGlobalWithBindings( 362 "JSON.parse(obj)", 363 { obj: JSON.stringify(obj) } 364 ); 365 if (result.throw) { 366 throw new Error("Unable to deserialize object"); 367 } 368 return result.return; 369 } 370 371 /** 372 * Given a `Debugger.Object` object, return a JSON-serializable description of it 373 * matching `RemoteObject` CDP type. 374 * 375 * @param {Debugger.Object} debuggerObj 376 * The object to serialize 377 * @return {RemoteObject} 378 * The serialized description of the given object 379 */ 380 _toRemoteObject(debuggerObj) { 381 const result = {}; 382 383 // First handle all non-primitive values which are going to be wrapped by the 384 // Debugger API into Debugger.Object instances 385 if (debuggerObj instanceof Debugger.Object) { 386 const rawObj = debuggerObj.unsafeDereference(); 387 388 result.objectId = this.setRemoteObject(debuggerObj); 389 result.type = typeof rawObj; 390 391 // Map the Debugger API `class` attribute to CDP `subtype` 392 const cls = debuggerObj.class; 393 if (debuggerObj.isProxy) { 394 result.subtype = "proxy"; 395 } else if (cls == "Array") { 396 result.subtype = "array"; 397 } else if (cls == "RegExp") { 398 result.subtype = "regexp"; 399 } else if (cls == "Date") { 400 result.subtype = "date"; 401 } else if (cls == "Map") { 402 result.subtype = "map"; 403 } else if (cls == "Set") { 404 result.subtype = "set"; 405 } else if (cls == "WeakMap") { 406 result.subtype = "weakmap"; 407 } else if (cls == "WeakSet") { 408 result.subtype = "weakset"; 409 } else if (cls == "Error") { 410 result.subtype = "error"; 411 } else if (cls == "Promise") { 412 result.subtype = "promise"; 413 } else if (TYPED_ARRAY_CLASSES.includes(cls)) { 414 result.subtype = "typedarray"; 415 } else if (Node.isInstance(rawObj)) { 416 result.subtype = "node"; 417 result.className = ChromeUtils.getClassName(rawObj); 418 result.description = rawObj.localName || rawObj.nodeName; 419 if (rawObj.id) { 420 result.description += `#${rawObj.id}`; 421 } 422 } 423 return result; 424 } 425 426 // Now, handle all values that Debugger API isn't wrapping into Debugger.API. 427 // This is all the primitive JS types. 428 result.type = typeof debuggerObj; 429 430 // Symbol and BigInt are primitive values but aren't serializable. 431 // CDP expects them to be considered as objects, with an objectId to later inspect 432 // them. 433 if (result.type == "symbol") { 434 result.description = debuggerObj.toString(); 435 result.objectId = this.setRemoteObject(debuggerObj); 436 437 return result; 438 } 439 440 // A few primitive type can't be serialized and CDP has special case for them 441 if (Object.is(debuggerObj, NaN)) { 442 result.unserializableValue = "NaN"; 443 } else if (Object.is(debuggerObj, -0)) { 444 result.unserializableValue = "-0"; 445 } else if (Object.is(debuggerObj, Infinity)) { 446 result.unserializableValue = "Infinity"; 447 } else if (Object.is(debuggerObj, -Infinity)) { 448 result.unserializableValue = "-Infinity"; 449 } else if (result.type == "bigint") { 450 result.unserializableValue = `${debuggerObj}n`; 451 } 452 453 if (result.unserializableValue) { 454 result.description = result.unserializableValue; 455 return result; 456 } 457 458 // Otherwise, we serialize the primitive values as-is via `value` attribute 459 result.value = debuggerObj; 460 461 // null is special as it has a dedicated subtype 462 if (debuggerObj === null) { 463 result.subtype = "null"; 464 } 465 466 return result; 467 } 468 469 /** 470 * Given a `Debugger.Object` object, return a JSON-serializable description of it 471 * matching `RemoteObject` CDP type. 472 * 473 * @param {Debugger.Object} debuggerObj 474 * The object to serialize 475 * @return {RemoteObject} 476 * The serialized description of the given object 477 */ 478 _toRemoteObjectByValue(debuggerObj) { 479 const type = typeof debuggerObj; 480 481 if (type == "undefined") { 482 return { type }; 483 } 484 485 let unserializableValue = undefined; 486 if (Object.is(debuggerObj, -0)) { 487 unserializableValue = "-0"; 488 } else if (Object.is(debuggerObj, NaN)) { 489 unserializableValue = "NaN"; 490 } else if (Object.is(debuggerObj, Infinity)) { 491 unserializableValue = "Infinity"; 492 } else if (Object.is(debuggerObj, -Infinity)) { 493 unserializableValue = "-Infinity"; 494 } else if (typeof debuggerObj == "bigint") { 495 unserializableValue = `${debuggerObj}n`; 496 } 497 498 if (unserializableValue) { 499 return { 500 type, 501 unserializableValue, 502 description: unserializableValue, 503 }; 504 } 505 506 const value = this._serialize(debuggerObj); 507 return { 508 type: typeof value, 509 value, 510 description: value != null ? value.toString() : value, 511 }; 512 } 513 514 /** 515 * Convert a given `Debugger.Object` to an object. 516 * 517 * @param {Debugger.Object} obj 518 * The object to convert 519 * 520 * @return {Object} 521 * The converted object 522 */ 523 _serialize(debuggerObj) { 524 const result = this._debuggee.executeInGlobalWithBindings( 525 "JSON.stringify(e)", 526 { e: debuggerObj } 527 ); 528 if (result.throw) { 529 throw new Error("Object is not serializable"); 530 } 531 532 return JSON.parse(result.return); 533 } 534} 535