browser_serviceworker_fetch_new_process.js (13419B)
1 const DIRPATH = getRootDirectory(gTestPath).replace( 2 "chrome://mochitests/content/", 3 "" 4 ); 5 6 /** 7 * We choose blob contents that will roundtrip cleanly through the `textContent` 8 * of our returned HTML page. 9 */ 10 const TEST_BLOB_CONTENTS = `I'm a disk-backed test blob! Hooray!`; 11 12 add_setup(async function () { 13 await SpecialPowers.pushPrefEnv({ 14 set: [ 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], 29 ], 30 }); 31 }); 32 33 function countRemoteType(remoteType) { 34 return ChromeUtils.getAllDOMProcesses().filter( 35 p => p.remoteType == remoteType 36 ).length; 37 } 38 39 /** 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 43 * at that instant. 44 */ 45 function debugRemotes() { 46 return ChromeUtils.getAllDOMProcesses() 47 .map(p => p.remoteType || "parent") 48 .join(","); 49 } 50 51 /** 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. 55 */ 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" 61 ); 62 } 63 64 /** 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. 77 */ 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"); 81 is( 82 countRemoteType(remoteType), 83 1, 84 `should have a single ${remoteType} process but have: ${debugRemotes()}` 85 ); 86 87 // Let's not wait too long for the process to shutdown. 88 await SpecialPowers.pushPrefEnv({ 89 set: [ 90 ["dom.serviceWorkers.idle_timeout", 0], 91 ["dom.serviceWorkers.idle_extended_timeout", 0], 92 ], 93 }); 94 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(); 101 102 // Eventually the length will reach 0, meaning we're done! 103 await waitForNoProcessesOfType(remoteType); 104 105 is( 106 countRemoteType(remoteType), 107 0, 108 `processes with remoteType=${remoteType} type should have shut down` 109 ); 110 111 // Make sure we never kill workers on idle except when this is called. 112 await SpecialPowers.popPrefEnv(); 113 } 114 115 async function do_test_sw(host, remoteType, swMode, fileBlob) { 116 info( 117 `### entering test: host=${host}, remoteType=${remoteType}, mode=${swMode}` 118 ); 119 120 const prin = Services.scriptSecurityManager.createContentPrincipal( 121 Services.io.newURI(`https://${host}`), 122 {} 123 ); 124 const sw = `https://${host}/${DIRPATH}file_service_worker_fetch_synthetic.js`; 125 const scope = `https://${host}/${DIRPATH}server_fetch_synthetic.sjs`; 126 127 const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( 128 Ci.nsIServiceWorkerManager 129 ); 130 const swRegInfo = await swm.registerForTest(prin, scope, sw); 131 swRegInfo.QueryInterface(Ci.nsIServiceWorkerRegistrationInfo); 132 133 info( 134 `service worker registered: ${JSON.stringify({ 135 principal: swRegInfo.principal.spec, 136 scope: swRegInfo.scope, 137 })}` 138 ); 139 140 // Wait for the worker to install & shut down. 141 await TestUtils.waitForCondition( 142 () => swRegInfo.activeWorker, 143 "wait for the worker to become active" 144 ); 145 await waitForWorkerAndProcessShutdown(swRegInfo, remoteType); 146 147 info( 148 `test navigation interception with mode=${swMode} starting from about:blank` 149 ); 150 await BrowserTestUtils.withNewTab( 151 { 152 gBrowser, 153 url: "about:blank", 154 }, 155 async browser => { 156 // NOTE: We intentionally trigger the navigation from content in order to 157 // make sure frontend doesn't eagerly process-switch for us. 158 SpecialPowers.spawn( 159 browser, 160 [scope, swMode, fileBlob], 161 // eslint-disable-next-line no-shadow 162 async (scope, swMode, fileBlob) => { 163 const pageUrl = `${scope}?mode=${swMode}`; 164 if (!fileBlob) { 165 content.location.href = pageUrl; 166 } else { 167 const doc = content.document; 168 const formElem = doc.createElement("form"); 169 doc.body.appendChild(formElem); 170 171 formElem.action = pageUrl; 172 formElem.method = "POST"; 173 formElem.enctype = "multipart/form-data"; 174 175 const fileElem = doc.createElement("input"); 176 formElem.appendChild(fileElem); 177 178 fileElem.type = "file"; 179 fileElem.name = "foo"; 180 181 fileElem.mozSetFileArray([fileBlob]); 182 183 formElem.submit(); 184 } 185 } 186 ); 187 188 await BrowserTestUtils.browserLoaded(browser); 189 190 is( 191 countRemoteType(remoteType), 192 1, 193 `should have spawned a content process with remoteType=${remoteType}` 194 ); 195 196 const { source, blobContents } = await SpecialPowers.spawn( 197 browser, 198 [], 199 () => { 200 return { 201 source: content.document.getElementById("source").textContent, 202 blobContents: content.document.getElementById("blob").textContent, 203 }; 204 } 205 ); 206 207 is( 208 source, 209 swMode === "synthetic" ? "ServiceWorker" : "ServerJS", 210 "The page contents should come from the right place." 211 ); 212 213 is( 214 blobContents, 215 fileBlob ? TEST_BLOB_CONTENTS : "", 216 "The request blob contents should be the blob/empty as appropriate." 217 ); 218 219 // Ensure the worker was loaded in this process. 220 const workerDebuggerURLs = await SpecialPowers.spawn( 221 browser, 222 [sw], 223 async url => { 224 if (!content.navigator.serviceWorker.controller) { 225 throw new Error("document not controlled!"); 226 } 227 const wdm = Cc[ 228 "@mozilla.org/dom/workers/workerdebuggermanager;1" 229 ].getService(Ci.nsIWorkerDebuggerManager); 230 231 return Array.from(wdm.getWorkerDebuggerEnumerator()) 232 .map(wd => { 233 return wd.url; 234 }) 235 .filter(swURL => swURL == url); 236 } 237 ); 238 if (remoteType.startsWith("webServiceWorker=")) { 239 Assert.notDeepEqual( 240 workerDebuggerURLs, 241 [sw], 242 "Isolated workers should not be running in the content child process" 243 ); 244 } else { 245 Assert.deepEqual( 246 workerDebuggerURLs, 247 [sw], 248 "The worker should be running in the correct child process" 249 ); 250 } 251 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(); 258 }); 259 } 260 ); 261 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); 268 } 269 270 /** 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 274 * writes. 275 */ 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); 283 284 var outStream = Cc[ 285 "@mozilla.org/network/file-output-stream;1" 286 ].createInstance(Ci.nsIFileOutputStream); 287 outStream.init( 288 tmpFile, 289 0x02 | 0x08 | 0x20, // write, create, truncate 290 0o666, 291 0 292 ); 293 outStream.write(blobContents, blobContents.length); 294 outStream.close(); 295 296 const fileBlob = await File.createFromNsIFile(tmpFile); 297 return fileBlob; 298 } 299 300 function getSWTelemetrySums() { 301 let telemetry = Cc["@mozilla.org/base/telemetry;1"].getService( 302 Ci.nsITelemetry 303 ); 304 let keyedhistograms = telemetry.getSnapshotForKeyedHistograms( 305 "main", 306 false 307 ).parent; 308 let keyedscalars = telemetry.getSnapshotForKeyedScalars("main", false).parent; 309 // We're not looking at the distribution of the histograms, just that they changed 310 return { 311 SERVICE_WORKER_RUNNING_All: keyedhistograms.SERVICE_WORKER_RUNNING 312 ? keyedhistograms.SERVICE_WORKER_RUNNING.All.sum 313 : 0, 314 SERVICE_WORKER_RUNNING_Fetch: keyedhistograms.SERVICE_WORKER_RUNNING 315 ? keyedhistograms.SERVICE_WORKER_RUNNING.Fetch.sum 316 : 0, 317 }; 318 } 319 320 add_task(async function test() { 321 // Can't test telemetry without this since we may not be on the nightly channel 322 let oldCanRecord = Services.telemetry.canRecordExtended; 323 Services.telemetry.canRecordExtended = true; 324 registerCleanupFunction(() => { 325 Services.telemetry.canRecordExtended = oldCanRecord; 326 }); 327 328 let initialSums = getSWTelemetrySums(); 329 330 // ## Isolated Privileged Process 331 // Trigger a straightforward intercepted navigation with no request body that 332 // returns a synthetic response. 333 await do_test_sw("example.org", "privilegedmozilla", "synthetic", null); 334 335 // Trigger an intercepted navigation with FormData containing an 336 // <input type="file"> which will result in the request body containing a 337 // RemoteLazyInputStream which will be consumed in the content process by the 338 // ServiceWorker while generating the synthetic response. 339 const fileBlob = await makeFileBlob(TEST_BLOB_CONTENTS); 340 await do_test_sw("example.org", "privilegedmozilla", "synthetic", fileBlob); 341 342 // Trigger an intercepted navigation with FormData containing an 343 // <input type="file"> which will result in the request body containing a 344 // RemoteLazyInputStream which will be relayed back to the parent process 345 // via direct invocation of fetch() on the event.request but without any 346 // cloning. 347 await do_test_sw("example.org", "privilegedmozilla", "fetch", fileBlob); 348 349 // Same as the above but cloning the request before fetching it. 350 await do_test_sw("example.org", "privilegedmozilla", "clone", fileBlob); 351 352 // ## Fission Isolation 353 if (Services.appinfo.fissionAutostart) { 354 // ## ServiceWorker isolation 355 const isolateUrl = "example.com"; 356 const isolateRemoteType = `webServiceWorker=https://` + isolateUrl; 357 await do_test_sw(isolateUrl, isolateRemoteType, "synthetic", null); 358 await do_test_sw(isolateUrl, isolateRemoteType, "synthetic", fileBlob); 359 } 360 let telemetrySums = getSWTelemetrySums(); 361 info(JSON.stringify(telemetrySums)); 362 info( 363 "Initial Running All: " + 364 initialSums.SERVICE_WORKER_RUNNING_All + 365 ", Fetch: " + 366 initialSums.SERVICE_WORKER_RUNNING_Fetch 367 ); 368 info( 369 "Running All: " + 370 telemetrySums.SERVICE_WORKER_RUNNING_All + 371 ", Fetch: " + 372 telemetrySums.SERVICE_WORKER_RUNNING_Fetch 373 ); 374 Assert.greater( 375 telemetrySums.SERVICE_WORKER_RUNNING_All, 376 initialSums.SERVICE_WORKER_RUNNING_All, 377 "ServiceWorker running count changed" 378 ); 379 Assert.greater( 380 telemetrySums.SERVICE_WORKER_RUNNING_Fetch, 381 initialSums.SERVICE_WORKER_RUNNING_Fetch, 382 "ServiceWorker running count changed" 383 ); 384 });