tor-browser

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

Launcher.sys.mjs (16160B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 // Keep this synchronized with the value of the same name in
      8 // toolkit/xre/nsAppRunner.cpp.
      9 const BROWSER_TOOLBOX_WINDOW_URL =
     10  "chrome://devtools/content/framework/browser-toolbox/window.html";
     11 const CHROME_DEBUGGER_PROFILE_NAME = "chrome_debugger_profile";
     12 
     13 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     14 import { require } from "resource://devtools/shared/loader/Loader.sys.mjs";
     15 import { Subprocess } from "resource://gre/modules/Subprocess.sys.mjs";
     16 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     17 
     18 const {
     19  useDistinctSystemPrincipalLoader,
     20  releaseDistinctSystemPrincipalLoader,
     21 } = ChromeUtils.importESModule(
     22  "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
     23  { global: "shared" }
     24 );
     25 const lazy = {};
     26 ChromeUtils.defineESModuleGetters(lazy, {
     27  BackgroundTasksUtils: "resource://gre/modules/BackgroundTasksUtils.sys.mjs",
     28 });
     29 
     30 XPCOMUtils.defineLazyServiceGetters(lazy, {
     31  XreDirProvider: [
     32    "@mozilla.org/xre/directory-provider;1",
     33    Ci.nsIXREDirProvider,
     34  ],
     35 });
     36 
     37 const Telemetry = require("resource://devtools/client/shared/telemetry.js");
     38 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     39 
     40 const processes = new Set();
     41 
     42 /**
     43 * @typedef {object} BrowserToolboxLauncherArgs
     44 * @property {function} onRun - A function called when the process starts running.
     45 * @property {boolean} overwritePreferences - Set to force overwriting the toolbox
     46 *                     profile's preferences with the current set of preferences.
     47 * @property {boolean} forceMultiprocess - Set to force the Browser Toolbox to be in
     48 *                     multiprocess mode.
     49 */
     50 
     51 export class BrowserToolboxLauncher extends EventEmitter {
     52  /**
     53   * Initializes and starts a chrome toolbox process if the appropriated prefs are enabled
     54   *
     55   * @param {BrowserToolboxLauncherArgs} args
     56   * @return {BrowserToolboxLauncher|null} The created instance, or null if the required prefs
     57   *         are not set.
     58   */
     59  static init(args) {
     60    if (
     61      !Services.prefs.getBoolPref("devtools.chrome.enabled") ||
     62      !Services.prefs.getBoolPref("devtools.debugger.remote-enabled")
     63    ) {
     64      console.error("Could not start Browser Toolbox, you need to enable it.");
     65      return null;
     66    }
     67    return new BrowserToolboxLauncher(args);
     68  }
     69 
     70  /**
     71   * Figure out if there are any open Browser Toolboxes that'll need to be restored.
     72   *
     73   * @return {boolean}
     74   */
     75  static getBrowserToolboxSessionState() {
     76    return processes.size !== 0;
     77  }
     78 
     79  #closed;
     80  #devToolsServer;
     81  #dbgProfilePath;
     82  #dbgProcess;
     83  #listener;
     84  #loader;
     85  #port;
     86  #telemetry = new Telemetry();
     87 
     88  /**
     89   * Constructor for creating a process that will hold a chrome toolbox.
     90   *
     91   * @param {...BrowserToolboxLauncherArgs} args
     92   */
     93  constructor({ forceMultiprocess, onRun, overwritePreferences } = {}) {
     94    super();
     95 
     96    if (onRun) {
     97      this.once("run", onRun);
     98    }
     99 
    100    this.close = this.close.bind(this);
    101    Services.obs.addObserver(this.close, "quit-application");
    102    this.#initServer();
    103    this.#initProfile(overwritePreferences);
    104    this.#create({ forceMultiprocess });
    105 
    106    processes.add(this);
    107  }
    108 
    109  /**
    110   * Initializes the devtools server.
    111   */
    112  #initServer() {
    113    if (this.#devToolsServer) {
    114      dumpn("The chrome toolbox server is already running.");
    115      return;
    116    }
    117 
    118    dumpn("Initializing the chrome toolbox server.");
    119 
    120    // Create a separate loader instance, so that we can be sure to receive a
    121    // separate instance of the DebuggingServer from the rest of the devtools.
    122    // This allows us to safely use the tools against even the actors and
    123    // DebuggingServer itself, especially since we can mark this loader as
    124    // invisible to the debugger (unlike the usual loader settings).
    125    this.#loader = useDistinctSystemPrincipalLoader(this);
    126    const { DevToolsServer } = this.#loader.require(
    127      "resource://devtools/server/devtools-server.js"
    128    );
    129    const { SocketListener } = this.#loader.require(
    130      "resource://devtools/shared/security/socket.js"
    131    );
    132    this.#devToolsServer = DevToolsServer;
    133    dumpn("Created a separate loader instance for the DevToolsServer.");
    134 
    135    this.#devToolsServer.init();
    136    // We mainly need a root actor and target actors for opening a toolbox, even
    137    // against chrome/content. But the "no auto hide" button uses the
    138    // preference actor, so also register the browser actors.
    139    this.#devToolsServer.registerAllActors();
    140    this.#devToolsServer.allowChromeProcess = true;
    141    dumpn("initialized and added the browser actors for the DevToolsServer.");
    142 
    143    const bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
    144      Ci.nsIBackgroundTasks
    145    );
    146    if (bts?.isBackgroundTaskMode) {
    147      // A special root actor, just for background tasks invoked with
    148      // `--backgroundtask TASK --jsdebugger`.
    149      const { createRootActor } = this.#loader.require(
    150        "resource://gre/modules/backgroundtasks/dbg-actors.js"
    151      );
    152      this.#devToolsServer.setRootActor(createRootActor);
    153    }
    154 
    155    const chromeDebuggingWebSocket = Services.prefs.getBoolPref(
    156      "devtools.debugger.chrome-debugging-websocket"
    157    );
    158    const socketOptions = {
    159      fromBrowserToolbox: true,
    160      portOrPath: -1,
    161      webSocket: chromeDebuggingWebSocket,
    162    };
    163    const listener = new SocketListener(this.#devToolsServer, socketOptions);
    164    listener.open();
    165    this.#listener = listener;
    166    this.#port = listener.port;
    167 
    168    if (!this.#port) {
    169      throw new Error("No devtools server port");
    170    }
    171 
    172    dumpn("Finished initializing the chrome toolbox server.");
    173    dump(
    174      `DevTools Server for Browser Toolbox listening on port: ${this.#port}\n`
    175    );
    176  }
    177 
    178  /**
    179   * Initializes a profile for the remote debugger process.
    180   */
    181  #initProfile(overwritePreferences) {
    182    dumpn("Initializing the chrome toolbox user profile.");
    183 
    184    const bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
    185      Ci.nsIBackgroundTasks
    186    );
    187 
    188    let debuggingProfileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
    189    if (bts?.isBackgroundTaskMode) {
    190      // Background tasks run with a temporary ephemeral profile.  We move the
    191      // browser toolbox profile out of that ephemeral profile so that it has
    192      // alonger life then the background task profile.  This preserves
    193      // breakpoints, etc, across repeated debugging invocations.  This
    194      // directory is close to the background task temporary profile name(s),
    195      // but doesn't match the prefix that will get purged by the stale
    196      // ephemeral profile cleanup mechanism.
    197      //
    198      // For example, the invocation
    199      // `firefox --backgroundtask success --jsdebugger --wait-for-jsdebugger`
    200      // might run with ephemeral profile
    201      // `/tmp/MozillaBackgroundTask-<HASH>-success`
    202      // and sibling directory browser toolbox profile
    203      // `/tmp/MozillaBackgroundTask-<HASH>-chrome_debugger_profile-success`
    204      //
    205      // See `BackgroundTasks::Shutdown` for ephemeral profile cleanup details.
    206      debuggingProfileDir = debuggingProfileDir.parent;
    207      debuggingProfileDir.append(
    208        `${Services.appinfo.vendor}BackgroundTask-` +
    209          `${lazy.XreDirProvider.getInstallHash()}-${CHROME_DEBUGGER_PROFILE_NAME}-${bts.backgroundTaskName()}`
    210      );
    211    } else {
    212      debuggingProfileDir.append(CHROME_DEBUGGER_PROFILE_NAME);
    213    }
    214    try {
    215      debuggingProfileDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
    216    } catch (ex) {
    217      if (ex.result === Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
    218        if (!overwritePreferences) {
    219          this.#dbgProfilePath = debuggingProfileDir.path;
    220          return;
    221        }
    222        // Fall through and copy the current set of prefs to the profile.
    223      } else {
    224        dumpn("Error trying to create a profile directory, failing.");
    225        dumpn("Error: " + (ex.message || ex));
    226        return;
    227      }
    228    }
    229 
    230    this.#dbgProfilePath = debuggingProfileDir.path;
    231 
    232    // We would like to copy prefs into this new profile...
    233    const prefsFile = debuggingProfileDir.clone();
    234    prefsFile.append("prefs.js");
    235 
    236    if (bts?.isBackgroundTaskMode) {
    237      // Background tasks run under a temporary profile.  In order to set
    238      // preferences for the launched browser toolbox, take the preferences from
    239      // the default profile.  This is the standard pattern for controlling
    240      // background task settings.  Without this, there'd be no way to increase
    241      // logging in the browser toolbox process, etc.
    242      const defaultProfile = lazy.BackgroundTasksUtils.getDefaultProfile();
    243      if (!defaultProfile) {
    244        throw new Error(
    245          "Cannot start Browser Toolbox from background task with no default profile"
    246        );
    247      }
    248 
    249      const defaultPrefsFile = defaultProfile.rootDir.clone();
    250      defaultPrefsFile.append("prefs.js");
    251      defaultPrefsFile.copyTo(prefsFile.parent, prefsFile.leafName);
    252 
    253      dumpn(
    254        `Copied browser toolbox prefs at '${prefsFile.path}'` +
    255          ` from default profiles prefs at '${defaultPrefsFile.path}'`
    256      );
    257    } else {
    258      // ... but unfortunately, when we run tests, it seems the starting profile
    259      // clears out the prefs file before re-writing it, and in practice the
    260      // file is empty when we get here. So just copying doesn't work in that
    261      // case.
    262      // We could force a sync pref flush and then copy it... but if we're doing
    263      // that, we might as well just flush directly to the new profile, which
    264      // always works:
    265      Services.prefs.savePrefFile(prefsFile);
    266    }
    267 
    268    dumpn(
    269      "Finished creating the chrome toolbox user profile at: " +
    270        this.#dbgProfilePath
    271    );
    272  }
    273 
    274  /**
    275   * Creates and initializes the profile & process for the remote debugger.
    276   *
    277   * @param {object} options
    278   * @param {boolean} options.forceMultiprocess: Set to true to force the Browser Toolbox to be in
    279   *                    multiprocess mode.
    280   */
    281  #create({ forceMultiprocess } = {}) {
    282    dumpn("Initializing chrome debugging process.");
    283 
    284    let command = Services.dirsvc.get("XREExeF", Ci.nsIFile).path;
    285    let profilePath = this.#dbgProfilePath;
    286 
    287    // MOZ_BROWSER_TOOLBOX_BINARY is an absolute file path to a custom firefox binary.
    288    // This is especially useful when debugging debug builds which are really slow
    289    // so that you could pass an optimized build for the browser toolbox.
    290    // This is also useful when debugging a patch that break devtools,
    291    // so that you could use a build that works for the browser toolbox.
    292    const customBinaryPath = Services.env.get("MOZ_BROWSER_TOOLBOX_BINARY");
    293    if (customBinaryPath) {
    294      command = customBinaryPath;
    295      profilePath = PathUtils.join(PathUtils.tempDir, "browserToolboxProfile");
    296    }
    297 
    298    dumpn("Running chrome debugging process.");
    299    const args = [
    300      "-foreground",
    301      "-profile",
    302      profilePath,
    303      "-chrome",
    304      BROWSER_TOOLBOX_WINDOW_URL,
    305    ];
    306 
    307    const environment = {
    308      // Allow recording the startup of the browser toolbox when setting
    309      // MOZ_BROWSER_TOOLBOX_PROFILER_STARTUP=1 when running firefox.
    310      MOZ_PROFILER_STARTUP: Services.env.get(
    311        "MOZ_BROWSER_TOOLBOX_PROFILER_STARTUP"
    312      ),
    313      // And prevent profiling any subsequent toolbox
    314      MOZ_BROWSER_TOOLBOX_PROFILER_STARTUP: "0",
    315 
    316      MOZ_BROWSER_TOOLBOX_FORCE_MULTIPROCESS: forceMultiprocess ? "1" : "0",
    317      // Disable safe mode for the new process in case this was opened via the
    318      // keyboard shortcut.
    319      MOZ_DISABLE_SAFE_MODE_KEY: "1",
    320      MOZ_BROWSER_TOOLBOX_PORT: String(this.#port),
    321      MOZ_HEADLESS: null,
    322      // Never enable Marionette for the new process.
    323      MOZ_MARIONETTE: null,
    324      // Don't inherit debug settings from the process launching us.  This can
    325      // cause errors when log files collide.
    326      MOZ_LOG: null,
    327      MOZ_LOG_FILE: null,
    328      XPCOM_MEM_BLOAT_LOG: null,
    329      XPCOM_MEM_LEAK_LOG: null,
    330      XPCOM_MEM_LOG_CLASSES: null,
    331      XPCOM_MEM_REFCNT_LOG: null,
    332      XRE_PROFILE_PATH: null,
    333      XRE_PROFILE_LOCAL_PATH: null,
    334    };
    335 
    336    // During local development, incremental builds can trigger the main process
    337    // to clear its startup cache with the "flag file" .purgecaches, but this
    338    // file is removed during app startup time, so we aren't able to know if it
    339    // was present in order to also clear the child profile's startup cache as
    340    // well.
    341    //
    342    // As an approximation of "isLocalBuild", check for an unofficial build.
    343    if (!AppConstants.MOZILLA_OFFICIAL) {
    344      args.push("-purgecaches");
    345    }
    346 
    347    dump(`Starting Browser Toolbox ${command} ${args.join(" ")}\n`);
    348    IOUtils.makeDirectory(profilePath, { ignoreExisting: true })
    349      .then(() =>
    350        Subprocess.call({
    351          command,
    352          arguments: args,
    353          environmentAppend: true,
    354          stderr: "stdout",
    355          environment,
    356        })
    357      )
    358      .then(proc => {
    359        this.#dbgProcess = proc;
    360 
    361        this.#telemetry.toolOpened("jsbrowserdebugger", this);
    362 
    363        dumpn("Chrome toolbox is now running...");
    364        this.emit("run", this, proc, this.#dbgProfilePath);
    365 
    366        proc.stdin.close();
    367        const dumpPipe = async pipe => {
    368          let leftover = "";
    369          let data = await pipe.readString();
    370          while (data) {
    371            data = leftover + data;
    372            const lines = data.split(/\r\n|\r|\n/);
    373            if (lines.length) {
    374              for (const line of lines.slice(0, -1)) {
    375                dump(`${proc.pid}> ${line}\n`);
    376              }
    377              leftover = lines[lines.length - 1];
    378            }
    379            data = await pipe.readString();
    380          }
    381          if (leftover) {
    382            dump(`${proc.pid}> ${leftover}\n`);
    383          }
    384        };
    385        dumpPipe(proc.stdout);
    386 
    387        proc.wait().then(() => this.close());
    388 
    389        return proc;
    390      })
    391      .catch(err => {
    392        console.log(
    393          `Error loading Browser Toolbox: ${command} ${args.join(" ")}`,
    394          err
    395        );
    396      });
    397  }
    398 
    399  /**
    400   * Closes the remote debugging server and kills the toolbox process.
    401   */
    402  async close() {
    403    if (this.#closed) {
    404      return;
    405    }
    406 
    407    this.#closed = true;
    408 
    409    dumpn("Cleaning up the chrome debugging process.");
    410 
    411    Services.obs.removeObserver(this.close, "quit-application");
    412 
    413    // We tear down before killing the browser toolbox process to avoid leaking
    414    // socket connection objects.
    415    if (this.#listener) {
    416      this.#listener.close();
    417    }
    418 
    419    // Note that the DevToolsServer can be shared with the DevToolsServer
    420    // spawned by DevToolsFrameChild. We shouldn't destroy it from here.
    421    // Instead we should let it auto-destroy itself once the last connection is closed.
    422    this.#devToolsServer = null;
    423 
    424    this.#dbgProcess.stdout.close();
    425    await this.#dbgProcess.kill();
    426 
    427    this.#telemetry.toolClosed("jsbrowserdebugger", this);
    428 
    429    dumpn("Chrome toolbox is now closed...");
    430    processes.delete(this);
    431 
    432    this.#dbgProcess = null;
    433    if (this.#loader) {
    434      releaseDistinctSystemPrincipalLoader(this);
    435    }
    436    this.#loader = null;
    437    this.#telemetry = null;
    438  }
    439 }
    440 
    441 /**
    442 * Helper method for debugging.
    443 *
    444 * @param string
    445 */
    446 function dumpn(str) {
    447  if (wantLogging) {
    448    dump("DBG-FRONTEND: " + str + "\n");
    449  }
    450 }
    451 
    452 var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
    453 const prefObserver = {
    454  observe: (...args) => {
    455    wantLogging = Services.prefs.getBoolPref(args.pop());
    456  },
    457 };
    458 Services.prefs.addObserver("devtools.debugger.log", prefObserver);
    459 const unloadObserver = function (subject) {
    460  if (subject.wrappedJSObject == require("@loader/unload")) {
    461    Services.prefs.removeObserver("devtools.debugger.log", prefObserver);
    462    Services.obs.removeObserver(unloadObserver, "devtools:loader:destroy");
    463  }
    464 };
    465 Services.obs.addObserver(unloadObserver, "devtools:loader:destroy");