Source code
Revision control
Copy as Markdown
Other Tools
/* Any copyright is dedicated to the Public Domain.
const { NetUtil } = ChromeUtils.importESModule(
"resource://gre/modules/NetUtil.sys.mjs"
);
const { FileUtils } = ChromeUtils.importESModule(
"resource://gre/modules/FileUtils.sys.mjs"
);
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
const { TelemetryTestUtils } = ChromeUtils.importESModule(
);
const NS_ERROR_START_PROFILE_MANAGER = 0x805800c9;
const UPDATE_CHANNEL = AppConstants.MOZ_UPDATE_CHANNEL;
let gProfD = do_get_profile();
let gDataHome = gProfD.clone();
gDataHome.append("data");
gDataHome.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
let gDataHomeLocal = gProfD.clone();
gDataHomeLocal.append("local");
gDataHomeLocal.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
let xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].getService(
Ci.nsIXREDirProvider
);
xreDirProvider.setUserDataDirectory(gDataHome, false);
xreDirProvider.setUserDataDirectory(gDataHomeLocal, true);
Services.dirsvc.set("UAppData", gDataHome);
let gProfilesRoot = gDataHome.clone();
let gProfilesTemp = gDataHomeLocal.clone();
if (!AppConstants.XP_UNIX || AppConstants.platform == "macosx") {
gProfilesRoot.append("Profiles");
gProfilesTemp.append("Profiles");
}
Services.dirsvc.set("DefProfRt", gProfilesRoot);
Services.dirsvc.set("DefProfLRt", gProfilesTemp);
let gIsDefaultApp = false;
const ShellService = {
register() {
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
let factory = {
createInstance(iid) {
return ShellService.QueryInterface(iid);
},
};
registrar.registerFactory(
this.ID,
"ToolkitShellService",
this.CONTRACT,
factory
);
},
isDefaultApplication() {
return gIsDefaultApp;
},
QueryInterface: ChromeUtils.generateQI(["nsIToolkitShellService"]),
ID: Components.ID("{ce724e0c-ed70-41c9-ab31-1033b0b591be}"),
CONTRACT: "@mozilla.org/toolkit/shell-service;1",
};
ShellService.register();
let gIsLegacy = false;
function enableLegacyProfiles() {
Services.env.set("MOZ_LEGACY_PROFILES", "1");
gIsLegacy = true;
}
function getProfileService() {
return Cc["@mozilla.org/toolkit/profile-service;1"].getService(
Ci.nsIToolkitProfileService
);
}
let PROFILE_DEFAULT = "default";
let DEDICATED_NAME = `default-${UPDATE_CHANNEL}`;
if (AppConstants.MOZ_DEV_EDITION) {
DEDICATED_NAME = PROFILE_DEFAULT = "dev-edition-default";
}
// Shared data for backgroundtasks tests.
const BACKGROUNDTASKS_PROFILE_DATA = (() => {
let hash = xreDirProvider.getInstallHash();
let profileData = {
options: {
startWithLastProfile: true,
},
profiles: [
{
name: "Profile1",
path: "Path1",
storeID: null,
default: false,
},
{
name: "Profile3",
path: "Path3",
storeID: null,
default: false,
},
],
installs: {
[hash]: {
default: "Path1",
},
},
backgroundTasksProfiles: [
{
name: `MozillaBackgroundTask-${hash}-unrelated_task`,
path: `saltsalt.MozillaBackgroundTask-${hash}-unrelated_task`,
},
],
};
return profileData;
})();
/**
* Creates a random profile path for use.
*/
function makeRandomProfileDir(name) {
let file = gDataHome.clone();
file.append(name);
file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
return file;
}
/**
* A wrapper around nsIToolkitProfileService.selectStartupProfile to make it
* a bit nicer to use from JS.
*/
function selectStartupProfile(args = [], isResetting = false, legacyHash = "") {
let service = getProfileService();
let rootDir = {};
let localDir = {};
let profile = {};
let didCreate = service.selectStartupProfile(
["xpcshell", ...args],
isResetting,
UPDATE_CHANNEL,
legacyHash,
rootDir,
localDir,
profile
);
if (profile.value) {
Assert.ok(
rootDir.value.equals(profile.value.rootDir),
"Should have matched the root dir."
);
Assert.ok(
localDir.value.equals(profile.value.localDir),
"Should have matched the local dir."
);
Assert.ok(
service.currentProfile === profile.value,
"Should have marked the profile as the current profile."
);
} else {
Assert.ok(!service.currentProfile, "Should be no current profile.");
}
return {
rootDir: rootDir.value,
localDir: localDir.value,
profile: profile.value,
didCreate,
};
}
function testStartsProfileManager(args = [], isResetting = false) {
try {
selectStartupProfile(args, isResetting);
Assert.ok(false, "Should have started the profile manager");
checkStartupReason();
} catch (e) {
Assert.equal(
e.result,
NS_ERROR_START_PROFILE_MANAGER,
"Should have started the profile manager"
);
}
}
function safeGet(ini, section, key) {
try {
return ini.getString(section, key);
} catch (e) {
return null;
}
}
/**
* Writes a compatibility.ini file that marks the give profile directory as last
* used by the given install path.
*/
function writeCompatibilityIni(
dir,
appDir = FileUtils.getDir("CurProcD", []),
greDir = FileUtils.getDir("GreD", [])
) {
let target = dir.clone();
target.append("compatibility.ini");
let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService(
Ci.nsIINIParserFactory
);
let ini = factory.createINIParser().QueryInterface(Ci.nsIINIParserWriter);
// The profile service doesn't care about these so just use fixed values
ini.setString(
"Compatibility",
"LastVersion",
"64.0a1_20180919123806/20180919123806"
);
ini.setString("Compatibility", "LastOSABI", "Darwin_x86_64-gcc3");
ini.setString(
"Compatibility",
"LastPlatformDir",
greDir.persistentDescriptor
);
ini.setString("Compatibility", "LastAppDir", appDir.persistentDescriptor);
ini.writeFile(target);
}
/**
* Writes a profiles.ini based on the passed profile data.
* profileData should contain two properties, options and profiles.
* options contains a single property, startWithLastProfile.
* profiles is an array of profiles each containing name, path and default
* properties.
*/
function writeProfilesIni(profileData) {
let target = gDataHome.clone();
target.append("profiles.ini");
let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService(
Ci.nsIINIParserFactory
);
let ini = factory.createINIParser().QueryInterface(Ci.nsIINIParserWriter);
const {
options = {},
profiles = [],
installs = null,
backgroundTasksProfiles = null,
} = profileData;
let { startWithLastProfile = true } = options;
ini.setString(
"General",
"StartWithLastProfile",
startWithLastProfile ? "1" : "0"
);
for (let i = 0; i < profiles.length; i++) {
let profile = profiles[i];
let section = `Profile${i}`;
ini.setString(section, "Name", profile.name);
ini.setString(section, "IsRelative", 1);
ini.setString(section, "Path", profile.path);
if ("storeID" in profile) {
ini.setString(section, "StoreID", profile.storeID);
}
if (profile.default) {
ini.setString(section, "Default", "1");
}
}
if (backgroundTasksProfiles) {
let section = "BackgroundTasksProfiles";
for (let backgroundTasksProfile of backgroundTasksProfiles) {
ini.setString(
section,
backgroundTasksProfile.name,
backgroundTasksProfile.path
);
}
}
if (installs) {
ini.setString("General", "Version", "2");
for (let hash of Object.keys(installs)) {
ini.setString(`Install${hash}`, "Default", installs[hash].default);
if ("locked" in installs[hash]) {
ini.setString(
`Install${hash}`,
"Locked",
installs[hash].locked ? "1" : "0"
);
}
}
writeInstallsIni({ installs });
} else {
writeInstallsIni(null);
}
ini.writeFile(target);
}
/**
* Reads the existing profiles.ini into the same structure as that accepted by
* writeProfilesIni above. The profiles property is sorted according to name
* because the order is irrelevant and it makes testing easier if we can make
* that assumption.
*/
function readProfilesIni() {
let target = gDataHome.clone();
target.append("profiles.ini");
let profileData = {
options: {
startWithLastProfile: true,
},
profiles: [],
installs: null,
};
if (!target.exists()) {
return profileData;
}
let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService(
Ci.nsIINIParserFactory
);
let ini = factory.createINIParser(target);
profileData.options.startWithLastProfile =
safeGet(ini, "General", "StartWithLastProfile") == "1";
if (safeGet(ini, "General", "Version") == "2") {
profileData.installs = {};
}
let sections = ini.getSections();
while (sections.hasMore()) {
let section = sections.getNext();
if (section == "General") {
continue;
}
if (section.startsWith("Profile")) {
let isRelative = safeGet(ini, section, "IsRelative");
if (isRelative === null) {
break;
}
Assert.equal(
isRelative,
"1",
"Paths should always be relative in these tests."
);
let profile = {
name: safeGet(ini, section, "Name"),
path: safeGet(ini, section, "Path"),
// TODO: currently, if there's a StoreID key but no value, this gets
// translated into JS as an empty string, while if there's no StoreID
// in the file at all, then it gets translated into JS as null.
// Work around this in the tests by converting empty strings to nulls,
// since otherwise some tests fail strict object comparisons.
storeID: safeGet(ini, section, "StoreID") || null,
};
try {
profile.default = ini.getString(section, "Default") == "1";
Assert.ok(
profile.default,
"The Default value is only written when true."
);
} catch (e) {
profile.default = false;
}
profileData.profiles.push(profile);
}
if (section.startsWith("Install")) {
Assert.ok(
profileData.installs,
"Should only see an install section if the ini version was correct."
);
profileData.installs[section.substring(7)] = {
default: safeGet(ini, section, "Default"),
};
let locked = safeGet(ini, section, "Locked");
if (locked !== null) {
profileData.installs[section.substring(7)].locked = locked;
}
}
if (section == "BackgroundTasksProfiles") {
profileData.backgroundTasksProfiles = [];
let backgroundTasksProfiles = ini.getKeys(section);
while (backgroundTasksProfiles.hasMore()) {
let name = backgroundTasksProfiles.getNext();
let path = ini.getString(section, name);
profileData.backgroundTasksProfiles.push({ name, path });
}
profileData.backgroundTasksProfiles.sort((a, b) =>
a.name.localeCompare(b.name)
);
}
}
profileData.profiles.sort((a, b) => a.name.localeCompare(b.name));
return profileData;
}
/**
* Writes an installs.ini based on the supplied data. Should be an object with
* keys for every installation hash each mapping to an object. Each object
* should have a default property for the relative path to the profile.
*/
function writeInstallsIni(installData) {
let target = gDataHome.clone();
target.append("installs.ini");
if (!installData) {
try {
target.remove(false);
} catch (e) {}
return;
}
const { installs = {} } = installData;
let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService(
Ci.nsIINIParserFactory
);
let ini = factory.createINIParser(null).QueryInterface(Ci.nsIINIParserWriter);
for (let hash of Object.keys(installs)) {
ini.setString(hash, "Default", installs[hash].default);
if ("locked" in installs[hash]) {
ini.setString(hash, "Locked", installs[hash].locked ? "1" : "0");
}
}
ini.writeFile(target);
}
/**
* Reads installs.ini into a structure like that used in the above function.
*/
function readInstallsIni() {
let target = gDataHome.clone();
target.append("installs.ini");
let installData = {
installs: {},
};
if (!target.exists()) {
return installData;
}
let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService(
Ci.nsIINIParserFactory
);
let ini = factory.createINIParser(target);
let sections = ini.getSections();
while (sections.hasMore()) {
let hash = sections.getNext();
if (hash != "General") {
installData.installs[hash] = {
default: safeGet(ini, hash, "Default"),
};
let locked = safeGet(ini, hash, "Locked");
if (locked !== null) {
installData.installs[hash].locked = locked;
}
}
}
return installData;
}
/**
* Check that the backup data in installs.ini matches the install data in
* profiles.ini.
*/
function checkBackup(
profileData = readProfilesIni(),
installData = readInstallsIni()
) {
if (!profileData.installs) {
// If the profiles db isn't of the right version we wouldn't expect the
// backup to be accurate.
return;
}
Assert.deepEqual(
profileData.installs,
installData.installs,
"Backup installs.ini should match installs in profiles.ini"
);
}
/**
* Checks that the profile service seems to have the right data in it compared
* to profile and install data structured as in the above functions.
*/
function checkProfileService(
profileData = readProfilesIni(),
verifyBackup = true
) {
let service = getProfileService();
let expectedStartWithLast = true;
if ("options" in profileData) {
expectedStartWithLast = profileData.options.startWithLastProfile;
}
Assert.equal(
service.startWithLastProfile,
expectedStartWithLast,
"Start with last profile should match."
);
let serviceProfiles = Array.from(service.profiles);
Assert.equal(
serviceProfiles.length,
profileData.profiles.length,
"Should be the same number of profiles."
);
// Sort to make matching easy.
serviceProfiles.sort((a, b) => a.name.localeCompare(b.name));
profileData.profiles.sort((a, b) => a.name.localeCompare(b.name));
let hash = xreDirProvider.getInstallHash();
let defaultPath =
profileData.installs && hash in profileData.installs
? profileData.installs[hash].default
: null;
let dedicatedProfile = null;
let legacyProfile = null;
for (let i = 0; i < serviceProfiles.length; i++) {
let serviceProfile = serviceProfiles[i];
let expectedProfile = profileData.profiles[i];
Assert.equal(
serviceProfile.name,
expectedProfile.name,
"Should have the same name."
);
let expectedPath = Cc["@mozilla.org/file/local;1"].createInstance(
Ci.nsIFile
);
expectedPath.setRelativeDescriptor(gDataHome, expectedProfile.path);
Assert.equal(
serviceProfile.rootDir.path,
expectedPath.path,
"Should have the same path."
);
// The StoreID is null if not present on the serviceProfile, so be sure
// we convert a possible missing storeID on expectedProfile from
// undefined to null.
Assert.equal(
serviceProfile.storeID,
expectedProfile.storeID || null,
"Should have the same (possibly null) StoreID."
);
if (expectedProfile.path == defaultPath) {
dedicatedProfile = serviceProfile;
}
if (AppConstants.MOZ_DEV_EDITION) {
if (expectedProfile.name == PROFILE_DEFAULT) {
legacyProfile = serviceProfile;
}
} else if (expectedProfile.default) {
legacyProfile = serviceProfile;
}
}
if (gIsLegacy || Services.env.get("SNAP_NAME")) {
Assert.equal(
service.defaultProfile,
legacyProfile,
"Should have seen the right profile selected."
);
} else {
Assert.equal(
service.defaultProfile,
dedicatedProfile,
"Should have seen the right profile selected."
);
}
if (verifyBackup) {
checkBackup(profileData);
}
}
// Maps the interesting scalar IDs to simple names that can be used as JS variables.
const SCALARS = {
selectionReason: "startup.profile_selection_reason",
databaseVersion: "startup.profile_database_version",
profileCount: "startup.profile_count",
};
function getTelemetryScalars() {
let scalars = TelemetryTestUtils.getProcessScalars("parent");
let results = {};
for (let [prop, scalarId] of Object.entries(SCALARS)) {
results[prop] = scalars[scalarId];
}
return results;
}
function checkStartupReason(expected = undefined) {
let { selectionReason } = getTelemetryScalars();
Assert.equal(
selectionReason,
expected,
"Should have seen the right startup reason."
);
}