397 lines
15 KiB
JavaScript
397 lines
15 KiB
JavaScript
/* 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);
|