Bug 1464461 - experiemental rewrite of evalWithDebugger; r=nchevobbe draft
authoryulia <ystartsev@mozilla.com>
Sat, 09 Jun 2018 04:37:16 -0700
changeset 806350 4e3f114645693ed2c92ba5fa17867c85b6f2de75
parent 806349 68bc4920c52b8e9fab9960e7d6964890855066c3
push id112873
push userbmo:ystartsev@mozilla.com
push dateSat, 09 Jun 2018 11:37:37 +0000
reviewersnchevobbe
bugs1464461
milestone62.0a1
Bug 1464461 - experiemental rewrite of evalWithDebugger; r=nchevobbe MozReview-Commit-ID: 6r1ZfkRdn55
devtools/server/actors/webconsole.js
devtools/server/actors/webconsole/eval-with-debugger.js
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -28,16 +28,17 @@ loader.lazyRequireGetter(this, "Parser",
 loader.lazyRequireGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm", true);
 loader.lazyRequireGetter(this, "addWebConsoleCommands", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "formatCommand", "devtools/server/actors/webconsole/commands", true);
 loader.lazyRequireGetter(this, "isCommand", "devtools/server/actors/webconsole/commands", true);
 loader.lazyRequireGetter(this, "CONSOLE_WORKER_IDS", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "WebConsoleUtils", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "EnvironmentActor", "devtools/server/actors/environment", true);
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+loader.lazyRequireGetter(this, "evalWithDebugger", "devtools/server/actors/webconsole/eval-with-debugger");
 
 // Overwrite implemented listeners for workers so that we don't attempt
 // to load an unsupported module.
 if (isWorker) {
   loader.lazyRequireGetter(this, "ConsoleAPIListener", "devtools/server/actors/webconsole/worker-listeners", true);
   loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/server/actors/webconsole/worker-listeners", true);
 } else {
   loader.lazyRequireGetter(this, "ConsoleAPIListener", "devtools/server/actors/webconsole/listeners", true);
@@ -920,17 +921,17 @@ WebConsoleActor.prototype =
     let evalOptions = {
       bindObjectActor: request.bindObjectActor,
       frameActor: request.frameActor,
       url: request.url,
       selectedNodeActor: request.selectedNodeActor,
       selectedObjectActor: request.selectedObjectActor,
     };
 
-    let evalInfo = this.evalWithDebugger(input, evalOptions);
+    let evalInfo = evalWithDebugger(input, evalOptions, this);
     let evalResult = evalInfo.result;
     let helperResult = evalInfo.helperResult;
 
     let result, errorDocURL, errorMessage, errorNotes = null, errorGrip = null,
       frame = null;
     if (evalResult) {
       if ("return" in evalResult) {
         result = evalResult.return;
@@ -1223,318 +1224,16 @@ WebConsoleActor.prototype =
         // Make sure the helpers can be used during eval.
         desc.value = debuggerGlobal.makeDebuggeeValue(desc.value);
       }
       Object.defineProperty(helpers.sandbox, name, desc);
     }
     return helpers;
   },
 
-  /**
-   * Evaluates a string using the debugger API.
-   *
-   * To allow the variables view to update properties from the Web Console we
-   * provide the "bindObjectActor" mechanism: the Web Console tells the
-   * ObjectActor ID for which it desires to evaluate an expression. The
-   * Debugger.Object pointed at by the actor ID is bound such that it is
-   * available during expression evaluation (executeInGlobalWithBindings()).
-   *
-   * Example:
-   *   _self['foobar'] = 'test'
-   * where |_self| refers to the desired object.
-   *
-   * The |frameActor| property allows the Web Console client to provide the
-   * frame actor ID, such that the expression can be evaluated in the
-   * user-selected stack frame.
-   *
-   * For the above to work we need the debugger and the Web Console to share
-   * a connection, otherwise the Web Console actor will not find the frame
-   * actor.
-   *
-   * The Debugger.Frame comes from the jsdebugger's Debugger instance, which
-   * is different from the Web Console's Debugger instance. This means that
-   * for evaluation to work, we need to create a new instance for the Web
-   * Console Commands helpers - they need to be Debugger.Objects coming from the
-   * jsdebugger's Debugger instance.
-   *
-   * When |bindObjectActor| is used objects can come from different iframes,
-   * from different domains. To avoid permission-related errors when objects
-   * come from a different window, we also determine the object's own global,
-   * such that evaluation happens in the context of that global. This means that
-   * evaluation will happen in the object's iframe, rather than the top level
-   * window.
-   *
-   * @param string string
-   *        String to evaluate.
-   * @param object [options]
-   *        Options for evaluation:
-   *        - bindObjectActor: the ObjectActor ID to use for evaluation.
-   *          |evalWithBindings()| will be called with one additional binding:
-   *          |_self| which will point to the Debugger.Object of the given
-   *          ObjectActor.
-   *        - selectedObjectActor: Like bindObjectActor, but executes with the
-   *          top level window as the global.
-   *        - frameActor: the FrameActor ID to use for evaluation. The given
-   *        debugger frame is used for evaluation, instead of the global window.
-   *        - selectedNodeActor: the NodeActor ID of the currently selected node
-   *        in the Inspector (or null, if there is no selection). This is used
-   *        for helper functions that make reference to the currently selected
-   *        node, like $0.
-   *         - url: the url to evaluate the script as. Defaults to
-   *         "debugger eval code".
-   * @return object
-   *         An object that holds the following properties:
-   *         - dbg: the debugger where the string was evaluated.
-   *         - frame: (optional) the frame where the string was evaluated.
-   *         - window: the Debugger.Object for the global where the string was
-   *         evaluated.
-   *         - result: the result of the evaluation.
-   *         - helperResult: any result coming from a Web Console commands
-   *         function.
-   */
-  /* eslint-disable complexity */
-  evalWithDebugger: function(string, options = {}) {
-    let trimmedString = string.trim();
-    // The help function needs to be easy to guess, so we make the () optional.
-    if (trimmedString == "help" || trimmedString == "?") {
-      string = "help()";
-    }
-
-    const isCmd = isCommand(string);
-    // we support Unix like syntax for commands if it is preceeded by `:`
-    if (isCmd) {
-      try {
-        string = formatCommand(string);
-      } catch (e) {
-        string = `throw "${e}"`;
-      }
-    }
-
-    // Add easter egg for console.mihai().
-    if (trimmedString == "console.mihai()" || trimmedString == "console.mihai();") {
-      string = "\"http://incompleteness.me/blog/2015/02/09/console-dot-mihai/\"";
-    }
-
-    // Find the Debugger.Frame of the given FrameActor.
-    let frame = null, frameActor = null;
-    if (options.frameActor) {
-      frameActor = this.conn.getActor(options.frameActor);
-      if (frameActor) {
-        frame = frameActor.frame;
-      } else {
-        DevToolsUtils.reportException("evalWithDebugger",
-          Error("The frame actor was not found: " + options.frameActor));
-      }
-    }
-
-    // If we've been given a frame actor in whose scope we should evaluate the
-    // expression, be sure to use that frame's Debugger (that is, the JavaScript
-    // debugger's Debugger) for the whole operation, not the console's Debugger.
-    // (One Debugger will treat a different Debugger's Debugger.Object instances
-    // as ordinary objects, not as references to be followed, so mixing
-    // debuggers causes strange behaviors.)
-    let dbg = frame ? frameActor.threadActor.dbg : this.dbg;
-    let dbgWindow = dbg.makeGlobalObjectReference(this.evalWindow);
-
-    // If we have an object to bind to |_self|, create a Debugger.Object
-    // referring to that object, belonging to dbg.
-    let bindSelf = null;
-    if (options.bindObjectActor || options.selectedObjectActor) {
-      let objActor = this.getActorByID(options.bindObjectActor ||
-                                       options.selectedObjectActor);
-      if (objActor) {
-        let jsVal = objActor.rawValue();
-
-        if (isObject(jsVal)) {
-          // If we use the makeDebuggeeValue method of jsVal's own global, then
-          // we'll get a D.O that sees jsVal as viewed from its own compartment -
-          // that is, without wrappers. The evalWithBindings call will then wrap
-          // jsVal appropriately for the evaluation compartment.
-          bindSelf = dbgWindow.makeDebuggeeValue(jsVal);
-          if (options.bindObjectActor) {
-            let global = Cu.getGlobalForObject(jsVal);
-            try {
-              let _dbgWindow = dbg.makeGlobalObjectReference(global);
-              dbgWindow = _dbgWindow;
-            } catch (err) {
-              // The above will throw if `global` is invisible to debugger.
-            }
-          }
-        } else {
-          bindSelf = jsVal;
-        }
-      }
-    }
-
-    // Get the Web Console commands for the given debugger window.
-    let helpers = this._getWebConsoleCommands(dbgWindow);
-    let bindings = helpers.sandbox;
-    if (bindSelf) {
-      bindings._self = bindSelf;
-    }
-
-    if (options.selectedNodeActor) {
-      let actor = this.conn.getActor(options.selectedNodeActor);
-      if (actor) {
-        helpers.selectedNode = actor.rawNode;
-      }
-    }
-
-    // Check if the Debugger.Frame or Debugger.Object for the global include
-    // $ or $$ or screenshot. We will not overwrite these functions with the Web Console
-    // commands.
-    let found$ = false, found$$ = false, foundScreenshot = false;
-    if (!isCmd) {
-      if (frame) {
-        let env = frame.environment;
-        if (env) {
-          found$ = !!env.find("$");
-          found$$ = !!env.find("$$");
-          foundScreenshot = !!env.find("screenshot");
-        }
-      } else {
-        found$ = !!dbgWindow.getOwnPropertyDescriptor("$");
-        found$$ = !!dbgWindow.getOwnPropertyDescriptor("$$");
-        foundScreenshot = !!dbgWindow.getOwnPropertyDescriptor("screenshot");
-      }
-    }
-
-    let $ = null, $$ = null, screenshot = null;
-    if (found$) {
-      $ = bindings.$;
-      delete bindings.$;
-    }
-    if (found$$) {
-      $$ = bindings.$$;
-      delete bindings.$$;
-    }
-    if (foundScreenshot) {
-      screenshot = bindings.screenshot;
-      delete bindings.screenshot;
-    }
-
-    // Ready to evaluate the string.
-    helpers.evalInput = string;
-
-    let evalOptions;
-    if (typeof options.url == "string") {
-      evalOptions = { url: options.url };
-    }
-
-    // If the debugger object is changed from the last evaluation,
-    // adopt this._lastConsoleInputEvaluation value in the new debugger,
-    // to prevents "Debugger.Object belongs to a different Debugger" exceptions
-    // related to the $_ bindings.
-    if (this._lastConsoleInputEvaluation &&
-        this._lastConsoleInputEvaluation.global !== dbgWindow) {
-      this._lastConsoleInputEvaluation = dbg.adoptDebuggeeValue(
-        this._lastConsoleInputEvaluation
-      );
-    }
-
-    let result;
-
-    if (frame) {
-      result = frame.evalWithBindings(string, bindings, evalOptions);
-    } else {
-      result = dbgWindow.executeInGlobalWithBindings(string, bindings, evalOptions);
-      // Attempt to initialize any declarations found in the evaluated string
-      // since they may now be stuck in an "initializing" state due to the
-      // error. Already-initialized bindings will be ignored.
-      if ("throw" in result) {
-        let ast;
-        // Parse errors will raise an exception. We can/should ignore the error
-        // since it's already being handled elsewhere and we are only interested
-        // in initializing bindings.
-        try {
-          ast = Parser.reflectionAPI.parse(string);
-        } catch (ex) {
-          ast = {"body": []};
-        }
-        for (let line of ast.body) {
-          // Only let and const declarations put bindings into an
-          // "initializing" state.
-          if (!(line.kind == "let" || line.kind == "const")) {
-            continue;
-          }
-
-          let identifiers = [];
-          for (let decl of line.declarations) {
-            switch (decl.id.type) {
-              case "Identifier":
-                // let foo = bar;
-                identifiers.push(decl.id.name);
-                break;
-              case "ArrayPattern":
-                // let [foo, bar]    = [1, 2];
-                // let [foo=99, bar] = [1, 2];
-                for (let e of decl.id.elements) {
-                  if (e.type == "Identifier") {
-                    identifiers.push(e.name);
-                  } else if (e.type == "AssignmentExpression") {
-                    identifiers.push(e.left.name);
-                  }
-                }
-                break;
-              case "ObjectPattern":
-                // let {bilbo, my}    = {bilbo: "baggins", my: "precious"};
-                // let {blah: foo}    = {blah: yabba()}
-                // let {blah: foo=99} = {blah: yabba()}
-                for (let prop of decl.id.properties) {
-                  // key
-                  if (prop.key.type == "Identifier") {
-                    identifiers.push(prop.key.name);
-                  }
-                  // value
-                  if (prop.value.type == "Identifier") {
-                    identifiers.push(prop.value.name);
-                  } else if (prop.value.type == "AssignmentExpression") {
-                    identifiers.push(prop.value.left.name);
-                  }
-                }
-                break;
-            }
-          }
-
-          for (let name of identifiers) {
-            dbgWindow.forceLexicalInitializationByName(name);
-          }
-        }
-      }
-    }
-
-    let helperResult = helpers.helperResult;
-    delete helpers.evalInput;
-    delete helpers.helperResult;
-    delete helpers.selectedNode;
-
-    if ($) {
-      bindings.$ = $;
-    }
-    if ($$) {
-      bindings.$$ = $$;
-    }
-    if (screenshot) {
-      bindings.screenshot = screenshot;
-    }
-
-    if (bindings._self) {
-      delete bindings._self;
-    }
-
-    return {
-      result: result,
-      helperResult: helperResult,
-      dbg: dbg,
-      frame: frame,
-      window: dbgWindow,
-    };
-  },
-  /* eslint-enable complexity */
-
   // Event handlers for various listeners.
 
   /**
    * Handler for messages received from the ConsoleServiceListener. This method
    * sends the nsIConsoleMessage to the remote Web Console client.
    *
    * @param nsIConsoleMessage message
    *        The message we need to send to the client.
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/webconsole/eval-with-debugger.js
@@ -0,0 +1,351 @@
+
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+loader.lazyRequireGetter(this, "formatCommand", "devtools/server/actors/webconsole/commands", true);
+loader.lazyRequireGetter(this, "isCommand", "devtools/server/actors/webconsole/commands", true);
+loader.lazyRequireGetter(this, "Parser", "resource://devtools/shared/Parser.jsm", true);
+
+/**
+ * Evaluates a string using the debugger API.
+ *
+ * To allow the variables view to update properties from the Web Console we
+ * provide the "bindObjectActor" mechanism: the Web Console tells the
+ * ObjectActor ID for which it desires to evaluate an expression. The
+ * Debugger.Object pointed at by the actor ID is bound such that it is
+ * available during expression evaluation (executeInGlobalWithBindings()).
+ *
+ * Example:
+ *   _self['foobar'] = 'test'
+ * where |_self| refers to the desired object.
+ *
+ * The |frameActor| property allows the Web Console client to provide the
+ * frame actor ID, such that the expression can be evaluated in the
+ * user-selected stack frame.
+ *
+ * For the above to work we need the debugger and the Web Console to share
+ * a connection, otherwise the Web Console actor will not find the frame
+ * actor.
+ *
+ * The Debugger.Frame comes from the jsdebugger's Debugger instance, which
+ * is different from the Web Console's Debugger instance. This means that
+ * for evaluation to work, we need to create a new instance for the Web
+ * Console Commands helpers - they need to be Debugger.Objects coming from the
+ * jsdebugger's Debugger instance.
+ *
+ * When |bindObjectActor| is used objects can come from different iframes,
+ * from different domains. To avoid permission-related errors when objects
+ * come from a different window, we also determine the object's own global,
+ * such that evaluation happens in the context of that global. This means that
+ * evaluation will happen in the object's iframe, rather than the top level
+ * window.
+ *
+ * @param string string
+ *        String to evaluate.
+ * @param object [options]
+ *        Options for evaluation:
+ *        - bindObjectActor: the ObjectActor ID to use for evaluation.
+ *          |evalWithBindings()| will be called with one additional binding:
+ *          |_self| which will point to the Debugger.Object of the given
+ *          ObjectActor.
+ *        - selectedObjectActor: Like bindObjectActor, but executes with the
+ *          top level window as the global.
+ *        - frameActor: the FrameActor ID to use for evaluation. The given
+ *        debugger frame is used for evaluation, instead of the global window.
+ *        - selectedNodeActor: the NodeActor ID of the currently selected node
+ *        in the Inspector (or null, if there is no selection). This is used
+ *        for helper functions that make reference to the currently selected
+ *        node, like $0.
+ *         - url: the url to evaluate the script as. Defaults to
+ *         "debugger eval code".
+ * @return object
+ *         An object that holds the following properties:
+ *         - dbg: the debugger where the string was evaluated.
+ *         - frame: (optional) the frame where the string was evaluated.
+ *         - window: the Debugger.Object for the global where the string was
+ *         evaluated.
+ *         - result: the result of the evaluation.
+ *         - helperResult: any result coming from a Web Console commands
+ *         function.
+ */
+
+exports.evalWithDebugger = function(string, options = {}, webConsole) {
+  const evalString = getEvalInput(string);
+  const { frame, dbg } = getFrameDbg(options, webConsole);
+  const { dbgWindow, bindSelf } = getDbgWindow(options, dbg, webConsole);
+  const { helpers, cleanupHelpers } = getHelpers(dbgWindow, options, webConsole);
+  const { bindings, cleanupBindings } = bindCommands(
+    isCommand(string),
+    dbgWindow,
+    bindSelf,
+    frame,
+    helpers
+  );
+
+  // Ready to evaluate the string.
+  helpers.evalInput = string;
+  const evalOptions = typeof options.url === "string" ? { url: options.url } : null;
+
+  updateConsoleInputEvaluation(dbg, dbgWindow, webConsole);
+
+  const result = getEvalResult(
+    evalString,
+    evalOptions,
+    bindings,
+    frame,
+    dbgWindow
+  );
+
+  const helperResult = helpers.helperResult;
+
+  cleanupHelpers();
+  cleanupBindings();
+
+  return {
+    result: result,
+    helperResult: helperResult,
+    dbg: dbg,
+    frame: frame,
+    window: dbgWindow,
+  };
+};
+
+function getEvalResult(string, evalOptions, bindings, frame, dbgWindow) {
+  if (frame) {
+    return frame.evalWithBindings(string, bindings, evalOptions);
+  }
+  const result = dbgWindow.executeInGlobalWithBindings(string, bindings, evalOptions);
+  // Attempt to initialize any declarations found in the evaluated string
+  // since they may now be stuck in an "initializing" state due to the
+  // error. Already-initialized bindings will be ignored.
+  if ("throw" in result) {
+    parseErrorOutput(dbgWindow, string);
+  }
+  return result;
+}
+
+function parseErrorOutput(dbgWindow, string) {
+  let ast;
+  // Parse errors will raise an exception. We can/should ignore the error
+  // since it's already being handled elsewhere and we are only interested
+  // in initializing bindings.
+  try {
+    ast = Parser.reflectionAPI.parse(string);
+  } catch (ex) {
+    return;
+  }
+  for (let line of ast.body) {
+    // Only let and const declarations put bindings into an
+    // "initializing" state.
+    if (!(line.kind == "let" || line.kind == "const")) {
+      continue;
+    }
+
+    let identifiers = [];
+    for (let decl of line.declarations) {
+      switch (decl.id.type) {
+        case "Identifier":
+          // let foo = bar;
+          identifiers.push(decl.id.name);
+          break;
+        case "ArrayPattern":
+          // let [foo, bar]    = [1, 2];
+          // let [foo=99, bar] = [1, 2];
+          for (let e of decl.id.elements) {
+            if (e.type == "Identifier") {
+              identifiers.push(e.name);
+            } else if (e.type == "AssignmentExpression") {
+              identifiers.push(e.left.name);
+            }
+          }
+          break;
+        case "ObjectPattern":
+          // let {bilbo, my}    = {bilbo: "baggins", my: "precious"};
+          // let {blah: foo}    = {blah: yabba()}
+          // let {blah: foo=99} = {blah: yabba()}
+          for (let prop of decl.id.properties) {
+            // key
+            if (prop.key.type == "Identifier") {
+              identifiers.push(prop.key.name);
+            }
+            // value
+            if (prop.value.type == "Identifier") {
+              identifiers.push(prop.value.name);
+            } else if (prop.value.type == "AssignmentExpression") {
+              identifiers.push(prop.value.left.name);
+            }
+          }
+          break;
+      }
+    }
+
+    for (let name of identifiers) {
+      dbgWindow.forceLexicalInitializationByName(name);
+    }
+  }
+}
+
+function updateConsoleInputEvaluation(dbg, dbgWindow, webConsole) {
+  // If the debugger object is changed from the last evaluation,
+  // adopt webConsole._lastConsoleInputEvaluation value in the new debugger,
+  // to prevents "Debugger.Object belongs to a different Debugger" exceptions
+  // related to the $_ bindings.
+  if (webConsole._lastConsoleInputEvaluation &&
+      webConsole._lastConsoleInputEvaluation.global !== dbgWindow) {
+    webConsole._lastConsoleInputEvaluation = dbg.adoptDebuggeeValue(
+      webConsole._lastConsoleInputEvaluation
+    );
+  }
+}
+
+function getEvalInput(string) {
+  const trimmedString = string.trim();
+  // The help function needs to be easy to guess, so we make the () optional.
+  if (trimmedString === "help" || trimmedString === "?") {
+    return "help()";
+  }
+
+  // we support Unix like syntax for commands if it is preceeded by `:`
+  if (isCommand(string)) {
+    try {
+      return formatCommand(string);
+    } catch (e) {
+      console.log(e);
+      return `throw "${e}"`;
+    }
+  }
+
+  // Add easter egg for console.mihai().
+  if (trimmedString == "console.mihai()" || trimmedString == "console.mihai();") {
+    return "\"http://incompleteness.me/blog/2015/02/09/console-dot-mihai/\"";
+  }
+  return string;
+}
+
+function getFrameDbg(options, webConsole) {
+  if (!options.frameActor) {
+    return { frame: null, dbg: webConsole.dbg };
+  }
+// Find the Debugger.Frame of the given FrameActor.
+  const frameActor = webConsole.conn.getActor(options.frameActor);
+  if (frameActor) {
+    // If we've been given a frame actor in whose scope we should evaluate the
+    // expression, be sure to use that frame's Debugger (that is, the JavaScript
+    // debugger's Debugger) for the whole operation, not the console's Debugger.
+    // (One Debugger will treat a different Debugger's Debugger.Object instances
+    // as ordinary objects, not as references to be followed, so mixing
+    // debuggers causes strange behaviors.)
+    return { frame: frameActor.frame, dbg: frameActor.threadActor.dbg };
+  }
+  return DevToolsUtils.reportException("evalWithDebugger",
+    Error("The frame actor was not found: " + options.frameActor));
+}
+
+function getDbgWindow(options, dbg, webConsole) {
+  const dbgWindow = dbg.makeGlobalObjectReference(webConsole.evalWindow);
+  // If we have an object to bind to |_self|, create a Debugger.Object
+  // referring to that object, belonging to dbg.
+  if (!options.bindObjectActor && !options.selectedObjectActor) {
+    return { bindSelf: null, dbgWindow };
+  }
+
+  const objActor = webConsole.getActorByID(
+    options.bindObjectActor || options.selectedObjectActor
+  );
+
+  if (!objActor) {
+    return { bindSelf: null, dbgWindow };
+  }
+
+  const jsVal = objActor.rawValue();
+
+  if (!isObject(jsVal)) {
+    return { bindSelf: jsVal, dbgWindow };
+  }
+
+  // If we use the makeDebuggeeValue method of jsVal's own global, then
+  // we'll get a D.O that sees jsVal as viewed from its own compartment -
+  // that is, without wrappers. The evalWithBindings call will then wrap
+  // jsVal appropriately for the evaluation compartment.
+  const bindSelf = dbgWindow.makeDebuggeeValue(jsVal);
+  if (options.bindObjectActor) {
+    const global = Cu.getGlobalForObject(jsVal);
+    try {
+      const _dbgWindow = dbg.makeGlobalObjectReference(global);
+      return { bindSelf, dbgWindow: _dbgWindow };
+    } catch (err) {
+      // The above will throw if `global` is invisible to debugger.
+    }
+  }
+  return { bindSelf, dbgWindow };
+}
+
+function getHelpers(dbgWindow, options, webConsole) {
+  // Get the Web Console commands for the given debugger window.
+  const helpers = webConsole._getWebConsoleCommands(dbgWindow);
+  if (options.selectedNodeActor) {
+    let actor = webConsole.conn.getActor(options.selectedNodeActor);
+    if (actor) {
+      helpers.selectedNode = actor.rawNode;
+    }
+  }
+
+  function cleanupHelpers() {
+    delete helpers.evalInput;
+    delete helpers.helperResult;
+    delete helpers.selectedNode;
+  }
+
+  return { helpers, cleanupHelpers };
+}
+
+function bindCommands(isCmd, dbgWindow, bindSelf, frame, helpers) {
+  const bindings = helpers.sandbox;
+  if (bindSelf) {
+    bindings._self = bindSelf;
+  }
+  // Check if the Debugger.Frame or Debugger.Object for the global include
+  // $ or $$. We will not overwrite these functions with the Web Console
+  // commands.
+  const overrides = ["$", "$$", "screenshot"];
+  const backups = [];
+  const env = frame && frame.environment;
+
+  overrides.forEach(override => {
+    const shouldOverride = (
+      !isCmd ||
+      env && !!env.find(override) ||
+      !!dbgWindow.getOwnPropertyDescriptor(override)
+    );
+
+    if (shouldOverride) {
+      backups[override] = bindings[override];
+      delete bindings[override];
+    }
+  });
+
+  function cleanupBindings() {
+    Object.entries(backups).forEach(([name, value]) => {
+      bindings[name] = value;
+    });
+    if (bindings._self) {
+      delete bindings._self;
+    }
+  }
+
+  return { bindings, cleanupBindings };
+}
+
+function isObject(value) {
+  return Object(value) === value;
+}
+
+function isCommand(string) {
+  return /^:/.test(string);
+}