/* 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/. */ /** * This XPCOM component is loaded very early. * Be careful to lazy load dependencies as much as possible. * * It manages all the possible entry points for DevTools: * - Handles command line arguments like -jsconsole, * - Register all key shortcuts, * - Listen for "Browser Tools" system menu opening, under "Tools", * - Inject the wrench icon in toolbar customization, which is used * by the "Browser Tools" list displayed in the hamburger menu, * - Register the JSON Viewer protocol handler. * - Inject the profiler recording button in toolbar customization. * * Only once any of these entry point is fired, this module ensures starting * core modules like 'devtools-browser.js' that hooks the browser windows * and ensure setting up tools. **/ const kDebuggerPrefs = [ "devtools.debugger.remote-enabled", "devtools.chrome.enabled", ]; const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs", CustomizableWidgets: "resource:///modules/CustomizableWidgets.sys.mjs", PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", WebChannel: "resource://gre/modules/WebChannel.sys.mjs", }); ChromeUtils.defineModuleGetter( lazy, "ProfilerMenuButton", "resource://devtools/client/performance-new/popup/menu-button.jsm.js" ); // We don't want to spend time initializing the full loader here so we create // our own lazy require. XPCOMUtils.defineLazyGetter(lazy, "Telemetry", function () { const { require } = ChromeUtils.importESModule( "resource://devtools/shared/loader/Loader.sys.mjs" ); // eslint-disable-next-line no-shadow const Telemetry = require("devtools/client/shared/telemetry"); return Telemetry; }); XPCOMUtils.defineLazyGetter(lazy, "KeyShortcutsBundle", function () { return new Localization(["devtools/startup/key-shortcuts.ftl"], true); }); /** * Safely retrieve a localized DevTools key shortcut from KeyShortcutsBundle. * If the shortcut is not available, this will return null. Consumer code * should rely on this to skip unavailable shortcuts. * * Note that all shortcuts should always be available, but there is a notable * exception, which is why we have to do this. When a localization change is * uplifted to beta, language packs will not be updated immediately when the * updated beta is available. * * This means that language pack users might get a new Beta version but will not * have a language pack with the new strings yet. */ function getLocalizedKeyShortcut(id) { try { return lazy.KeyShortcutsBundle.formatValueSync(id); } catch (e) { console.error("Failed to retrieve DevTools localized shortcut for id", id); return null; } } XPCOMUtils.defineLazyGetter(lazy, "KeyShortcuts", function () { const isMac = AppConstants.platform == "macosx"; // Common modifier shared by most key shortcuts const modifiers = isMac ? "accel,alt" : "accel,shift"; // List of all key shortcuts triggering installation UI // `id` should match tool's id from client/definitions.js const shortcuts = [ // The following keys are also registered in /client/menus.js // And should be synced. // Both are toggling the toolbox on the last selected panel // or the default one. { id: "toggleToolbox", shortcut: getLocalizedKeyShortcut("devtools-commandkey-toggle-toolbox"), modifiers, }, // All locales are using F12 { id: "toggleToolboxF12", shortcut: getLocalizedKeyShortcut( "devtools-commandkey-toggle-toolbox-f12" ), modifiers: "", // F12 is the only one without modifiers }, // Open the Browser Toolbox { id: "browserToolbox", shortcut: getLocalizedKeyShortcut("devtools-commandkey-browser-toolbox"), modifiers: "accel,alt,shift", }, // Open the Browser Console { id: "browserConsole", shortcut: getLocalizedKeyShortcut("devtools-commandkey-browser-console"), modifiers: "accel,shift", }, // Toggle the Responsive Design Mode { id: "responsiveDesignMode", shortcut: getLocalizedKeyShortcut( "devtools-commandkey-responsive-design-mode" ), modifiers, }, // The following keys are also registered in /client/definitions.js // and should be synced. // Key for opening the Inspector { toolId: "inspector", shortcut: getLocalizedKeyShortcut("devtools-commandkey-inspector"), modifiers, }, // Key for opening the Web Console { toolId: "webconsole", shortcut: getLocalizedKeyShortcut("devtools-commandkey-webconsole"), modifiers, }, // Key for opening the Debugger { toolId: "jsdebugger", shortcut: getLocalizedKeyShortcut("devtools-commandkey-jsdebugger"), modifiers, }, // Key for opening the Network Monitor { toolId: "netmonitor", shortcut: getLocalizedKeyShortcut("devtools-commandkey-netmonitor"), modifiers, }, // Key for opening the Style Editor { toolId: "styleeditor", shortcut: getLocalizedKeyShortcut("devtools-commandkey-styleeditor"), modifiers: "shift", }, // Key for opening the Performance Panel { toolId: "performance", shortcut: getLocalizedKeyShortcut("devtools-commandkey-performance"), modifiers: "shift", }, // Key for opening the Storage Panel { toolId: "storage", shortcut: getLocalizedKeyShortcut("devtools-commandkey-storage"), modifiers: "shift", }, // Key for opening the DOM Panel { toolId: "dom", shortcut: getLocalizedKeyShortcut("devtools-commandkey-dom"), modifiers, }, // Key for opening the Accessibility Panel { toolId: "accessibility", shortcut: getLocalizedKeyShortcut( "devtools-commandkey-accessibility-f12" ), modifiers: "shift", }, ]; if (isMac) { // Add the extra key command for macOS, so you can open the inspector with cmd+shift+C // like on Chrome DevTools. shortcuts.push({ id: "inspectorMac", toolId: "inspector", shortcut: getLocalizedKeyShortcut("devtools-commandkey-inspector"), modifiers: "accel,shift", }); } if (lazy.ProfilerMenuButton.isInNavbar()) { shortcuts.push(...getProfilerKeyShortcuts()); } // Allow toggling the JavaScript tracing not only from DevTools UI, // but also from the web page when it is focused. if ( Services.prefs.getBoolPref( "devtools.debugger.features.javascript-tracing", false ) ) { shortcuts.push({ id: "javascriptTracingToggle", shortcut: getLocalizedKeyShortcut( "devtools-commandkey-javascript-tracing-toggle" ), modifiers: "control,shift", }); } return shortcuts; }); function getProfilerKeyShortcuts() { return [ // Start/stop the profiler { id: "profilerStartStop", shortcut: getLocalizedKeyShortcut( "devtools-commandkey-profiler-start-stop" ), modifiers: "control,shift", }, // Capture a profile { id: "profilerCapture", shortcut: getLocalizedKeyShortcut("devtools-commandkey-profiler-capture"), modifiers: "control,shift", }, ]; } /** * Validate the URL that will be used for the WebChannel for the profiler. * * @param {string} targetUrl * @returns {string} */ export function validateProfilerWebChannelUrl(targetUrl) { const frontEndUrl = "https://profiler.firefox.com"; if (targetUrl !== frontEndUrl) { // The user can specify either localhost or deploy previews as well as // the official frontend URL for testing. if ( // Allow a test URL. /^https?:\/\/example\.com$/.test(targetUrl) || // Allows the following: // "http://localhost:4242" // "http://localhost:4242/" // "http://localhost:3" // "http://localhost:334798455" /^http:\/\/localhost:\d+\/?$/.test(targetUrl) || // Allows the following: // "https://deploy-preview-1234--perf-html.netlify.com" // "https://deploy-preview-1234--perf-html.netlify.com/" // "https://deploy-preview-1234567--perf-html.netlify.app" // "https://main--perf-html.netlify.app" /^https:\/\/(?:deploy-preview-\d+|main)--perf-html\.netlify\.(?:com|app)\/?$/.test( targetUrl ) ) { // This URL is one of the allowed ones to be used for configuration. return targetUrl; } console.error( `The preference "devtools.performance.recording.ui-base-url" was set to a ` + "URL that is not allowed. No WebChannel messages will be sent between the " + `browser and that URL. Falling back to ${frontEndUrl}. Only localhost ` + "and deploy previews URLs are allowed.", targetUrl ); } return frontEndUrl; } XPCOMUtils.defineLazyGetter(lazy, "ProfilerPopupBackground", function () { return ChromeUtils.import( "resource://devtools/client/performance-new/shared/background.jsm.js" ); }); export function DevToolsStartup() { this.onWindowReady = this.onWindowReady.bind(this); this.addDevToolsItemsToSubview = this.addDevToolsItemsToSubview.bind(this); this.onMoreToolsViewShowing = this.onMoreToolsViewShowing.bind(this); this.toggleProfilerKeyShortcuts = this.toggleProfilerKeyShortcuts.bind(this); } DevToolsStartup.prototype = { /** * Boolean flag to check if DevTools have been already initialized or not. * By initialized, we mean that its main modules are loaded. */ initialized: false, /** * Boolean flag to check if the devtools initialization was already sent to telemetry. * We only want to record one devtools entry point per Firefox run, but we are not * interested in all the entry points. */ recorded: false, get telemetry() { if (!this._telemetry) { this._telemetry = new lazy.Telemetry(); this._telemetry.setEventRecordingEnabled(true); } return this._telemetry; }, /** * Flag that indicates if the developer toggle was already added to customizableUI. */ developerToggleCreated: false, /** * Flag that indicates if the profiler recording popup was already added to * customizableUI. */ profilerRecordingButtonCreated: false, isDisabledByPolicy() { return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false); }, handle(cmdLine) { const flags = this.readCommandLineFlags(cmdLine); // handle() can be called after browser startup (e.g. opening links from other apps). const isInitialLaunch = cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH; if (isInitialLaunch) { // Store devtoolsFlag to check it later in onWindowReady. this.devtoolsFlag = flags.devtools; /* eslint-disable mozilla/balanced-observers */ // We are not expecting to remove those listeners until Firefox closes. // Only top level Firefox Windows fire a browser-delayed-startup-finished event Services.obs.addObserver( this.onWindowReady, "browser-delayed-startup-finished" ); // Add DevTools menu items to the "More Tools" view. Services.obs.addObserver( this.onMoreToolsViewShowing, "web-developer-tools-view-showing" ); /* eslint-enable mozilla/balanced-observers */ if (!this.isDisabledByPolicy()) { if (AppConstants.MOZ_DEV_EDITION) { // On DevEdition, the developer toggle is displayed by default in the navbar // area and should be created before the first paint. this.hookDeveloperToggle(); } this.hookProfilerRecordingButton(); } } if (flags.console) { this.commandLine = true; this.handleConsoleFlag(cmdLine); } if (flags.debugger) { this.commandLine = true; const binaryPath = typeof flags.debugger == "string" ? flags.debugger : null; this.handleDebuggerFlag(cmdLine, binaryPath); } if (flags.devToolsServer) { this.handleDevToolsServerFlag(cmdLine, flags.devToolsServer); } // If Firefox is already opened, and DevTools are also already opened, // try to open links passed via command line arguments. if (!isInitialLaunch && this.initialized && cmdLine.length) { this.checkForDebuggerLink(cmdLine); } }, /** * Lookup in all arguments passed to firefox binary to find * URLs including a precise location, like this: * https://domain.com/file.js:1:10 (URL ending with `:${line}:${number}`) * When such argument exists, try to open this source and precise location * in the debugger. * * @param {nsICommandLine} cmdLine */ checkForDebuggerLink(cmdLine) { const urlFlagIdx = cmdLine.findFlag("url", false); // Bail out when there is no -url argument, or if that's last and so there is no URL after it. if (urlFlagIdx == -1 && urlFlagIdx + 1 < cmdLine.length) { return; } // The following code would only work if we have a top level browser window opened const window = Services.wm.getMostRecentWindow("navigator:browser"); if (!window) { return; } const urlParam = cmdLine.getArgument(urlFlagIdx + 1); // Avoid processing valid url like: // http://foo@user:123 // Note that when loading `http://foo.com` the URL of the default html page will be `http://foo.com/`. // So that there will always be another `/` after `https://` if ( (urlParam.startsWith("http://") || urlParam.startsWith("https://")) && urlParam.lastIndexOf("/") <= 7 ) { return; } let match = urlParam.match(/^(?\w+:.+):(?\d+):(?\d+)$/); if (!match) { // fallback on only having the line when there is no column match = urlParam.match(/^(?\w+:.+):(?\d+)?$/); if (!match) { return; } } const { url, line, column } = match.groups; // If for any reason the final url is invalid, ignore it try { Services.io.newURI(url); } catch (e) { return; } // Avoid regular Firefox code from processing this argument, // otherwise we would open the source in DevTools and in a new tab. // // /!\ This has to be called synchronously from the call to `DevToolsStartup.handle(cmdLine)` // Otherwise the next command lines listener will interpret the argument redundantly. cmdLine.removeArguments(urlFlagIdx, urlFlagIdx + 1); // Avoid opening a new empty top level window if there is no more arguments if (!cmdLine.length) { cmdLine.preventDefault = true; } // Note that the following method is async and returns a promise. // But the current method has to be synchronous because of cmdLine.removeArguments. this.openSourceInDebugger(window, { url, line: parseInt(line, 10), column: parseInt(column || 0, 10), }); }, /** * If DevTools and the debugger are opened, try to open the source * at specified location in the debugger. * Otherwise fallback by opening this location via view-source. * * @param {Window} window * The top level browser window into which we should open the URL. * @param {String} url * @param {Number} line * @param {Number} column */ async openSourceInDebugger(window, { url, line, column }) { const require = this.initDevTools("CommandLine"); const { gDevTools } = require("devtools/client/framework/devtools"); await gDevTools.openSourceInDebugger(window, { url, line, column }); }, readCommandLineFlags(cmdLine) { // All command line flags are disabled if DevTools are disabled by policy. if (this.isDisabledByPolicy()) { return { console: false, debugger: false, devtools: false, devToolsServer: false, }; } const console = cmdLine.handleFlag("jsconsole", false); const devtools = cmdLine.handleFlag("devtools", false); let devToolsServer; try { devToolsServer = cmdLine.handleFlagWithParam( "start-debugger-server", false ); } catch (e) { // We get an error if the option is given but not followed by a value. // By catching and trying again, the value is effectively optional. devToolsServer = cmdLine.handleFlag("start-debugger-server", false); } let debuggerFlag; try { debuggerFlag = cmdLine.handleFlagWithParam("jsdebugger", false); } catch (e) { // We get an error if the option is given but not followed by a value. // By catching and trying again, the value is effectively optional. debuggerFlag = cmdLine.handleFlag("jsdebugger", false); } return { console, debugger: debuggerFlag, devtools, devToolsServer }; }, /** * Called when receiving the "browser-delayed-startup-finished" event for a new * top-level window. */ onWindowReady(window) { if ( this.isDisabledByPolicy() || AppConstants.MOZ_APP_NAME == "thunderbird" ) { return; } this.hookWindow(window); // This listener is called for all Firefox windows, but we want to execute some code // only once. if (!this._firstWindowReadyReceived) { this.onFirstWindowReady(window); this._firstWindowReadyReceived = true; } JsonView.initialize(); }, onFirstWindowReady(window) { if (this.devtoolsFlag) { this.handleDevToolsFlag(window); // In the case of the --jsconsole and --jsdebugger command line parameters // there was no browser window when they were processed so we act on the // this.commandline flag instead. if (this.commandLine) { this.sendEntryPointTelemetry("CommandLine"); } } this.setSlowScriptDebugHandler(); }, /** * Register listeners to all possible entry points for Developer Tools. * But instead of implementing the actual actions, defer to DevTools codebase. * In most cases, it only needs to call this.initDevTools which handles the rest. * We do that to prevent loading any DevTools module until the user intent to use them. */ hookWindow(window) { // Key Shortcuts need to be added on all the created windows. this.hookKeyShortcuts(window); // In some situations (e.g. starting Firefox with --jsconsole) DevTools will be // initialized before the first browser-delayed-startup-finished event is received. // We use a dedicated flag because we still need to hook the developer toggle. this.hookDeveloperToggle(); this.hookProfilerRecordingButton(); // The developer menu hook only needs to be added if devtools have not been // initialized yet. if (!this.initialized) { this.hookBrowserToolsMenu(window); } }, /** * Dynamically register a wrench icon in the customization menu. * You can use this button by right clicking on Firefox toolbar * and dragging it from the customization panel to the toolbar. * (i.e. this isn't displayed by default to users!) * * _But_, the "Browser Tools" entry in the hamburger menu (the menu with * 3 horizontal lines), is using this "developer-button" view to populate * its menu. So we have to register this button for the menu to work. * * Also, this menu duplicates its own entries from the "Browser Tools" * menu in the system menu, under "Tools" main menu item. The system * menu is being hooked by "hookBrowserToolsMenu" which ends up calling * devtools/client/framework/browser-menus to create the items for real, * initDevTools, from onViewShowing is also calling browser-menu. */ hookDeveloperToggle() { if (this.developerToggleCreated) { return; } const id = "developer-button"; const widget = lazy.CustomizableUI.getWidget(id); if (widget && widget.provider == lazy.CustomizableUI.PROVIDER_API) { return; } const panelviewId = "PanelUI-developer-tools"; const subviewId = "PanelUI-developer-tools-view"; const item = { id, type: "view", viewId: panelviewId, shortcutId: "key_toggleToolbox", tooltiptext: "developer-button.tooltiptext2", onViewShowing: event => { const doc = event.target.ownerDocument; const developerItems = lazy.PanelMultiView.getViewNode(doc, subviewId); this.addDevToolsItemsToSubview(developerItems); }, onInit(anchor) { // Since onBeforeCreated already bails out when initialized, we can call // it right away. this.onBeforeCreated(anchor.ownerDocument); }, onBeforeCreated: doc => { // The developer toggle needs the "key_toggleToolbox" element. // In DEV EDITION, the toggle is added before 1st paint and hookKeyShortcuts() is // not called yet when CustomizableUI creates the widget. this.hookKeyShortcuts(doc.defaultView); }, }; lazy.CustomizableUI.createWidget(item); lazy.CustomizableWidgets.push(item); this.developerToggleCreated = true; }, addDevToolsItemsToSubview(subview) { // Initialize DevTools to create all menuitems in the system menu before // trying to copy them. this.initDevTools("HamburgerMenu"); // Populate the subview with whatever menuitems are in the developer // menu. We skip menu elements, because the menu panel has no way // of dealing with those right now. const doc = subview.ownerDocument; const menu = doc.getElementById("menuWebDeveloperPopup"); const itemsToDisplay = [...menu.children]; lazy.CustomizableUI.clearSubview(subview); lazy.CustomizableUI.fillSubviewFromMenuItems(itemsToDisplay, subview); }, onMoreToolsViewShowing(moreToolsView) { this.addDevToolsItemsToSubview(moreToolsView); }, /** * Register the profiler recording button. This button will be available * in the customization palette for the Firefox toolbar. In addition, it can be * enabled from profiler.firefox.com. */ hookProfilerRecordingButton() { if (this.profilerRecordingButtonCreated) { return; } const featureFlagPref = "devtools.performance.popup.feature-flag"; const isPopupFeatureFlagEnabled = Services.prefs.getBoolPref(featureFlagPref); this.profilerRecordingButtonCreated = true; // Listen for messages from the front-end. This needs to happen even if the // button isn't enabled yet. This will allow the front-end to turn on the // popup for our users, regardless of if the feature is enabled by default. this.initializeProfilerWebChannel(); if (isPopupFeatureFlagEnabled) { // Initialize the CustomizableUI widget. lazy.ProfilerMenuButton.initialize(this.toggleProfilerKeyShortcuts); } else { // The feature flag is not enabled, but watch for it to be enabled. If it is, // initialize everything. const enable = () => { lazy.ProfilerMenuButton.initialize(this.toggleProfilerKeyShortcuts); Services.prefs.removeObserver(featureFlagPref, enable); }; Services.prefs.addObserver(featureFlagPref, enable); } }, /** * Initialize the WebChannel for profiler.firefox.com. This function happens at * startup, so care should be taken to minimize its performance impact. The WebChannel * is a mechanism that is used to communicate between the browser, and front-end code. */ initializeProfilerWebChannel() { let channel; // Register a channel for the URL in preferences. Also update the WebChannel if // the URL changes. const urlPref = "devtools.performance.recording.ui-base-url"; // This method is only run once per Firefox instance, so it should not be // strictly necessary to remove observers here. // eslint-disable-next-line mozilla/balanced-observers Services.prefs.addObserver(urlPref, registerWebChannel); registerWebChannel(); function registerWebChannel() { if (channel) { channel.stopListening(); } const urlForWebChannel = Services.io.newURI( validateProfilerWebChannelUrl(Services.prefs.getStringPref(urlPref)) ); channel = new lazy.WebChannel("profiler.firefox.com", urlForWebChannel); channel.listen((id, message, target) => { // Defer loading the ProfilerPopupBackground script until it's absolutely needed, // as this code path gets loaded at startup. lazy.ProfilerPopupBackground.handleWebChannelMessage( channel, id, message, target ); }); } }, /* * We listen to the "Browser Tools" system menu, which is under "Tools" main item. * This menu item is hardcoded empty in Firefox UI. We listen for its opening to * populate it lazily. Loading main DevTools module is going to populate it. */ hookBrowserToolsMenu(window) { const menu = window.document.getElementById("browserToolsMenu"); const onPopupShowing = () => { menu.removeEventListener("popupshowing", onPopupShowing); this.initDevTools("SystemMenu"); }; menu.addEventListener("popupshowing", onPopupShowing); }, /** * Check if the user is a DevTools user by looking at our selfxss pref. * This preference is incremented everytime the console is used (up to 5). * * @return {Boolean} true if the user can be considered as a devtools user. */ isDevToolsUser() { const selfXssCount = Services.prefs.getIntPref("devtools.selfxss.count", 0); return selfXssCount > 0; }, hookKeyShortcuts(window) { const doc = window.document; // hookKeyShortcuts can be called both from hookWindow and from the developer toggle // onBeforeCreated. Make sure shortcuts are only added once per window. if (doc.getElementById("devtoolsKeyset")) { return; } const keyset = doc.createXULElement("keyset"); keyset.setAttribute("id", "devtoolsKeyset"); this.attachKeys(doc, lazy.KeyShortcuts, keyset); // Appending a element is not always enough. The needs // to be detached and reattached to make sure the is taken into // account (see bug 832984). const mainKeyset = doc.getElementById("mainKeyset"); mainKeyset.parentNode.insertBefore(keyset, mainKeyset); }, /** * This method attaches on the key elements to the devtools keyset. */ attachKeys(doc, keyShortcuts, keyset = doc.getElementById("devtoolsKeyset")) { const window = doc.defaultView; for (const key of keyShortcuts) { if (!key.shortcut) { // Shortcuts might be missing when a user relies on a language packs // which is missing a recently uplifted shortcut. Language packs are // typically updated a few days after a code uplift. continue; } const xulKey = this.createKey(doc, key, () => this.onKey(window, key)); keyset.appendChild(xulKey); } }, /** * This method removes keys from the devtools keyset. */ removeKeys(doc, keyShortcuts) { for (const key of keyShortcuts) { const keyElement = doc.getElementById(this.getKeyElementId(key)); if (keyElement) { keyElement.remove(); } } }, /** * We only want to have the keyboard shortcuts active when the menu button is on. * This function either adds or removes the elements. * @param {boolean} isEnabled */ toggleProfilerKeyShortcuts(isEnabled) { const profilerKeyShortcuts = getProfilerKeyShortcuts(); for (const { document } of Services.wm.getEnumerator(null)) { const devtoolsKeyset = document.getElementById("devtoolsKeyset"); const mainKeyset = document.getElementById("mainKeyset"); if (!devtoolsKeyset || !mainKeyset) { // There may not be devtools keyset on this window. continue; } const areProfilerKeysPresent = !!document.getElementById( "key_profilerStartStop" ); if (isEnabled === areProfilerKeysPresent) { // Don't double add or double remove the shortcuts. continue; } if (isEnabled) { this.attachKeys(document, profilerKeyShortcuts); } else { this.removeKeys(document, profilerKeyShortcuts); } // Appending a element is not always enough. The needs // to be detached and reattached to make sure the is taken into // account (see bug 832984). mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset); } }, async onKey(window, key) { try { // The profiler doesn't care if DevTools is loaded, so provide a quick check // first to bail out of checking if DevTools is available. switch (key.id) { case "profilerStartStop": { lazy.ProfilerPopupBackground.toggleProfiler("aboutprofiling"); return; } case "profilerCapture": { lazy.ProfilerPopupBackground.captureProfile("aboutprofiling"); return; } } // Ignore the following key shortcut if DevTools aren't yet opened. // The key shortcut is registered in this core component in order to // work even when the web page is focused. if (key.id == "javascriptTracingToggle" && !this.initialized) { return; } // Record the timing at which this event started in order to compute later in // gDevTools.showToolbox, the complete time it takes to open the toolbox. // i.e. especially take `initDevTools` into account. const startTime = Cu.now(); const require = this.initDevTools("KeyShortcut", key); const { gDevToolsBrowser, } = require("devtools/client/framework/devtools-browser"); await gDevToolsBrowser.onKeyShortcut(window, key, startTime); } catch (e) { console.error(`Exception while trigerring key ${key}: ${e}\n${e.stack}`); } }, getKeyElementId({ id, toolId }) { return "key_" + (id || toolId); }, // Create a DOM Element createKey(doc, key, oncommand) { const { shortcut, modifiers: mod } = key; const k = doc.createXULElement("key"); k.id = this.getKeyElementId(key); if (shortcut.startsWith("VK_")) { k.setAttribute("keycode", shortcut); if (shortcut.match(/^VK_\d$/)) { // Add the event keydown attribute to ensure that shortcuts work for combinations // such as ctrl shift 1. k.setAttribute("event", "keydown"); } } else { k.setAttribute("key", shortcut); } if (mod) { k.setAttribute("modifiers", mod); } k.addEventListener("command", oncommand); return k; }, initDevTools(reason, key = "") { // In the case of the --jsconsole and --jsdebugger command line parameters // there is no browser window yet so we don't send any telemetry yet. if (reason !== "CommandLine") { this.sendEntryPointTelemetry(reason, key); } this.initialized = true; const { require } = ChromeUtils.importESModule( "resource://devtools/shared/loader/Loader.sys.mjs" ); // Ensure loading main devtools module that hooks up into browser UI // and initialize all devtools machinery. // eslint-disable-next-line import/no-unassigned-import require("devtools/client/framework/devtools-browser"); return require; }, handleConsoleFlag(cmdLine) { const window = Services.wm.getMostRecentWindow("devtools:webconsole"); if (!window) { const require = this.initDevTools("CommandLine"); const { BrowserConsoleManager, } = require("devtools/client/webconsole/browser-console-manager"); BrowserConsoleManager.toggleBrowserConsole().catch(console.error); } else { // the Browser Console was already open window.focus(); } if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) { cmdLine.preventDefault = true; } }, // Open the toolbox on the selected tab once the browser starts up. async handleDevToolsFlag(window) { const require = this.initDevTools("CommandLine"); const { gDevTools } = require("devtools/client/framework/devtools"); await gDevTools.showToolboxForTab(window.gBrowser.selectedTab); }, _isRemoteDebuggingEnabled() { let remoteDebuggingEnabled = false; try { remoteDebuggingEnabled = kDebuggerPrefs.every(pref => { return Services.prefs.getBoolPref(pref); }); } catch (ex) { console.error(ex); return false; } if (!remoteDebuggingEnabled) { const errorMsg = "Could not run chrome debugger! You need the following " + "prefs to be set to true: " + kDebuggerPrefs.join(", "); console.error(new Error(errorMsg)); // Dump as well, as we're doing this from a commandline, make sure people // don't miss it: dump(errorMsg + "\n"); } return remoteDebuggingEnabled; }, handleDebuggerFlag(cmdLine, binaryPath) { if (!this._isRemoteDebuggingEnabled()) { return; } let devtoolsThreadResumed = false; const pauseOnStartup = cmdLine.handleFlag("wait-for-jsdebugger", false); if (pauseOnStartup) { const observe = function (subject, topic, data) { devtoolsThreadResumed = true; Services.obs.removeObserver(observe, "devtools-thread-ready"); }; Services.obs.addObserver(observe, "devtools-thread-ready"); } const { BrowserToolboxLauncher } = ChromeUtils.importESModule( "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs" ); // --jsdebugger $binaryPath is an helper alias to set MOZ_BROWSER_TOOLBOX_BINARY=$binaryPath // See comment within BrowserToolboxLauncher. // Setting it as an environment variable helps it being reused if we restart the browser via CmdOrCtrl+R Services.env.set("MOZ_BROWSER_TOOLBOX_BINARY", binaryPath); const browserToolboxLauncherConfig = {}; // If user passed the --jsdebugger in mochitests, we want to enable the // multiprocess Browser Toolbox (by default it's parent process only) if (Services.prefs.getBoolPref("devtools.testing", false)) { browserToolboxLauncherConfig.forceMultiprocess = true; } BrowserToolboxLauncher.init(browserToolboxLauncherConfig); if (pauseOnStartup) { // Spin the event loop until the debugger connects. const tm = Cc["@mozilla.org/thread-manager;1"].getService(); tm.spinEventLoopUntil("DevToolsStartup.jsm:handleDebuggerFlag", () => { return devtoolsThreadResumed; }); } if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) { cmdLine.preventDefault = true; } }, /** * Handle the --start-debugger-server command line flag. The options are: * --start-debugger-server * The portOrPath parameter is boolean true in this case. Reads and uses the defaults * from devtools.debugger.remote-port and devtools.debugger.remote-websocket prefs. * The default values of these prefs are port 6000, WebSocket disabled. * * --start-debugger-server 6789 * Start the non-WebSocket server on port 6789. * * --start-debugger-server /path/to/filename * Start the server on a Unix domain socket. * * --start-debugger-server ws:6789 * Start the WebSocket server on port 6789. * * --start-debugger-server ws: * Start the WebSocket server on the default port (taken from d.d.remote-port) */ handleDevToolsServerFlag(cmdLine, portOrPath) { if (!this._isRemoteDebuggingEnabled()) { return; } let webSocket = false; const defaultPort = Services.prefs.getIntPref( "devtools.debugger.remote-port" ); if (portOrPath === true) { // Default to pref values if no values given on command line webSocket = Services.prefs.getBoolPref( "devtools.debugger.remote-websocket" ); portOrPath = defaultPort; } else if (portOrPath.startsWith("ws:")) { webSocket = true; const port = portOrPath.slice(3); portOrPath = Number(port) ? port : defaultPort; } const { useDistinctSystemPrincipalLoader, releaseDistinctSystemPrincipalLoader, } = ChromeUtils.importESModule( "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs" ); try { // Create a separate loader instance, so that we can be sure to receive // a separate instance of the DebuggingServer from the rest of the // devtools. This allows us to safely use the tools against even the // actors and DebuggingServer itself, especially since we can mark // serverLoader as invisible to the debugger (unlike the usual loader // settings). const serverLoader = useDistinctSystemPrincipalLoader(this); const { DevToolsServer: devToolsServer } = serverLoader.require( "resource://devtools/server/devtools-server.js" ); const { SocketListener } = serverLoader.require( "resource://devtools/shared/security/socket.js" ); devToolsServer.init(); // Force the server to be kept running when the last connection closes. // So that another client can connect after the previous one is disconnected. devToolsServer.keepAlive = true; devToolsServer.registerAllActors(); devToolsServer.allowChromeProcess = true; const socketOptions = { portOrPath, webSocket }; const listener = new SocketListener(devToolsServer, socketOptions); listener.open(); dump("Started devtools server on " + portOrPath + "\n"); // Prevent leaks on shutdown. const close = () => { Services.obs.removeObserver(close, "quit-application"); dump("Stopped devtools server on " + portOrPath + "\n"); if (listener) { listener.close(); } if (devToolsServer) { devToolsServer.destroy(); } releaseDistinctSystemPrincipalLoader(this); }; Services.obs.addObserver(close, "quit-application"); } catch (e) { dump("Unable to start devtools server on " + portOrPath + ": " + e); } if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) { cmdLine.preventDefault = true; } }, /** * Send entry point telemetry explaining how the devtools were launched. This * functionality also lives inside `devtools/client/framework/browser-menus.js` * because this codepath is only used the first time a toolbox is opened for a * tab. * * @param {String} reason * One of "KeyShortcut", "SystemMenu", "HamburgerMenu", "ContextMenu", * "CommandLine". * @param {String} key * The key used by a key shortcut. */ sendEntryPointTelemetry(reason, key = "") { if (!reason) { return; } let keys = ""; if (reason === "KeyShortcut") { let { modifiers, shortcut } = key; modifiers = modifiers.replace(",", "+"); if (shortcut.startsWith("VK_")) { shortcut = shortcut.substr(3); } keys = `${modifiers}+${shortcut}`; } const window = Services.wm.getMostRecentWindow("navigator:browser"); this.telemetry.addEventProperty( window, "open", "tools", null, "shortcut", keys ); this.telemetry.addEventProperty( window, "open", "tools", null, "entrypoint", reason ); if (this.recorded) { return; } // Only save the first call for each firefox run as next call // won't necessarely start the tool. For example key shortcuts may // only change the currently selected tool. try { this.telemetry.getHistogramById("DEVTOOLS_ENTRY_POINT").add(reason); } catch (e) { dump("DevTools telemetry entry point failed: " + e + "\n"); } this.recorded = true; }, /** * Hook the debugger tool to the "Debug Script" button of the slow script dialog. */ setSlowScriptDebugHandler() { const debugService = Cc["@mozilla.org/dom/slow-script-debug;1"].getService( Ci.nsISlowScriptDebug ); debugService.activationHandler = window => { const chromeWindow = window.browsingContext.topChromeWindow; let setupFinished = false; this.slowScriptDebugHandler(chromeWindow.gBrowser.selectedTab).then( () => { setupFinished = true; } ); // Don't return from the interrupt handler until the debugger is brought // up; no reason to continue executing the slow script. const utils = window.windowUtils; utils.enterModalState(); Services.tm.spinEventLoopUntil( "devtools-browser.js:debugService.activationHandler", () => { return setupFinished; } ); utils.leaveModalState(); }; debugService.remoteActivationHandler = async (browser, callback) => { try { // Force selecting the freezing tab const chromeWindow = browser.ownerGlobal; const tab = chromeWindow.gBrowser.getTabForBrowser(browser); chromeWindow.gBrowser.selectedTab = tab; await this.slowScriptDebugHandler(tab); } catch (e) { console.error(e); } callback.finishDebuggerStartup(); }; }, /** * Called by setSlowScriptDebugHandler, when a tab freeze because of a slow running script */ async slowScriptDebugHandler(tab) { const require = this.initDevTools("SlowScript"); const { gDevTools } = require("devtools/client/framework/devtools"); const toolbox = await gDevTools.showToolboxForTab(tab, { toolId: "jsdebugger", }); const threadFront = toolbox.threadFront; // Break in place, which means resuming the debuggee thread and pausing // right before the next step happens. switch (threadFront.state) { case "paused": // When the debugger is already paused. threadFront.resumeThenPause(); break; case "attached": // When the debugger is already open. const onPaused = threadFront.once("paused"); threadFront.interrupt(); await onPaused; threadFront.resumeThenPause(); break; case "resuming": // The debugger is newly opened. const onResumed = threadFront.once("resumed"); await threadFront.interrupt(); await onResumed; threadFront.resumeThenPause(); break; default: throw Error( "invalid thread front state in slow script debug handler: " + threadFront.state ); } }, // Used by tests and the toolbox to register the same key shortcuts in toolboxes loaded // in a window window. get KeyShortcuts() { return lazy.KeyShortcuts; }, get wrappedJSObject() { return this; }, get jsdebuggerHelpInfo() { return ` --jsdebugger [] Open the Browser Toolbox. Defaults to the local build but can be overridden by a firefox path. --wait-for-jsdebugger Spin event loop until JS debugger connects. Enables debugging (some) application startup code paths. Only has an effect when \`--jsdebugger\` is also supplied. --start-debugger-server [ws:][ | ] Start the devtools server on a TCP port or Unix domain socket path. Defaults to TCP port 6000. Use WebSocket protocol if ws: prefix is specified. `; }, get helpInfo() { return ` --jsconsole Open the Browser Console. --devtools Open DevTools on initial load. ${this.jsdebuggerHelpInfo}`; }, classID: Components.ID("{9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}"), QueryInterface: ChromeUtils.generateQI(["nsICommandLineHandler"]), }; /** * Singleton object that represents the JSON View in-content tool. * It has the same lifetime as the browser. */ const JsonView = { initialized: false, initialize() { // Prevent loading the frame script multiple times if we call this more than once. if (this.initialized) { return; } this.initialized = true; // Register for messages coming from the child process. // This is never removed as there is no particular need to unregister // it during shutdown. Services.mm.addMessageListener("devtools:jsonview:save", this.onSave); }, // Message handlers for events from child processes /** * Save JSON to a file needs to be implemented here * in the parent process. */ onSave(message) { const browser = message.target; const chrome = browser.ownerGlobal; if (message.data === null) { // Save original contents chrome.saveBrowser(browser); } else { if ( !message.data.startsWith("blob:null") || !browser.contentPrincipal.isNullPrincipal ) { console.error("Got invalid request to save JSON data"); return; } // The following code emulates saveBrowser, but: // - Uses the given blob URL containing the custom contents to save. // - Obtains the file name from the URL of the document, not the blob. // - avoids passing the document and explicitly passes system principal. // We have a blob created by a null principal to save, and the null // principal is from the child. Null principals don't survive crossing // over IPC, so there's no other principal that'll work. const persistable = browser.frameLoader; persistable.startPersistence(null, { onDocumentReady(doc) { const uri = chrome.makeURI(doc.documentURI, doc.characterSet); const filename = chrome.getDefaultFileName(undefined, uri, doc, null); chrome.internalSave( message.data, null /* originalURL */, null, filename, null, doc.contentType, false /* bypass cache */, null /* filepicker title key */, null /* file chosen */, null /* referrer */, doc.cookieJarSettings, null /* initiating document */, false /* don't skip prompt for a location */, null /* cache key */, lazy.PrivateBrowsingUtils.isBrowserPrivate( browser ) /* private browsing ? */, Services.scriptSecurityManager.getSystemPrincipal() ); }, onError(status) { throw new Error("JSON Viewer's onSave failed in startPersistence"); }, }); } }, };