summaryrefslogtreecommitdiffstats
path: root/devtools/shared/commands/commands-factory.js
blob: 257f50ce2faf379851c02b372a84adf3de88fb79 (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
/* 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 {
  createCommandsDictionary,
} = require("resource://devtools/shared/commands/index.js");
const { DevToolsLoader } = ChromeUtils.importESModule(
  "resource://devtools/shared/loader/Loader.sys.mjs"
);
loader.lazyRequireGetter(
  this,
  "DevToolsServer",
  "resource://devtools/server/devtools-server.js",
  true
);
// eslint-disable-next-line mozilla/reject-some-requires
loader.lazyRequireGetter(
  this,
  "DevToolsClient",
  "resource://devtools/client/devtools-client.js",
  true
);

/**
 * Functions for creating Commands for all debuggable contexts.
 *
 * All methods of this `CommandsFactory` object receive argument to describe to
 * which particular context we want to debug. And all returns a new instance of `commands` object.
 * Commands are implemented by modules defined in devtools/shared/commands.
 */
exports.CommandsFactory = {
  /**
   * Create commands for a given local tab.
   *
   * @param {Tab} tab: A local Firefox tab, running in this process.
   * @param {Object} options
   * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
   *        a new one will be created.
   * @param {DevToolsClient} options.isWebExtension: An optional boolean to flag commands
   *        that are created for the WebExtension codebase.
   * @returns {Object} Commands
   */
  async forTab(tab, { client, isWebExtension } = {}) {
    if (!client) {
      client = await createLocalClient();
    }

    const descriptor = await client.mainRoot.getTab({ tab, isWebExtension });
    descriptor.doNotAttachThreadActor = isWebExtension;
    const commands = await createCommandsDictionary(descriptor);
    return commands;
  },

  /**
   * Chrome mochitest don't have access to any "tab",
   * so that the only way to attach to a fake tab is call RootFront.getTab
   * without any argument.
   */
  async forCurrentTabInChromeMochitest() {
    const client = await createLocalClient();
    const descriptor = await client.mainRoot.getTab();
    const commands = await createCommandsDictionary(descriptor);
    return commands;
  },

  /**
   * Create commands for the main process.
   *
   * @param {Object} options
   * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
   *        a new one will be created.
   * @returns {Object} Commands
   */
  async forMainProcess({ client } = {}) {
    if (!client) {
      client = await createLocalClient();
    }

    const descriptor = await client.mainRoot.getMainProcess();
    const commands = await createCommandsDictionary(descriptor);
    return commands;
  },

  /**
   * Create commands for a given remote tab.
   *
   * Note that it can also be used for local tab, but isLocalTab attribute
   * on commands.descriptorFront will be false.
   *
   * @param {Number} browserId: Identify which tab we should create commands for.
   * @param {Object} options
   * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
   *        a new one will be created.
   * @returns {Object} Commands
   */
  async forRemoteTab(browserId, { client } = {}) {
    if (!client) {
      client = await createLocalClient();
    }

    const descriptor = await client.mainRoot.getTab({ browserId });
    const commands = await createCommandsDictionary(descriptor);
    return commands;
  },

  /**
   * Create commands for a given main process worker.
   *
   * @param {String} id: WorkerDebugger's id, which is a unique ID computed by the platform code.
   *        These ids are exposed via WorkerDescriptor's id attributes.
   *        WorkerDescriptors can be retrieved via MainFront.listAllWorkers()/listWorkers().
   * @param {Object} options
   * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
   *        a new one will be created.
   * @returns {Object} Commands
   */
  async forWorker(id, { client } = {}) {
    if (!client) {
      client = await createLocalClient();
    }

    const descriptor = await client.mainRoot.getWorker(id);
    const commands = await createCommandsDictionary(descriptor);
    return commands;
  },

  /**
   * Create commands for a Web Extension.
   *
   * @param {String} id The Web Extension ID to debug.
   * @param {Object} options
   * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
   *        a new one will be created.
   * @returns {Object} Commands
   */
  async forAddon(id, { client } = {}) {
    if (!client) {
      client = await createLocalClient();
    }

    const descriptor = await client.mainRoot.getAddon({ id });
    const commands = await createCommandsDictionary(descriptor);
    return commands;
  },

  /**
   * This method will spawn a special `DevToolsClient`
   * which is meant to debug the same Firefox instance
   * and especially be able to debug chrome code.
   * The chrome code typically runs in the system principal.
   * This principal is a singleton which is shared among most Firefox internal codebase
   * (JSM, privileged html documents, JS-XPCOM,...)
   * In order to be able to debug these script we need to connect to a special DevToolsServer
   * that runs in a dedicated and distinct system principal which is different from
   * the one shared with the rest of Firefox frontend codebase.
   */
  async spawnClientToDebugSystemPrincipal() {
    // The Browser console ends up using the debugger in autocomplete.
    // Because the debugger can't be running in the same compartment than its debuggee,
    // we have to load the server in a dedicated Loader, flagged with
    // `freshCompartment`, which will force it to be loaded in another compartment.
    // We aren't using `invisibleToDebugger` in order to allow the Browser toolbox to
    // debug the Browser console. This is fine as they will spawn distinct Loaders and
    // so distinct `DevToolsServer` and actor modules.
    const customLoader = new DevToolsLoader({
      freshCompartment: true,
    });
    const { DevToolsServer: customDevToolsServer } = customLoader.require(
      "resource://devtools/server/devtools-server.js"
    );

    customDevToolsServer.init();

    // We want all the actors (root, browser and target-scoped) to be registered on the
    // DevToolsServer. This is needed so the Browser Console can retrieve:
    // - the console actors, which are target-scoped (See Bug 1416105)
    // - the screenshotActor, which is browser-scoped (for the `:screenshot` command)
    customDevToolsServer.registerAllActors();

    customDevToolsServer.allowChromeProcess = true;

    const client = new DevToolsClient(customDevToolsServer.connectPipe());
    await client.connect();

    return client;
  },

  /**
   * One method to handle the whole setup sequence to connect to RDP backend for the Browser Console.
   *
   * This will instantiate a special DevTools module loader for the DevToolsServer.
   * Then spawn a DevToolsClient to connect to it.
   * Get a Main Process Descriptor from it.
   * Finally spawn a commands object for this descriptor.
   */
  async forBrowserConsole() {
    // The Browser console ends up using the debugger in autocomplete.
    // Because the debugger can't be running in the same compartment than its debuggee,
    // we have to load the server in a dedicated Loader and so spawn a special client
    const client = await this.spawnClientToDebugSystemPrincipal();

    const descriptor = await client.mainRoot.getMainProcess();

    descriptor.doNotAttachThreadActor = true;

    // Force fetching the first top level target right away.
    await descriptor.getTarget();

    const commands = await createCommandsDictionary(descriptor);
    return commands;
  },
};

async function createLocalClient() {
  // Make sure the DevTools server is started.
  ensureDevToolsServerInitialized();

  // Create the client and connect it to the local server.
  const client = new DevToolsClient(DevToolsServer.connectPipe());
  await client.connect();

  return client;
}
// Also expose this method for tests which would like to create a client
// without involving commands. This would typically be tests against the Watcher actor
// and requires to prevent having TargetCommand from running.
// Or tests which are covering RootFront or global actor's fronts.
exports.createLocalClientForTests = createLocalClient;

function ensureDevToolsServerInitialized() {
  // Since a remote protocol connection will be made, let's start the
  // DevToolsServer here, once and for all tools.
  DevToolsServer.init();

  // Enable all the actors. We may not need all of them and registering
  // only root and target might be enough
  DevToolsServer.registerAllActors();

  // Enable being able to get child process actors
  // Same, this might not be useful
  DevToolsServer.allowChromeProcess = true;
}