Last active
April 26, 2024 21:49
-
-
Save rhelmer/66c1804fe5c3c7d02727fbc31525c4a8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/browser/components/BrowserContentHandler.jsm b/browser/components/BrowserContentHandler.jsm | |
--- a/browser/components/BrowserContentHandler.jsm | |
+++ b/browser/components/BrowserContentHandler.jsm | |
@@ -19,6 +19,7 @@ XPCOMUtils.defineLazyModuleGetters(this, | |
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", | |
HeadlessShell: "resource:///modules/HeadlessShell.jsm", | |
HomePage: "resource:///modules/HomePage.jsm", | |
+ FirstStartup: "resource://gre/modules/FirstStartup.jsm", | |
LaterRun: "resource:///modules/LaterRun.jsm", | |
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", | |
SessionStartup: "resource:///modules/sessionstore/SessionStartup.jsm", | |
@@ -558,6 +559,10 @@ nsBrowserContentHandler.prototype = { | |
ShellService.setDefaultBrowser(true, true); | |
} | |
+ if (cmdLine.handleFlag("first-startup", false)) { | |
+ FirstStartup.init(); | |
+ } | |
+ | |
var fileParam = cmdLine.handleFlagWithParam("file", false); | |
if (fileParam) { | |
var file = cmdLine.resolveFile(fileParam); | |
@@ -599,6 +604,8 @@ nsBrowserContentHandler.prototype = { | |
info += | |
" --search <term> Search <term> with your default search engine.\n"; | |
info += " --setDefaultBrowser Set this app as the default browser.\n"; | |
+ info += | |
+ " --first-startup Run post-install actions before opening a new window.\n"; | |
return info; | |
}, | |
diff --git a/browser/installer/windows/nsis/installer.nsi b/browser/installer/windows/nsis/installer.nsi | |
--- a/browser/installer/windows/nsis/installer.nsi | |
+++ b/browser/installer/windows/nsis/installer.nsi | |
@@ -949,7 +949,7 @@ Function LaunchApp | |
${GetParameters} $0 | |
${GetOptions} "$0" "/UAC:" $1 | |
${If} ${Errors} | |
- ${ExecAndWaitForInputIdle} "$\"$INSTDIR\${FileMainEXE}$\"" | |
+ ${ExecAndWaitForInputIdle} "$\"$INSTDIR\${FileMainEXE}$\" -first-startup" | |
${Else} | |
GetFunctionAddress $0 LaunchAppFromElevatedProcess | |
UAC::ExecCodeSegment $0 | |
@@ -962,7 +962,7 @@ Function LaunchAppFromElevatedProcess | |
; Set our current working directory to the application's install directory | |
; otherwise the 7-Zip temp directory will be in use and won't be deleted. | |
SetOutPath "$INSTDIR" | |
- ${ExecAndWaitForInputIdle} "$\"$INSTDIR\${FileMainEXE}$\"" | |
+ ${ExecAndWaitForInputIdle} "$\"$INSTDIR\${FileMainEXE}$\" -first-startup" | |
FunctionEnd | |
Function SendPing | |
diff --git a/browser/installer/windows/nsis/stub.nsi b/browser/installer/windows/nsis/stub.nsi | |
--- a/browser/installer/windows/nsis/stub.nsi | |
+++ b/browser/installer/windows/nsis/stub.nsi | |
@@ -1789,9 +1789,9 @@ Function LaunchApp | |
${GetOptions} "$0" "/UAC:" $1 | |
${If} ${Errors} | |
${If} $CheckboxCleanupProfile == 1 | |
- ${ExecAndWaitForInputIdle} "$\"$INSTDIR\${FileMainEXE}$\" -reset-profile -migration" | |
+ ${ExecAndWaitForInputIdle} "$\"$INSTDIR\${FileMainEXE}$\" -reset-profile -migration -first-startup" | |
${Else} | |
- ${ExecAndWaitForInputIdle} "$\"$INSTDIR\${FileMainEXE}$\"" | |
+ ${ExecAndWaitForInputIdle} "$\"$INSTDIR\${FileMainEXE}$\" -first-startup" | |
${EndIf} | |
${Else} | |
StrCpy $R1 $CheckboxCleanupProfile | |
@@ -1807,9 +1807,9 @@ Function LaunchAppFromElevatedProcess | |
; Set the current working directory to the installation directory | |
SetOutPath "$INSTDIR" | |
${If} $R1 == 1 | |
- ${ExecAndWaitForInputIdle} "$\"$INSTDIR\${FileMainEXE}$\" -reset-profile -migration" | |
+ ${ExecAndWaitForInputIdle} "$\"$INSTDIR\${FileMainEXE}$\" -reset-profile -migration -first-startup" | |
${Else} | |
- ${ExecAndWaitForInputIdle} "$\"$INSTDIR\${FileMainEXE}$\"" | |
+ ${ExecAndWaitForInputIdle} "$\"$INSTDIR\${FileMainEXE}$\" -first-startup" | |
${EndIf} | |
FunctionEnd | |
diff --git a/toolkit/components/normandy/Normandy.jsm b/toolkit/components/normandy/Normandy.jsm | |
--- a/toolkit/components/normandy/Normandy.jsm | |
+++ b/toolkit/components/normandy/Normandy.jsm | |
@@ -43,7 +43,7 @@ var Normandy = { | |
studyPrefsChanged: {}, | |
rolloutPrefsChanged: {}, | |
- async init() { | |
+ async init({ runAsync = true } = {}) { | |
// Initialization that needs to happen before the first paint on startup. | |
await NormandyMigrations.applyAll(); | |
this.rolloutPrefsChanged = this.applyStartupPrefs( | |
@@ -53,8 +53,16 @@ var Normandy = { | |
STARTUP_EXPERIMENT_PREFS_BRANCH | |
); | |
- // Wait until the UI is available before finishing initialization. | |
- Services.obs.addObserver(this, UI_AVAILABLE_NOTIFICATION); | |
+ if (runAsync) { | |
+ Services.obs.addObserver(this, UI_AVAILABLE_NOTIFICATION); | |
+ } else { | |
+ // Remove any observers, if present. | |
+ try { | |
+ Services.obs.removeObserver(this, UI_AVAILABLE_NOTIFICATION); | |
+ } catch (e) {} | |
+ | |
+ await this.finishInit(); | |
+ } | |
}, | |
observe(subject, topic, data) { | |
diff --git a/toolkit/components/normandy/lib/RecipeRunner.jsm b/toolkit/components/normandy/lib/RecipeRunner.jsm | |
--- a/toolkit/components/normandy/lib/RecipeRunner.jsm | |
+++ b/toolkit/components/normandy/lib/RecipeRunner.jsm | |
@@ -238,7 +238,7 @@ var RecipeRunner = { | |
if (!this.enabled) { | |
return; | |
} | |
- this.run({ trigger: "sync" }); | |
+ await this.run({ trigger: "sync" }); | |
}; | |
gRemoteSettingsClient.on("sync", this._onSync); | |
diff --git a/toolkit/components/utils/ClientEnvironment.jsm b/toolkit/components/utils/ClientEnvironment.jsm | |
--- a/toolkit/components/utils/ClientEnvironment.jsm | |
+++ b/toolkit/components/utils/ClientEnvironment.jsm | |
@@ -36,6 +36,17 @@ ChromeUtils.defineModuleGetter( | |
"AppConstants", | |
"resource://gre/modules/AppConstants.jsm" | |
); | |
+ChromeUtils.defineModuleGetter( | |
+ this, | |
+ "FirstStartup", | |
+ "resource://gre/modules/FirstStartup.jsm" | |
+); | |
+ | |
+ChromeUtils.defineModuleGetter( | |
+ this, | |
+ "AttributionCode", | |
+ "resource:///modules/AttributionCode.jsm" | |
+); | |
var EXPORTED_SYMBOLS = ["ClientEnvironmentBase"]; | |
@@ -96,6 +107,11 @@ class ClientEnvironmentBase { | |
} | |
static get searchEngine() { | |
+ // Telemetry Environment is not available in early first-startup. | |
+ if (FirstStartup.state === FirstStartup.IN_PROGRESS) { | |
+ return undefined; | |
+ } | |
+ | |
return (async () => { | |
await TelemetryEnvironment.onInitialized(); | |
return TelemetryEnvironment.currentEnvironment.settings | |
@@ -164,6 +180,11 @@ class ClientEnvironmentBase { | |
} | |
static get os() { | |
+ // Telemetry Environment is not available in early first-startup. | |
+ if (FirstStartup.state === FirstStartup.IN_PROGRESS) { | |
+ return undefined; | |
+ } | |
+ | |
function coerceToNumber(version) { | |
const parts = version.split("."); | |
return parseFloat(parts.slice(0, 2).join(".")); | |
@@ -202,4 +223,8 @@ class ClientEnvironmentBase { | |
return rv; | |
})(); | |
} | |
+ | |
+ static get attribution() { | |
+ return AttributionCode.getAttrDataAsync(); | |
+ } | |
} | |
diff --git a/toolkit/modules/FirstStartup.jsm b/toolkit/modules/FirstStartup.jsm | |
new file mode 100644 | |
--- /dev/null | |
+++ b/toolkit/modules/FirstStartup.jsm | |
@@ -0,0 +1,78 @@ | |
+/* 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/. */ | |
+ | |
+var EXPORTED_SYMBOLS = ["FirstStartup"]; | |
+ | |
+const { AppConstants } = ChromeUtils.import( | |
+ "resource://gre/modules/AppConstants.jsm" | |
+); | |
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); | |
+const { XPCOMUtils } = ChromeUtils.import( | |
+ "resource://gre/modules/XPCOMUtils.jsm" | |
+); | |
+ | |
+XPCOMUtils.defineLazyModuleGetters(this, { | |
+ Normandy: "resource://normandy/Normandy.jsm", | |
+}); | |
+ | |
+const PREF_TIMEOUT = "first-startup.timeout"; | |
+const PROBE_NAME = "firstStartup.statusCode"; | |
+ | |
+/** | |
+ * Service for blocking application startup, to be used on the first install. The intended | |
+ * use case is for `FirstStartup` to be invoked when the application is called by an installer, | |
+ * such as the Windows Stub Installer, to allow the application to do some first-install tasks | |
+ * such as performance tuning and downloading critical data. | |
+ * | |
+ * In this scenario, the installer does not exit until the first application window appears, | |
+ * which gives the user experience of the application starting up quickly on first install. | |
+ */ | |
+var FirstStartup = { | |
+ NOT_STARTED: 0, | |
+ IN_PROGRESS: 1, | |
+ TIMED_OUT: 2, | |
+ SUCCESS: 3, | |
+ UNSUPPORTED: 4, | |
+ | |
+ _state: this.NOT_STARTED, | |
+ /** | |
+ * Initialize and run first-startup services. This will always run synchronously | |
+ * and spin the event loop until either all required services have | |
+ * completed, or until a timeout is reached. | |
+ * | |
+ * In the latter case, services are expected to run post-UI instead as usual. | |
+ */ | |
+ init() { | |
+ this._state = this.IN_PROGRESS; | |
+ const timeout = Services.prefs.getIntPref(PREF_TIMEOUT, 5000); // default to 5 seconds | |
+ let expiredTime = Date.now() + timeout; | |
+ | |
+ if (AppConstants.MOZ_NORMANDY) { | |
+ let normandyInitialized = false; | |
+ | |
+ Normandy.init({ runAsync: false }).then( | |
+ () => (normandyInitialized = true) | |
+ ); | |
+ | |
+ Services.tm.spinEventLoopUntil(() => { | |
+ if (Date.now() >= expiredTime) { | |
+ this._state = this.TIMED_OUT; | |
+ return true; | |
+ } else if (normandyInitialized) { | |
+ this._state = this.SUCCESS; | |
+ return true; | |
+ } | |
+ return false; | |
+ }); | |
+ } else { | |
+ this._state = this.UNSUPPORTED; | |
+ } | |
+ | |
+ Services.telemetry.scalarSet(PROBE_NAME, this._state); | |
+ }, | |
+ | |
+ get state() { | |
+ return this._state; | |
+ }, | |
+}; | |
diff --git a/toolkit/modules/docs/FirstStartup.rst b/toolkit/modules/docs/FirstStartup.rst | |
new file mode 100644 | |
--- /dev/null | |
+++ b/toolkit/modules/docs/FirstStartup.rst | |
@@ -0,0 +1,81 @@ | |
+.. _FirstStartup: | |
+ | |
+============== | |
+FirstStartup | |
+============== | |
+ | |
+``FirstStartup`` is a module which is invoked on application startup by the Windows Installer, | |
+to initialize services before the first application window appears. | |
+ | |
+This is useful for: | |
+ | |
+- one-time performance tuning | |
+- downloading critical data (hotfixes, experiments, etc) | |
+ | |
+Blocking until the first Application window appears is important because the Installer | |
+will show a progress bar until this happens. This gives a user experience of: | |
+ | |
+1. User downloads and starts the Windows Stub Installer. | |
+2. Progress bar advances while the application is downloaded and installed. | |
+3. Installer invokes the application with ``--first-startup``. | |
+4. Application window appears, and the installer window closes. | |
+ | |
+Overall, the user experiences a very fast first-startup, with critical tasks that normally | |
+would be deferred until after UI startup already complete. | |
+ | |
+.. _FirstStartup Architecture: | |
+ | |
+FirstStartup: Example use case | |
+============================== | |
+ | |
+An example use of the ``FirstStartup`` module is to invoke the Normandy client to download an experiment | |
+that will be used to customize the first-run page that Firefox shows. | |
+ | |
+In this example, the first-run page would be loaded experimentally based on an attribution code provided | |
+by the Installer. The flow for this looks like: | |
+ | |
+1. User clicks on download link containing an attribution (UTM) code(s). | |
+2. The download page serves a custom Windows Stub Installer with the appropriate attribution code embedded. | |
+3. The installer invokes Firefox with the `--first-startup` flag, which blocks the first window. | |
+4. Normandy is run by ``FirstStartup`` and downloads a list of available experiments, or "recipes". | |
+5. Recipes are evaluated and filtered based on local information, such as the OS platform and the attribution codes. | |
+6. A recipe is found which matches the current attribution code, and appropriate data is made available to the first-run page. | |
+7. ``FirstStartup`` completes and unblocks, which causes Firefox to show the first window and load the appropriate first-run data. | |
+ | |
+List of phases | |
+============== | |
+ | |
+``FirstStartup.NOT_STARTED`` | |
+ | |
+ The ``FirstStartup`` module has not been initalized (the ``init()`` | |
+ function has not been called). This is the default state. | |
+ | |
+``FirstStartup.IN_PROGRESS`` | |
+ | |
+ ``FirstStartup.init()`` has been called, and the event loop is | |
+ spinning. This state will persist until either all startup tasks | |
+ have finished, or time-out has been reached. | |
+ | |
+ The time-out defaults to 5 seconds, but is configurable via the | |
+ ``first-startup.timeout`` pref, which is specified in milliseconds. | |
+ | |
+``FirstStartup.TIMED_OUT`` | |
+ | |
+ The time-out has been reached before startup tasks are complete. | |
+ | |
+ This status code is reported to Telemetry via the ``firstStartup.statusCode`` | |
+ scalar. | |
+ | |
+``FirstStartup.SUCCESS`` | |
+ | |
+ All startup tasks have completed successfully, and application startup may resume. | |
+ | |
+ This status code is reported to Telemetry via the ``firstStartup.statusCode`` | |
+ scalar. | |
+ | |
+``FirstStartup.UNSUPPORTED`` | |
+ | |
+ No startup tasks are supported, and `FirstStartup` exited. | |
+ | |
+ This status code is reported to Telemetry via the ``firstStartup.statusCode`` | |
+ scalar. | |
diff --git a/toolkit/modules/docs/index.rst b/toolkit/modules/docs/index.rst | |
--- a/toolkit/modules/docs/index.rst | |
+++ b/toolkit/modules/docs/index.rst | |
@@ -8,3 +8,4 @@ The ``/toolkit/modules`` directory conta | |
:maxdepth: 1 | |
AsyncShutdown | |
+ FirstStartup | |
diff --git a/toolkit/modules/tests/xpcshell/test_firstStartup.js b/toolkit/modules/tests/xpcshell/test_firstStartup.js | |
new file mode 100644 | |
--- /dev/null | |
+++ b/toolkit/modules/tests/xpcshell/test_firstStartup.js | |
@@ -0,0 +1,50 @@ | |
+"use strict"; | |
+ | |
+const { AppConstants } = ChromeUtils.import( | |
+ "resource://gre/modules/AppConstants.jsm" | |
+); | |
+const { FirstStartup } = ChromeUtils.import( | |
+ "resource://gre/modules/FirstStartup.jsm" | |
+); | |
+ | |
+const PREF_TIMEOUT = "first-startup.timeout"; | |
+const PROBE_NAME = "firstStartup.statusCode"; | |
+ | |
+add_task(async function test_success() { | |
+ FirstStartup.init(); | |
+ if (AppConstants.MOZ_NORMANDY) { | |
+ equal(FirstStartup.state, FirstStartup.SUCCESS); | |
+ } else { | |
+ equal(FirstStartup.state, FirstStartup.UNSUPPORTED); | |
+ } | |
+ | |
+ const scalars = Services.telemetry.getSnapshotForScalars("main", false) | |
+ .parent; | |
+ ok(PROBE_NAME in scalars); | |
+ | |
+ if (AppConstants.MOZ_NORMANDY) { | |
+ equal(scalars[PROBE_NAME], FirstStartup.SUCCESS); | |
+ } else { | |
+ equal(scalars[PROBE_NAME], FirstStartup.UNSUPPORTED); | |
+ } | |
+}); | |
+ | |
+add_task(async function test_timeout() { | |
+ Services.prefs.setIntPref(PREF_TIMEOUT, 0); | |
+ FirstStartup.init(); | |
+ | |
+ if (AppConstants.MOZ_NORMANDY) { | |
+ equal(FirstStartup.state, FirstStartup.TIMED_OUT); | |
+ } else { | |
+ equal(FirstStartup.state, FirstStartup.UNSUPPORTED); | |
+ } | |
+ | |
+ const scalars = Services.telemetry.getSnapshotForScalars("main", false) | |
+ .parent; | |
+ ok(PROBE_NAME in scalars); | |
+ if (AppConstants.MOZ_NORMANDY) { | |
+ equal(scalars[PROBE_NAME], FirstStartup.TIMED_OUT); | |
+ } else { | |
+ equal(scalars[PROBE_NAME], FirstStartup.UNSUPPORTED); | |
+ } | |
+}); | |
diff --git a/toolkit/components/telemetry/Scalars.yaml b/toolkit/components/telemetry/Scalars.yaml | |
--- a/toolkit/components/telemetry/Scalars.yaml | |
+++ b/toolkit/components/telemetry/Scalars.yaml | |
@@ -4586,6 +4586,22 @@ blocklist: | |
record_in_processes: | |
- main | |
+firstStartup: | |
+ statusCode: | |
+ bug_numbers: | |
+ - 1515712 | |
+ description: >- | |
+ Status of the FirstRun service, which runs post-install/early-startup in Firefox. | |
+ expires: "75" | |
+ kind: uint | |
+ notification_emails: | |
+ - [email protected] | |
+ release_channel_collection: opt-out | |
+ products: | |
+ - 'firefox' | |
+ record_in_processes: | |
+ - main | |
+ | |
# The following section is for probes testing the Telemetry system. They will not be | |
# submitted in pings and are only used for testing. | |
telemetry.test: |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment