Bug 1464461 - experiemental rewrite of evalWithDebugger; r=nchevobbe
MozReview-Commit-ID: 6r1ZfkRdn55
--- 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);
+}