browser_sw_lifetime_extension.js (15225B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /** 5 * Verify that ServiceWorkers interacting with each other can only set/extend 6 * the lifetime of other ServiceWorkers to match their own lifetime, while 7 * other clients that correspond to an open tab can provide fresh lifetime 8 * extensions. The specific scenario we want to ensure is impossible is two 9 * ServiceWorkers interacting to keep each other alive indefinitely without the 10 * involvement of a live tab. 11 * 12 * ### Test Machinery 13 * 14 * #### Determining Lifetimes 15 * 16 * In order to determine the lifetime deadline of ServiceWorkers, we have 17 * exposed `lifetimeDeadline` on nsIServiceWorkerInfo. This is a value 18 * maintained exclusively by the ServiceWorkerManager on the 19 * ServiceWorkerPrivate instances corresponding to each ServiceWorker. It's not 20 * something the ServiceWorker workers know, so it's appropriate to implement 21 * this as a browser test with most of the logic in the parent process. 22 * 23 * #### Communicating with ServiceWorkers 24 * 25 * We use BroadcastChannel to communicate from a page in the test origin that 26 * does not match any ServiceWorker scopes with the ServiceWorkers under test. 27 * BroadcastChannel explicitly will not do anything to extend the lifetime of 28 * the ServiceWorkers and is much simpler for us to use than trying to transfer 29 * MessagePorts around since that would involve ServiceWorker.postMessage() 30 * which will extend the ServiceWorker lifetime if used from a window client. 31 * 32 * #### Making a Service Worker that can Keep Updating 33 * 34 * ServiceWorker update checks do a byte-wise comparison; if the underlying 35 * script/imports have not changed, the update process will be aborted. So we 36 * use an .sjs script that generates a script payload that has a "version" that 37 * updates every time the script is fetched. 38 * 39 * Note that one has to be careful with an .sjs like that because 40 * non-subresource fetch events will automatically run a soft update check, and 41 * functional events will run a soft update if the registration is stale. We 42 * never expect the registration to be stale in our tests because 24 hours won't 43 * have passed, but page navigation is obviously a very common testing thing. 44 * We ensure we don't perform any intercepted navigations. 45 * 46 * To minimize code duplication, we have that script look like: 47 * ``` 48 * var version = ${COUNTER}; 49 * importScripts("sw_inter_sw_postmessage.js"); 50 * ``` 51 */ 52 53 /* import-globals-from browser_head.js */ 54 Services.scriptloader.loadSubScript( 55 "chrome://mochitests/content/browser/dom/serviceworkers/test/browser_head.js", 56 this 57 ); 58 59 const TEST_ORIGIN = "https://test1.example.org"; 60 61 /** 62 * Install equivalent ServiceWorkers on 2 scopes that will message each other 63 * on request via BroadcastChannel message, verifying that the ServiceWorkers 64 * cannot extend each other's deadlines beyond their own deadline. 65 */ 66 async function test_post_message_between_service_workers() { 67 info("## Installing the ServiceWorkers"); 68 const aSwDesc = { 69 origin: TEST_ORIGIN, 70 scope: "sw-a", 71 script: "sw_inter_sw_postmessage.js?a", 72 }; 73 const bSwDesc = { 74 origin: TEST_ORIGIN, 75 scope: "sw-b", 76 script: "sw_inter_sw_postmessage.js?b", 77 }; 78 79 // Wipe the origin for cleanup; this will remove the registrations too. 80 registerCleanupFunction(async () => { 81 await clear_qm_origin_group_via_clearData(TEST_ORIGIN); 82 }); 83 84 const aReg = await install_sw(aSwDesc); 85 const bReg = await install_sw(bSwDesc); 86 87 info("## Terminating the ServiceWorkers"); 88 // We always want to wait for the workers to be fully terminated because they 89 // listen for our BroadcastChannel messages and until ServiceWorkers are no 90 // longer owned by the main thread, a race is possible if we don't wait. 91 const aSWInfo = aReg.activeWorker; 92 await aSWInfo.terminateWorker(); 93 94 const bSWInfo = bReg.activeWorker; 95 await bSWInfo.terminateWorker(); 96 97 is(aSWInfo.lifetimeDeadline, 0, "SW A not running."); 98 is(bSWInfo.lifetimeDeadline, 0, "SW B not running."); 99 is(aSWInfo.launchCount, 1, "SW A did run once, though."); 100 is(bSWInfo.launchCount, 1, "SW B did run once, though."); 101 102 info("## Beginning PostMessage Checks"); 103 let testStart = ChromeUtils.now(); 104 105 const { closeHelperTab, postMessageScopeAndWaitFor, broadcastAndWaitFor } = 106 await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage"); 107 registerCleanupFunction(closeHelperTab); 108 109 // - Have the helper page postMessage SW A to spawn it, waiting for SW A to report in. 110 await postMessageScopeAndWaitFor( 111 "sw-a", 112 "Hello, the contents of this message don't matter!", 113 "a:received-post-message-from:wc-helper" 114 ); 115 116 let aLifetime = aSWInfo.lifetimeDeadline; 117 Assert.greater( 118 aLifetime, 119 testStart, 120 "SW A should be running with a deadline in the future." 121 ); 122 is(bSWInfo.lifetimeDeadline, 0, "SW B not running."); 123 is(aSWInfo.launchCount, 2, "SW A was launched by our postMessage."); 124 is(bSWInfo.launchCount, 1, "SW B has not been re-launched yet."); 125 126 // - Ask SW A to postMessage SW B, waiting for SW B to report in. 127 await broadcastAndWaitFor( 128 "a:post-message-to:reg-sw-b", 129 "b:received-post-message-from:sw-a" 130 ); 131 132 is( 133 bSWInfo.lifetimeDeadline, 134 aLifetime, 135 "SW B has same deadline as SW A after cross-SW postMessage" 136 ); 137 138 // - Ask SW B to postMessage SW A, waiting for SW A to report in. 139 await broadcastAndWaitFor( 140 "b:post-message-to:reg-sw-a", 141 "a:received-post-message-from:sw-b" 142 ); 143 144 is( 145 bSWInfo.lifetimeDeadline, 146 aLifetime, 147 "SW A still has the same deadline after B's cross-SW postMessage" 148 ); 149 is(bSWInfo.launchCount, 2, "SW B was re-launched."); 150 is(aSWInfo.lifetimeDeadline, aLifetime, "SW A deadline unchanged"); 151 is(aSWInfo.launchCount, 2, "SW A launch count unchanged."); 152 153 // - Have the helper page postMessage SW B, waiting for B to report in. 154 await postMessageScopeAndWaitFor( 155 "sw-b", 156 "Hello, the contents of this message don't matter!", 157 "b:received-post-message-from:wc-helper" 158 ); 159 let bLifetime = bSWInfo.lifetimeDeadline; 160 Assert.greater( 161 bLifetime, 162 aLifetime, 163 "SW B should have a deadline after A's after the page postMessage" 164 ); 165 is(aSWInfo.lifetimeDeadline, aLifetime, "SW A deadline unchanged"); 166 is(aSWInfo.launchCount, 2, "SW A launch count unchanged."); 167 168 // - Have SW B postMessage SW A, waiting for SW A to report in. 169 await broadcastAndWaitFor( 170 "b:post-message-to:reg-sw-a", 171 "a:received-post-message-from:sw-b" 172 ); 173 is( 174 aSWInfo.lifetimeDeadline, 175 bLifetime, 176 "SW A should have the same deadline as B after B's cross-SW postMessage" 177 ); 178 is(aSWInfo.launchCount, 2, "SW A launch count unchanged."); 179 is(bSWInfo.lifetimeDeadline, bLifetime, "SW B deadline unchanged"); 180 is(bSWInfo.launchCount, 2, "SW B launch count unchanged."); 181 } 182 add_task(test_post_message_between_service_workers); 183 184 /** 185 * Install a ServiceWorker that will update itself on request via 186 * BroadcastChannel message and verify that the lifetimes of the new updated 187 * ServiceWorker are the same as the requesting ServiceWorker. We also want to 188 * verify that a request to update from a page gets a fresh lifetime. 189 */ 190 async function test_eternally_updating_service_worker() { 191 info("## Installing the Eternally Updating ServiceWorker"); 192 const swDesc = { 193 origin: TEST_ORIGIN, 194 scope: "sw-u", 195 script: "sw_always_updating_inter_sw_postmessage.sjs?u", 196 }; 197 198 // Wipe the origin for cleanup; this will remove the registrations too. 199 registerCleanupFunction(async () => { 200 await clear_qm_origin_group_via_clearData(TEST_ORIGIN); 201 }); 202 203 let testStart = ChromeUtils.now(); 204 205 const reg = await install_sw(swDesc); 206 const firstInfo = reg.activeWorker; 207 const firstLifetime = firstInfo.lifetimeDeadline; 208 209 Assert.greater( 210 firstLifetime, 211 testStart, 212 "The first generation should be running with a deadline in the future." 213 ); 214 215 const { closeHelperTab, broadcastAndWaitFor, updateScopeAndWaitFor } = 216 await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage"); 217 registerCleanupFunction(closeHelperTab); 218 219 info("## Beginning Self-Update Requests"); 220 221 // - Ask 1st gen SW to update the reg, 2nd gen SW should have same lifetime. 222 await broadcastAndWaitFor("u#1:update-reg:sw-u", "u:version-activated:2"); 223 224 // We don't have to worry about async races here because these state changes 225 // are authoritative on the parent process main thread, which is where we are 226 // running; we can only have heard about the activation via BroadcastChannel 227 // after the state has updated. 228 const secondInfo = reg.activeWorker; 229 const secondLifetime = secondInfo.lifetimeDeadline; 230 is(firstLifetime, secondLifetime, "Version 2 has same lifetime as 1."); 231 232 // - Ask 2nd gen SW to update the reg, 3rd gen SW should have same lifetime. 233 await broadcastAndWaitFor("u#2:update-reg:sw-u", "u:version-activated:3"); 234 235 const thirdInfo = reg.activeWorker; 236 const thirdLifetime = thirdInfo.lifetimeDeadline; 237 is(firstLifetime, thirdLifetime, "Version 3 has same lifetime as 1 and 2."); 238 239 // - Ask the helper page to update the reg, 4th gen SW should have fresh life. 240 await updateScopeAndWaitFor("sw-u", "u:version-activated:4"); 241 242 const fourthInfo = reg.activeWorker; 243 const fourthLifetime = fourthInfo.lifetimeDeadline; 244 Assert.greater( 245 fourthLifetime, 246 firstLifetime, 247 "Version 4 has a fresh lifetime." 248 ); 249 250 // - Ask 4th gen SW to update the reg, 5th gen SW should have same lifetime. 251 await broadcastAndWaitFor("u#4:update-reg:sw-u", "u:version-activated:5"); 252 253 const fifthInfo = reg.activeWorker; 254 const fifthLifetime = fifthInfo.lifetimeDeadline; 255 is(fourthLifetime, fifthLifetime, "Version 5 has same lifetime as 4."); 256 } 257 add_task(test_eternally_updating_service_worker); 258 259 /** 260 * Install a ServiceWorker that will create a new registration and verify that 261 * the lifetime for the new ServiceWorker being installed for the new 262 * registration is the same as the requesting ServiceWorker. 263 */ 264 async function test_service_worker_creating_new_registrations() { 265 info("## Installing the Bootstrap ServiceWorker"); 266 const cSwDesc = { 267 origin: TEST_ORIGIN, 268 scope: "sw-c", 269 script: "sw_inter_sw_postmessage.js?c", 270 }; 271 272 // Wipe the origin for cleanup; this will remove the registrations too. 273 registerCleanupFunction(async () => { 274 await clear_qm_origin_group_via_clearData(TEST_ORIGIN); 275 }); 276 277 let testStart = ChromeUtils.now(); 278 279 const cReg = await install_sw(cSwDesc); 280 const cSWInfo = cReg.activeWorker; 281 const cLifetime = cSWInfo.lifetimeDeadline; 282 283 Assert.greater( 284 cLifetime, 285 testStart, 286 "The bootstrap registration worker should be running with a deadline in the future." 287 ); 288 289 const { closeHelperTab, broadcastAndWaitFor, updateScopeAndWaitFor } = 290 await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage"); 291 registerCleanupFunction(closeHelperTab); 292 293 info("## Beginning Propagating Registrations"); 294 295 // - Ask the SW to install a ServiceWorker at scope d 296 await broadcastAndWaitFor("c:install-reg:d", "d:version-activated:0"); 297 298 const dSwDesc = { 299 origin: TEST_ORIGIN, 300 scope: "sw-d", 301 script: "sw_inter_sw_postmessage.js?d", 302 }; 303 const dReg = swm_lookup_reg(dSwDesc); 304 ok(dReg, "found the new 'd' registration"); 305 const dSWInfo = dReg.activeWorker; 306 ok(dSWInfo, "The 'd' registration has the expected active worker."); 307 const dLifetime = dSWInfo.lifetimeDeadline; 308 is( 309 dLifetime, 310 cLifetime, 311 "The new worker has the same lifetime as the worker that triggered its installation." 312 ); 313 } 314 add_task(test_service_worker_creating_new_registrations); 315 316 /** 317 * In bug 1927247 a ServiceWorker respawned sufficiently soon after its 318 * termination resulted in a defensive content-process crash when the new SW's 319 * ClientSource was registered with the same Client Id while the old SW's 320 * ClientSource still existed. We synthetically induce this situation through 321 * use of nsIServiceWorkerInfo::terminateWorker() immediately followed by use of 322 * nsIServiceWorkerInfo::attachDebugger(). 323 */ 324 async function test_respawn_immediately_after_termination() { 325 // Make WorkerTestUtils work in the SW. 326 await SpecialPowers.pushPrefEnv({ 327 set: [["dom.workers.testing.enabled", true]], 328 }); 329 330 // We need to ensure all ServiceWorkers are spawned consistently in the same 331 // process because of the trick we do with workerrefs and using the observer 332 // service, so force us to only use a single process if fission is not on. 333 if (!Services.appinfo.fissionAutostart) { 334 // Force use of only a single process 335 await SpecialPowers.pushPrefEnv({ 336 set: [["dom.ipc.processCount", 1]], 337 }); 338 } 339 340 info("## Installing the ServiceWorker we will terminate and respawn"); 341 const tSwDesc = { 342 origin: TEST_ORIGIN, 343 scope: "sw-t", 344 script: "sw_inter_sw_postmessage.js?t", 345 }; 346 347 // Wipe the origin for cleanup; this will remove the registrations too. 348 registerCleanupFunction(async () => { 349 await clear_qm_origin_group_via_clearData(TEST_ORIGIN); 350 }); 351 352 const tReg = await install_sw(tSwDesc); 353 const tSWInfo = tReg.activeWorker; 354 355 info("## Induce the SW to acquire a WorkerRef that prevents shutdown."); 356 357 const { closeHelperTab, broadcastAndWaitFor, postMessageScopeAndWaitFor } = 358 await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage"); 359 registerCleanupFunction(closeHelperTab); 360 361 // Tell the ServiceWorker to block on a monitor that will prevent the worker 362 // from transitioning to the Canceling state and notifying its WorkerRefs, 363 // thereby preventing the ClientManagerChild from beginning teardown of itself 364 // and thereby the ClientSourceChild. The monitor will be released when we 365 // cause "serviceworker-t-release" to be notified on that process's main 366 // thread. 367 await broadcastAndWaitFor( 368 "t:block:serviceworker-t-release", 369 "t:blocking:serviceworker-t-release" 370 ); 371 372 info("## Terminating and respawning the ServiceWorker via attachDebugger"); 373 // We must not await the termination if we want to create the lifetime overlap 374 const terminationPromise = tSWInfo.terminateWorker(); 375 // Message the ServiceWorker to cause it to spawn, waiting for the newly 376 // spawned ServiceWorker to indicate it is alive and running. 377 await postMessageScopeAndWaitFor( 378 "sw-t", 379 "Hello, the contents of this message don't matter!", 380 "t:received-post-message-from:wc-helper" 381 ); 382 383 // Tell the successor to generate an observer notification that will release 384 // the ThreadSafeWorkerRef. Note that this does assume the ServiceWorker is 385 // placed in the same process as its predecessor. When isolation is enabled, 386 // like on desktop, this will always be the same process because there will 387 // only be the one possible process. "browser" tests like this are only run 388 // on desktop, never on Android! But if we weren't isolating, 389 await broadcastAndWaitFor( 390 "t:notify-observer:serviceworker-t-release", 391 "t:notified-observer:serviceworker-t-release" 392 ); 393 394 info("## Awaiting the termination"); 395 await terminationPromise; 396 } 397 add_task(test_respawn_immediately_after_termination);