summaryrefslogtreecommitdiffstats
path: root/devtools/startup/DevToolsShim.sys.mjs
blob: 32270505a47e7249173da1a2d8669301a39fcd7a (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
331
332
/* 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/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};
XPCOMUtils.defineLazyGetter(lazy, "DevToolsStartup", () => {
  return Cc["@mozilla.org/devtools/startup-clh;1"].getService(
    Ci.nsICommandLineHandler
  ).wrappedJSObject;
});

// 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;
});

const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled";

function removeItem(array, callback) {
  const index = array.findIndex(callback);
  if (index >= 0) {
    array.splice(index, 1);
  }
}

/**
 * DevToolsShim is a singleton that provides a set of helpers to interact with DevTools,
 * that work whether Devtools are enabled or not.
 *
 * It can be used to start listening to devtools events before DevTools are ready. As soon
 * as DevTools are ready, the DevToolsShim will forward all the requests received until
 * then to the real DevTools instance.
 */
export const DevToolsShim = {
  _gDevTools: null,
  listeners: [],

  get telemetry() {
    if (!this._telemetry) {
      this._telemetry = new lazy.Telemetry();
      this._telemetry.setEventRecordingEnabled(true);
    }
    return this._telemetry;
  },

  /**
   * Returns true if DevTools are enabled. This now only depends on the policy.
   * TODO: Merge isEnabled and isDisabledByPolicy.
   */
  isEnabled() {
    return !this.isDisabledByPolicy();
  },

  /**
   * Returns true if the devtools are completely disabled and can not be enabled. All
   * entry points should return without throwing, initDevTools should never be called.
   */
  isDisabledByPolicy() {
    return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false);
  },

  /**
   * Check if DevTools have already been initialized.
   *
   * @return {Boolean} true if DevTools are initialized.
   */
  isInitialized() {
    return !!this._gDevTools;
  },

  /**
   * Returns the array of the existing toolboxes. This method is part of the compatibility
   * layer for webextensions.
   *
   * @return {Array<Toolbox>}
   *   An array of toolboxes.
   */
  getToolboxes() {
    if (this.isInitialized()) {
      return this._gDevTools.getToolboxes();
    }

    return [];
  },

  /**
   * Register an instance of gDevTools. Should be called by DevTools during startup.
   *
   * @param {DevTools} a devtools instance (from client/framework/devtools)
   */
  register(gDevTools) {
    this._gDevTools = gDevTools;
    this._onDevToolsRegistered();
    this._gDevTools.emit("devtools-registered");
  },

  /**
   * Unregister the current instance of gDevTools. Should be called by DevTools during
   * shutdown.
   */
  unregister() {
    if (this.isInitialized()) {
      this._gDevTools.emit("devtools-unregistered");
      this._gDevTools = null;
    }
  },

  /**
   * The following methods can be called before DevTools are initialized:
   * - on
   * - off
   *
   * If DevTools are not initialized when calling the method, DevToolsShim will call the
   * appropriate method as soon as a gDevTools instance is registered.
   */

  /**
   * This method is used by browser/components/extensions/ext-devtools.js for the events:
   * - toolbox-ready
   * - toolbox-destroyed
   */
  on(event, listener) {
    if (this.isInitialized()) {
      this._gDevTools.on(event, listener);
    } else {
      this.listeners.push([event, listener]);
    }
  },

  /**
   * This method is currently only used by devtools code, but is kept here for consistency
   * with on().
   */
  off(event, listener) {
    if (this.isInitialized()) {
      this._gDevTools.off(event, listener);
    } else {
      removeItem(this.listeners, ([e, l]) => e === event && l === listener);
    }
  },

  /**
   * Called from SessionStore.sys.mjs in mozilla-central when saving the current state.
   *
   * @param {Object} state
   *                 A SessionStore state object that gets modified by reference
   */
  saveDevToolsSession(state) {
    if (!this.isInitialized()) {
      return;
    }

    this._gDevTools.saveDevToolsSession(state);
  },

  /**
   * Called from SessionStore.sys.mjs in mozilla-central when restoring a previous session.
   * Will always be called, even if the session does not contain DevTools related items.
   */
  restoreDevToolsSession(session) {
    if (!this.isEnabled()) {
      return;
    }

    const { browserConsole, browserToolbox } = session;
    const hasDevToolsData = browserConsole || browserToolbox;
    if (!hasDevToolsData) {
      // Do not initialize DevTools unless there is DevTools specific data in the session.
      return;
    }

    this.initDevTools("SessionRestore");
    this._gDevTools.restoreDevToolsSession(session);
  },

  isDevToolsUser() {
    return lazy.DevToolsStartup.isDevToolsUser();
  },

  /**
   * Called from nsContextMenu.js in mozilla-central when using the Inspect Accessibility
   * context menu item.
   *
   * @param {XULTab} tab
   *        The browser tab on which inspect accessibility was used.
   * @param {ElementIdentifier} domReference
   *        Identifier generated by ContentDOMReference. It is a unique pair of
   *        BrowsingContext ID and a numeric ID.
   * @return {Promise} a promise that resolves when the accessible node is selected in the
   *         accessibility inspector or that resolves immediately if DevTools are not
   *         enabled.
   */
  inspectA11Y(tab, domReference) {
    if (!this.isEnabled()) {
      return Promise.resolve();
    }

    // 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 `DevToolsStartup.initDevTools` into account.
    const startTime = Cu.now();

    this.initDevTools("ContextMenu");

    return this._gDevTools.inspectA11Y(tab, domReference, startTime);
  },

  /**
   * Called from nsContextMenu.js in mozilla-central when using the Inspect Element
   * context menu item.
   *
   * @param {XULTab} tab
   *        The browser tab on which inspect node was used.
   * @param {ElementIdentifier} domReference
   *        Identifier generated by ContentDOMReference. It is a unique pair of
   *        BrowsingContext ID and a numeric ID.
   * @return {Promise} a promise that resolves when the node is selected in the inspector
   *         markup view or that resolves immediately if DevTools are not enabled.
   */
  inspectNode(tab, domReference) {
    if (!this.isEnabled()) {
      return Promise.resolve();
    }

    // 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 `DevToolsStartup.initDevTools` into account.
    const startTime = Cu.now();

    this.initDevTools("ContextMenu");

    return this._gDevTools.inspectNode(tab, domReference, startTime);
  },

  _onDevToolsRegistered() {
    // Register all pending event listeners on the real gDevTools object.
    for (const [event, listener] of this.listeners) {
      this._gDevTools.on(event, listener);
    }

    this.listeners = [];
  },

  /**
   * Initialize DevTools via DevToolsStartup if needed. This method throws if DevTools are
   * not enabled.
   *
   * @param {String} reason
   *        optional, if provided should be a valid entry point for DEVTOOLS_ENTRY_POINT
   *        in toolkit/components/telemetry/Histograms.json
   */
  initDevTools(reason) {
    if (!this.isEnabled()) {
      throw new Error("DevTools are not enabled and can not be initialized.");
    }

    if (reason) {
      const window = Services.wm.getMostRecentWindow("navigator:browser");

      this.telemetry.addEventProperty(
        window,
        "open",
        "tools",
        null,
        "shortcut",
        ""
      );
      this.telemetry.addEventProperty(
        window,
        "open",
        "tools",
        null,
        "entrypoint",
        reason
      );
    }

    if (!this.isInitialized()) {
      lazy.DevToolsStartup.initDevTools(reason);
    }
  },
};

/**
 * Compatibility layer for webextensions.
 *
 * Those methods are called only after a DevTools webextension was loaded in DevTools,
 * therefore DevTools should always be available when they are called.
 */
const webExtensionsMethods = [
  "createCommandsForTabForWebExtension",
  "getTheme",
  "openBrowserConsole",
];

/**
 * Compatibility layer for other third parties.
 */
const otherToolMethods = [
  // gDevTools.showToolboxForTab is used by wptrunner to start devtools
  // https://github.com/web-platform-tests/wpt
  // And also, Quick Actions on URL bar.
  "showToolboxForTab",
  // Used for Quick Actions on URL bar.
  "hasToolboxForTab",
  // Used for Quick Actions test.
  "getToolboxForTab",
];

for (const method of [...webExtensionsMethods, ...otherToolMethods]) {
  DevToolsShim[method] = function () {
    if (!this.isEnabled()) {
      throw new Error(
        "Could not call a DevToolsShim webextension method ('" +
          method +
          "'): DevTools are not initialized."
      );
    }

    this.initDevTools();
    return this._gDevTools[method].apply(this._gDevTools, arguments);
  };
}