1 const DIRPATH = getRootDirectory(gTestPath).replace(
2 "chrome://mochitests/content/",
7 * We choose blob contents that will roundtrip cleanly through the `textContent`
8 * of our returned HTML page.
10 const TEST_BLOB_CONTENTS = `I'm a disk-backed test blob! Hooray!`;
12 add_setup(async function () {
13 await SpecialPowers.pushPrefEnv({
15 // Set preferences so that opening a page with the origin "example.org"
16 // will result in a remoteType of "privilegedmozilla" for both the
17 // page and the ServiceWorker.
18 ["browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", true],
19 ["browser.tabs.remote.separatedMozillaDomains", "example.org"],
20 ["dom.ipc.processCount.privilegedmozilla", 1],
21 ["dom.ipc.processPrelaunch.enabled", false],
22 ["dom.serviceWorkers.enabled", true],
23 ["dom.serviceWorkers.testing.enabled", true],
24 // ServiceWorker worker instances should stay alive until explicitly
25 // caused to terminate by dropping these timeouts to 0 in
26 // `waitForWorkerAndProcessShutdown`.
27 ["dom.serviceWorkers.idle_timeout", 299999],
28 ["dom.serviceWorkers.idle_extended_timeout", 299999],
33 function countRemoteType(remoteType) {
34 return ChromeUtils.getAllDOMProcesses().filter(
35 p => p.remoteType == remoteType
40 * Helper function to get a list of all current processes and their remote
41 * types. Note that when in used in a templated literal that it is
42 * synchronously invoked when the string is evaluated and captures system state
45 function debugRemotes() {
46 return ChromeUtils.getAllDOMProcesses()
47 .map(p => p.remoteType || "parent")
52 * Wait for there to be zero processes of the given remoteType. This check is
53 * considered successful if there are already no processes of the given type
54 * at this very moment.
56 async function waitForNoProcessesOfType(remoteType) {
57 info(`waiting for there to be no ${remoteType} procs`);
58 await TestUtils.waitForCondition(
59 () => countRemoteType(remoteType) == 0,
60 "wait for the worker's process to shutdown"
65 * Given a ServiceWorkerRegistrationInfo with an active ServiceWorker that
66 * has no active ExtendableEvents but would otherwise continue running thanks
67 * to the idle keepalive:
68 * - Assert that there is a ServiceWorker instance in the given registration's
69 * active slot. (General invariant check.)
70 * - Assert that a single process with the given remoteType currently exists.
71 * (This doesn't mean the SW is alive in that process, though this test
72 * verifies that via other checks when appropriate.)
73 * - Induce the worker to shutdown by temporarily dropping the idle timeout to 0
74 * and causing the idle timer to be reset due to rapid debugger attach/detach.
75 * - Wait for the the single process with the given remoteType to go away.
76 * - Reset the idle timeouts back to their previous high values.
78 async function waitForWorkerAndProcessShutdown(swRegInfo, remoteType) {
79 info(`terminating worker and waiting for ${remoteType} procs to shut down`);
80 ok(swRegInfo.activeWorker, "worker should be in the active slot");
82 countRemoteType(remoteType),
84 `should have a single ${remoteType} process but have: ${debugRemotes()}`
87 // Let's not wait too long for the process to shutdown.
88 await SpecialPowers.pushPrefEnv({
90 ["dom.serviceWorkers.idle_timeout", 0],
91 ["dom.serviceWorkers.idle_extended_timeout", 0],
95 // We need to cause the worker to re-evaluate its idle timeout. The easiest
96 // way to do this I could think of is to attach and then detach the debugger
97 // from the active worker.
98 swRegInfo.activeWorker.attachDebugger();
99 await new Promise(resolve => Cu.dispatch(resolve));
100 swRegInfo.activeWorker.detachDebugger();
102 // Eventually the length will reach 0, meaning we're done!
103 await waitForNoProcessesOfType(remoteType);
106 countRemoteType(remoteType),
108 `processes with remoteType=${remoteType} type should have shut down`
111 // Make sure we never kill workers on idle except when this is called.
112 await SpecialPowers.popPrefEnv();
115 async function do_test_sw(host, remoteType, swMode, fileBlob) {
117 `### entering test: host=${host}, remoteType=${remoteType}, mode=${swMode}`
120 const prin = Services.scriptSecurityManager.createContentPrincipal(
121 Services.io.newURI(`https://${host}`),
124 const sw = `https://${host}/${DIRPATH}file_service_worker_fetch_synthetic.js`;
125 const scope = `https://${host}/${DIRPATH}server_fetch_synthetic.sjs`;
127 const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
128 Ci.nsIServiceWorkerManager
130 const swRegInfo = await swm.registerForTest(prin, scope, sw);
131 swRegInfo.QueryInterface(Ci.nsIServiceWorkerRegistrationInfo);
134 `service worker registered: ${JSON.stringify({
135 principal: swRegInfo.principal.spec,
136 scope: swRegInfo.scope,
140 // Wait for the worker to install & shut down.
141 await TestUtils.waitForCondition(
142 () => swRegInfo.activeWorker,
143 "wait for the worker to become active"
145 await waitForWorkerAndProcessShutdown(swRegInfo, remoteType);
148 `test navigation interception with mode=${swMode} starting from about:blank`
150 await BrowserTestUtils.withNewTab(
156 // NOTE: We intentionally trigger the navigation from content in order to
157 // make sure frontend doesn't eagerly process-switch for us.
160 [scope, swMode, fileBlob],
161 // eslint-disable-next-line no-shadow
162 async (scope, swMode, fileBlob) => {
163 const pageUrl = `${scope}?mode=${swMode}`;
165 content.location.href = pageUrl;
167 const doc = content.document;
168 const formElem = doc.createElement("form");
169 doc.body.appendChild(formElem);
171 formElem.action = pageUrl;
172 formElem.method = "POST";
173 formElem.enctype = "multipart/form-data";
175 const fileElem = doc.createElement("input");
176 formElem.appendChild(fileElem);
178 fileElem.type = "file";
179 fileElem.name = "foo";
181 fileElem.mozSetFileArray([fileBlob]);
188 await BrowserTestUtils.browserLoaded(browser);
191 countRemoteType(remoteType),
193 `should have spawned a content process with remoteType=${remoteType}`
196 const { source, blobContents } = await SpecialPowers.spawn(
201 source: content.document.getElementById("source").textContent,
202 blobContents: content.document.getElementById("blob").textContent,
209 swMode === "synthetic" ? "ServiceWorker" : "ServerJS",
210 "The page contents should come from the right place."
215 fileBlob ? TEST_BLOB_CONTENTS : "",
216 "The request blob contents should be the blob/empty as appropriate."
219 // Ensure the worker was loaded in this process.
220 const workerDebuggerURLs = await SpecialPowers.spawn(
224 if (!content.navigator.serviceWorker.controller) {
225 throw new Error("document not controlled!");
228 "@mozilla.org/dom/workers/workerdebuggermanager;1"
229 ].getService(Ci.nsIWorkerDebuggerManager);
231 return Array.from(wdm.getWorkerDebuggerEnumerator())
235 .filter(swURL => swURL == url);
238 if (remoteType.startsWith("webServiceWorker=")) {
242 "Isolated workers should not be running in the content child process"
248 "The worker should be running in the correct child process"
252 // Unregister the ServiceWorker. The registration will continue to control
253 // `browser` and therefore continue to exist and its worker to continue
254 // running until the tab is closed.
255 await SpecialPowers.spawn(browser, [], async () => {
256 let registration = await content.navigator.serviceWorker.ready;
257 await registration.unregister();
262 // Now that the controlled tab is closed and the registration has been
263 // removed, the ServiceWorker will be made redundant which will forcibly
264 // terminate it, which will result in the shutdown of the given content
265 // process. Wait for that to happen both as a verification and so the next
266 // test has a sufficiently clean slate.
267 await waitForNoProcessesOfType(remoteType);
271 * Create a File-backed blob. This will happen synchronously from the main
272 * thread, which isn't optimal, but the test blocks on this progress anyways.
273 * Bug 1669578 has been filed on improving this idiom and avoiding the sync
276 async function makeFileBlob(blobContents) {
277 const tmpFile = Cc["@mozilla.org/file/directory_service;1"]
278 .getService(Ci.nsIDirectoryService)
279 .QueryInterface(Ci.nsIProperties)
280 .get("TmpD", Ci.nsIFile);
281 tmpFile.append("test-file-backed-blob.txt");
282 tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
285 "@mozilla.org/network/file-output-stream;1"
286 ].createInstance(Ci.nsIFileOutputStream);
289 0x02 | 0x08 | 0x20, // write, create, truncate
293 outStream.write(blobContents, blobContents.length);
296 const fileBlob = await File.createFromNsIFile(tmpFile);
300 function getSWTelemetrySums() {
301 let telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(
304 let keyedhistograms = telemetry.getSnapshotForKeyedHistograms(
308 let keyedscalars = telemetry.getSnapshotForKeyedScalars("main", false).parent;
309 // We're not looking at the distribution of the histograms, just that they changed
311 SERVICE_WORKER_RUNNING_All: keyedhistograms.SERVICE_WORKER_RUNNING
312 ? keyedhistograms.SERVICE_WORKER_RUNNING.All.sum
314 SERVICE_WORKER_RUNNING_Fetch: keyedhistograms.SERVICE_WORKER_RUNNING
315 ? keyedhistograms.SERVICE_WORKER_RUNNING.Fetch.sum
317 SERVICEWORKER_RUNNING_MAX_All: keyedscalars["serviceworker.running_max"]
318 ? keyedscalars["serviceworker.running_max"].All
320 SERVICEWORKER_RUNNING_MAX_Fetch: keyedscalars["serviceworker.running_max"]
321 ? keyedscalars["serviceworker.running_max"].Fetch
326 add_task(async function test() {
327 // Can't test telemetry without this since we may not be on the nightly channel
328 let oldCanRecord = Services.telemetry.canRecordExtended;
329 Services.telemetry.canRecordExtended = true;
330 registerCleanupFunction(() => {
331 Services.telemetry.canRecordExtended = oldCanRecord;
334 let initialSums = getSWTelemetrySums();
336 // ## Isolated Privileged Process
337 // Trigger a straightforward intercepted navigation with no request body that
338 // returns a synthetic response.
339 await do_test_sw("example.org", "privilegedmozilla", "synthetic", null);
341 // Trigger an intercepted navigation with FormData containing an
342 // <input type="file"> which will result in the request body containing a
343 // RemoteLazyInputStream which will be consumed in the content process by the
344 // ServiceWorker while generating the synthetic response.
345 const fileBlob = await makeFileBlob(TEST_BLOB_CONTENTS);
346 await do_test_sw("example.org", "privilegedmozilla", "synthetic", fileBlob);
348 // Trigger an intercepted navigation with FormData containing an
349 // <input type="file"> which will result in the request body containing a
350 // RemoteLazyInputStream which will be relayed back to the parent process
351 // via direct invocation of fetch() on the event.request but without any
353 await do_test_sw("example.org", "privilegedmozilla", "fetch", fileBlob);
355 // Same as the above but cloning the request before fetching it.
356 await do_test_sw("example.org", "privilegedmozilla", "clone", fileBlob);
358 // ## Fission Isolation
359 if (Services.appinfo.fissionAutostart) {
360 // ## ServiceWorker isolation
361 const isolateUrl = "example.com";
362 const isolateRemoteType = `webServiceWorker=https://` + isolateUrl;
363 await do_test_sw(isolateUrl, isolateRemoteType, "synthetic", null);
364 await do_test_sw(isolateUrl, isolateRemoteType, "synthetic", fileBlob);
366 let telemetrySums = getSWTelemetrySums();
367 info(JSON.stringify(telemetrySums));
369 "Initial Running All: " +
370 initialSums.SERVICE_WORKER_RUNNING_All +
372 initialSums.SERVICE_WORKER_RUNNING_Fetch
375 "Initial Max Running All: " +
376 initialSums.SERVICEWORKER_RUNNING_MAX_All +
378 initialSums.SERVICEWORKER_RUNNING_MAX_Fetch
382 telemetrySums.SERVICE_WORKER_RUNNING_All +
384 telemetrySums.SERVICE_WORKER_RUNNING_Fetch
387 "Max Running All: " +
388 telemetrySums.SERVICEWORKER_RUNNING_MAX_All +
390 telemetrySums.SERVICEWORKER_RUNNING_MAX_Fetch
393 telemetrySums.SERVICE_WORKER_RUNNING_All,
394 initialSums.SERVICE_WORKER_RUNNING_All,
395 "ServiceWorker running count changed"
398 telemetrySums.SERVICE_WORKER_RUNNING_Fetch,
399 initialSums.SERVICE_WORKER_RUNNING_Fetch,
400 "ServiceWorker running count changed"
402 // We don't use ok()'s for MAX because MAX may have been set before we
403 // set canRecordExtended, and if so we won't record a new value unless
404 // the max increases again.