Application Update Concepts
This page lays out many of the fundamental concepts underlying the design of Application Update.
Staging
Update installation needs to happen when Firefox is not running. While running, Firefox is still using those files and reacts poorly to having them swapped out from underneath it. See Bug 1480452, Comment 8 for more details on this.
Applying updates can take some time. And since this happens at Firefox startup, the user is likely watching and waiting. To mitigate this, we default to staging updates. Staging involves doing most of the work of installing while Firefox is still running. Basically, it copies the installation into a different directory and updates the copy. Then, when Firefox is restarted, the updater does a “Replace Request” and swaps the installation with the updated copy and then deletes the original installation.
Staging can be disabled by setting app.update.staging.enabled to false.
Update Directory
The update directory is where we put all update related files as well as some of the update related preferences. On Windows and Linux, the update directory is installation-specific. On macOS, the update directory is specific to both the user and installation.
The path to the update directory can be found by navigating Firefox to
about:support and finding the entry called “Update folder”. Alternately, it
can be looked up in the browser console using
Services.dirsvc.get("UpdRootD", Ci.nsIFile).path.
Install and Uninstall Cleanup
When Firefox is uninstalled, we do our best to remove the files in the update directory in order to properly clean up after our installation.
When someone performs a paveover install (installing Firefox without uninstalling it first), we don’t delete the entire update directory, as this could cause settings to be lost. But we do delete any in-progress updates. This is to prevent situations such as a user disabling automatic update, downgrading, and then Firefox immediately installing an already-downloaded update.
Elevation
Firefox is often installed into directories with permissions that prevent regular, unprivileged processes from changing them. This means that Firefox may require elevation in order to update. We have several operating system-specific ways of dealing with this.
Windows
When updating on Windows, we have two options for elevation, the Mozilla Maintenance Service or a User Account Control (UAC) prompt. More details on the Mozilla Maintenance Service can be found here. If the Maintenance Service is not available and elevation is needed to update, Firefox must show a UAC prompt every time it updates.
macOS
Details on macOS elevated update can be found here.
Other
We do not currently have a method of elevating on other operating systems. We do provide a Linux package repository that can be used by Linux package managers which are better able to keep Linux installations up-to-date in a secure manner. These are the most highly recommended ways of updating on Linux. More information on using this method can be found here.
BITS
Windows has a component called the Background Intelligent Transfer Service, or BITS. This allows the operating system to download something for us. On Windows, we use BITS by default for update downloads, allowing the updates to continue downloading even after Firefox closes.
BITS has a number of limitations. We cannot easily connect to a BITS transfer that was started by another user. We also cannot easily get it to use Firefox’s proxy configuration. When we detect these situations, BITS will not be used.
Update Mutex and Update Sync Manager
The update mutex and the update sync manager are two similar but different components.
Early in Firefox startup, we initialize the update sync manager. This
essentially involves taking a non-exclusive lock on an installation-specific
file. When other instances of Firefox launch, they take another non-exclusive
lock on the same file. At certain stages in the update process, we check to see
if other instances are running by briefly attempting to take an exclusive lock
on the file. If we are unable to take it, we know that other Firefox processes
must still be running and we introduce delays into the update process in an
attempt to mitigate
Bug 1480452. This
behavior can be disabled with app.update.checkOnlyInstance.enabled. The delay
that we introduce into update can be changed by setting
app.update.checkOnlyInstance.timeout to the desired number of milliseconds to
delay for. Note that this timeout cannot be increased beyond 2 days.
The update mutex serves a different purpose. It is designed to keep two instances of Firefox from running the update process at the same time, interfering with each other. This is accomplished by taking the mutex during update initialization. If the mutex cannot be obtained, we hold off on running update until we obtain it. Firefox checks again to see if it can get it each time it does an update check.
Note that the User Interfaces for Firefox update cannot be used unless the current Firefox instance both has the mutex and is the only instance running.
Update State
In theory, the Application Update Service is a state machine. However, there are
two state machines running in parallel: the state machine of the update.status
file and the in-memory state machine of the Update Service itself.
The update.status state machine is older and describes a number of states that
are effectively variations of other states (ex: pending-service is basically a
variation of pending):
null: When there are no updates in-progress, the state file is deleted.nullrepresents this state."downloading": An update download is in-progress."pending": There is an update ready to be installed or staged. If Firefox starts with this state the update will be installed. Whether the updater runs staging or does the full installation depends on what arguments are passed. This state has some implications for any elevation that may be needed to install the update, which vary depending on the operating system. On Windows, this state means that the Mozilla Maintenance Service may not be used. If Windows elevation is needed to install the update, our only option is to display a User Account Control (UAC) prompt. On macOS, this state means that we have permission to update, even if elevation is needed. Since we do not support elevating on Linux, this has no elevation-related implications there."pending-service": This state is relevant only on Windows. There is an update ready to be installed or staged, and the Mozilla Maintenance Service can be used to do this."pending-elevate": This state is relevant only on macOS. There is an update ready to be installed, but we are not going to install it yet. On the next Firefox launch, we will display a notification telling the user that elevation is needed to install this update. Once this notification has been accepted, the state will be changed to"pending"."applying": This state is only written by the updater binary, not Firefox, and indicates that it is currently staging or installing an update."applied": The update has been successfully staged and is ready to be installed. Note that this state does not grant permission to use the Mozilla Maintenance Service."applied-service": The update has been successfully staged and is ready to be installed. The Mozilla Maintenance Service can be used to install it."failed": The update failed during staging or installation. Theupdate.statusfile will generally represent this state with the state name followed by an error code fromtoolkit/mozapps/update/common/updatererrors.h.
To make things slightly more complicated, these states are also used for the
state property of update objects used in the Update Service. The Update
Service currently tracks basically 3 types of updates.
Updates in the update history. These should generally always be in a
"succeeded","failed", or"download-failed"state. Note that"download-failed"should never get written out toupdate.statusand thus was not listed above.downloadingUpdateis the currently downloading update. This should always be in the"downloading"state.readyUpdateis an update that we have finished downloading but we have not tried to install yet (though we may have staged it). This should be in one of the"pending"or"applied"states. However, while the updater binary writes state changes back toupdate.statusto communicate them to Firefox, it has no awareness of these update objects so they will not be changed until Firefox updates them afterwards.
Note that if there is a readyUpdate and a downloadingUpdate at the same
time, update.status will reflect the status of the readyUpdate, not the
downloadingUpdate.
The Update Service state machine was added to resolve a couple of issues with
the update.status state machine:
Having the canonical state of the Update Service live on disk incurs more disk reads than necessary.
There are multiple states that, for most purposes, represent the same state (ex:
"pending"and"pending-service").The Update Service didn’t really have an existing way of checking if staging was still in progress that didn’t involve race conditions.
Multiple Firefox instances can exist simultaneously, but only one of them can drive update at once. This leads to a situation where the state on disk doesn’t really match the state of some Firefox instances.
Once support for multiple downloads per session was added, there was no good way of representing aspects of this in the existing state machine.
Because of this, a different but closely related state machine was added to
nsIApplicationUpdateService with these states:
STATE_IDLE: An instance of Firefox that doesn’t have the update mutex will always be in this state until it manages to get it. If the mutex is held, this state means that there is no update downloading, staging, or ready to be installed.STATE_DOWNLOADING: The update mutex is held and there is not a downloading, staging or ready-to-install update.STATE_STAGING: The update mutex is held, an update has been downloaded, and it is now being staged.STATE_PENDING: The update mutex is held and an update is ready to be installed.STATE_SWAP: The update mutex is held, an update has been readied, and now a second update has finished downloading. Firefox is now swapping the old update with the new update.
The following flowchart describes the update state transitions in terms of both types of update states. It describes the states from the perspective of the Firefox instance that holds the update mutex during this process. Note that some of the more unusual error cases are not shown here. The “happy path” (the simplest, most ideal flow) through the system has thicker arrows.
flowchart TD
%% If these state names aren't short and manual line-breaks aren't used,
%% we seem to end up with very small boxes with invisible text in them.
idle[AUS:IDLE<br>status:null]
check{Update Check}
waitToRetryDownload{{Wait}}
downloading[AUS:DOWNLOADING<br>status:downloading]
morePatchesToTryDownloading{More patches?}
maybeStage{Staging?}
stagingService[AUS: STAGING<br>status: pending-service]
stagingNoService[AUS: STAGING<br>status: pending]
launchStaging{{Run<br>Updater<br>binary}}
stagingStart[[status: applying]]
stagingSuccess[[status: applied]]
stagingFail[[status: failed <code>]]
stagingEnd{Firefox}
appliedMaybeService{Use MMS?}
appliedNoService[AUS:PENDING<br>status: applied]
appliedService[AUS:PENDING<br>status: applied-service]
doWeNeedElevation{Elevation<br>needed?}
pendingNoElevation[AUS: PENDING<br>status: pending]
pendingService[AUS: PENDING<br>status: pending-service]
pendingElevate[AUS: PENDING<br>status: pending-elevate]
waitForRestart{{Wait for<br>restart<br>or update}}
additionalDownload{{Found<br>update}}
additionalDownloadSuccessful[AUS: SWAP<br>status: null]
restarts{{Firefox restarts}}
askToElevate{Permission<br>to elevate?}
askToElevateAccepted[AUS: PENDING<br>status: pending]
waitForNewVersion{{Block this<br>version}}
launchUpdate{{Run<br>Updater<br>binary}}
updateStart[[status: applying]]
updateSuccess[[status: succeeded]]
updateFail[[status: failed <code>]]
updateEnd{Firefox}
completeMarFallback{Complete<br>MAR<br>available?}
updateComplete([Update complete!])
idle==>check
check== Update Found ==>downloading
check-- No Updates Found -->idle
downloading-- Transient<br>or BITS<br>Failure -->waitToRetryDownload-->downloading
downloading-- Patch verification failed -->morePatchesToTryDownloading
downloading-- Other failure -->idle
downloading== Download success ==>maybeStage
morePatchesToTryDownloading-- Yes -->downloading
morePatchesToTryDownloading-- No -->idle
maybeStage-- Yes, with Windows elevation -->stagingService
maybeStage== Yes, without elevation ==>stagingNoService
maybeStage-- No -->doWeNeedElevation
stagingService-->launchStaging
stagingNoService==>launchStaging
launchStaging==>stagingStart
stagingStart== Staging successful ==>stagingSuccess
stagingStart-- Staging failure -->stagingFail
stagingSuccess==>stagingEnd
stagingFail-->stagingEnd
stagingEnd== Success ==>appliedMaybeService
stagingEnd-- Staging Specific Error -->doWeNeedElevation
stagingEnd-- Maintenance Service Specific Error -->pendingNoElevation
stagingEnd-- Other Error -->completeMarFallback
appliedMaybeService-- Yes -->appliedService-->waitForRestart
appliedMaybeService== No ==>appliedNoService==>waitForRestart
doWeNeedElevation-- No or Maintenance Service is not available -->pendingNoElevation
doWeNeedElevation-- Windows elevation, Maintenance Service is available -->pendingService
doWeNeedElevation-- macOS elevation -->pendingElevate
pendingNoElevation-->waitForRestart
pendingService-->waitForRestart
pendingElevate-->waitForRestart
waitForRestart-->additionalDownload
waitForRestart==>restarts
additionalDownload-- Download Failed -->waitForRestart
additionalDownload-- Download Successful -->additionalDownloadSuccessful
additionalDownloadSuccessful-->maybeStage
restarts-- status is pending-elevate -->askToElevate
restarts== status is pending or pending-service ==>launchUpdate
askToElevate-- User declines -->waitForNewVersion
askToElevate-- User accepts -->askToElevateAccepted
askToElevateAccepted-->restarts
waitForNewVersion-->idle
launchUpdate==>updateStart
updateStart== Update successful ==>updateSuccess
updateStart-- Update failure -->updateFail
updateSuccess==>updateEnd
updateFail-->updateEnd
updateEnd-- Error writing or elevating -->doWeNeedElevation
updateEnd-- Maintenance Service specific error -->pendingNoElevation
updateEnd-- Other error -->completeMarFallback
updateEnd== Success ==>updateComplete
completeMarFallback-- Yes -->downloading
completeMarFallback-- No -->idle
Update Channel
There is one wrinkle that informs some confusing parts of update system and MAR design. We don’t want the update to change the channel of the installation, even if the update appears to be from a different channel. There are two main reasons for this. The first is that QA uses this in their testing. The second is release candidates.
Release candidates are slightly weird. They are built precisely as if they were meant for the Release channel. But we actually release them on the Beta channel. This is to help more thoroughly test for problems that might come from this slight change in the build process. Because of this, release candidate installers will not be advertised to users looking for the beta channel installer. But, when we serve a release candidate as an update, we expect the updater to produce an installation that is identical to a Release installation except that it will continue to be on the Beta channel.
In order to allow for this, we isolate the definition of our current update channel to just a few files. And we support having the update contain files that will only be installed if they do not already exist. This allows us to ensure that the updater will always leave those files untouched.
Examples of these files include default/pref/channel-prefs.js and
update-settings.ini on Windows and Linux. The macOS equivalents of these files
are Contents/Frameworks/ChannelPrefs.framework and
Contents/MacOS/updater.app/Contents/Frameworks/UpdateSettings.framework,
respectively.
precomplete
Sometimes, we remove files from the Firefox installation. This is a bit problematic for complete MARs. They are intended to be able to update from almost any version to the current version. But it’s tricky to have the complete MAR know what files should be removed when updating an arbitrary installed version.
We don’t want to just delete everything in the current installation, just in case there are, for some reason, files in our installation directory that do not really “belong to” our installation (i.e. files that the user placed there). Additionally, there are some files that are part of the installation that should never be deleted on update (see Update Channel, above).
This problem is solved by the precomplete file. When installing a complete
MAR, the updater looks for this file in the existing installation. The
precomplete file tells it what files and directories are a part of the
current installation so that the updater knows what to remove to effectively
“uninstall” the current installation before installing the updated installation.
Background Update
Unfortunately, background update has come to mean two things in Firefox. In the initial update design, we referred to updates without user interaction as background updates. This was to distinguish them from foreground updates, which involve user interaction. Later on, the Background Update Task (also known as the Background Update Agent) was added to update Firefox when it is not running. Confusingly, this is often shortened to “Background Update”.
While Firefox is running, background update (the in-app one, not the task) is
initiated via the TimerManager in
toolkit/components/timermanager/UpdateTimerManager.sys.mjs. The update timer
is added to the timer manager implicitly, via
toolkit/mozapps/update/nsUpdateService.manifest. The update checking interval
can be changed by setting app.update.interval to the desired number of
seconds. When the update timer expires, UpdateService.notify() is called,
initiating an update check.
The Background Update Task is a type of Background Task, which is basically just a copy of Firefox running in a stripped down, headless mode. It is described in detail, here, but a short summary will be provided below.
The Background Update Task is registered, if possible, at Firefox startup. There
are a number of reasons that it might not be registered, which are listed in the
definition of BackgroundUpdate.REASON. We only re-register the task if it
does not exist or if the task version has been changed since the last time we
registered the task. In order to ensure that this happens, be sure to update
TASK_DEF_CURRENT_VERSION when changing the definition of the task.
The Background Update Task essentially just runs update the same way that the
update UI would: via the AppUpdater. It requires that BITS be used
for downloads so that it can keep its run time brief. Note that in order to
prevent race conditions, even if the download completes instantly, the Update
Service is prevented from proceeding with update since the Task may already be
shutting down. It will continue the update process the next time the task runs.
Langpacks
One way of setting the language of a Firefox installation is via
langpacks.
When using a langpack, the updated langpack ought to be staged before Firefox
is updated. The Update Service currently takes care of this. When it starts an
update download, it also starts the process of downloading and staging a
langpack. It then waits for the langpack to be ready before it sets the
state to STATE_PENDING.
Update XMLs
There are actually three XMLs pertaining to update, each of which will be
described here. The format of all of them is basically equivalent. Each of them
are parsed into an array of Update objects (defined in
toolkit/mozapps/update/UpdateService.sys.mjs). Each Update can contain
multiple UpdatePatch objects. Generally speaking, an Update will contain a
patch for a complete MAR and possibly a patch for
a partial MAR, if one is available.
When Firefox checks for updates, the update server (Balrog) replies with an XML describing the most up-to-date update that is available from the installed version.
While updates are in-progress, they will be stored in active-update.xml. The
first update listed is the readyUpdate and the second is the
downloadingUpdate. If there is only one update in the XML in the downloading
state, it is the downloadingUpdate. If there is one update in another state,
it is the readyUpdate.
The update history is stored in updates.xml. It stores the last 10 successful
and failed updates, starting with the most recent.
Force Parameter
Balrog supports throttling updates. This is implemented by picking a ratio, on
the server side, to throttle the update at. Then, based on that probability,
pick to return one of two updates (generally the newest update or the one
before that). However, this can be overridden by passing the force query
parameter. When an update is initiated by the user (from the update UI),
force=1 is sent, overriding the probability and retrieving the newer update.
It is technically also possible to send force=0 to specifically request the
older update, but we do not use this in practice.
Pinning
Firefox has an update version pinning mechanism available as an enterprise policy. This causes a query parameter to be sent to convey the update pin to the server (Balrog). The pinning logic is entirely implemented on the server side. It works by keeping a table of the most recent version for each major and minor version. It then uses the pin requested to do a lookup in that table.
No Window Auto Restart
On macOS, Firefox (like most other applications) continues running even when the
last window is closed. Since the user isn’t using the browser at this point,
this is a good time for us to update. The problem is that Firefox can’t really
update while it is running. To address this, RestartOnLastWindowClosed was
added to toolkit/mozapps/update/UpdateService.sys.mjs. When the last Firefox
window is closed and an update is ready, RestartOnLastWindowClosed starts a
timer. When this expires, it attempts to perform a silent update in which no UI
is shown and we bail out if any kind of UI is needed. Firefox is then restarted
in a special state where no windows open.
This mechanism can be disabled by setting
app.update.noWindowAutoRestart.enabled to false. The delay until the restart
can be changed by setting app.update.noWindowAutoRestart.delayMs to the
desired number of milliseconds.