tor-browser

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

worker.sys.mjs (15835B)


      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 import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs";
      6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      7 
      8 const lazy = {};
      9 XPCOMUtils.defineLazyServiceGetter(
     10  lazy,
     11  "wdm",
     12  "@mozilla.org/dom/workers/workerdebuggermanager;1",
     13  Ci.nsIWorkerDebuggerManager
     14 );
     15 
     16 const { TYPE_DEDICATED, TYPE_SERVICE, TYPE_SHARED } = Ci.nsIWorkerDebugger;
     17 
     18 export class WorkerTargetWatcherClass {
     19  constructor(workerTargetType = "worker") {
     20    this.#workerTargetType = workerTargetType;
     21    this.#workerDebuggerListener = {
     22      onRegister: this.#onWorkerRegister.bind(this),
     23      onUnregister: this.#onWorkerUnregister.bind(this),
     24    };
     25  }
     26 
     27  // {String}
     28  #workerTargetType;
     29  // {nsIWorkerDebuggerListener}
     30  #workerDebuggerListener;
     31 
     32  watch() {
     33    lazy.wdm.addListener(this.#workerDebuggerListener);
     34  }
     35 
     36  unwatch() {
     37    lazy.wdm.removeListener(this.#workerDebuggerListener);
     38  }
     39 
     40  createTargetsForWatcher(watcherDataObject) {
     41    const { sessionData } = watcherDataObject;
     42    for (const dbg of lazy.wdm.getWorkerDebuggerEnumerator()) {
     43      if (!this.shouldHandleWorker(sessionData, dbg, this.#workerTargetType)) {
     44        continue;
     45      }
     46      this.createWorkerTargetActor(watcherDataObject, dbg);
     47    }
     48  }
     49 
     50  async addOrSetSessionDataEntry(watcherDataObject, type, entries, updateType) {
     51    // Collect the SessionData update into `pendingWorkers` in order to notify
     52    // about the updates to workers which are still in process of being hooked by devtools.
     53    for (const concurrentSessionUpdates of watcherDataObject.pendingWorkers[
     54      this.#workerTargetType
     55    ]) {
     56      concurrentSessionUpdates.push({
     57        type,
     58        entries,
     59        updateType,
     60      });
     61    }
     62 
     63    const promises = [];
     64    for (const { dbg, workerThreadServerForwardingPrefix } of watcherDataObject
     65      .workers[this.#workerTargetType]) {
     66      promises.push(
     67        addOrSetSessionDataEntryInWorkerTarget({
     68          dbg,
     69          workerThreadServerForwardingPrefix,
     70          type,
     71          entries,
     72          updateType,
     73        })
     74      );
     75    }
     76    await Promise.all(promises);
     77  }
     78 
     79  removeSessionDataEntry(watcherDataObject, type, entries) {
     80    for (const { dbg, workerThreadServerForwardingPrefix } of watcherDataObject
     81      .workers[this.#workerTargetType]) {
     82      if (!isWorkerDebuggerAlive(dbg)) {
     83        continue;
     84      }
     85 
     86      dbg.postMessage(
     87        JSON.stringify({
     88          type: "remove-session-data-entry",
     89          forwardingPrefix: workerThreadServerForwardingPrefix,
     90          dataEntryType: type,
     91          entries,
     92        })
     93      );
     94    }
     95  }
     96 
     97  /**
     98   * Called whenever a new Worker is instantiated in the current process
     99   *
    100   * @param {WorkerDebugger} dbg
    101   */
    102  #onWorkerRegister(dbg) {
    103    // Create a Target Actor for each watcher currently watching for Workers
    104    for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects(
    105      this.#workerTargetType
    106    )) {
    107      const { sessionData } = watcherDataObject;
    108      if (this.shouldHandleWorker(sessionData, dbg, this.#workerTargetType)) {
    109        this.createWorkerTargetActor(watcherDataObject, dbg);
    110      }
    111    }
    112  }
    113 
    114  /**
    115   * Called whenever a Worker is destroyed in the current process
    116   *
    117   * @param {WorkerDebugger} dbg
    118   */
    119  #onWorkerUnregister(dbg) {
    120    for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects(
    121      this.#workerTargetType
    122    )) {
    123      const { watcherActorID } = watcherDataObject;
    124      const workerList = watcherDataObject.workers[this.#workerTargetType];
    125      // Check if the worker registration was handled for this watcherActorID.
    126      const unregisteredActorIndex = workerList.findIndex(worker => {
    127        try {
    128          // Accessing the WorkerDebugger id might throw (NS_ERROR_UNEXPECTED).
    129          return worker.dbg.id === dbg.id;
    130        } catch (e) {
    131          return false;
    132        }
    133      });
    134      if (unregisteredActorIndex === -1) {
    135        continue;
    136      }
    137 
    138      const { workerTargetForm, transport } =
    139        workerList[unregisteredActorIndex];
    140      // Close the transport made to the worker thread
    141      transport.close();
    142 
    143      try {
    144        watcherDataObject.jsProcessActor.sendAsyncMessage(
    145          "DevToolsProcessChild:targetDestroyed",
    146          {
    147            actors: [
    148              {
    149                watcherActorID,
    150                targetActorForm: workerTargetForm,
    151              },
    152            ],
    153            options: {},
    154          }
    155        );
    156      } catch (e) {
    157        // This often throws as the JSActor is being destroyed when DevTools closes
    158        // and we are trying to notify about the destroyed targets.
    159      }
    160 
    161      workerList.splice(unregisteredActorIndex, 1);
    162    }
    163  }
    164 
    165  /**
    166   * Instantiate a worker target actor related to a given WorkerDebugger object
    167   * and for a given watcher actor.
    168   *
    169   * @param {object} watcherDataObject
    170   * @param {WorkerDebugger} dbg
    171   */
    172  async createWorkerTargetActor(watcherDataObject, dbg) {
    173    // Prevent the debuggee from executing in this worker until the client has
    174    // finished attaching to it. This call will throw if the debugger is already "registered"
    175    // (i.e. if this is called outside of the register listener)
    176    // See https://searchfox.org/mozilla-central/rev/84922363f4014eae684aabc4f1d06380066494c5/dom/workers/nsIWorkerDebugger.idl#55-66
    177    try {
    178      dbg.setDebuggerReady(false);
    179    } catch (e) {
    180      if (!e.message.startsWith("Component returned failure code")) {
    181        throw e;
    182      }
    183    }
    184 
    185    const { watcherActorID } = watcherDataObject;
    186    const { connection, loader } =
    187      ContentProcessWatcherRegistry.getOrCreateConnectionForWatcher(
    188        watcherActorID
    189      );
    190 
    191    // Compute a unique prefix for the bridge made between this content process main thread
    192    // and the worker thread.
    193    const workerThreadServerForwardingPrefix =
    194      connection.allocID("workerTarget");
    195 
    196    const { connectToWorker } = loader.require(
    197      "resource://devtools/server/connectors/worker-connector.js"
    198    );
    199 
    200    // Create the actual worker target actor, in the worker thread.
    201    const { sessionData, sessionContext } = watcherDataObject;
    202    const onConnectToWorker = connectToWorker(
    203      connection,
    204      dbg,
    205      workerThreadServerForwardingPrefix,
    206      {
    207        sessionData,
    208        sessionContext,
    209      }
    210    );
    211 
    212    // Only add data to the connection if we successfully send the
    213    // workerTargetAvailable message.
    214    const workerInfo = {
    215      dbg,
    216      workerThreadServerForwardingPrefix,
    217    };
    218    const workerList = watcherDataObject.workers[this.#workerTargetType];
    219    workerList.push(workerInfo);
    220 
    221    // The onConnectToWorker is async and we may receive new Session Data (e.g breakpoints)
    222    // while we are instantiating the worker targets.
    223    // Let cache the pending session data and flush it after the targets are being instantiated.
    224    const concurrentSessionUpdates = [];
    225    const pendingWorkers =
    226      watcherDataObject.pendingWorkers[this.#workerTargetType];
    227    pendingWorkers.add(concurrentSessionUpdates);
    228 
    229    try {
    230      await onConnectToWorker;
    231    } catch (e) {
    232      // connectToWorker is supposed to call setDebuggerReady(true) to release the worker execution.
    233      // But if anything goes wrong and an exception is thrown, ensure releasing its execution,
    234      // otherwise if devtools is broken, it will freeze the worker indefinitely.
    235      //
    236      // onConnectToWorker can reject if the Worker Debugger is closed; so we only want to
    237      // resume the debugger if it is not closed (otherwise it can cause crashes).
    238      if (!dbg.isClosed) {
    239        dbg.setDebuggerReady(true);
    240      }
    241      // Also unregister the worker
    242      workerList.splice(workerList.indexOf(workerInfo), 1);
    243      pendingWorkers.delete(concurrentSessionUpdates);
    244      return;
    245    }
    246    pendingWorkers.delete(concurrentSessionUpdates);
    247 
    248    const { workerTargetForm, transport } = await onConnectToWorker;
    249    workerInfo.workerTargetForm = workerTargetForm;
    250    workerInfo.transport = transport;
    251 
    252    // Bail out and cleanup the actor by closing the transport,
    253    // if we stopped listening for workers while waiting for onConnectToWorker resolution.
    254    if (!workerList.includes(workerInfo)) {
    255      transport.close();
    256      return;
    257    }
    258 
    259    const { forwardingPrefix } = watcherDataObject;
    260    // Immediately queue a message for the parent process, before applying any SessionData
    261    // as it may start emitting RDP events on the target actor and be lost if the client
    262    // didn't get notified about the target actor first
    263    try {
    264      watcherDataObject.jsProcessActor.sendAsyncMessage(
    265        "DevToolsProcessChild:targetAvailable",
    266        {
    267          watcherActorID,
    268          forwardingPrefix,
    269          targetActorForm: workerTargetForm,
    270        }
    271      );
    272    } catch (e) {
    273      // If there was an error while sending the message, we are not going to use this
    274      // connection to communicate with the worker.
    275      transport.close();
    276      // Also unregister the worker
    277      workerList.splice(workerList.indexOf(workerInfo), 1);
    278      return;
    279    }
    280 
    281    // Dispatch to the worker thread any SessionData updates which may have been notified
    282    // while we were waiting for onConnectToWorker to resolve.
    283    const promises = [];
    284    for (const { type, entries, updateType } of concurrentSessionUpdates) {
    285      promises.push(
    286        addOrSetSessionDataEntryInWorkerTarget({
    287          dbg,
    288          workerThreadServerForwardingPrefix,
    289          type,
    290          entries,
    291          updateType,
    292        })
    293      );
    294    }
    295    await Promise.all(promises);
    296  }
    297 
    298  destroyTargetsForWatcher(watcherDataObject) {
    299    // Notify to all worker threads to destroy their target actor running in them
    300    for (const { transport } of watcherDataObject.workers[
    301      this.#workerTargetType
    302    ]) {
    303      // The transport may not be set if the worker is still being connected to from createWorkerTargetActor.
    304      if (transport) {
    305        // Clean the DevToolsTransport created in the main thread to bridge RDP to the worker thread.
    306        // This will also send a last message to the worker to clean things up in the other thread.
    307        transport.close();
    308      }
    309    }
    310    // Wipe all workers info
    311    watcherDataObject.workers[this.#workerTargetType] = [];
    312  }
    313 
    314  /**
    315   * Indicates whether or not we should handle the worker debugger
    316   *
    317   * @param {object} sessionData
    318   *        The session data for a given watcher, which includes metadata
    319   *        about the debugged context.
    320   * @param {WorkerDebugger} dbg
    321   *        The worker debugger we want to check.
    322   * @param {string} targetType
    323   *        The expected worker target type.
    324   * @returns {boolean}
    325   */
    326  shouldHandleWorker(sessionData, dbg, targetType) {
    327    if (!isWorkerDebuggerAlive(dbg)) {
    328      return false;
    329    }
    330 
    331    if (
    332      (dbg.type === TYPE_DEDICATED && targetType != "worker") ||
    333      (dbg.type === TYPE_SERVICE && targetType != "service_worker") ||
    334      (dbg.type === TYPE_SHARED && targetType != "shared_worker")
    335    ) {
    336      return false;
    337    }
    338 
    339    // subprocess workers are ignored because they take several seconds to
    340    // attach to when opening the browser toolbox. See bug 1594597.
    341    if (
    342      /resource:\/\/gre\/modules\/subprocess\/subprocess_.*\.worker\.js/.test(
    343        dbg.url
    344      )
    345    ) {
    346      return false;
    347    }
    348 
    349    const { type: sessionContextType } = sessionData.sessionContext;
    350    if (sessionContextType == "all") {
    351      return true;
    352    }
    353    if (sessionContextType == "content-process") {
    354      throw new Error(
    355        "Content process session type shouldn't try to spawn workers"
    356      );
    357    }
    358    if (sessionContextType == "worker") {
    359      throw new Error(
    360        "worker session type should spawn only one target via the WorkerDescriptor"
    361      );
    362    }
    363 
    364    if (dbg.type === TYPE_DEDICATED) {
    365      // Assume that all dedicated workers executes in the same process as the debugged document.
    366      const browsingContext = BrowsingContext.getCurrentTopByBrowserId(
    367        sessionData.sessionContext.browserId
    368      );
    369      // If we aren't executing in the same process as the worker and its BrowsingContext,
    370      // it will be undefined.
    371      if (!browsingContext) {
    372        return false;
    373      }
    374      for (const subBrowsingContext of browsingContext.getAllBrowsingContextsInSubtree()) {
    375        if (
    376          subBrowsingContext.currentWindowContext &&
    377          dbg.windowIDs.includes(
    378            subBrowsingContext.currentWindowContext.innerWindowId
    379          )
    380        ) {
    381          return true;
    382        }
    383      }
    384      return false;
    385    }
    386 
    387    if (dbg.type === TYPE_SERVICE) {
    388      // Accessing `nsIPrincipal.host` may easily throw on non-http URLs.
    389      // Ignore all non-HTTP as they most likely don't have any valid host name.
    390      if (!dbg.principal.scheme.startsWith("http")) {
    391        return false;
    392      }
    393 
    394      const workerHost = dbg.principal.hostPort;
    395      return workerHost == sessionData["browser-element-host"][0];
    396    }
    397 
    398    if (dbg.type === TYPE_SHARED) {
    399      // Bug 1607778 - Don't expose shared workers when debugging a tab.
    400      // For now, they are only exposed in the browser toolbox, when Session Context Type is set to "all".
    401      return false;
    402    }
    403 
    404    return false;
    405  }
    406 }
    407 
    408 /**
    409 * Communicate the type and entries to the Worker Target actor, via the WorkerDebugger.
    410 *
    411 * @param {WorkerDebugger} dbg
    412 * @param {string} workerThreadServerForwardingPrefix
    413 * @param {string} type
    414 *        Session data type name
    415 * @param {Array} entries
    416 *        Session data entries to add or set.
    417 * @param {string} updateType
    418 *        Either "add" or "set", to control if we should only add some items,
    419 *        or replace the whole data set with the new entries.
    420 * @returns {Promise} Returns a Promise that resolves once the data entry were handled
    421 *                    by the worker target.
    422 */
    423 function addOrSetSessionDataEntryInWorkerTarget({
    424  dbg,
    425  workerThreadServerForwardingPrefix,
    426  type,
    427  entries,
    428  updateType,
    429 }) {
    430  if (!isWorkerDebuggerAlive(dbg)) {
    431    return Promise.resolve();
    432  }
    433 
    434  return new Promise(resolve => {
    435    // Wait until we're notified by the worker that the resources are watched.
    436    // This is important so we know existing resources were handled.
    437    const listener = {
    438      onMessage: message => {
    439        message = JSON.parse(message);
    440        if (message.type === "session-data-entry-added-or-set") {
    441          dbg.removeListener(listener);
    442          resolve();
    443        }
    444      },
    445      // Resolve if the worker is being destroyed so we don't have a dangling promise.
    446      onClose: () => {
    447        dbg.removeListener(listener);
    448        resolve();
    449      },
    450    };
    451 
    452    dbg.addListener(listener);
    453 
    454    dbg.postMessage(
    455      JSON.stringify({
    456        type: "add-or-set-session-data-entry",
    457        forwardingPrefix: workerThreadServerForwardingPrefix,
    458        dataEntryType: type,
    459        entries,
    460        updateType,
    461      })
    462    );
    463  });
    464 }
    465 
    466 function isWorkerDebuggerAlive(dbg) {
    467  if (dbg.isClosed) {
    468    return false;
    469  }
    470  // Some workers are zombies. `isClosed` is false, but nothing works.
    471  // `postMessage` is a noop, `addListener`'s `onClosed` doesn't work.
    472  return (
    473    dbg.window?.docShell ||
    474    // consider dbg without `window` as being alive, as they aren't related
    475    // to any docShell and probably do not suffer from this issue
    476    !dbg.window
    477  );
    478 }
    479 
    480 export const WorkerTargetWatcher = new WorkerTargetWatcherClass();