1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 var EXPORTED_SYMBOLS = ["AppUpdater"];
9 var { XPCOMUtils } = ChromeUtils.importESModule(
10 "resource://gre/modules/XPCOMUtils.sys.mjs"
13 var gLogfileOutputStream;
15 const { AppConstants } = ChromeUtils.import(
16 "resource://gre/modules/AppConstants.jsm"
18 const { FileUtils } = ChromeUtils.import(
19 "resource://gre/modules/FileUtils.jsm"
21 const PREF_APP_UPDATE_LOG = "app.update.log";
22 const PREF_APP_UPDATE_LOG_FILE = "app.update.log.file";
23 const KEY_PROFILE_DIR = "ProfD";
24 const FILE_UPDATE_MESSAGES = "update_messages.log";
26 XPCOMUtils.defineLazyModuleGetters(lazy, {
27 UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
29 XPCOMUtils.defineLazyGetter(lazy, "gLogEnabled", function aus_gLogEnabled() {
31 Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false) ||
32 Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false)
35 XPCOMUtils.defineLazyGetter(
38 function aus_gLogfileEnabled() {
39 return Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false);
43 const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx";
44 const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never";
47 * This class checks for app updates in the foreground. It has several public
48 * methods for checking for updates, downloading updates, stopping the current
49 * update, and getting the current update status. It can also register
50 * listeners that will be called back as different stages of updates occur.
54 this._listeners = new Set();
55 XPCOMUtils.defineLazyServiceGetter(
58 "@mozilla.org/updates/update-service;1",
59 "nsIApplicationUpdateService"
61 XPCOMUtils.defineLazyServiceGetter(
64 "@mozilla.org/updates/update-checker;1",
67 XPCOMUtils.defineLazyServiceGetter(
70 "@mozilla.org/updates/update-manager;1",
73 this.QueryInterface = ChromeUtils.generateQI([
75 "nsIProgressEventSink",
77 "nsISupportsWeakReference",
79 Services.obs.addObserver(this, "update-swap", /* ownsWeak */ true);
81 // This one call observes PREF_APP_UPDATE_LOG and PREF_APP_UPDATE_LOG_FILE
82 Services.prefs.addObserver(PREF_APP_UPDATE_LOG, this);
86 * The main entry point for checking for updates. As different stages of the
87 * check and possible subsequent update occur, the updater's status is set and
88 * listeners are called.
91 if (!AppConstants.MOZ_UPDATER || this.updateDisabledByPackage) {
93 "AppUpdater:check -" +
94 "AppConstants.MOZ_UPDATER=" +
95 AppConstants.MOZ_UPDATER +
96 "this.updateDisabledByPackage: " +
97 this.updateDisabledByPackage
99 this._setStatus(AppUpdater.STATUS.NO_UPDATER);
103 if (this.updateDisabledByPolicy) {
104 LOG("AppUpdater:check - this.updateDisabledByPolicy");
105 this._setStatus(AppUpdater.STATUS.UPDATE_DISABLED_BY_POLICY);
109 if (this.isReadyForRestart) {
110 LOG("AppUpdater:check - this.isReadyForRestart");
111 this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
115 if (this.aus.isOtherInstanceHandlingUpdates) {
116 LOG("AppUpdater:check - this.aus.isOtherInstanceHandlingUpdates");
117 this._setStatus(AppUpdater.STATUS.OTHER_INSTANCE_HANDLING_UPDATES);
121 if (this.isDownloading) {
122 LOG("AppUpdater:check - this.isDownloading");
123 this.startDownload();
127 if (this.isStaging) {
128 LOG("AppUpdater:check - this.isStaging");
129 this._waitForUpdateToStage();
133 // We might need this value later, so start loading it from the disk now.
134 this.promiseAutoUpdateSetting = lazy.UpdateUtils.getAppUpdateAutoEnabled();
136 // That leaves the options
137 // "Check for updates, but let me choose whether to install them", and
138 // "Automatically install updates".
139 // In both cases, we check for updates without asking.
140 // In the "let me choose" case, we ask before downloading though, in onCheckComplete.
141 this.checkForUpdates();
144 // true when there is an update ready to be applied on restart or staged.
148 this.update.state == "pending" ||
149 this.update.state == "pending-service" ||
150 this.update.state == "pending-elevate"
154 this.um.readyUpdate &&
155 (this.um.readyUpdate.state == "pending" ||
156 this.um.readyUpdate.state == "pending-service" ||
157 this.um.readyUpdate.state == "pending-elevate")
161 // true when there is an update already staged.
165 this.update.state == "applied" || this.update.state == "applied-service"
169 this.um.readyUpdate &&
170 (this.um.readyUpdate.state == "applied" ||
171 this.um.readyUpdate.state == "applied-service")
176 if (!this.updateStagingEnabled) {
181 errorCode = this.update.errorCode;
182 } else if (this.um.readyUpdate) {
183 errorCode = this.um.readyUpdate.errorCode;
185 // If the state is pending and the error code is not 0, staging must have
187 return this.isPending && errorCode == 0;
190 // true when an update ready to restart to finish the update process.
191 get isReadyForRestart() {
192 if (this.updateStagingEnabled) {
195 errorCode = this.update.errorCode;
196 } else if (this.um.readyUpdate) {
197 errorCode = this.um.readyUpdate.errorCode;
199 // If the state is pending and the error code is not 0, staging must have
200 // failed and Firefox should be restarted to try to apply the update
202 return this.isApplied || (this.isPending && errorCode != 0);
204 return this.isPending;
207 // true when there is an update download in progress.
208 get isDownloading() {
210 return this.update.state == "downloading";
213 this.um.downloadingUpdate &&
214 this.um.downloadingUpdate.state == "downloading"
218 // true when updating has been disabled by enterprise policy
219 get updateDisabledByPolicy() {
220 return Services.policies && !Services.policies.isAllowed("appUpdate");
223 // true if updating is disabled because we're running in an app package.
224 // This is distinct from updateDisabledByPolicy because we need to avoid
225 // messages being shown to the user about an "administrator" handling
226 // updates; packaged apps may be getting updated by an administrator or they
227 // may not be, and we don't have a good way to tell the difference from here,
228 // so we err to the side of less confusion for unmanaged users.
229 get updateDisabledByPackage() {
231 return Services.sysinfo.getProperty("hasWinPackageId");
233 // The hasWinPackageId property doesn't exist; assume it would be false.
238 // true when updating in background is enabled.
239 get updateStagingEnabled() {
241 "AppUpdater:updateStagingEnabled" +
242 "canStageUpdates: " +
243 this.aus.canStageUpdates
246 !this.updateDisabledByPolicy &&
247 !this.updateDisabledByPackage &&
248 this.aus.canStageUpdates
256 // Clear prefs that could prevent a user from discovering available updates.
257 if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) {
258 Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
260 if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
261 Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER);
263 this._setStatus(AppUpdater.STATUS.CHECKING);
264 this.checker.checkForUpdates(this._updateCheckListener, true);
265 // after checking, onCheckComplete() is called
266 LOG("AppUpdater:checkForUpdates - waiting for onCheckComplete()");
270 * Implements nsIUpdateCheckListener. The methods implemented by
271 * nsIUpdateCheckListener are in a different scope from nsIIncrementalDownload
272 * to make it clear which are used by each interface.
274 get _updateCheckListener() {
275 if (!this.__updateCheckListener) {
276 this.__updateCheckListener = {
278 * See nsIUpdateService.idl
280 onCheckComplete: async (aRequest, aUpdates) => {
281 LOG("AppUpdater:_updateCheckListener:onCheckComplete - reached.");
282 this.update = this.aus.selectUpdate(aUpdates);
285 "AppUpdater:_updateCheckListener:onCheckComplete - result: " +
288 this._setStatus(AppUpdater.STATUS.NO_UPDATES_FOUND);
292 if (this.update.unsupported) {
294 "AppUpdater:_updateCheckListener:onCheckComplete - result: " +
297 this._setStatus(AppUpdater.STATUS.UNSUPPORTED_SYSTEM);
301 if (!this.aus.canApplyUpdates) {
303 "AppUpdater:_updateCheckListener:onCheckComplete - result: " +
306 this._setStatus(AppUpdater.STATUS.MANUAL_UPDATE);
310 if (!this.promiseAutoUpdateSetting) {
311 this.promiseAutoUpdateSetting = lazy.UpdateUtils.getAppUpdateAutoEnabled();
313 this.promiseAutoUpdateSetting.then(updateAuto => {
314 if (updateAuto && !this.aus.manualUpdateOnly) {
316 "AppUpdater:_updateCheckListener:onCheckComplete - " +
317 "updateAuto is active and " +
318 "manualUpdateOnlydateOnly is inactive." +
319 "start the download."
321 // automatically download and install
322 this.startDownload();
325 this._setStatus(AppUpdater.STATUS.DOWNLOAD_AND_INSTALL);
331 * See nsIUpdateService.idl
333 onError: async (aRequest, aUpdate) => {
334 // Errors in the update check are treated as no updates found. If the
335 // update check fails repeatedly without a success the user will be
336 // notified with the normal app update user interface so this is safe.
337 LOG("AppUpdater:_updateCheckListener:onError: NO_UPDATES_FOUND");
338 this._setStatus(AppUpdater.STATUS.NO_UPDATES_FOUND);
342 * See nsISupports.idl
344 QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckListener"]),
347 return this.__updateCheckListener;
351 * Sets the status to STAGING. The status will then be set again when the
352 * update finishes staging.
354 _waitForUpdateToStage() {
356 this.update = this.um.readyUpdate;
358 this.update.QueryInterface(Ci.nsIWritablePropertyBag);
359 this.update.setProperty("foregroundDownload", "true");
360 this._setStatus(AppUpdater.STATUS.STAGING);
361 this._awaitStagingComplete();
365 * Starts the download of an update mar.
369 this.update = this.um.downloadingUpdate;
371 this.update.QueryInterface(Ci.nsIWritablePropertyBag);
372 this.update.setProperty("foregroundDownload", "true");
374 let success = this.aus.downloadUpdate(this.update, false);
376 LOG("AppUpdater:startDownload - downloadUpdate failed.");
377 this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
381 this._setupDownloadListener();
385 * Starts tracking the download.
387 _setupDownloadListener() {
388 this._setStatus(AppUpdater.STATUS.DOWNLOADING);
389 this.aus.addDownloadListener(this);
390 LOG("AppUpdater:_setupDownloadListener - registered a download listener");
394 * See nsIRequestObserver.idl
396 onStartRequest(aRequest) {
397 LOG("AppUpdater:onStartRequest - aRequest: " + aRequest);
401 * See nsIRequestObserver.idl
403 onStopRequest(aRequest, aStatusCode) {
405 "AppUpdater:onStopRequest " +
411 switch (aStatusCode) {
412 case Cr.NS_ERROR_UNEXPECTED:
414 this.update.selectedPatch.state == "download-failed" &&
415 (this.update.isCompleteUpdate || this.update.patchCount != 2)
417 // Verification error of complete patch, informational text is held in
418 // the update object.
419 this.aus.removeDownloadListener(this);
421 "AppUpdater:onStopRequest " +
422 "- download failed with unexpected error" +
423 ", removed download listener"
425 this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
428 // Verification failed for a partial patch, complete patch is now
429 // downloading so return early and do NOT remove the download listener!
431 case Cr.NS_BINDING_ABORTED:
432 // Do not remove UI listener since the user may resume downloading again.
435 this.aus.removeDownloadListener(this);
437 "AppUpdater:onStopRequest " +
439 ", removed download listener"
441 if (this.updateStagingEnabled) {
442 // It could be that another instance was started during the download,
443 // and if that happened, then we actually should not advance to the
444 // STAGING status because the staging process isn't really happening
445 // until that instance exits (or we time out waiting).
446 if (this.aus.isOtherInstanceHandlingUpdates) {
448 "AppUpdater:onStopRequest " +
449 "- aStatusCode=Cr.NS_OK" +
450 ", another instance is handling updates"
452 this._setStatus(AppUpdater.OTHER_INSTANCE_HANDLING_UPDATES);
455 "AppUpdater:onStopRequest " +
456 "- aStatusCode=Cr.NS_OK" +
457 ", no competitive instance found."
459 this._setStatus(AppUpdater.STATUS.STAGING);
461 // But we should register the staging observer in either case, because
462 // if we do time out waiting for the other instance to exit, then
463 // staging really will start at that point.
464 this._awaitStagingComplete();
466 this._awaitDownloadComplete();
470 this.aus.removeDownloadListener(this);
472 "AppUpdater:onStopRequest " +
474 ", removing download listener" +
475 ", because the download failed."
477 this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
483 * See nsIProgressEventSink.idl
485 onStatus(aRequest, aStatus, aStatusArg) {
487 "AppUpdater:onStatus " +
498 * See nsIProgressEventSink.idl
500 onProgress(aRequest, aProgress, aProgressMax) {
502 "AppUpdater:onProgress " +
510 this._setStatus(AppUpdater.STATUS.DOWNLOADING, aProgress, aProgressMax);
514 * This function registers an observer that watches for the download
515 * to complete. Once it does, it updates the status accordingly.
517 _awaitDownloadComplete() {
518 let observer = (aSubject, aTopic, aData) => {
519 // Update the UI when the download is finished
521 "AppUpdater:_awaitStagingComplete - observer reached" +
522 ", status changes to READY_FOR_RESTART"
524 this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
525 Services.obs.removeObserver(observer, "update-downloaded");
527 Services.obs.addObserver(observer, "update-downloaded");
531 * This function registers an observer that watches for the staging process
532 * to complete. Once it does, it sets the status to either request that the
533 * user restarts to install the update on success, request that the user
534 * manually download and install the newer version, or automatically download
535 * a complete update if applicable.
537 _awaitStagingComplete() {
538 let observer = (aSubject, aTopic, aData) => {
540 "AppUpdater:_awaitStagingComplete:observer" +
545 "- aData (=status): " +
548 // Update the UI when the background updater is finished
550 case "update-staged":
553 status == "applied" ||
554 status == "applied-service" ||
555 status == "pending" ||
556 status == "pending-service" ||
557 status == "pending-elevate"
559 // If the update is successfully applied, or if the updater has
560 // fallen back to non-staged updates, show the "Restart to Update"
562 this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
563 } else if (status == "failed") {
564 // Background update has failed, let's show the UI responsible for
565 // prompting the user to update manually.
566 this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
567 } else if (status == "downloading") {
568 // We've fallen back to downloading the complete update because the
569 // partial update failed to get staged in the background.
570 // Therefore we need to keep our observer.
571 this._setupDownloadListener();
576 this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
579 Services.obs.removeObserver(observer, "update-staged");
580 Services.obs.removeObserver(observer, "update-error");
582 Services.obs.addObserver(observer, "update-staged");
583 Services.obs.addObserver(observer, "update-error");
587 * Stops the current check for updates and any ongoing download.
590 LOG("AppUpdater:stop called, remove download listener");
591 this.checker.stopCurrentCheck();
592 this.aus.removeDownloadListener(this);
596 * {AppUpdater.STATUS} The status of the current check or update.
600 if (!AppConstants.MOZ_UPDATER || this.updateDisabledByPackage) {
601 LOG("AppUpdater:status - no updater or updates disabled by package.");
602 this._status = AppUpdater.STATUS.NO_UPDATER;
603 } else if (this.updateDisabledByPolicy) {
604 LOG("AppUpdater:status - updateDisabledByPolicy");
605 this._status = AppUpdater.STATUS.UPDATE_DISABLED_BY_POLICY;
606 } else if (this.isReadyForRestart) {
607 LOG("AppUpdater:status - isReadyForRestart");
608 this._status = AppUpdater.STATUS.READY_FOR_RESTART;
609 } else if (this.aus.isOtherInstanceHandlingUpdates) {
610 LOG("AppUpdater:status - another instance is handling updates");
611 this._status = AppUpdater.STATUS.OTHER_INSTANCE_HANDLING_UPDATES;
612 } else if (this.isDownloading) {
613 LOG("AppUpdater:status - isDownloading");
614 this._status = AppUpdater.STATUS.DOWNLOADING;
615 } else if (this.isStaging) {
616 LOG("AppUpdater:status - isStaging");
617 this._status = AppUpdater.STATUS.STAGING;
619 LOG("AppUpdater:status - NEVER_CHECKED");
620 this._status = AppUpdater.STATUS.NEVER_CHECKED;
627 * Adds a listener function that will be called back on status changes as
628 * different stages of updates occur. The function will be called without
629 * arguments for most status changes; see the comments around the STATUS value
630 * definitions below. This is safe to call multiple times with the same
631 * function. It will be added only once.
633 * @param {function} listener
634 * The listener function to add.
636 addListener(listener) {
637 this._listeners.add(listener);
641 * Removes a listener. This is safe to call multiple times with the same
642 * function, or with a function that was never added.
644 * @param {function} listener
645 * The listener function to remove.
647 removeListener(listener) {
648 this._listeners.delete(listener);
652 * Sets the updater's current status and calls listeners.
654 * @param {AppUpdater.STATUS} status
655 * The new updater status.
656 * @param {*} listenerArgs
657 * Arguments to pass to listeners.
659 _setStatus(status, ...listenerArgs) {
660 this._status = status;
661 for (let listener of this._listeners) {
662 listener(status, ...listenerArgs);
667 observe(subject, topic, status) {
669 "AppUpdater:observe " +
679 this._handleUpdateSwap();
681 case "nsPref:changed":
683 status == PREF_APP_UPDATE_LOG ||
684 status == PREF_APP_UPDATE_LOG_FILE
686 lazy.gLogEnabled; // Assigning this before it is lazy-loaded is an error.
688 Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false) ||
689 Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false);
692 case "quit-application":
693 Services.prefs.removeObserver(PREF_APP_UPDATE_LOG, this);
694 Services.obs.removeObserver(this, topic);
698 _handleUpdateSwap() {
699 // This function exists to deal with the fact that we support handling 2
700 // updates at once: a ready update and a downloading update. But AppUpdater
701 // only ever really considers a single update at a time.
702 // We see an update swap just when the downloading update has finished
703 // downloading and is being swapped into UpdateManager.readyUpdate. At this
704 // point, we are in one of two states. Either:
705 // a) The update that is being swapped in is the update that this
706 // AppUpdater has already been tracking, or
707 // b) We've been tracking the ready update. Now that the downloading
708 // update is about to be swapped into the place of the ready update, we
709 // need to switch over to tracking the new update.
711 this._status == AppUpdater.STATUS.DOWNLOADING ||
712 this._status == AppUpdater.STATUS.STAGING
714 // We are already tracking the correct update.
718 if (this.updateStagingEnabled) {
719 LOG("AppUpdater:_handleUpdateSwap - updateStagingEnabled");
720 this._setStatus(AppUpdater.STATUS.STAGING);
721 this._awaitStagingComplete();
723 LOG("AppUpdater:_handleUpdateSwap - updateStagingDisabled");
724 this._setStatus(AppUpdater.STATUS.DOWNLOADING);
725 this._awaitDownloadComplete();
730 AppUpdater.STATUS = {
731 // Updates are allowed and there's no downloaded or staged update, but the
732 // AppUpdater hasn't checked for updates yet, so it doesn't know more than
736 // The updater isn't available (AppConstants.MOZ_UPDATER is falsey).
739 // "appUpdate" is not allowed by policy.
740 UPDATE_DISABLED_BY_POLICY: 2,
742 // Another app instance is handling updates.
743 OTHER_INSTANCE_HANDLING_UPDATES: 3,
745 // There's an update, but it's not supported on this system.
746 UNSUPPORTED_SYSTEM: 4,
748 // The user must apply updates manually.
751 // The AppUpdater is checking for updates.
754 // The AppUpdater checked for updates and none were found.
757 // The AppUpdater is downloading an update. Listeners are notified of this
758 // status as a download starts. They are also notified on download progress,
759 // and in that case they are passed two arguments: the current download
760 // progress and the total download size.
763 // The AppUpdater tried to download an update but it failed.
766 // There's an update available, but the user wants us to ask them to download
768 DOWNLOAD_AND_INSTALL: 10,
770 // An update is staging.
773 // An update is downloaded and staged and will be applied on restart.
774 READY_FOR_RESTART: 12,
777 * Is the given `status` a terminal state in the update state machine?
779 * A terminal state means that the `check()` method has completed.
781 * N.b.: `DOWNLOAD_AND_INSTALL` is not considered terminal because the normal
782 * flow is that Firefox will show UI prompting the user to install, and when
783 * the user interacts, the `check()` method will continue through the update
786 * @returns {boolean} `true` if `status` is terminal.
788 isTerminalStatus(status) {
790 AppUpdater.STATUS.CHECKING,
791 AppUpdater.STATUS.DOWNLOAD_AND_INSTALL,
792 AppUpdater.STATUS.DOWNLOADING,
793 AppUpdater.STATUS.NEVER_CHECKED,
794 AppUpdater.STATUS.STAGING,
799 * Turn the given `status` into a string for debugging.
801 * @returns {?string} representation of given numerical `status`.
803 debugStringFor(status) {
804 for (let [k, v] of Object.entries(AppUpdater.STATUS)) {
814 * Logs a string to the error console. If enabled, also logs to the update
817 * The string to write to the error console.
819 function LOG(string) {
820 if (lazy.gLogEnabled) {
821 dump("*** AUS:AUM " + string + "\n");
822 if (!Cu.isInAutomation) {
823 Services.console.logStringMessage("AUS:AUM " + string);
826 if (lazy.gLogfileEnabled) {
827 if (!gLogfileOutputStream) {
828 let logfile = Services.dirsvc.get(KEY_PROFILE_DIR, Ci.nsIFile);
829 logfile.append(FILE_UPDATE_MESSAGES);
830 gLogfileOutputStream = FileUtils.openAtomicFileOutputStream(logfile);
834 let encoded = new TextEncoder().encode(string + "\n");
835 gLogfileOutputStream.write(encoded, encoded.length);
836 gLogfileOutputStream.flush();
838 dump("*** AUS:AUM Unable to write to messages file: " + e + "\n");
839 Services.console.logStringMessage(
840 "AUS:AUM Unable to write to messages file: " + e