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 390 391 392 393 394 395 396 397
|
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Verify that ServiceWorkers interacting with each other can only set/extend
* the lifetime of other ServiceWorkers to match their own lifetime, while
* other clients that correspond to an open tab can provide fresh lifetime
* extensions. The specific scenario we want to ensure is impossible is two
* ServiceWorkers interacting to keep each other alive indefinitely without the
* involvement of a live tab.
*
* ### Test Machinery
*
* #### Determining Lifetimes
*
* In order to determine the lifetime deadline of ServiceWorkers, we have
* exposed `lifetimeDeadline` on nsIServiceWorkerInfo. This is a value
* maintained exclusively by the ServiceWorkerManager on the
* ServiceWorkerPrivate instances corresponding to each ServiceWorker. It's not
* something the ServiceWorker workers know, so it's appropriate to implement
* this as a browser test with most of the logic in the parent process.
*
* #### Communicating with ServiceWorkers
*
* We use BroadcastChannel to communicate from a page in the test origin that
* does not match any ServiceWorker scopes with the ServiceWorkers under test.
* BroadcastChannel explicitly will not do anything to extend the lifetime of
* the ServiceWorkers and is much simpler for us to use than trying to transfer
* MessagePorts around since that would involve ServiceWorker.postMessage()
* which will extend the ServiceWorker lifetime if used from a window client.
*
* #### Making a Service Worker that can Keep Updating
*
* ServiceWorker update checks do a byte-wise comparison; if the underlying
* script/imports have not changed, the update process will be aborted. So we
* use an .sjs script that generates a script payload that has a "version" that
* updates every time the script is fetched.
*
* Note that one has to be careful with an .sjs like that because
* non-subresource fetch events will automatically run a soft update check, and
* functional events will run a soft update if the registration is stale. We
* never expect the registration to be stale in our tests because 24 hours won't
* have passed, but page navigation is obviously a very common testing thing.
* We ensure we don't perform any intercepted navigations.
*
* To minimize code duplication, we have that script look like:
* ```
* var version = ${COUNTER};
* importScripts("sw_inter_sw_postmessage.js");
* ```
*/
/* import-globals-from browser_head.js */
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/dom/serviceworkers/test/browser_head.js",
this
);
const TEST_ORIGIN = "https://test1.example.org";
/**
* Install equivalent ServiceWorkers on 2 scopes that will message each other
* on request via BroadcastChannel message, verifying that the ServiceWorkers
* cannot extend each other's deadlines beyond their own deadline.
*/
async function test_post_message_between_service_workers() {
info("## Installing the ServiceWorkers");
const aSwDesc = {
origin: TEST_ORIGIN,
scope: "sw-a",
script: "sw_inter_sw_postmessage.js?a",
};
const bSwDesc = {
origin: TEST_ORIGIN,
scope: "sw-b",
script: "sw_inter_sw_postmessage.js?b",
};
// Wipe the origin for cleanup; this will remove the registrations too.
registerCleanupFunction(async () => {
await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
});
const aReg = await install_sw(aSwDesc);
const bReg = await install_sw(bSwDesc);
info("## Terminating the ServiceWorkers");
// We always want to wait for the workers to be fully terminated because they
// listen for our BroadcastChannel messages and until ServiceWorkers are no
// longer owned by the main thread, a race is possible if we don't wait.
const aSWInfo = aReg.activeWorker;
await aSWInfo.terminateWorker();
const bSWInfo = bReg.activeWorker;
await bSWInfo.terminateWorker();
is(aSWInfo.lifetimeDeadline, 0, "SW A not running.");
is(bSWInfo.lifetimeDeadline, 0, "SW B not running.");
is(aSWInfo.launchCount, 1, "SW A did run once, though.");
is(bSWInfo.launchCount, 1, "SW B did run once, though.");
info("## Beginning PostMessage Checks");
let testStart = Cu.now();
const { closeHelperTab, postMessageScopeAndWaitFor, broadcastAndWaitFor } =
await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage");
registerCleanupFunction(closeHelperTab);
// - Have the helper page postMessage SW A to spawn it, waiting for SW A to report in.
await postMessageScopeAndWaitFor(
"sw-a",
"Hello, the contents of this message don't matter!",
"a:received-post-message-from:wc-helper"
);
let aLifetime = aSWInfo.lifetimeDeadline;
Assert.greater(
aLifetime,
testStart,
"SW A should be running with a deadline in the future."
);
is(bSWInfo.lifetimeDeadline, 0, "SW B not running.");
is(aSWInfo.launchCount, 2, "SW A was launched by our postMessage.");
is(bSWInfo.launchCount, 1, "SW B has not been re-launched yet.");
// - Ask SW A to postMessage SW B, waiting for SW B to report in.
await broadcastAndWaitFor(
"a:post-message-to:reg-sw-b",
"b:received-post-message-from:sw-a"
);
is(
bSWInfo.lifetimeDeadline,
aLifetime,
"SW B has same deadline as SW A after cross-SW postMessage"
);
// - Ask SW B to postMessage SW A, waiting for SW A to report in.
await broadcastAndWaitFor(
"b:post-message-to:reg-sw-a",
"a:received-post-message-from:sw-b"
);
is(
bSWInfo.lifetimeDeadline,
aLifetime,
"SW A still has the same deadline after B's cross-SW postMessage"
);
is(bSWInfo.launchCount, 2, "SW B was re-launched.");
is(aSWInfo.lifetimeDeadline, aLifetime, "SW A deadline unchanged");
is(aSWInfo.launchCount, 2, "SW A launch count unchanged.");
// - Have the helper page postMessage SW B, waiting for B to report in.
await postMessageScopeAndWaitFor(
"sw-b",
"Hello, the contents of this message don't matter!",
"b:received-post-message-from:wc-helper"
);
let bLifetime = bSWInfo.lifetimeDeadline;
Assert.greater(
bLifetime,
aLifetime,
"SW B should have a deadline after A's after the page postMessage"
);
is(aSWInfo.lifetimeDeadline, aLifetime, "SW A deadline unchanged");
is(aSWInfo.launchCount, 2, "SW A launch count unchanged.");
// - Have SW B postMessage SW A, waiting for SW A to report in.
await broadcastAndWaitFor(
"b:post-message-to:reg-sw-a",
"a:received-post-message-from:sw-b"
);
is(
aSWInfo.lifetimeDeadline,
bLifetime,
"SW A should have the same deadline as B after B's cross-SW postMessage"
);
is(aSWInfo.launchCount, 2, "SW A launch count unchanged.");
is(bSWInfo.lifetimeDeadline, bLifetime, "SW B deadline unchanged");
is(bSWInfo.launchCount, 2, "SW B launch count unchanged.");
}
add_task(test_post_message_between_service_workers);
/**
* Install a ServiceWorker that will update itself on request via
* BroadcastChannel message and verify that the lifetimes of the new updated
* ServiceWorker are the same as the requesting ServiceWorker. We also want to
* verify that a request to update from a page gets a fresh lifetime.
*/
async function test_eternally_updating_service_worker() {
info("## Installing the Eternally Updating ServiceWorker");
const swDesc = {
origin: TEST_ORIGIN,
scope: "sw-u",
script: "sw_always_updating_inter_sw_postmessage.sjs?u",
};
// Wipe the origin for cleanup; this will remove the registrations too.
registerCleanupFunction(async () => {
await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
});
let testStart = Cu.now();
const reg = await install_sw(swDesc);
const firstInfo = reg.activeWorker;
const firstLifetime = firstInfo.lifetimeDeadline;
Assert.greater(
firstLifetime,
testStart,
"The first generation should be running with a deadline in the future."
);
const { closeHelperTab, broadcastAndWaitFor, updateScopeAndWaitFor } =
await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage");
registerCleanupFunction(closeHelperTab);
info("## Beginning Self-Update Requests");
// - Ask 1st gen SW to update the reg, 2nd gen SW should have same lifetime.
await broadcastAndWaitFor("u#1:update-reg:sw-u", "u:version-activated:2");
// We don't have to worry about async races here because these state changes
// are authoritative on the parent process main thread, which is where we are
// running; we can only have heard about the activation via BroadcastChannel
// after the state has updated.
const secondInfo = reg.activeWorker;
const secondLifetime = secondInfo.lifetimeDeadline;
is(firstLifetime, secondLifetime, "Version 2 has same lifetime as 1.");
// - Ask 2nd gen SW to update the reg, 3rd gen SW should have same lifetime.
await broadcastAndWaitFor("u#2:update-reg:sw-u", "u:version-activated:3");
const thirdInfo = reg.activeWorker;
const thirdLifetime = thirdInfo.lifetimeDeadline;
is(firstLifetime, thirdLifetime, "Version 3 has same lifetime as 1 and 2.");
// - Ask the helper page to update the reg, 4th gen SW should have fresh life.
await updateScopeAndWaitFor("sw-u", "u:version-activated:4");
const fourthInfo = reg.activeWorker;
const fourthLifetime = fourthInfo.lifetimeDeadline;
Assert.greater(
fourthLifetime,
firstLifetime,
"Version 4 has a fresh lifetime."
);
// - Ask 4th gen SW to update the reg, 5th gen SW should have same lifetime.
await broadcastAndWaitFor("u#4:update-reg:sw-u", "u:version-activated:5");
const fifthInfo = reg.activeWorker;
const fifthLifetime = fifthInfo.lifetimeDeadline;
is(fourthLifetime, fifthLifetime, "Version 5 has same lifetime as 4.");
}
add_task(test_eternally_updating_service_worker);
/**
* Install a ServiceWorker that will create a new registration and verify that
* the lifetime for the new ServiceWorker being installed for the new
* registration is the same as the requesting ServiceWorker.
*/
async function test_service_worker_creating_new_registrations() {
info("## Installing the Bootstrap ServiceWorker");
const cSwDesc = {
origin: TEST_ORIGIN,
scope: "sw-c",
script: "sw_inter_sw_postmessage.js?c",
};
// Wipe the origin for cleanup; this will remove the registrations too.
registerCleanupFunction(async () => {
await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
});
let testStart = Cu.now();
const cReg = await install_sw(cSwDesc);
const cSWInfo = cReg.activeWorker;
const cLifetime = cSWInfo.lifetimeDeadline;
Assert.greater(
cLifetime,
testStart,
"The bootstrap registration worker should be running with a deadline in the future."
);
const { closeHelperTab, broadcastAndWaitFor, updateScopeAndWaitFor } =
await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage");
registerCleanupFunction(closeHelperTab);
info("## Beginning Propagating Registrations");
// - Ask the SW to install a ServiceWorker at scope d
await broadcastAndWaitFor("c:install-reg:d", "d:version-activated:0");
const dSwDesc = {
origin: TEST_ORIGIN,
scope: "sw-d",
script: "sw_inter_sw_postmessage.js?d",
};
const dReg = swm_lookup_reg(dSwDesc);
ok(dReg, "found the new 'd' registration");
const dSWInfo = dReg.activeWorker;
ok(dSWInfo, "The 'd' registration has the expected active worker.");
const dLifetime = dSWInfo.lifetimeDeadline;
is(
dLifetime,
cLifetime,
"The new worker has the same lifetime as the worker that triggered its installation."
);
}
add_task(test_service_worker_creating_new_registrations);
/**
* In bug 1927247 a ServiceWorker respawned sufficiently soon after its
* termination resulted in a defensive content-process crash when the new SW's
* ClientSource was registered with the same Client Id while the old SW's
* ClientSource still existed. We synthetically induce this situation through
* use of nsIServiceWorkerInfo::terminateWorker() immediately followed by use of
* nsIServiceWorkerInfo::attachDebugger().
*/
async function test_respawn_immediately_after_termination() {
// Make WorkerTestUtils work in the SW.
await SpecialPowers.pushPrefEnv({
set: [["dom.workers.testing.enabled", true]],
});
// We need to ensure all ServiceWorkers are spawned consistently in the same
// process because of the trick we do with workerrefs and using the observer
// service, so force us to only use a single process if fission is not on.
if (!Services.appinfo.fissionAutostart) {
// Force use of only a single process
await SpecialPowers.pushPrefEnv({
set: [["dom.ipc.processCount", 1]],
});
}
info("## Installing the ServiceWorker we will terminate and respawn");
const tSwDesc = {
origin: TEST_ORIGIN,
scope: "sw-t",
script: "sw_inter_sw_postmessage.js?t",
};
// Wipe the origin for cleanup; this will remove the registrations too.
registerCleanupFunction(async () => {
await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
});
const tReg = await install_sw(tSwDesc);
const tSWInfo = tReg.activeWorker;
info("## Induce the SW to acquire a WorkerRef that prevents shutdown.");
const { closeHelperTab, broadcastAndWaitFor, postMessageScopeAndWaitFor } =
await createMessagingHelperTab(TEST_ORIGIN, "inter-sw-postmessage");
registerCleanupFunction(closeHelperTab);
// Tell the ServiceWorker to block on a monitor that will prevent the worker
// from transitioning to the Canceling state and notifying its WorkerRefs,
// thereby preventing the ClientManagerChild from beginning teardown of itself
// and thereby the ClientSourceChild. The monitor will be released when we
// cause "serviceworker-t-release" to be notified on that process's main
// thread.
await broadcastAndWaitFor(
"t:block:serviceworker-t-release",
"t:blocking:serviceworker-t-release"
);
info("## Terminating and respawning the ServiceWorker via attachDebugger");
// We must not await the termination if we want to create the lifetime overlap
const terminationPromise = tSWInfo.terminateWorker();
// Message the ServiceWorker to cause it to spawn, waiting for the newly
// spawned ServiceWorker to indicate it is alive and running.
await postMessageScopeAndWaitFor(
"sw-t",
"Hello, the contents of this message don't matter!",
"t:received-post-message-from:wc-helper"
);
// Tell the successor to generate an observer notification that will release
// the ThreadSafeWorkerRef. Note that this does assume the ServiceWorker is
// placed in the same process as its predecessor. When isolation is enabled,
// like on desktop, this will always be the same process because there will
// only be the one possible process. "browser" tests like this are only run
// on desktop, never on Android! But if we weren't isolating,
await broadcastAndWaitFor(
"t:notify-observer:serviceworker-t-release",
"t:notified-observer:serviceworker-t-release"
);
info("## Awaiting the termination");
await terminationPromise;
}
add_task(test_respawn_immediately_after_termination);
|