summaryrefslogtreecommitdiffstats
path: root/devtools/client/fronts/descriptors/tab.js
blob: 097aed76741bc328ca60e4d4c37fcf521e7adeda (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

const {
  tabDescriptorSpec,
} = require("resource://devtools/shared/specs/descriptors/tab.js");
const DESCRIPTOR_TYPES = require("resource://devtools/client/fronts/descriptors/descriptor-types.js");

loader.lazyRequireGetter(
  this,
  "gDevTools",
  "resource://devtools/client/framework/devtools.js",
  true
);
loader.lazyRequireGetter(
  this,
  "WindowGlobalTargetFront",
  "resource://devtools/client/fronts/targets/window-global.js",
  true
);
const {
  FrontClassWithSpec,
  registerFront,
} = require("resource://devtools/shared/protocol.js");
const {
  DescriptorMixin,
} = require("resource://devtools/client/fronts/descriptors/descriptor-mixin.js");

const POPUP_DEBUG_PREF = "devtools.popups.debug";

/**
 * DescriptorFront for tab targets.
 *
 * @fires remoteness-change
 *        Fired only for target switching, when the debugged tab is a local tab.
 *        TODO: This event could move to the server in order to support
 *        remoteness change for remote debugging.
 */
class TabDescriptorFront extends DescriptorMixin(
  FrontClassWithSpec(tabDescriptorSpec)
) {
  constructor(client, targetFront, parentFront) {
    super(client, targetFront, parentFront);

    // The tab descriptor can be configured to create either local tab targets
    // (eg, regular tab toolbox) or browsing context targets (eg tab remote
    // debugging).
    this._localTab = null;

    // Flag to prevent the server from trying to spawn targets by the watcher actor.
    this._disableTargetSwitching = false;

    this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
    this._handleTabEvent = this._handleTabEvent.bind(this);

    // When the target is created from the server side,
    // it is not created via TabDescriptor.getTarget.
    // Instead, it is retrieved by the TargetCommand which
    // will call TabDescriptor.setTarget from TargetCommand.onTargetAvailable
    if (this.isServerTargetSwitchingEnabled()) {
      this._targetFrontPromise = new Promise(
        r => (this._resolveTargetFrontPromise = r)
      );
    }
  }

  descriptorType = DESCRIPTOR_TYPES.TAB;

  form(json) {
    this.actorID = json.actor;
    this._form = json;
    this.traits = json.traits || {};
  }

  /**
   * Destroy the front.
   *
   * @param Boolean If true, it means that we destroy the front when receiving the descriptor-destroyed
   *                event from the server.
   */
  destroy({ isServerDestroyEvent = false } = {}) {
    if (this.isDestroyed()) {
      return;
    }

    // The descriptor may be destroyed first by the frontend.
    // When closing the tab, the toolbox document is almost immediately removed from the DOM.
    // The `unload` event fires and toolbox destroys itself, as well as its related client.
    //
    // In such case, we emit the descriptor-destroyed event
    if (!isServerDestroyEvent) {
      this.emit("descriptor-destroyed");
    }
    if (this.isLocalTab) {
      this._teardownLocalTabListeners();
    }
    super.destroy();
  }

  getWatcher() {
    const isPopupDebuggingEnabled = Services.prefs.getBoolPref(
      POPUP_DEBUG_PREF,
      false
    );
    return super.getWatcher({
      isServerTargetSwitchingEnabled: this.isServerTargetSwitchingEnabled(),
      isPopupDebuggingEnabled,
    });
  }

  setLocalTab(localTab) {
    this._localTab = localTab;
    this._setupLocalTabListeners();
  }

  get isTabDescriptor() {
    return true;
  }

  get isLocalTab() {
    return !!this._localTab;
  }

  get localTab() {
    return this._localTab;
  }

  _setupLocalTabListeners() {
    this.localTab.addEventListener("TabClose", this._handleTabEvent);
    this.localTab.addEventListener("TabRemotenessChange", this._handleTabEvent);
  }

  _teardownLocalTabListeners() {
    this.localTab.removeEventListener("TabClose", this._handleTabEvent);
    this.localTab.removeEventListener(
      "TabRemotenessChange",
      this._handleTabEvent
    );
  }

  isServerTargetSwitchingEnabled() {
    return !this._disableTargetSwitching;
  }

  /**
   * Called by CommandsFactory, when the WebExtension codebase instantiates
   * a commands. We have to flag the TabDescriptor for them as they don't support
   * target switching and gets severely broken when enabling server target which
   * introduce target switching for all navigations and reloads
   */
  setIsForWebExtension() {
    this.disableTargetSwitching();
  }

  /**
   * Method used by the WebExtension which still need to disable server side targets,
   * and also a few xpcshell tests which are using legacy API and don't support watcher actor.
   */
  disableTargetSwitching() {
    this._disableTargetSwitching = true;
    // Delete these two attributes which have to be set early from the constructor,
    // but we don't know yet if target switch should be disabled.
    delete this._targetFrontPromise;
    delete this._resolveTargetFrontPromise;
  }

  get isZombieTab() {
    return this._form.isZombieTab;
  }

  get browserId() {
    return this._form.browserId;
  }

  get selected() {
    return this._form.selected;
  }

  get title() {
    return this._form.title;
  }

  get url() {
    return this._form.url;
  }

  get favicon() {
    // Note: the favicon is not part of the default form() payload, it will be
    // added in `retrieveFavicon`.
    return this._form.favicon;
  }

  _createTabTarget(form) {
    const front = new WindowGlobalTargetFront(this._client, null, this);

    // As these fronts aren't instantiated by protocol.js, we have to set their actor ID
    // manually like that:
    front.actorID = form.actor;
    front.form(form);
    this.manage(front);
    return front;
  }

  _onTargetDestroyed() {
    // Clear the cached targetFront when the target is destroyed.
    // Note that we are also checking that _targetFront has a valid actorID
    // in getTarget, this acts as an additional security to avoid races.
    this._targetFront = null;
  }

  /**
   * Safely retrieves the favicon via getFavicon() and populates this._form.favicon.
   *
   * We could let callers explicitly retrieve the favicon instead of inserting it in the
   * form dynamically.
   */
  async retrieveFavicon() {
    try {
      this._form.favicon = await this.getFavicon();
    } catch (e) {
      // We might request the data for a tab which is going to be destroyed.
      // In this case the TargetFront will be destroyed. Otherwise log an error.
      if (!this.isDestroyed()) {
        console.error("Failed to retrieve the favicon for " + this.url, e);
      }
    }
  }

  /**
   * Top-level targets created on the server will not be created and managed
   * by a descriptor front. Instead they are created by the Watcher actor.
   * On the client side we manually re-establish a link between the descriptor
   * and the new top-level target.
   */
  setTarget(targetFront) {
    // Completely ignore the previous target.
    // We might nullify the _targetFront unexpectely due to previous target
    // being destroyed after the new is created
    if (this._targetFront) {
      this._targetFront.off("target-destroyed", this._onTargetDestroyed);
    }
    this._targetFront = targetFront;

    targetFront.on("target-destroyed", this._onTargetDestroyed);

    if (this.isServerTargetSwitchingEnabled()) {
      this._resolveTargetFrontPromise(targetFront);

      // Set a new promise in order to:
      // 1) Avoid leaking the targetFront we just resolved into the previous promise.
      // 2) Never return an empty target from `getTarget`
      //
      // About the second point:
      // There is a race condition where we call `onTargetDestroyed` (which clears `this.targetFront`)
      // a bit before calling `setTarget`. So that `this.targetFront` could be null,
      // while we now a new target will eventually come when calling `setTarget`.
      // Setting a new promise will help wait for the next target while `_targetFront` is null.
      // Note that `getTarget` first look into `_targetFront` before checking for `_targetFrontPromise`.
      this._targetFrontPromise = new Promise(
        r => (this._resolveTargetFrontPromise = r)
      );
    }
  }
  getCachedTarget() {
    return this._targetFront;
  }
  async getTarget() {
    if (this._targetFront && !this._targetFront.isDestroyed()) {
      return this._targetFront;
    }

    if (this._targetFrontPromise) {
      return this._targetFrontPromise;
    }

    this._targetFrontPromise = (async () => {
      let newTargetFront = null;
      try {
        const targetForm = await super.getTarget();
        newTargetFront = this._createTabTarget(targetForm);
        this.setTarget(newTargetFront);
      } catch (e) {
        console.log(
          `Request to connect to TabDescriptor "${this.id}" failed: ${e}`
        );
      }

      this._targetFrontPromise = null;
      return newTargetFront;
    })();
    return this._targetFrontPromise;
  }

  /**
   * Handle tabs events.
   */
  async _handleTabEvent(event) {
    switch (event.type) {
      case "TabClose":
        // Always destroy the toolbox opened for this local tab descriptor.
        // When the toolbox is in a Window Host, it won't be removed from the
        // DOM when the tab is closed.
        const toolbox = gDevTools.getToolboxForDescriptorFront(this);
        if (toolbox) {
          // Toolbox.destroy will call target.destroy eventually.
          await toolbox.destroy();
        }
        break;
      case "TabRemotenessChange":
        this._onRemotenessChange();
        break;
    }
  }

  /**
   * Automatically respawn the toolbox when the tab changes between being
   * loaded within the parent process and loaded from a content process.
   * Process change can go in both ways.
   */
  async _onRemotenessChange() {
    // In a near future, this client side code should be replaced by actor code,
    // notifying about new tab targets.
    this.emit("remoteness-change", this._targetFront);
  }
}

exports.TabDescriptorFront = TabDescriptorFront;
registerFront(TabDescriptorFront);