Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Errors

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
* vim: sw=4 ts=4 sts=4 et
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* This test records code loaded during a dummy background task.
*
* To run this test similar to try server, you need to run:
* ./mach package
* ./mach test --appname=dist <path to test>
*
* If you made changes that cause this test to fail, it's likely
* because you are changing the application startup process. In
* general, you should prefer to defer loading code as long as you
* can, especially if it's not going to be used in background tasks.
*/
"use strict";
const Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
// Shortcuts for conditions.
const LINUX = AppConstants.platform == "linux";
const WIN = AppConstants.platform == "win";
const MAC = AppConstants.platform == "macosx";
const backgroundtaskPhases = {
AfterRunBackgroundTaskNamed: {
allowlist: {
modules: [
"resource://gre/modules/AppConstants.sys.mjs",
"resource://gre/modules/AsyncShutdown.sys.mjs",
"resource://gre/modules/BackgroundTasksManager.sys.mjs",
"resource://gre/modules/Console.sys.mjs",
"resource://gre/modules/EnterprisePolicies.sys.mjs",
"resource://gre/modules/EnterprisePoliciesParent.sys.mjs",
"resource://gre/modules/XPCOMUtils.sys.mjs",
"resource://gre/modules/nsAsyncShutdown.sys.mjs",
],
// Human-readable contract IDs are many-to-one mapped to CIDs, so this
// list is a little misleading. For example, all of
// "@mozilla.org/xre/app-info;1", "@mozilla.org/xre/runtime;1", and
// "@mozilla.org/toolkit/crash-reporter;1", map to the CID
// {95d89e3e-a169-41a3-8e56-719978e15b12}, but only one is listed here.
// We could be more precise by listing CIDs, but that's a good deal harder
// to read and modify.
services: [
"@mozilla.org/async-shutdown-service;1",
"@mozilla.org/backgroundtasks;1",
"@mozilla.org/backgroundtasksmanager;1",
"@mozilla.org/base/telemetry;1",
"@mozilla.org/categorymanager;1",
"@mozilla.org/chrome/chrome-registry;1",
"@mozilla.org/cookieService;1",
"@mozilla.org/docloaderservice;1",
"@mozilla.org/embedcomp/window-watcher;1",
"@mozilla.org/enterprisepolicies;1",
"@mozilla.org/file/directory_service;1",
"@mozilla.org/intl/stringbundle;1",
"@mozilla.org/layout/content-policy;1",
"@mozilla.org/memory-reporter-manager;1",
"@mozilla.org/network/captive-portal-service;1",
"@mozilla.org/network/effective-tld-service;1",
"@mozilla.org/network/idn-service;1",
"@mozilla.org/network/io-service;1",
"@mozilla.org/network/network-link-service;1",
"@mozilla.org/network/protocol;1?name=file",
"@mozilla.org/network/protocol;1?name=jar",
"@mozilla.org/network/protocol;1?name=resource",
"@mozilla.org/network/socket-transport-service;1",
"@mozilla.org/network/stream-transport-service;1",
"@mozilla.org/network/url-parser;1?auth=maybe",
"@mozilla.org/network/url-parser;1?auth=no",
"@mozilla.org/network/url-parser;1?auth=yes",
"@mozilla.org/observer-service;1",
"@mozilla.org/power/powermanagerservice;1",
"@mozilla.org/preferences-service;1",
"@mozilla.org/process/environment;1",
"@mozilla.org/storage/service;1",
"@mozilla.org/thirdpartyutil;1",
"@mozilla.org/toolkit/app-startup;1",
{
name: "@mozilla.org/widget/appshell/mac;1",
condition: MAC,
},
{
name: "@mozilla.org/widget/appshell/gtk;1",
condition: LINUX,
},
{
name: "@mozilla.org/widget/appshell/win;1",
condition: WIN,
},
"@mozilla.org/xpcom/debug;1",
"@mozilla.org/xre/app-info;1",
"@mozilla.org/mime;1",
],
},
},
AfterFindRunBackgroundTask: {
allowlist: {
modules: [
// We have a profile marker for this, even though it failed to load!
"resource://gre/modules/ConsoleAPIStorage.sys.mjs",
"resource://gre/modules/Timer.sys.mjs",
// We have a profile marker for this, even though it failed to load!
],
services: ["@mozilla.org/consoleAPI-storage;1"],
},
},
AfterAwaitRunBackgroundTask: {
allowlist: {
modules: [
"resource://devtools/shared/security/DevToolsSocketStatus.sys.mjs",
],
services: [],
},
},
};
function getStackFromProfile(profile, stack, libs) {
const stackPrefixCol = profile.stackTable.schema.prefix;
const stackFrameCol = profile.stackTable.schema.frame;
const frameLocationCol = profile.frameTable.schema.location;
let index = 1;
let result = [];
while (stack) {
let sp = profile.stackTable.data[stack];
let frame = profile.frameTable.data[sp[stackFrameCol]];
stack = sp[stackPrefixCol];
frame = profile.stringTable[frame[frameLocationCol]];
if (frame.startsWith("0x")) {
try {
let addr = frame.slice("0x".length);
addr = Number.parseInt(addr, 16);
for (let lib of libs) {
if (lib.start <= addr && addr <= lib.end) {
// Only handle two digits for now.
let indexString = index.toString(10);
if (indexString.length == 1) {
indexString = "0" + indexString;
}
let offset = addr - lib.start;
frame = `#${indexString}: ???[${lib.debugPath} ${
"+0x" + offset.toString(16)
}]`;
break;
}
}
} catch (e) {
// Fall through.
}
}
if (frame != "js::RunScript" && !frame.startsWith("next (self-hosted:")) {
result.push(frame);
index += 1;
}
}
return result;
}
add_task(async function test_xpcom_graph_wait() {
TestUtils.assertPackagedBuild();
let profilePath = Services.env.get("MOZ_UPLOAD_DIR");
profilePath =
profilePath ||
(await IOUtils.createUniqueDirectory(
PathUtils.profileDir,
"testBackgroundTask",
0o700
));
profilePath = PathUtils.join(profilePath, "profile_backgroundtask_wait.json");
await IOUtils.remove(profilePath, { ignoreAbsent: true });
let extraEnv = {
MOZ_PROFILER_STARTUP: "1",
MOZ_PROFILER_SHUTDOWN: profilePath,
};
let exitCode = await do_backgroundtask("wait", { extraEnv });
Assert.equal(0, exitCode);
let rootProfile = await IOUtils.readJSON(profilePath);
let profile = rootProfile.threads[0];
const nameCol = profile.markers.schema.name;
const dataCol = profile.markers.schema.data;
function newMarkers() {
return {
// The equivalent of `Cu.loadedJSModules` + `Cu.loadedESModules`.
modules: [],
services: [],
};
}
let phases = {};
let markersForCurrentPhase = newMarkers();
// If a subsequent phase loads an already loaded resource, that's
// fine. Track all loaded resources to ignore such repeated loads.
let markersForAllPhases = newMarkers();
for (let m of profile.markers.data) {
let markerName = profile.stringTable[m[nameCol]];
if (markerName.startsWith("BackgroundTasksManager:")) {
phases[markerName.split("BackgroundTasksManager:")[1]] =
markersForCurrentPhase;
markersForCurrentPhase = newMarkers();
continue;
}
if (
![
"ChromeUtils.import", // JSMs.
"ChromeUtils.importESModule", // System ESMs.
"ChromeUtils.importESModule static import",
"GetService", // XPCOM services.
].includes(markerName)
) {
continue;
}
let markerData = m[dataCol];
if (
markerName == "ChromeUtils.import" ||
markerName == "ChromeUtils.importESModule" ||
markerName == "ChromeUtils.importESModule static import"
) {
let module = markerData.name;
if (!markersForAllPhases.modules.includes(module)) {
markersForAllPhases.modules.push(module);
markersForCurrentPhase.modules.push(module);
}
}
if (markerName == "GetService") {
// We get a CID from the marker itself, but not a human-readable contract
// ID. Now, most of the time, the stack will contain a label like
// `GetServiceByContractID @...;1`, and we could extract the contract ID
// from that. But there are multiple ways to instantiate services, and
// not all of them are annotated in this manner. Therefore we "go the
// other way" and use the component manager's mapping from contract IDs to
// CIDs. This opens up the possibility for that mapping to be different
// between `--backgroundtask` and `xpcshell`, but that's not an issue
// right at this moment. It's worth noting that one CID can (and
// sometimes does) correspond to more than one contract ID.
let cid = markerData.name;
if (!markersForAllPhases.services.includes(cid)) {
markersForAllPhases.services.push(cid);
markersForCurrentPhase.services.push(cid);
}
}
}
// Turn `["1", {name: "2", condition: false}, {name: "3", condition: true}]`
// into `new Set(["1", "3"])`.
function filterConditions(l) {
let set = new Set([]);
for (let entry of l) {
if (typeof entry == "object") {
if ("condition" in entry && !entry.condition) {
continue;
}
entry = entry.name;
}
set.add(entry);
}
return set;
}
for (let phaseName in backgroundtaskPhases) {
for (let listName in backgroundtaskPhases[phaseName]) {
for (let scriptType in backgroundtaskPhases[phaseName][listName]) {
backgroundtaskPhases[phaseName][listName][scriptType] =
filterConditions(
backgroundtaskPhases[phaseName][listName][scriptType]
);
}
// Turn human-readable contract IDs into CIDs. It's worth noting that one
// CID can (and sometimes does) correspond to more than one contract ID.
let services = Array.from(
backgroundtaskPhases[phaseName][listName].services || new Set([])
);
services = services
.map(contractID => {
try {
return Cm.contractIDToCID(contractID).toString();
} catch (e) {
return null;
}
})
.filter(cid => cid);
services.sort();
backgroundtaskPhases[phaseName][listName].services = new Set(services);
info(
`backgroundtaskPhases[${phaseName}][${listName}].services = ${JSON.stringify(
services.map(c => c.toString())
)}`
);
}
}
// Turn `{CID}` into `{CID} (@contractID)` or `{CID} (one of
// @contractID1, ..., @contractIDn)` as appropriate.
function renderResource(resource) {
const UUID_PATTERN =
/^\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}$/i;
if (UUID_PATTERN.test(resource)) {
let foundContractIDs = [];
for (let contractID of Cm.getContractIDs()) {
try {
if (resource == Cm.contractIDToCID(contractID).toString()) {
foundContractIDs.push(contractID);
}
} catch (e) {
// This can throw for contract IDs that are filtered. The common
// reason is that they're limited to a particular process.
}
}
if (!foundContractIDs.length) {
return `${resource} (CID with no human-readable contract IDs)`;
} else if (foundContractIDs.length == 1) {
return `${resource} (CID with human-readable contract ID ${foundContractIDs[0]})`;
}
foundContractIDs.sort();
return `${resource} (CID with human-readable contract IDs ${JSON.stringify(
foundContractIDs
)})`;
}
return resource;
}
for (let phase in backgroundtaskPhases) {
let loadedList = phases[phase];
let allowlist = backgroundtaskPhases[phase].allowlist || null;
if (allowlist) {
for (let scriptType in allowlist) {
loadedList[scriptType] = loadedList[scriptType].filter(c => {
if (!allowlist[scriptType].has(c)) {
return true;
}
allowlist[scriptType].delete(c);
return false;
});
Assert.deepEqual(
loadedList[scriptType],
[],
`${phase}: should have no unexpected ${scriptType} loaded`
);
// Present errors in deterministic order.
let unexpected = Array.from(loadedList[scriptType]);
unexpected.sort();
for (let script of unexpected) {
// It would be nice to show stacks here, but that can be follow-up.
ok(
false,
`${phase}: unexpected ${scriptType}: ${renderResource(script)}`
);
}
Assert.deepEqual(
allowlist[scriptType].size,
0,
`${phase}: all ${scriptType} allowlist entries should have been used`
);
let unused = Array.from(allowlist[scriptType]);
unused.sort();
for (let script of unused) {
ok(
false,
`${phase}: unused ${scriptType} allowlist entry: ${renderResource(
script
)}`
);
}
}
}
let denylist = backgroundtaskPhases[phase].denylist || null;
if (denylist) {
for (let scriptType in denylist) {
let resources = denylist[scriptType];
resources.sort();
for (let resource of resources) {
let loaded = loadedList[scriptType].includes(resource);
let message = `${phase}: ${renderResource(resource)} is not allowed`;
// It would be nice to show stacks here, but that can be follow-up.
ok(!loaded, message);
}
}
}
}
});