diff options
Diffstat (limited to 'toolkit/profile/xpcshell/head.js')
-rw-r--r-- | toolkit/profile/xpcshell/head.js | 623 |
1 files changed, 623 insertions, 0 deletions
diff --git a/toolkit/profile/xpcshell/head.js b/toolkit/profile/xpcshell/head.js new file mode 100644 index 0000000000..dc354f5b62 --- /dev/null +++ b/toolkit/profile/xpcshell/head.js @@ -0,0 +1,623 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +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( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +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); + +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", + default: false, + }, + { + name: "Profile3", + path: "Path3", + 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 (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"), + }; + + 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." + ); + + 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." + ); +} |