tor-browser

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

target-mixin.js (20744B)


      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 loader.lazyRequireGetter(
      8  this,
      9  "getFront",
     10  "resource://devtools/shared/protocol.js",
     11  true
     12 );
     13 
     14 /**
     15 * A Target represents a debuggable context. It can be a browser tab, a tab on
     16 * a remote device, like a tab on Firefox for Android. But it can also be an add-on,
     17 * as well as firefox parent process, or just one of its content process.
     18 * A Target is related to a given TargetActor, for which we derive this class.
     19 *
     20 * Providing a generalized abstraction of a web-page or web-browser (available
     21 * either locally or remotely) is beyond the scope of this class (and maybe
     22 * also beyond the scope of this universe) However Target does attempt to
     23 * abstract some common events and read-only properties common to many Tools.
     24 *
     25 * Supported read-only properties:
     26 * - name, url
     27 *
     28 * Target extends EventEmitter and provides support for the following events:
     29 * - close: The target window has been closed. All tools attached to this
     30 *          target should close. This event is not currently cancelable.
     31 *
     32 * Optional events only dispatched by WindowGlobalTarget:
     33 * - will-navigate: The target window will navigate to a different URL
     34 * - navigate: The target window has navigated to a different URL
     35 */
     36 function TargetMixin(parentClass) {
     37  class Target extends parentClass {
     38    constructor(client, targetFront, parentFront) {
     39      super(client, targetFront, parentFront);
     40 
     41      // TargetCommand._onTargetAvailable will set this public attribute.
     42      // This is a reference to the related `commands` object and helps all fronts
     43      // easily call any command method. Without this bit of magic, Fronts wouldn't
     44      // be able to interact with any commands while it is frequently useful.
     45      this.commands = null;
     46 
     47      this.destroy = this.destroy.bind(this);
     48 
     49      this.threadFront = null;
     50 
     51      this._client = client;
     52 
     53      // Cache of already created targed-scoped fronts
     54      // [typeName:string => Front instance]
     55      this.fronts = new Map();
     56 
     57      // `resources-available-array` and `resources-updated-array` events can be emitted
     58      // by target actors before the ResourceCommand could add event listeners.
     59      // The target front will cache those events until the ResourceCommand has
     60      // added the listeners.
     61      this._resourceCache = {};
     62 
     63      // In order to avoid destroying the `_resourceCache[event]`, we need to call `super.on()`
     64      // instead of `this.on()`.
     65      const offResourceAvailableArray = super.on(
     66        "resources-available-array",
     67        this._onResourceEventArray.bind(this, "resources-available-array")
     68      );
     69      const offResourceUpdatedArray = super.on(
     70        "resources-updated-array",
     71        this._onResourceEventArray.bind(this, "resources-updated-array")
     72      );
     73 
     74      this._offResourceEvent = new Map([
     75        ["resources-available-array", offResourceAvailableArray],
     76        ["resources-updated-array", offResourceUpdatedArray],
     77      ]);
     78 
     79      // Expose a promise that is resolved once the target front is usable
     80      // i.e. once attachAndInitThread has been called and resolved.
     81      this.initialized = new Promise(resolve => {
     82        this._onInitialized = resolve;
     83      });
     84    }
     85 
     86    on(eventName, listener) {
     87      if (this._offResourceEvent && this._offResourceEvent.has(eventName)) {
     88        // If a callsite sets an event listener for resource-(available|update)-(form|array):
     89 
     90        // we want to remove the listener we set here in the constructor…
     91        const off = this._offResourceEvent.get(eventName);
     92        this._offResourceEvent.delete(eventName);
     93        off();
     94 
     95        // …and call the new listener with the resources that were put in the cache.
     96        if (this._resourceCache[eventName]) {
     97          for (const cache of this._resourceCache[eventName]) {
     98            listener(cache);
     99          }
    100          delete this._resourceCache[eventName];
    101        }
    102      }
    103 
    104      return super.on(eventName, listener);
    105    }
    106 
    107    /**
    108     * Boolean flag to help distinguish Target Fronts from other Fronts.
    109     * As we are using a Mixin, we can't easily distinguish these fronts via instanceof().
    110     */
    111    get isTargetFront() {
    112      return true;
    113    }
    114 
    115    get targetType() {
    116      return this.targetForm.targetType;
    117    }
    118 
    119    get isTopLevel() {
    120      // We can't use `getTrait` here as this might be called from a destroyed target (e.g.
    121      // from an onTargetDestroyed callback that was triggered by a legacy listener), which
    122      // means `this.client` would be null, which would make `getTrait` throw (See Bug 1714974)
    123      if (!this.targetForm.hasOwnProperty("isTopLevelTarget")) {
    124        return !!this._isTopLevel;
    125      }
    126 
    127      return this.targetForm.isTopLevelTarget;
    128    }
    129 
    130    setIsTopLevel(isTopLevel) {
    131      if (!this.getTrait("supportsTopLevelTargetFlag")) {
    132        this._isTopLevel = isTopLevel;
    133      }
    134    }
    135 
    136    /**
    137     * Get the immediate parent target for this target.
    138     *
    139     * @return {TargetMixin} the parent target.
    140     */
    141    async getParentTarget() {
    142      return this.commands.targetCommand.getParentTarget(this);
    143    }
    144 
    145    /**
    146     * Returns a Promise that resolves to a boolean indicating if the provided target is
    147     * an ancestor of this instance.
    148     *
    149     * @param {TargetFront} target: The possible ancestor target.
    150     * @returns Promise<Boolean>
    151     */
    152    async isTargetAnAncestor(target) {
    153      const parentTargetFront = await this.getParentTarget();
    154      if (!parentTargetFront) {
    155        return false;
    156      }
    157 
    158      if (parentTargetFront == target) {
    159        return true;
    160      }
    161 
    162      return parentTargetFront.isTargetAnAncestor(target);
    163    }
    164 
    165    /**
    166     * Get the target for the given Browsing Context ID.
    167     *
    168     * @return {TargetMixin} the requested target.
    169     */
    170    async getWindowGlobalTarget(browsingContextID) {
    171      // Just for sanity as commands attribute is set late from TargetCommand._onTargetAvailable
    172      // but ideally target front should be used before this happens.
    173      if (!this.commands) {
    174        return null;
    175      }
    176      // Tab and Process Descriptors expose a Watcher, which is creating the
    177      // targets and should be used to fetch any.
    178      const { watcherFront } = this.commands;
    179      if (watcherFront) {
    180        // Safety check, in theory all watcher should support frames.
    181        if (watcherFront.traits.frame) {
    182          return watcherFront.getWindowGlobalTarget(browsingContextID);
    183        }
    184        return null;
    185      }
    186 
    187      // For descriptors which don't expose a watcher (e.g. WebExtension)
    188      // we used to call RootActor::getBrowsingContextDescriptor, but it was
    189      // removed in FF77.
    190      // Support for watcher in WebExtension descriptors is Bug 1644341.
    191      throw new Error(
    192        `Unable to call getWindowGlobalTarget for ${this.actorID}`
    193      );
    194    }
    195 
    196    /**
    197     * Returns a boolean indicating whether or not the specific actor
    198     * type exists.
    199     *
    200     * @param {string} actorName
    201     * @return {boolean}
    202     */
    203    hasActor(actorName) {
    204      if (this.targetForm) {
    205        return !!this.targetForm[actorName + "Actor"];
    206      }
    207      return false;
    208    }
    209 
    210    /**
    211     * Returns a trait from the target actor if it exists,
    212     * if not it will fallback to that on the root actor.
    213     *
    214     * @param {string} traitName
    215     * @return {Mixed}
    216     */
    217    getTrait(traitName) {
    218      if (this.isDestroyedOrBeingDestroyed()) {
    219        return null;
    220      }
    221      // If the targeted actor exposes traits and has a defined value for this
    222      // traits, override the root actor traits
    223      if (this.targetForm.traits && traitName in this.targetForm.traits) {
    224        return this.targetForm.traits[traitName];
    225      }
    226 
    227      return this.client.traits[traitName];
    228    }
    229 
    230    // Get a Front for a target-scoped actor.
    231    // i.e. an actor served by RootActor.listTabs or RootActorActor.getTab requests
    232    async getFront(typeName) {
    233      if (this.isDestroyed()) {
    234        throw new Error(
    235          "Target already destroyed, unable to fetch children fronts"
    236        );
    237      }
    238      let front = this.fronts.get(typeName);
    239      if (front) {
    240        // XXX: This is typically the kind of spot where switching to
    241        // `isDestroyed()` is complicated, because `front` is not necessarily a
    242        // Front...
    243        const isFrontInitializing = typeof front.then === "function";
    244        const isFrontAlive = !isFrontInitializing && !front.isDestroyed();
    245        if (isFrontInitializing || isFrontAlive) {
    246          return front;
    247        }
    248      }
    249 
    250      front = getFront(this.client, typeName, this.targetForm, this);
    251      this.fronts.set(typeName, front);
    252      // replace the placeholder with the instance of the front once it has loaded
    253      front = await front;
    254      this.fronts.set(typeName, front);
    255      return front;
    256    }
    257 
    258    getCachedFront(typeName) {
    259      // do not wait for async fronts;
    260      const front = this.fronts.get(typeName);
    261      // ensure that the front is a front, and not async front
    262      if (front?.actorID) {
    263        return front;
    264      }
    265      return null;
    266    }
    267 
    268    get client() {
    269      return this._client;
    270    }
    271 
    272    // Tells us if the related actor implements WindowGlobalTargetActor
    273    // interface and requires to call `attach` request before being used and
    274    // `detach` during cleanup.
    275    get isBrowsingContext() {
    276      return this.typeName === "windowGlobalTarget";
    277    }
    278 
    279    /**
    280     * Return the name to be displayed in the debugger and console context selector.
    281     */
    282    get name() {
    283      // When debugging Web Extensions, all documents have moz-extension://${uuid}/... URL
    284      // When the developer don't set a custom title, fallback on displaying the pathname
    285      // to avoid displaying long URL prefix with the addon internal UUID.
    286      if (this.commands.descriptorFront.isWebExtensionDescriptor) {
    287        if (this._title) {
    288          return this._title;
    289        }
    290        const parsedURL = URL.parse(this._url);
    291        if (parsedURL) {
    292          return parsedURL.pathname;
    293        }
    294        // If document URL can't be parsed, fallback to the raw URL.
    295        return this._url;
    296      }
    297 
    298      if (this.isContentProcess) {
    299        return this.targetForm.name;
    300      }
    301      return this.title;
    302    }
    303 
    304    get title() {
    305      return this._title || this.url;
    306    }
    307 
    308    get url() {
    309      return this._url;
    310    }
    311 
    312    get isWorkerTarget() {
    313      // XXX Remove the check on `workerDescriptor` as part of Bug 1667404.
    314      return (
    315        this.typeName === "workerTarget" || this.typeName === "workerDescriptor"
    316      );
    317    }
    318 
    319    get isContentProcess() {
    320      // browser content toolbox's form will be of the form:
    321      //   server0.conn0.content-process0/contentProcessTarget7
    322      // while xpcshell debugging will be:
    323      //   server1.conn0.contentProcessTarget7
    324      return !!(
    325        this.targetForm &&
    326        this.targetForm.actor &&
    327        this.targetForm.actor.match(
    328          /conn\d+\.(content-process\d+\/)?contentProcessTarget\d+/
    329        )
    330      );
    331    }
    332 
    333    get isParentProcess() {
    334      return !!(
    335        this.targetForm &&
    336        this.targetForm.actor &&
    337        this.targetForm.actor.match(/conn\d+\.parentProcessTarget\d+/)
    338      );
    339    }
    340 
    341    /**
    342     * This method attaches the target and then attaches its related thread, sending it
    343     * the options it needs (e.g. breakpoints, pause on exception setting, …).
    344     * This function can be called multiple times, it will only perform the actual
    345     * initialization process once; on subsequent call the original promise (_onThreadInitialized)
    346     * will be returned.
    347     *
    348     * @param {TargetCommand} targetCommand
    349     * @returns {Promise} A promise that resolves once the thread is attached and resumed.
    350     */
    351    attachAndInitThread(targetCommand) {
    352      if (this._onThreadInitialized) {
    353        return this._onThreadInitialized;
    354      }
    355 
    356      this._onThreadInitialized = this._attachAndInitThread(targetCommand);
    357      // Resolve the `initialized` promise, while ignoring errors
    358      // The empty function passed to catch will avoid spawning a new possibly rejected promise
    359      this._onThreadInitialized.catch(() => {}).then(this._onInitialized);
    360      return this._onThreadInitialized;
    361    }
    362 
    363    /**
    364     * This method attach the target and then attach its related thread, sending it the
    365     * options it needs (e.g. breakpoints, pause on exception setting, …)
    366     *
    367     * @private
    368     * @param {TargetCommand} targetCommand
    369     * @returns {Promise} A promise that resolves once the thread is attached and resumed.
    370     */
    371    async _attachAndInitThread(targetCommand) {
    372      // If the target is destroyed or soon will be, don't go further
    373      if (this.isDestroyedOrBeingDestroyed()) {
    374        return;
    375      }
    376 
    377      // The current class we have is actually the WorkerDescriptorFront,
    378      // which will morph into a target by fetching the underlying target's form.
    379      // Ideally, worker targets would be spawn by the server, and we would no longer
    380      // have the hybrid descriptor/target class which brings lots of complexity and confusion.
    381      // To be removed in bug 1651522.
    382      if (this.morphWorkerDescriptorIntoWorkerTarget) {
    383        await this.morphWorkerDescriptorIntoWorkerTarget();
    384      }
    385 
    386      const isBrowserToolbox =
    387        targetCommand.descriptorFront.isBrowserProcessDescriptor;
    388      const isNonTopLevelFrameTarget =
    389        !this.isTopLevel && this.targetType === targetCommand.TYPES.FRAME;
    390 
    391      if (isBrowserToolbox && isNonTopLevelFrameTarget) {
    392        // In the BrowserToolbox, non-top-level frame targets are already
    393        // debugged via content-process targets.
    394        // Do not attach the thread here, as it was already done by the
    395        // corresponding content-process target.
    396        return;
    397      }
    398 
    399      // Avoid attaching any thread actor in the browser console or in
    400      // webextension commands in order to avoid triggering any type of
    401      // breakpoint.
    402      if (targetCommand.descriptorFront.doNotAttachThreadActor) {
    403        return;
    404      }
    405 
    406      // If the target is destroyed or soon will be, don't go further
    407      if (this.isDestroyedOrBeingDestroyed()) {
    408        return;
    409      }
    410      if (!this.targetForm || !this.targetForm.threadActor) {
    411        throw new Error(
    412          "TargetMixin sub class should set targetForm.threadActor before calling attachAndInitThread"
    413        );
    414      }
    415      this.threadFront = await this.getFront("thread");
    416    }
    417 
    418    isDestroyedOrBeingDestroyed() {
    419      return this.isDestroyed() || this._destroyer;
    420    }
    421 
    422    /**
    423     * Target is not alive anymore.
    424     */
    425    destroy() {
    426      // If several things call destroy then we give them all the same
    427      // destruction promise so we're sure to destroy only once
    428      if (this._destroyer) {
    429        return this._destroyer;
    430      }
    431 
    432      // This pattern allows to immediately return the destroyer promise.
    433      // See Bug 1602727 for more details.
    434      let destroyerResolve;
    435      this._destroyer = new Promise(r => (destroyerResolve = r));
    436      this._destroyTarget().then(destroyerResolve);
    437 
    438      return this._destroyer;
    439    }
    440 
    441    async _destroyTarget() {
    442      // If the target is being attached, try to wait until it's done, to prevent having
    443      // pending connection to the server when the toolbox is destroyed.
    444      if (this._onThreadInitialized) {
    445        try {
    446          await this._onThreadInitialized;
    447        } catch (e) {
    448          // We might still get into cases where attaching fails (e.g. the worker we're
    449          // trying to attach to is already closed). Since the target is being destroyed,
    450          // we don't need to do anything special here.
    451        }
    452      }
    453 
    454      for (let [name, front] of this.fronts) {
    455        try {
    456          // If a Front with an async initialize method is still being instantiated,
    457          // we should wait for completion before trying to destroy it.
    458          if (front instanceof Promise) {
    459            front = await front;
    460          }
    461          front.destroy();
    462        } catch (e) {
    463          console.warn("Error while destroying front:", name, e);
    464        }
    465      }
    466      this.fronts.clear();
    467 
    468      this.threadFront = null;
    469      this._offResourceEvent = null;
    470 
    471      // This event should be emitted before calling super.destroy(), because
    472      // super.destroy() will remove all event listeners attached to this front.
    473      this.emit("target-destroyed");
    474 
    475      // Not all targets supports attach/detach. For example content process doesn't.
    476      // Also ensure that the front is still active before trying to do the request.
    477      if (this.detach && !this.isDestroyed()) {
    478        // The client was handed to us, so we are not responsible for closing
    479        // it. We just need to detach from the tab, if already attached.
    480        // |detach| may fail if the connection is already dead, so proceed with
    481        // cleanup directly after this.
    482        try {
    483          await this.detach();
    484        } catch (e) {
    485          this.logDetachError(e);
    486        }
    487      }
    488 
    489      // Do that very last in order to let a chance to dispatch `detach` requests.
    490      super.destroy();
    491 
    492      this._cleanup();
    493    }
    494 
    495    /**
    496     * Detach can fail under regular circumstances, if the target was already
    497     * destroyed on the server side. All target fronts should handle detach
    498     * error logging in similar ways so this might be used by subclasses
    499     * with custom detach() implementations.
    500     *
    501     * @param {Error} e
    502     *        The real error object.
    503     * @param {string} targetType
    504     *        The type of the target front ("worker", "browsing-context", ...)
    505     */
    506    logDetachError(e, targetType) {
    507      const ignoredError =
    508        e?.message.includes("noSuchActor") ||
    509        e?.message.includes("Connection closed");
    510 
    511      // Silence exceptions for already destroyed actors and fronts:
    512      // - "noSuchActor" errors from the server
    513      // - "Connection closed" errors from the client, when purging requests
    514      if (ignoredError) {
    515        return;
    516      }
    517 
    518      // Properly log any other error.
    519      const message = targetType
    520        ? `Error while detaching the ${targetType} target:`
    521        : "Error while detaching target:";
    522      console.warn(message, e);
    523    }
    524 
    525    /**
    526     * Clean up references to what this target points to.
    527     */
    528    _cleanup() {
    529      this.threadFront = null;
    530      this._client = null;
    531 
    532      this._title = null;
    533      this._url = null;
    534    }
    535 
    536    _onResourceEventArray(eventName, array) {
    537      if (!this._resourceCache[eventName]) {
    538        this._resourceCache[eventName] = [];
    539      }
    540      this._resourceCache[eventName].push(array);
    541    }
    542 
    543    toString() {
    544      const id = this.targetForm ? this.targetForm.actor : null;
    545      return `Target:${id}`;
    546    }
    547 
    548    dumpPools() {
    549      // NOTE: dumpPools is defined in the Thread actor to avoid
    550      // adding it to multiple target specs and actors.
    551      return this.threadFront.dumpPools();
    552    }
    553 
    554    /**
    555     * Log an error of some kind to the tab's console.
    556     *
    557     * @param {string} text
    558     *                 The text to log.
    559     * @param {string} category
    560     *                 The category of the message.  @see nsIScriptError.
    561     * @returns {Promise}
    562     */
    563    logErrorInPage(text, category) {
    564      if (this.getTrait("logInPage")) {
    565        const errorFlag = 0;
    566        return this.logInPage({ text, category, flags: errorFlag });
    567      }
    568      return Promise.resolve();
    569    }
    570 
    571    /**
    572     * Log a warning of some kind to the tab's console.
    573     *
    574     * @param {string} text
    575     *                 The text to log.
    576     * @param {string} category
    577     *                 The category of the message.  @see nsIScriptError.
    578     * @returns {Promise}
    579     */
    580    logWarningInPage(text, category) {
    581      if (this.getTrait("logInPage")) {
    582        const warningFlag = 1;
    583        return this.logInPage({ text, category, flags: warningFlag });
    584      }
    585      return Promise.resolve();
    586    }
    587 
    588    /**
    589     * The tracer actor emits frames which should be collected per target/thread.
    590     * The tracer will emit other resources, refering to the frame indexes in that collected array.
    591     * The indexes and this array in general is specific to a given tracer actor instance
    592     * and so is specific per thread and target.
    593     */
    594    #jsTracerCollectedFrames = [];
    595    getJsTracerCollectedFramesArray() {
    596      return this.#jsTracerCollectedFrames;
    597    }
    598  }
    599  return Target;
    600 }
    601 exports.TargetMixin = TargetMixin;