tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

dump-scope.sys.mjs (9295B)


      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 /* eslint-disable no-console */
      6 
      7 import { addDebuggerToGlobal } from "resource://gre/modules/jsdebugger.sys.mjs";
      8 
      9 // Exclude frames from the test harness.
     10 const hiddenSourceURLs = [
     11  "chrome://mochikit/content/browser-test.js",
     12  "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js",
     13 ];
     14 
     15 const TYPED_ARRAY_CLASSES = [
     16  "Uint8Array",
     17  "Uint8ClampedArray",
     18  "Uint16Array",
     19  "Uint32Array",
     20  "Int8Array",
     21  "Int16Array",
     22  "Int32Array",
     23  "Float32Array",
     24  "Float64Array",
     25  "BigInt64Array",
     26  "BigUint64Array",
     27 ];
     28 
     29 /**
     30 * Copied from the similar helper at devtools/server/actors/object/utils.js
     31 */
     32 function isArray(object) {
     33  return TYPED_ARRAY_CLASSES.includes(object.class) || object.class === "Array";
     34 }
     35 
     36 // Avoid serializing any object more than once by using IDs and refering to them
     37 const USE_REFERENCES = false;
     38 
     39 // Avoid serializing more than n-th nested object attributes
     40 const MAX_DEPTH = 5;
     41 
     42 // Limit in number of properties/items in a single object
     43 const MAX_PROPERTIES = 100;
     44 
     45 // Used by USE_REFERENCES=true to store the already logged objects
     46 const objects = new Map();
     47 
     48 const PROMISE_REACTIONS = new WeakMap();
     49 const getAsyncParentFrame = frame => {
     50  if (!frame.asyncPromise) {
     51    return null;
     52  }
     53 
     54  // We support returning Frame actors for frames that are suspended
     55  // at an 'await', and here we want to walk upward to look for the first
     56  // frame that will be resumed when the current frame's promise resolves.
     57  let reactions =
     58    PROMISE_REACTIONS.get(frame.asyncPromise) ||
     59    frame.asyncPromise.getPromiseReactions();
     60 
     61  // eslint-disable-next-line no-constant-condition
     62  while (true) {
     63    // We loop here because we may have code like:
     64    //
     65    //   async function inner(){ debugger; }
     66    //
     67    //   async function outer() {
     68    //     await Promise.resolve().then(() => inner());
     69    //   }
     70    //
     71    // where we can see that when `inner` resolves, we will resume from
     72    // `outer`, even though there is a layer of promises between, and
     73    // that layer could be any number of promises deep.
     74    if (!(reactions[0] instanceof Debugger.Object)) {
     75      break;
     76    }
     77 
     78    reactions = reactions[0].getPromiseReactions();
     79  }
     80 
     81  if (reactions[0] instanceof Debugger.Frame) {
     82    return reactions[0];
     83  }
     84  return null;
     85 };
     86 
     87 /**
     88 * Serialize any arbitrary object to a JSON-serializable object
     89 */
     90 function serialize(dbgObj, depth) {
     91  // If the variable is initialized after calling dumpScope.
     92  if (dbgObj?.uninitialized) {
     93    return "(uninitialized)";
     94  }
     95 
     96  // If for any reason SpiderMonkey could not preserve the arguments.
     97  if (dbgObj?.missingArguments) {
     98    return "(missing arguments)";
     99  }
    100 
    101  // If the variable was optimized out by SpiderMonkey.
    102  if (dbgObj?.optimizedOut) {
    103    return "(optimized out)";
    104  }
    105 
    106  if (dbgObj?.unsafeDereference) {
    107    if (dbgObj.isClassConstructor) {
    108      return "Class " + dbgObj.name;
    109    }
    110    return serializeObject(dbgObj, depth);
    111  }
    112  return serializePrimitive(dbgObj);
    113 }
    114 
    115 /**
    116 * Serialize any JavaScript object (i.e. non primitives) to a JSON-serializable object
    117 */
    118 function serializeObject(dbgObj, depth) {
    119  depth++;
    120  if (depth >= MAX_DEPTH) {
    121    return dbgObj.class + " (max depth)";
    122  }
    123  if (dbgObj.class == "Function") {
    124    return "Function " + dbgObj.displayName;
    125  }
    126 
    127  let clone = isArray(dbgObj) ? [] : {};
    128  if (USE_REFERENCES) {
    129    // Avoid dumping the same object twice by using references
    130    clone = objects.get(dbgObj);
    131    if (clone) {
    132      return "(object #" + clone["object #"] + ")";
    133    }
    134 
    135    clone["object #"] = objects.size;
    136    objects.set(dbgObj, clone);
    137  }
    138 
    139  let i = 0;
    140  for (const propertyName of dbgObj.getOwnPropertyNames()) {
    141    const descriptor = dbgObj.getOwnPropertyDescriptor(propertyName);
    142    if (!descriptor) {
    143      continue;
    144    }
    145    if (i >= MAX_PROPERTIES) {
    146      clone[propertyName] = "(max properties/items count)";
    147      break;
    148    }
    149    if (descriptor.getter) {
    150      clone[propertyName] = "(getter)";
    151    } else {
    152      clone[propertyName] = serialize(descriptor.value, depth);
    153    }
    154 
    155    i++;
    156  }
    157  return clone;
    158 }
    159 
    160 /**
    161 * Serialize any JavaScript primitive value to a JSON-serializable object
    162 */
    163 function serializePrimitive(value) {
    164  const type = typeof value;
    165  if (type === "string") {
    166    return value;
    167  } else if (type === "bigint") {
    168    return `BigInt(${value})`;
    169  } else if (value && typeof value.toString === "function") {
    170    // Use toString as it allows to stringify Symbols. Converting them to string throws.
    171    return value.toString();
    172  }
    173 
    174  try {
    175    // Ensure that the value is really stringifiable
    176    JSON.stringify(value);
    177    return value;
    178  } catch (e) {}
    179 
    180  try {
    181    // Otherwise we try to stringify it
    182    return String(value);
    183  } catch (e) {}
    184  return "(unserializable: " + type + ")";
    185 }
    186 
    187 async function saveAsJsonFile(obj) {
    188  const jsonString = JSON.stringify(obj, null, 2);
    189  const encoder = new TextEncoder();
    190  const jsonBytes = encoder.encode(jsonString);
    191  if (!Array.isArray(obj) || !obj.length) {
    192    return;
    193  }
    194 
    195  // Build a fileName from the last recorded frame information.
    196  // It should usually match with the actual test file from which the failure
    197  // was recorded.
    198  const { columnNumber, frameScriptUrl, lineNumber } = obj.at(-1).details;
    199  const fileName = [
    200    frameScriptUrl.substr(frameScriptUrl.lastIndexOf("/") + 1),
    201    lineNumber,
    202    columnNumber,
    203  ].join("_");
    204 
    205  // Add the current timestamp in the filename, it should be impossible to have
    206  // two failures for the same frame at the same timestamp.
    207  const hash = Date.now();
    208 
    209  // Save the JSON file either under MOZ_UPLOAD_DIR or under the profile root.
    210  const filePath = PathUtils.join(
    211    Services.env.get("MOZ_UPLOAD_DIR") || PathUtils.profileDir,
    212    `scope-variables-${hash}-${fileName}.json`
    213  );
    214 
    215  dump(`[dump-scope] Saving scope variables as a JSON file: ${filePath}\n`);
    216 
    217  // Write to file
    218  await IOUtils.write(filePath, jsonBytes, { compress: false });
    219 }
    220 
    221 function serializeFrame(frame) {
    222  const frameScriptUrl = frame.script.url;
    223  const { lineNumber, columnNumber } = frame.script.getOffsetMetadata(
    224    frame.offset
    225  );
    226  const frameLocation = `${frameScriptUrl} @ ${lineNumber}:${columnNumber}`;
    227  dump(`[dump-scope] Serializing variables for frame: ${frameLocation}\n`);
    228 
    229  if (hiddenSourceURLs.includes(frameScriptUrl)) {
    230    return null;
    231  }
    232 
    233  const blocks = [];
    234  const obj = {
    235    frame: frameLocation,
    236    // Details will be used to build the filename for the JSON file.
    237    details: {
    238      columnNumber,
    239      frameScriptUrl,
    240      lineNumber,
    241    },
    242    blocks,
    243  };
    244 
    245  let env = frame.environment;
    246  while (env && env.type == "declarative" && env.scopeKind != null) {
    247    const scope = {};
    248    const names = env.names();
    249    // Serialize each variable found in the current frame.
    250    for (const name of names) {
    251      scope[name] = serialize(env.getVariable(name), 0);
    252    }
    253    blocks.push(scope);
    254    env = env.parent;
    255  }
    256  return obj;
    257 }
    258 
    259 /**
    260 * @typedef JSONFrameDetails
    261 * @property {number} columnNumber
    262 *     The column number of the exported frame.
    263 * @property {string} frameScriptUrl
    264 *     The URL of the script from which the frame was exported.
    265 * @property {number} lineNumber
    266 *     The line number of the exported frame.
    267 */
    268 
    269 /**
    270 * @typedef JSONFrame
    271 * @property {string} frame
    272 *     The frame location represented as a string built from the original script
    273 *     url, the line number and the column number
    274 * @property {JSONFrameDetails} details
    275 *     Same information as in frame, but as an object.
    276 * @property {object} scope
    277 */
    278 
    279 /**
    280 * The dumpScope helper will attempt to export variables in the current frame
    281 * and all its ancestor frames to a JSON file that can be stored and inspected
    282 * later.
    283 *
    284 * This is typically intended to be used from tests in continious integration.
    285 * By default the helper will save all variables in a JSON file stored under
    286 * MOZ_UPLOAD_DIR if the environment variable is defined, and otherwise saved
    287 * under the profile root folder.
    288 *
    289 * The export will be a best effort snapshot of the variables. The structure
    290 * of the json file will be an Array of JSONFrame.
    291 *
    292 * @param {object} options
    293 * @param {boolean=} saveAsFile
    294 *     Set to true to save as a JSON file. Set to false to simply return the
    295 *     object that would have been stringified to a JSON file.
    296 */
    297 export const dumpScope = async function ({ saveAsFile = true } = {}) {
    298  // This will inject `Debugger` in the global scope
    299  // eslint-disable-next-line mozilla/reject-globalThis-modification
    300  addDebuggerToGlobal(globalThis);
    301 
    302  const dbg = new Debugger();
    303  dbg.addAllGlobalsAsDebuggees();
    304 
    305  const scopes = [];
    306  let frame = dbg.getNewestFrame();
    307  while (frame) {
    308    try {
    309      const scope = serializeFrame(frame);
    310      if (scope) {
    311        scopes.push(scope);
    312      }
    313    } catch (e) {
    314      dump("Exception while serializing frame : " + e + "\n");
    315    }
    316    frame = frame.older || frame.asyncOlder || getAsyncParentFrame(frame);
    317  }
    318  objects.clear();
    319 
    320  if (saveAsFile) {
    321    return saveAsJsonFile(scopes);
    322  }
    323 
    324  return scopes;
    325 };