summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/watcher/target-helpers/process-helper.js
blob: 8895d7ed66c7a7245ffb15fe2981a5e5dc86621c (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
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
/* 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 { WatcherRegistry } = ChromeUtils.importESModule(
  "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs",
  {
    // WatcherRegistry needs to be a true singleton and loads ActorManagerParent
    // which also has to be a true singleton.
    loadInDevToolsLoader: false,
  }
);

loader.lazyRequireGetter(
  this,
  "ChildDebuggerTransport",
  "resource://devtools/shared/transport/child-transport.js",
  true
);

const CONTENT_PROCESS_SCRIPT =
  "resource://devtools/server/startup/content-process-script.js";

/**
 * Map a MessageManager key to an Array of ContentProcessTargetActor "description" objects.
 * A single MessageManager might be linked to several ContentProcessTargetActors if there are several
 * Watcher actors instantiated on the DevToolsServer, via a single connection (in theory), but rather
 * via distinct connections (ex: a content toolbox and the browser toolbox).
 * Note that if we spawn two DevToolsServer, this module will be instantiated twice.
 *
 * Each ContentProcessTargetActor "description" object is structured as follows
 * - {Object} actor: form of the content process target actor
 * - {String} prefix: forwarding prefix used to redirect all packet to the right content process's transport
 * - {ChildDebuggerTransport} childTransport: Transport forwarding all packets to the target's content process
 * - {WatcherActor} watcher: The Watcher actor for which we instantiated this content process target actor
 */
const actors = new WeakMap();

// Save the list of all watcher actors that are watching for processes
const watchers = new Set();

function onContentProcessActorCreated(msg) {
  const { watcherActorID, prefix, actor } = msg.data;
  const watcher = WatcherRegistry.getWatcher(watcherActorID);
  if (!watcher) {
    throw new Error(
      `Receiving a content process actor without a watcher actor ${watcherActorID}`
    );
  }
  // Ignore watchers of other connections.
  // We may have two browser toolbox connected to the same process.
  // This will spawn two distinct Watcher actor and two distinct process target helper module.
  // Avoid processing the event many times, otherwise we will notify about the same target
  // multiple times.
  if (!watchers.has(watcher)) {
    return;
  }
  const messageManager = msg.target;
  const connection = watcher.conn;

  // Pipe Debugger message from/to parent/child via the message manager
  const childTransport = new ChildDebuggerTransport(messageManager, prefix);
  childTransport.hooks = {
    onPacket: connection.send.bind(connection),
  };
  childTransport.ready();

  connection.setForwarding(prefix, childTransport);

  const list = actors.get(messageManager) || [];
  list.push({
    prefix,
    childTransport,
    actor,
    watcher,
  });
  actors.set(messageManager, list);

  watcher.notifyTargetAvailable(actor);
}

function onContentProcessActorDestroyed(msg) {
  const { watcherActorID } = msg.data;
  const watcher = WatcherRegistry.getWatcher(watcherActorID);
  if (!watcher) {
    throw new Error(
      `Receiving a content process actor destruction without a watcher actor ${watcherActorID}`
    );
  }
  // Ignore watchers of other connections.
  // We may have two browser toolbox connected to the same process.
  // This will spawn two distinct Watcher actor and two distinct process target helper module.
  // Avoid processing the event many times, otherwise we will notify about the same target
  // multiple times.
  if (!watchers.has(watcher)) {
    return;
  }
  const messageManager = msg.target;
  unregisterWatcherForMessageManager(watcher, messageManager);
}

function onMessageManagerClose(messageManager, topic, data) {
  const list = actors.get(messageManager);
  if (!list || !list.length) {
    return;
  }
  for (const { prefix, childTransport, actor, watcher } of list) {
    watcher.notifyTargetDestroyed(actor);

    // If we have a child transport, the actor has already
    // been created. We need to stop using this message manager.
    childTransport.close();
    watcher.conn.cancelForwarding(prefix);
  }
  actors.delete(messageManager);
}

/**
 * Unregister everything created for a given watcher against a precise message manager:
 * - clear up things from `actors` WeakMap,
 * - notify all related target actors as being destroyed,
 * - close all DevTools Transports being created for each Message Manager.
 *
 * @param {WatcherActor} watcher
 * @param {MessageManager}
 * @param {object} options
 * @param {boolean} options.isModeSwitching
 *        true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
 */
function unregisterWatcherForMessageManager(watcher, messageManager, options) {
  const targetActorDescriptions = actors.get(messageManager);
  if (!targetActorDescriptions || !targetActorDescriptions.length) {
    return;
  }

  // Destroy all transports related to this watcher and tells the client to purge all related actors
  const matchingTargetActorDescriptions = targetActorDescriptions.filter(
    item => item.watcher === watcher
  );
  for (const {
    prefix,
    childTransport,
    actor,
  } of matchingTargetActorDescriptions) {
    watcher.notifyTargetDestroyed(actor, options);

    childTransport.close();
    watcher.conn.cancelForwarding(prefix);
  }

  // Then update global `actors` WeakMap by stripping all data about this watcher
  const remainingTargetActorDescriptions = targetActorDescriptions.filter(
    item => item.watcher !== watcher
  );
  if (!remainingTargetActorDescriptions.length) {
    actors.delete(messageManager);
  } else {
    actors.set(messageManager, remainingTargetActorDescriptions);
  }
}

/**
 * Destroy everything related to a given watcher that has been created in this module:
 * (See unregisterWatcherForMessageManager)
 *
 * @param {WatcherActor} watcher
 * @param {object} options
 * @param {boolean} options.isModeSwitching
 *        true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
 */
function closeWatcherTransports(watcher, options) {
  for (let i = 0; i < Services.ppmm.childCount; i++) {
    const messageManager = Services.ppmm.getChildAt(i);
    unregisterWatcherForMessageManager(watcher, messageManager, options);
  }
}

function maybeRegisterMessageListeners(watcher) {
  const sizeBefore = watchers.size;
  watchers.add(watcher);
  if (sizeBefore == 0 && watchers.size == 1) {
    Services.ppmm.addMessageListener(
      "debug:content-process-actor",
      onContentProcessActorCreated
    );
    Services.ppmm.addMessageListener(
      "debug:content-process-actor-destroyed",
      onContentProcessActorDestroyed
    );
    Services.obs.addObserver(onMessageManagerClose, "message-manager-close");

    // Load the content process server startup script only once,
    // otherwise it will be evaluated twice, listen to events twice and create
    // target actors twice.
    // We may try to load it twice when opening one Browser Toolbox via about:debugging
    // and another regular Browser Toolbox. Both will spawn a WatcherActor and watch for processes.
    const isContentProcessScripLoaded = Services.ppmm
      .getDelayedProcessScripts()
      .some(([uri]) => uri === CONTENT_PROCESS_SCRIPT);
    if (!isContentProcessScripLoaded) {
      Services.ppmm.loadProcessScript(CONTENT_PROCESS_SCRIPT, true);
    }
  }
}

/**
 * @param {WatcherActor} watcher
 * @param {object} options
 * @param {boolean} options.isModeSwitching
 *        true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
 */
function maybeUnregisterMessageListeners(watcher, options = {}) {
  const sizeBefore = watchers.size;
  watchers.delete(watcher);
  closeWatcherTransports(watcher, options);

  if (sizeBefore == 1 && watchers.size == 0) {
    Services.ppmm.removeMessageListener(
      "debug:content-process-actor",
      onContentProcessActorCreated
    );
    Services.ppmm.removeMessageListener(
      "debug:content-process-actor-destroyed",
      onContentProcessActorDestroyed
    );
    Services.obs.removeObserver(onMessageManagerClose, "message-manager-close");

    // We inconditionally remove the process script, while we should only remove it
    // once the last DevToolsServer stop watching for processes.
    // We might have many server, using distinct loaders, so that this module
    // will be spawn many times and we should remove the script only once the last
    // module unregister the last watcher of all.
    Services.ppmm.removeDelayedProcessScript(CONTENT_PROCESS_SCRIPT);

    Services.ppmm.broadcastAsyncMessage("debug:destroy-process-script", {
      options,
    });
  }
}

async function createTargets(watcher) {
  // XXX: Should this move to WatcherRegistry??
  maybeRegisterMessageListeners(watcher);

  // Bug 1648499: This could be simplified when migrating to JSProcessActor by using sendQuery.
  // For now, hack into WatcherActor in order to know when we created one target
  // actor for each existing content process.
  // Also, we substract one as the parent process has a message manager and is counted
  // in `childCount`, but we ignore it from the process script and it won't reply.
  let contentProcessCount = Services.ppmm.childCount - 1;
  if (contentProcessCount == 0) {
    return;
  }
  const onTargetsCreated = new Promise(resolve => {
    let receivedTargetCount = 0;
    const listener = () => {
      receivedTargetCount++;
      mayBeResolve();
    };
    watcher.on("target-available-form", listener);
    const onContentProcessClosed = () => {
      // Update the content process count as one has been just destroyed
      contentProcessCount--;
      mayBeResolve();
    };
    Services.obs.addObserver(onContentProcessClosed, "message-manager-close");
    function mayBeResolve() {
      if (receivedTargetCount >= contentProcessCount) {
        watcher.off("target-available-form", listener);
        Services.obs.removeObserver(
          onContentProcessClosed,
          "message-manager-close"
        );
        resolve();
      }
    }
  });

  Services.ppmm.broadcastAsyncMessage("debug:instantiate-already-available", {
    watcherActorID: watcher.actorID,
    connectionPrefix: watcher.conn.prefix,
    sessionData: watcher.sessionData,
  });

  await onTargetsCreated;
}

/**
 * @param {WatcherActor} watcher
 * @param {object} options
 * @param {boolean} options.isModeSwitching
 *        true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
 */
function destroyTargets(watcher, options) {
  maybeUnregisterMessageListeners(watcher, options);

  Services.ppmm.broadcastAsyncMessage("debug:destroy-target", {
    watcherActorID: watcher.actorID,
  });
}

/**
 * Go over all existing content processes in order to communicate about new data entries
 *
 * @param {Object} options
 * @param {WatcherActor} options.watcher
 *        The Watcher Actor providing new data entries
 * @param {string} options.type
 *        The type of data to be added
 * @param {Array<Object>} options.entries
 *        The values to be added to this type of data
 * @param String updateType
 *        "add" will only add the new entries in the existing data set.
 *        "set" will update the data set with the new entries.
 */
async function addOrSetSessionDataEntry({
  watcher,
  type,
  entries,
  updateType,
}) {
  let expectedCount = Services.ppmm.childCount - 1;
  if (expectedCount == 0) {
    return;
  }
  const onAllReplied = new Promise(resolve => {
    let count = 0;
    const listener = msg => {
      if (msg.data.watcherActorID != watcher.actorID) {
        return;
      }
      count++;
      maybeResolve();
    };
    Services.ppmm.addMessageListener(
      "debug:add-or-set-session-data-entry-done",
      listener
    );
    const onContentProcessClosed = (messageManager, topic, data) => {
      expectedCount--;
      maybeResolve();
    };
    const maybeResolve = () => {
      if (count == expectedCount) {
        Services.ppmm.removeMessageListener(
          "debug:add-or-set-session-data-entry-done",
          listener
        );
        Services.obs.removeObserver(
          onContentProcessClosed,
          "message-manager-close"
        );
        resolve();
      }
    };
    Services.obs.addObserver(onContentProcessClosed, "message-manager-close");
  });

  Services.ppmm.broadcastAsyncMessage("debug:add-or-set-session-data-entry", {
    watcherActorID: watcher.actorID,
    type,
    entries,
    updateType,
  });

  await onAllReplied;
}

/**
 * Notify all existing content processes that some data entries have been removed
 *
 * See addOrSetSessionDataEntry for argument documentation.
 */
function removeSessionDataEntry({ watcher, type, entries }) {
  Services.ppmm.broadcastAsyncMessage("debug:remove-session-data-entry", {
    watcherActorID: watcher.actorID,
    type,
    entries,
  });
}

module.exports = {
  createTargets,
  destroyTargets,
  addOrSetSessionDataEntry,
  removeSessionDataEntry,
};