Push notification helper
The push notification helper lets web push notifications reach the user while
Firefox is closed. It’s a small Windows program, notification-helper.exe,
that keeps a connection to the push server (autopush) open on behalf of a
profile. When a push message arrives, it launches Firefox in the background so
the Push service can deliver the message to its service worker, which can then
show a notification.
The helper is Windows only for now. It’s built when MOZ_PUSH_NOTIFICATION_HELPER
is set, which is the default on Windows. --disable-push-notification-helper
turns it off.
Components
In toolkit/mozapps/pushnotificationhelper/:
src/: the Rust source ofnotification-helper.exe.main.rs: argument parsing, the main loop and launching Firefox.lifecycle.rs: the named Windows objects that make sure only one helper runs per profile and that let other processes stop it.pushconnection.rs: the WebSocket connection to autopush, built on WinHTTP.preferences.rs: reads and updates the profile’sprefs.js.
PushNotificationHelper.sys.mjs: decides whether a helper should run, based on prefs.WindowsPushNotificationHelper.sys.mjs: starts and stops the helper on Windows.PushNotificationHelperRegistry.sys.mjs: records running helpers in the registry, so the installer can restart them after an update.
In toolkit/components/push/:
PushCommandLineHandler.sys.mjs: handles the--receive-push-messagesargument that the helper passes when it launches Firefox.
Workflow
On startup, Firefox calls
PushNotificationHelper.init()from thebrowser-idle-startupcategory.If the helper is allowed and enabled (see Preferences),
WindowsPushNotificationHelper.start()launchesnotification-helper.exe --profile <profile directory>as a hidden, detached process, and records it in the registry. Otherwise it asks any helper for this profile to stop.The helper outlives the Firefox session that started it. It checks whether Firefox is running with its profile. While Firefox is running, Firefox’s own Push service talks to autopush, so the helper does nothing.
When Firefox isn’t running, the helper connects to autopush with the profile’s push identity (see Connection to autopush) and waits for a message.
When autopush sends a notification, the helper closes the connection and runs
firefox.exe --receive-push-messages --profile <profile directory>.PushCommandLineHandler.sys.mjskeeps Firefox running without opening a window. The Push service connects to autopush, receives the pending messages and dispatches them to their service workers.Once no messages have arrived for a while and all received messages have been handled, Firefox exits. The helper sleeps for the check interval (60 seconds by default) and goes back to step 3.
One helper per profile
Firefox starts the helper on every launch while it’s enabled, so finding one already running is the normal case. Each helper takes a named mutex for its profile when it starts. If the mutex already exists, another helper is serving that profile, so the new one exits with success. The mutex is released when the process exits.
A helper serves exactly one profile. Several profiles, or several installations, can each have their own helper at the same time.
The names of the Windows objects are built from a hash of the installation directory and the profile directory:
Local\MozillaNotificationHelper-profile-<hash>: the per profile mutex.Local\MozillaNotificationHelper-stop-<hash>: the per profile stop event.Global\MozillaNotificationHelper-stop-<hash>: the stop event shared by all helpers of an installation. Its hash only covers the installation directory.
Paths are canonicalized before hashing, so different spellings of the same directory map to the same objects.
Detecting a running Firefox
The helper doesn’t talk to Firefox. It checks the profile’s parent.lock file
instead. Firefox keeps this file open without sharing while it uses the
profile, so if the helper can’t open it, Firefox is running.
The helper only checks between connections. If Firefox starts while the helper is connected to autopush, the helper keeps its connection until the server closes it or a notification arrives. In the second case it launches Firefox again, and the already running Firefox ignores the extra command line.
The helper finds firefox.exe from the LastPlatformDir entry in the
profile’s compatibility.ini, or else from its own directory. It also uses
compatibility.ini to check that --profile names a real profile directory.
Connection to autopush
The helper reads two prefs from the profile’s prefs.js:
dom.push.userAgentID: the push identity (UAID) of the profile. Without it, the helper doesn’t connect.dom.push.serverURL: the autopush URL. It defaults towss://push.services.mozilla.com/.
It opens a WebSocket to that URL with WinHTTP, honoring the system proxy
settings, and sends a hello message with the profile’s UAID, like Firefox’s
Push service does. Then it waits for messages:
hello: the server’s reply. If it carries a different UAID, the helper writes the new value toprefs.js, so Firefox uses it next time.notification: a push message is waiting. The helper closes the connection and launches Firefox. It never acknowledges the message, so autopush keeps it and delivers it again to Firefox’s Push service.The server closing the connection: the helper goes back to its check loop.
The helper only knows that a message is waiting. It can’t decrypt it or tell which site or service worker it’s for. All of that happens in Firefox.
After each pass of the loop, whether it connected or not, the helper sleeps
for the check interval before checking again. The interval defaults to 60
seconds. The FX_NOTIFICATION_HELPER_CONNECTION_CHECK_TIMEOUT_SECS environment
variable overrides it, which is useful when testing.
Receiving messages in Firefox
PushCommandLineHandler.sys.mjs only acts when --receive-push-messages is on
the command line of a new Firefox process. It prevents the default window and
keeps Firefox alive while it waits for the Push service.
There’s no way to know how many messages are pending, so it waits until they stop arriving:
app.backgroundNotifications.receivePushMessages.perMessageTimeoutMs(default 5 seconds) restarts on every message. When it expires, receiving stops.app.backgroundNotifications.receivePushMessages.totalTimeoutMs(default 60 seconds) never restarts, so a stream of messages can’t keep Firefox alive forever. When it expires, new messages are ignored and left on the server for the next time.
A message counts as pending until its push-message-handled notification
fires, which happens after its service worker’s push event settles. Receiving
doesn’t stop while any message is pending. Then Firefox exits.
Stopping the helper
notification-helper.exe --stop --profile <profile directory> signals the per
profile stop event, so only that profile’s helper exits.
notification-helper.exe --stop signals the installation’s shared event, so
every helper of that installation exits. Both succeed when no helper is
running.
The shared event lives in the Global\ namespace and any authenticated user
can signal it. This lets the installer stop the helpers even when it runs
elevated or as a different user.
These are the callers:
WindowsPushNotificationHelper.stop()stops the profile’s helper when the helper is turned off. It only does so if the helper is recorded in the registry, so profiles that never had a helper don’t launch a process on every startup.The full installer, the stub installer and the uninstaller stop every helper of the installation before they check for files in use or remove files (
SignalPushNotificationHelperStop).
Restarting after an update
PushNotificationHelperRegistry.sys.mjs keeps one registry value per running
helper under HKCU\Software\<vendor>\<app name>\Notification Helper, which is
HKCU\Software\Mozilla\Firefox\Notification Helper for Firefox. The value
name is <install directory>|<profile directory>. On every start and stop,
values of the current installation whose profile directory no longer exists
are removed.
After an update, the PostUpdate step of the installer starts a helper for
every entry of its installation (StartPushNotificationHelpers). This runs in
the non-elevated part of PostUpdate, so the helpers run as the user.
Helpers aren’t restarted when Windows restarts. They come back the next time Firefox runs with their profile.
Preferences
app.backgroundNotifications.helper.available: whether the feature is available at all. Nimbus sets it through thepushNotificationHelperfeature inFeatureManifest.yaml.app.backgroundNotifications.helper.enabled: the user’s choice.
The helper runs only when both are true. Both default to false, and
PushNotificationHelper starts or stops the helper as soon as either one
changes.
app.backgroundNotifications.helper.loglevel controls the logging of the
JavaScript modules.
Telemetry
The metrics are defined in toolkit/mozapps/pushnotificationhelper/metrics.yaml
under the background_notification_helper category. They count how often the
user turns the helper on or off, how often it wakes Firefox, and how many
messages and notifications each wake delivers.
Tests
./mach rusttestsruns the Rust unit tests insrc/.toolkit/mozapps/pushnotificationhelper/tests/xpcshell/tests the JavaScript modules.toolkit/components/push/test/browser/tests the--receive-push-messageshandler.