tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 1dc0dfe400cfa1fc8c650283404e1499bdba475a
parent 792a8ccf58ec9d8cc1adbc1e4a4a417c7018e485
Author: Mike Conley <mconley@mozilla.com>
Date:   Wed, 17 Dec 2025 16:19:02 +0000

Bug 2001387 - Part 2: Have FirstStartup provide a category hook for modules that need to block first-window opening for a new profile on Windows. r=Gijs

Differential Revision: https://phabricator.services.mozilla.com/D276295

Diffstat:
Mbrowser/app/profile/firefox.js | 1+
Mtoolkit/modules/FirstStartup.sys.mjs | 48++++++++++++++++++++++++++++++++++++++++++++----
Mtoolkit/modules/metrics.yaml | 23++++++++++++++++++-----
Atoolkit/modules/tests/xpcshell/FirstStartupCategoryModule.sys.mjs | 9+++++++++
Mtoolkit/modules/tests/xpcshell/test_firstStartup.js | 5+++++
Atoolkit/modules/tests/xpcshell/test_firstStartup_categoryTasks.js | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/modules/tests/xpcshell/xpcshell.toml | 5+++++
7 files changed, 242 insertions(+), 9 deletions(-)

diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js @@ -3318,6 +3318,7 @@ pref("devtools.popup.disable_autohide", false); // FirstStartup service time-out in ms pref("first-startup.timeout", 30000); +pref("first-startup.category-tasks-enabled", true); // Enable the default browser agent. // The agent still runs as scheduled if this pref is disabled, diff --git a/toolkit/modules/FirstStartup.sys.mjs b/toolkit/modules/FirstStartup.sys.mjs @@ -7,11 +7,14 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { + BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", Normandy: "resource://normandy/Normandy.sys.mjs", TaskScheduler: "resource://gre/modules/TaskScheduler.sys.mjs", }); const PREF_TIMEOUT = "first-startup.timeout"; +const PREF_CATEGORY_TASKS = "first-startup.category-tasks-enabled"; +const CATEGORY_NAME = "first-startup-new-profile"; /** * Service for blocking application startup, to be used on the first install. The intended @@ -63,12 +66,14 @@ export var FirstStartup = { let promises = []; let normandyInitEndTime = null; + let normandyInitPromise = null; if (AppConstants.MOZ_NORMANDY) { - promises.push( - lazy.Normandy.init({ runAsync: false }).finally(() => { + normandyInitPromise = lazy.Normandy.init({ runAsync: false }).finally( + () => { normandyInitEndTime = ChromeUtils.now(); - }) + } ); + promises.push(normandyInitPromise); } let deleteTasksEndTime = null; @@ -84,8 +89,37 @@ export var FirstStartup = { ); } + // Very important things that need to run before we launch the first window + // during new profile setup can register a hook with CATEGORY_NAME. + // + // Consumers should be aware that: + // + // * This blocks first startup window opening for new installs on Windows + // ONLY for the first created default profile + // * If PREF_TIMEOUT elapses before all of these promises complete, the + // registered category entries might not have all had a chance to + // successfully complete before the first browser window appears. + const CATEGORY_TASKS_ENABLED = Services.prefs.getBoolPref( + PREF_CATEGORY_TASKS, + false + ); + let categoryTasksEndTime = null; + if (CATEGORY_TASKS_ENABLED && AppConstants.MOZ_NORMANDY) { + promises.push( + normandyInitPromise.finally(() => { + return lazy.BrowserUtils.callModulesFromCategory({ + categoryName: CATEGORY_NAME, + profileMarker: "first-startup-new-profile-tasks", + idleDispatch: false, + }).finally(() => { + categoryTasksEndTime = ChromeUtils.now(); + }); + }) + ); + } + if (promises.length) { - Promise.all(promises).then(() => (initialized = true)); + Promise.allSettled(promises).then(() => (initialized = true)); this.elapsed = 0; Services.tm.spinEventLoopUntil("FirstStartup.sys.mjs:init", () => { @@ -115,6 +149,12 @@ export var FirstStartup = { ); } + if (CATEGORY_TASKS_ENABLED) { + Glean.firstStartup.categoryTasksTime.set( + Math.ceil(categoryTasksEndTime || ChromeUtils.now() - startingTime) + ); + } + Glean.firstStartup.statusCode.set(this._state); Glean.firstStartup.elapsed.set(this.elapsed); GleanPings.firstStartup.submit(); diff --git a/toolkit/modules/metrics.yaml b/toolkit/modules/metrics.yaml @@ -24,7 +24,6 @@ first_startup: data_sensitivity: - technical notification_emails: - - rhelmer@mozilla.com - mconley@mozilla.com expires: never send_in_pings: @@ -42,7 +41,6 @@ first_startup: data_sensitivity: - technical notification_emails: - - rhelmer@mozilla.com - mconley@mozilla.com expires: never send_in_pings: @@ -60,7 +58,6 @@ first_startup: data_sensitivity: - technical notification_emails: - - rhelmer@mozilla.com - mconley@mozilla.com expires: never send_in_pings: @@ -78,7 +75,24 @@ first_startup: data_sensitivity: - technical notification_emails: - - rhelmer@mozilla.com + - mconley@mozilla.com + expires: never + send_in_pings: + - first-startup + + category_tasks_time: + type: quantity + unit: milliseconds + description: > + Number of milliseconds until the tasks registered for first-startup-new-profile-task + have resolved. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=2001387 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=2001387 + data_sensitivity: + - technical + notification_emails: - mconley@mozilla.com expires: never send_in_pings: @@ -98,7 +112,6 @@ first_startup: data_sensitivity: - technical notification_emails: - - rhelmer@mozilla.com - mconley@mozilla.com expires: never send_in_pings: diff --git a/toolkit/modules/tests/xpcshell/FirstStartupCategoryModule.sys.mjs b/toolkit/modules/tests/xpcshell/FirstStartupCategoryModule.sys.mjs @@ -0,0 +1,9 @@ +export let FirstStartupCategoryModule = { + async firstStartupNewProfile() { + Services.obs.notifyObservers( + null, + "first-startup-category-task-called", + "executed" + ); + }, +}; diff --git a/toolkit/modules/tests/xpcshell/test_firstStartup.js b/toolkit/modules/tests/xpcshell/test_firstStartup.js @@ -7,6 +7,7 @@ const { updateAppInfo } = ChromeUtils.importESModule( "resource://testing-common/AppInfo.sys.mjs" ); +const CATEGORY_NAME = "first-startup-new-profile"; const PREF_TIMEOUT = "first-startup.timeout"; add_setup(function test_setup() { @@ -15,6 +16,10 @@ add_setup(function test_setup() { // FOG needs to be initialized in order for data to flow. Services.fog.initializeFOG(); + + // Delete any categories that have been registered statically so that we're + // just running the one here under test. + Services.catMan.deleteCategory(CATEGORY_NAME); }); add_task(async function test_success() { diff --git a/toolkit/modules/tests/xpcshell/test_firstStartup_categoryTasks.js b/toolkit/modules/tests/xpcshell/test_firstStartup_categoryTasks.js @@ -0,0 +1,160 @@ +"use strict"; + +const { FirstStartup } = ChromeUtils.importESModule( + "resource://gre/modules/FirstStartup.sys.mjs" +); +const { updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +const PREF_CATEGORY_TASKS = "first-startup.category-tasks-enabled"; +const CATEGORY_NAME = "first-startup-new-profile"; +const TEST_MODULE = "resource://test/FirstStartupCategoryModule.sys.mjs"; +const CATEGORY_TASK_TOPIC = "first-startup-category-task-called"; + +add_setup(function test_setup() { + do_get_profile(); + Services.fog.initializeFOG(); + // Delete any categories that have been registered statically so that we're + // just running the one here under test. + Services.catMan.deleteCategory(CATEGORY_NAME); +}); + +/** + * Test that category tasks registered with the first-startup-new-profile + * category are invoked during FirstStartup initialization, and that they run + * after Normandy initialization completes. Verifies that the + * categoryTasksTime Glean metric is recorded. + */ +add_task(async function test_categoryTasks_called_after_normandy() { + if (!AppConstants.MOZ_NORMANDY) { + info("Skipping test - MOZ_NORMANDY not enabled"); + return; + } + + updateAppInfo(); + Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true); + FirstStartup.resetForTesting(); + + let catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added"); + Services.catMan.addCategoryEntry( + CATEGORY_NAME, + TEST_MODULE, + "FirstStartupCategoryModule.firstStartupNewProfile", + false, + false + ); + await catManUpdated; + + const { Normandy } = ChromeUtils.importESModule( + "resource://normandy/Normandy.sys.mjs" + ); + let sandbox = sinon.createSandbox(); + let normandyInitComplete = false; + let originalInit = Normandy.init.bind(Normandy); + + sandbox.stub(Normandy, "init").callsFake(function (...args) { + return originalInit(...args).finally(() => { + normandyInitComplete = true; + }); + }); + + let categoryTaskCalled = false; + let categoryTaskPromise = TestUtils.topicObserved(CATEGORY_TASK_TOPIC).then( + ([_subj, data]) => { + Assert.ok( + Normandy.init.calledOnce, + "Normandy.init should have been called" + ); + Assert.ok( + normandyInitComplete, + "Normandy init should be complete before category task starts" + ); + categoryTaskCalled = true; + return data; + } + ); + + let submissionPromise = new Promise(resolve => { + GleanPings.firstStartup.testBeforeNextSubmit(() => { + Assert.equal(FirstStartup.state, FirstStartup.SUCCESS); + Assert.ok(Glean.firstStartup.newProfile.testGetValue()); + Assert.equal( + Glean.firstStartup.statusCode.testGetValue(), + FirstStartup.SUCCESS + ); + Assert.greater(Glean.firstStartup.normandyInitTime.testGetValue(), 0); + Assert.greater( + Glean.firstStartup.categoryTasksTime.testGetValue(), + 0, + "Category tasks time should be recorded" + ); + resolve(); + }); + }); + + FirstStartup.init(true); + + await submissionPromise; + + Assert.equal( + await categoryTaskPromise, + "executed", + "Category task should have been called" + ); + Assert.ok(categoryTaskCalled, "Category task was invoked"); + + sandbox.restore(); + Services.catMan.deleteCategoryEntry(CATEGORY_NAME, TEST_MODULE, false); + Services.prefs.clearUserPref(PREF_CATEGORY_TASKS); +}); + +/** + * Test that category tasks are not invoked when the category tasks pref is + * disabled, even when tasks are registered with the category. + */ +add_task(async function test_categoryTasks_disabled_by_pref() { + if (!AppConstants.MOZ_NORMANDY) { + info("Skipping test - MOZ_NORMANDY not enabled"); + return; + } + + updateAppInfo(); + Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, false); + FirstStartup.resetForTesting(); + + let catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added"); + Services.catMan.addCategoryEntry( + CATEGORY_NAME, + TEST_MODULE, + "FirstStartupCategoryModule.firstStartupNewProfile", + false, + false + ); + await catManUpdated; + + let categoryTaskCalled = false; + Services.obs.addObserver(() => { + categoryTaskCalled = true; + }, CATEGORY_TASK_TOPIC); + + let submissionPromise = new Promise(resolve => { + GleanPings.firstStartup.testBeforeNextSubmit(() => { + resolve(); + }); + }); + + FirstStartup.init(true); + await submissionPromise; + + Assert.ok( + !categoryTaskCalled, + "Category task should not be called when pref is false" + ); + + Services.catMan.deleteCategoryEntry(CATEGORY_NAME, TEST_MODULE, false); + Services.prefs.clearUserPref(PREF_CATEGORY_TASKS); +}); diff --git a/toolkit/modules/tests/xpcshell/xpcshell.toml b/toolkit/modules/tests/xpcshell/xpcshell.toml @@ -127,6 +127,11 @@ tags = "os_integration" ["test_UrlUtils.js"] ["test_firstStartup.js"] + +["test_firstStartup_categoryTasks.js"] +support-files = [ + "FirstStartupCategoryModule.sys.mjs", +] skip-if = [ "os == 'android'", ]