Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

const DIRPATH = getRootDirectory(gTestPath).replace(
""
);
/**
* We choose blob contents that will roundtrip cleanly through the `textContent`
* of our returned HTML page.
*/
const TEST_BLOB_CONTENTS = `I'm a disk-backed test blob! Hooray!`;
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
// Set preferences so that opening a page with the origin "example.org"
// will result in a remoteType of "privilegedmozilla" for both the
// page and the ServiceWorker.
["browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", true],
["browser.tabs.remote.separatedMozillaDomains", "example.org"],
["dom.ipc.processCount.privilegedmozilla", 1],
["dom.ipc.processPrelaunch.enabled", false],
["dom.serviceWorkers.enabled", true],
["dom.serviceWorkers.testing.enabled", true],
// ServiceWorker worker instances should stay alive until explicitly
// caused to terminate by dropping these timeouts to 0 in
// `waitForWorkerAndProcessShutdown`.
["dom.serviceWorkers.idle_timeout", 299999],
["dom.serviceWorkers.idle_extended_timeout", 299999],
],
});
});
function countRemoteType(remoteType) {
return ChromeUtils.getAllDOMProcesses().filter(
p => p.remoteType == remoteType
).length;
}
/**
* Helper function to get a list of all current processes and their remote
* types. Note that when in used in a templated literal that it is
* synchronously invoked when the string is evaluated and captures system state
* at that instant.
*/
function debugRemotes() {
return ChromeUtils.getAllDOMProcesses()
.map(p => p.remoteType || "parent")
.join(",");
}
/**
* Wait for there to be zero processes of the given remoteType. This check is
* considered successful if there are already no processes of the given type
* at this very moment.
*/
async function waitForNoProcessesOfType(remoteType) {
info(`waiting for there to be no ${remoteType} procs`);
await TestUtils.waitForCondition(
() => countRemoteType(remoteType) == 0,
"wait for the worker's process to shutdown"
);
}
/**
* Given a ServiceWorkerRegistrationInfo with an active ServiceWorker that
* has no active ExtendableEvents but would otherwise continue running thanks
* to the idle keepalive:
* - Assert that there is a ServiceWorker instance in the given registration's
* active slot. (General invariant check.)
* - Assert that a single process with the given remoteType currently exists.
* (This doesn't mean the SW is alive in that process, though this test
* verifies that via other checks when appropriate.)
* - Induce the worker to shutdown by temporarily dropping the idle timeout to 0
* and causing the idle timer to be reset due to rapid debugger attach/detach.
* - Wait for the the single process with the given remoteType to go away.
* - Reset the idle timeouts back to their previous high values.
*/
async function waitForWorkerAndProcessShutdown(swRegInfo, remoteType) {
info(`terminating worker and waiting for ${remoteType} procs to shut down`);
ok(swRegInfo.activeWorker, "worker should be in the active slot");
is(
countRemoteType(remoteType),
1,
`should have a single ${remoteType} process but have: ${debugRemotes()}`
);
// Let's not wait too long for the process to shutdown.
await SpecialPowers.pushPrefEnv({
set: [
["dom.serviceWorkers.idle_timeout", 0],
["dom.serviceWorkers.idle_extended_timeout", 0],
],
});
// We need to cause the worker to re-evaluate its idle timeout. The easiest
// way to do this I could think of is to attach and then detach the debugger
// from the active worker.
swRegInfo.activeWorker.attachDebugger();
await new Promise(resolve => Cu.dispatch(resolve));
swRegInfo.activeWorker.detachDebugger();
// Eventually the length will reach 0, meaning we're done!
await waitForNoProcessesOfType(remoteType);
is(
countRemoteType(remoteType),
0,
`processes with remoteType=${remoteType} type should have shut down`
);
// Make sure we never kill workers on idle except when this is called.
await SpecialPowers.popPrefEnv();
}
async function do_test_sw(host, remoteType, swMode, fileBlob) {
info(
`### entering test: host=${host}, remoteType=${remoteType}, mode=${swMode}`
);
const prin = Services.scriptSecurityManager.createContentPrincipal(
Services.io.newURI(`https://${host}`),
{}
);
const sw = `https://${host}/${DIRPATH}file_service_worker_fetch_synthetic.js`;
const scope = `https://${host}/${DIRPATH}server_fetch_synthetic.sjs`;
const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
Ci.nsIServiceWorkerManager
);
const swRegInfo = await swm.registerForTest(prin, scope, sw);
swRegInfo.QueryInterface(Ci.nsIServiceWorkerRegistrationInfo);
info(
`service worker registered: ${JSON.stringify({
principal: swRegInfo.principal.spec,
scope: swRegInfo.scope,
})}`
);
// Wait for the worker to install & shut down.
await TestUtils.waitForCondition(
() => swRegInfo.activeWorker,
"wait for the worker to become active"
);
await waitForWorkerAndProcessShutdown(swRegInfo, remoteType);
info(
`test navigation interception with mode=${swMode} starting from about:blank`
);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "about:blank",
},
async browser => {
// NOTE: We intentionally trigger the navigation from content in order to
// make sure frontend doesn't eagerly process-switch for us.
SpecialPowers.spawn(
browser,
[scope, swMode, fileBlob],
// eslint-disable-next-line no-shadow
async (scope, swMode, fileBlob) => {
const pageUrl = `${scope}?mode=${swMode}`;
if (!fileBlob) {
content.location.href = pageUrl;
} else {
const doc = content.document;
const formElem = doc.createElement("form");
doc.body.appendChild(formElem);
formElem.action = pageUrl;
formElem.method = "POST";
formElem.enctype = "multipart/form-data";
const fileElem = doc.createElement("input");
formElem.appendChild(fileElem);
fileElem.type = "file";
fileElem.name = "foo";
fileElem.mozSetFileArray([fileBlob]);
formElem.submit();
}
}
);
await BrowserTestUtils.browserLoaded(browser);
is(
countRemoteType(remoteType),
1,
`should have spawned a content process with remoteType=${remoteType}`
);
const { source, blobContents } = await SpecialPowers.spawn(
browser,
[],
() => {
return {
source: content.document.getElementById("source").textContent,
blobContents: content.document.getElementById("blob").textContent,
};
}
);
is(
source,
swMode === "synthetic" ? "ServiceWorker" : "ServerJS",
"The page contents should come from the right place."
);
is(
blobContents,
fileBlob ? TEST_BLOB_CONTENTS : "",
"The request blob contents should be the blob/empty as appropriate."
);
// Ensure the worker was loaded in this process.
const workerDebuggerURLs = await SpecialPowers.spawn(
browser,
[sw],
async url => {
if (!content.navigator.serviceWorker.controller) {
throw new Error("document not controlled!");
}
const wdm = Cc[
"@mozilla.org/dom/workers/workerdebuggermanager;1"
].getService(Ci.nsIWorkerDebuggerManager);
return Array.from(wdm.getWorkerDebuggerEnumerator())
.map(wd => {
return wd.url;
})
.filter(swURL => swURL == url);
}
);
if (remoteType.startsWith("webServiceWorker=")) {
Assert.notDeepEqual(
workerDebuggerURLs,
[sw],
"Isolated workers should not be running in the content child process"
);
} else {
Assert.deepEqual(
workerDebuggerURLs,
[sw],
"The worker should be running in the correct child process"
);
}
// Unregister the ServiceWorker. The registration will continue to control
// `browser` and therefore continue to exist and its worker to continue
// running until the tab is closed.
await SpecialPowers.spawn(browser, [], async () => {
let registration = await content.navigator.serviceWorker.ready;
await registration.unregister();
});
}
);
// Now that the controlled tab is closed and the registration has been
// removed, the ServiceWorker will be made redundant which will forcibly
// terminate it, which will result in the shutdown of the given content
// process. Wait for that to happen both as a verification and so the next
// test has a sufficiently clean slate.
await waitForNoProcessesOfType(remoteType);
}
/**
* Create a File-backed blob. This will happen synchronously from the main
* thread, which isn't optimal, but the test blocks on this progress anyways.
* Bug 1669578 has been filed on improving this idiom and avoiding the sync
* writes.
*/
async function makeFileBlob(blobContents) {
const tmpFile = Cc["@mozilla.org/file/directory_service;1"]
.getService(Ci.nsIDirectoryService)
.QueryInterface(Ci.nsIProperties)
.get("TmpD", Ci.nsIFile);
tmpFile.append("test-file-backed-blob.txt");
tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
var outStream = Cc[
"@mozilla.org/network/file-output-stream;1"
].createInstance(Ci.nsIFileOutputStream);
outStream.init(
tmpFile,
0x02 | 0x08 | 0x20, // write, create, truncate
0o666,
0
);
outStream.write(blobContents, blobContents.length);
outStream.close();
const fileBlob = await File.createFromNsIFile(tmpFile);
return fileBlob;
}
function getSWTelemetrySums() {
let telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(
Ci.nsITelemetry
);
let keyedhistograms = telemetry.getSnapshotForKeyedHistograms(
"main",
false
).parent;
let keyedscalars = telemetry.getSnapshotForKeyedScalars("main", false).parent;
// We're not looking at the distribution of the histograms, just that they changed
return {
SERVICE_WORKER_RUNNING_All: keyedhistograms.SERVICE_WORKER_RUNNING
? keyedhistograms.SERVICE_WORKER_RUNNING.All.sum
: 0,
SERVICE_WORKER_RUNNING_Fetch: keyedhistograms.SERVICE_WORKER_RUNNING
? keyedhistograms.SERVICE_WORKER_RUNNING.Fetch.sum
: 0,
};
}
add_task(async function test() {
// Can't test telemetry without this since we may not be on the nightly channel
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
registerCleanupFunction(() => {
Services.telemetry.canRecordExtended = oldCanRecord;
});
let initialSums = getSWTelemetrySums();
// ## Isolated Privileged Process
// Trigger a straightforward intercepted navigation with no request body that
// returns a synthetic response.
await do_test_sw("example.org", "privilegedmozilla", "synthetic", null);
// Trigger an intercepted navigation with FormData containing an
// <input type="file"> which will result in the request body containing a
// RemoteLazyInputStream which will be consumed in the content process by the
// ServiceWorker while generating the synthetic response.
const fileBlob = await makeFileBlob(TEST_BLOB_CONTENTS);
await do_test_sw("example.org", "privilegedmozilla", "synthetic", fileBlob);
// Trigger an intercepted navigation with FormData containing an
// <input type="file"> which will result in the request body containing a
// RemoteLazyInputStream which will be relayed back to the parent process
// via direct invocation of fetch() on the event.request but without any
// cloning.
await do_test_sw("example.org", "privilegedmozilla", "fetch", fileBlob);
// Same as the above but cloning the request before fetching it.
await do_test_sw("example.org", "privilegedmozilla", "clone", fileBlob);
// ## Fission Isolation
if (Services.appinfo.fissionAutostart) {
// ## ServiceWorker isolation
const isolateUrl = "example.com";
const isolateRemoteType = `webServiceWorker=https://` + isolateUrl;
await do_test_sw(isolateUrl, isolateRemoteType, "synthetic", null);
await do_test_sw(isolateUrl, isolateRemoteType, "synthetic", fileBlob);
}
let telemetrySums = getSWTelemetrySums();
info(JSON.stringify(telemetrySums));
info(
"Initial Running All: " +
initialSums.SERVICE_WORKER_RUNNING_All +
", Fetch: " +
initialSums.SERVICE_WORKER_RUNNING_Fetch
);
info(
"Running All: " +
telemetrySums.SERVICE_WORKER_RUNNING_All +
", Fetch: " +
telemetrySums.SERVICE_WORKER_RUNNING_Fetch
);
Assert.greater(
telemetrySums.SERVICE_WORKER_RUNNING_All,
initialSums.SERVICE_WORKER_RUNNING_All,
"ServiceWorker running count changed"
);
Assert.greater(
telemetrySums.SERVICE_WORKER_RUNNING_Fetch,
initialSums.SERVICE_WORKER_RUNNING_Fetch,
"ServiceWorker running count changed"
);
});