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
|
/* 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 Services = require("Services");
const {
WatcherRegistry,
} = require("devtools/server/actors/watcher/WatcherRegistry.jsm");
loader.lazyRequireGetter(
this,
"ChildDebuggerTransport",
"devtools/shared/transport/child-transport",
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 onMessageManagerClose(messageManager, topic, data) {
const list = actors.get(messageManager);
if (!list || list.length == 0) {
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);
}
function closeWatcherTransports(watcher) {
for (let i = 0; i < Services.ppmm.childCount; i++) {
const messageManager = Services.ppmm.getChildAt(i);
let list = actors.get(messageManager);
if (!list || list.length == 0) {
continue;
}
list = list.filter(item => item.watcher != watcher);
for (const item of list) {
// If we have a child transport, the actor has already
// been created. We need to stop using this message manager.
item.childTransport.close();
watcher.conn.cancelForwarding(item.prefix);
}
if (list.length == 0) {
actors.delete(messageManager);
} else {
actors.set(messageManager, list);
}
}
}
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.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);
}
}
}
function maybeUnregisterMessageListeners(watcher) {
const sizeBefore = watchers.size;
watchers.delete(watcher);
closeWatcherTransports(watcher);
if (sizeBefore == 1 && watchers.size == 0) {
Services.ppmm.removeMessageListener(
"debug:content-process-actor",
onContentProcessActorCreated
);
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");
}
}
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.
const contentProcessCount = Services.ppmm.childCount - 1;
if (contentProcessCount == 0) {
return;
}
const onTargetsCreated = new Promise(resolve => {
let receivedTargetCount = 0;
const listener = () => {
if (++receivedTargetCount == contentProcessCount) {
watcher.off("target-available-form", listener);
resolve();
}
};
watcher.on("target-available-form", listener);
});
Services.ppmm.broadcastAsyncMessage("debug:instantiate-already-available", {
watcherActorID: watcher.actorID,
connectionPrefix: watcher.conn.prefix,
watchedData: watcher.watchedData,
});
await onTargetsCreated;
}
function destroyTargets(watcher) {
maybeUnregisterMessageListeners(watcher);
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
*/
async function addWatcherDataEntry({ watcher, type, entries }) {
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-watcher-data-entry-done",
listener
);
const onContentProcessClosed = (messageManager, topic, data) => {
expectedCount--;
maybeResolve();
};
const maybeResolve = () => {
if (count == expectedCount) {
Services.ppmm.removeMessageListener(
"debug:add-watcher-data-entry-done",
listener
);
Services.obs.removeObserver(
onContentProcessClosed,
"message-manager-close"
);
resolve();
}
};
Services.obs.addObserver(onContentProcessClosed, "message-manager-close");
});
Services.ppmm.broadcastAsyncMessage("debug:add-watcher-data-entry", {
watcherActorID: watcher.actorID,
type,
entries,
});
await onAllReplied;
}
/**
* Notify all existing content processes that some data entries have been removed
*
* See addWatcherDataEntry for argument documentation.
*/
function removeWatcherDataEntry({ watcher, type, entries }) {
Services.ppmm.broadcastAsyncMessage("debug:remove-watcher-data-entry", {
watcherActorID: watcher.actorID,
type,
entries,
});
}
module.exports = {
createTargets,
destroyTargets,
addWatcherDataEntry,
removeWatcherDataEntry,
};
|