зеркало из https://github.com/mozilla/gecko-dev.git
644 строки
20 KiB
JavaScript
644 строки
20 KiB
JavaScript
/* 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/. */
|
|
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = ["AppUpdater"];
|
|
|
|
var { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
AppConstants: "resource://gre/modules/AppConstants.jsm",
|
|
Services: "resource://gre/modules/Services.jsm",
|
|
UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
|
|
});
|
|
|
|
const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx";
|
|
const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never";
|
|
|
|
/**
|
|
* This class checks for app updates in the foreground. It has several public
|
|
* methods for checking for updates, downloading updates, stopping the current
|
|
* update, and getting the current update status. It can also register
|
|
* listeners that will be called back as different stages of updates occur.
|
|
*/
|
|
class AppUpdater {
|
|
constructor() {
|
|
this._listeners = new Set();
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"aus",
|
|
"@mozilla.org/updates/update-service;1",
|
|
"nsIApplicationUpdateService"
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"checker",
|
|
"@mozilla.org/updates/update-checker;1",
|
|
"nsIUpdateChecker"
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"um",
|
|
"@mozilla.org/updates/update-manager;1",
|
|
"nsIUpdateManager"
|
|
);
|
|
this.QueryInterface = ChromeUtils.generateQI([
|
|
"nsIObserver",
|
|
"nsIProgressEventSink",
|
|
"nsIRequestObserver",
|
|
"nsISupportsWeakReference",
|
|
]);
|
|
Services.obs.addObserver(this, "update-swap", /* ownsWeak */ true);
|
|
}
|
|
|
|
/**
|
|
* The main entry point for checking for updates. As different stages of the
|
|
* check and possible subsequent update occur, the updater's status is set and
|
|
* listeners are called.
|
|
*/
|
|
check() {
|
|
if (!AppConstants.MOZ_UPDATER || this.updateDisabledByPackage) {
|
|
this._setStatus(AppUpdater.STATUS.NO_UPDATER);
|
|
return;
|
|
}
|
|
|
|
if (this.updateDisabledByPolicy) {
|
|
this._setStatus(AppUpdater.STATUS.UPDATE_DISABLED_BY_POLICY);
|
|
return;
|
|
}
|
|
|
|
if (this.isReadyForRestart) {
|
|
this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
|
|
return;
|
|
}
|
|
|
|
if (this.aus.isOtherInstanceHandlingUpdates) {
|
|
this._setStatus(AppUpdater.STATUS.OTHER_INSTANCE_HANDLING_UPDATES);
|
|
return;
|
|
}
|
|
|
|
if (this.isDownloading) {
|
|
this.startDownload();
|
|
return;
|
|
}
|
|
|
|
if (this.isStaging) {
|
|
this._waitForUpdateToStage();
|
|
return;
|
|
}
|
|
|
|
// We might need this value later, so start loading it from the disk now.
|
|
this.promiseAutoUpdateSetting = UpdateUtils.getAppUpdateAutoEnabled();
|
|
|
|
// That leaves the options
|
|
// "Check for updates, but let me choose whether to install them", and
|
|
// "Automatically install updates".
|
|
// In both cases, we check for updates without asking.
|
|
// In the "let me choose" case, we ask before downloading though, in onCheckComplete.
|
|
this.checkForUpdates();
|
|
}
|
|
|
|
// true when there is an update ready to be applied on restart or staged.
|
|
get isPending() {
|
|
if (this.update) {
|
|
return (
|
|
this.update.state == "pending" ||
|
|
this.update.state == "pending-service" ||
|
|
this.update.state == "pending-elevate"
|
|
);
|
|
}
|
|
return (
|
|
this.um.readyUpdate &&
|
|
(this.um.readyUpdate.state == "pending" ||
|
|
this.um.readyUpdate.state == "pending-service" ||
|
|
this.um.readyUpdate.state == "pending-elevate")
|
|
);
|
|
}
|
|
|
|
// true when there is an update already staged.
|
|
get isApplied() {
|
|
if (this.update) {
|
|
return (
|
|
this.update.state == "applied" || this.update.state == "applied-service"
|
|
);
|
|
}
|
|
return (
|
|
this.um.readyUpdate &&
|
|
(this.um.readyUpdate.state == "applied" ||
|
|
this.um.readyUpdate.state == "applied-service")
|
|
);
|
|
}
|
|
|
|
get isStaging() {
|
|
if (!this.updateStagingEnabled) {
|
|
return false;
|
|
}
|
|
let errorCode;
|
|
if (this.update) {
|
|
errorCode = this.update.errorCode;
|
|
} else if (this.um.readyUpdate) {
|
|
errorCode = this.um.readyUpdate.errorCode;
|
|
}
|
|
// If the state is pending and the error code is not 0, staging must have
|
|
// failed.
|
|
return this.isPending && errorCode == 0;
|
|
}
|
|
|
|
// true when an update ready to restart to finish the update process.
|
|
get isReadyForRestart() {
|
|
if (this.updateStagingEnabled) {
|
|
let errorCode;
|
|
if (this.update) {
|
|
errorCode = this.update.errorCode;
|
|
} else if (this.um.readyUpdate) {
|
|
errorCode = this.um.readyUpdate.errorCode;
|
|
}
|
|
// If the state is pending and the error code is not 0, staging must have
|
|
// failed and Firefox should be restarted to try to apply the update
|
|
// without staging.
|
|
return this.isApplied || (this.isPending && errorCode != 0);
|
|
}
|
|
return this.isPending;
|
|
}
|
|
|
|
// true when there is an update download in progress.
|
|
get isDownloading() {
|
|
if (this.update) {
|
|
return this.update.state == "downloading";
|
|
}
|
|
return (
|
|
this.um.downloadingUpdate &&
|
|
this.um.downloadingUpdate.state == "downloading"
|
|
);
|
|
}
|
|
|
|
// true when updating has been disabled by enterprise policy
|
|
get updateDisabledByPolicy() {
|
|
return Services.policies && !Services.policies.isAllowed("appUpdate");
|
|
}
|
|
|
|
// true if updating is disabled because we're running in an app package.
|
|
// This is distinct from updateDisabledByPolicy because we need to avoid
|
|
// messages being shown to the user about an "administrator" handling
|
|
// updates; packaged apps may be getting updated by an administrator or they
|
|
// may not be, and we don't have a good way to tell the difference from here,
|
|
// so we err to the side of less confusion for unmanaged users.
|
|
get updateDisabledByPackage() {
|
|
try {
|
|
return Services.sysinfo.getProperty("hasWinPackageId");
|
|
} catch (_ex) {
|
|
// The hasWinPackageId property doesn't exist; assume it would be false.
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// true when updating in background is enabled.
|
|
get updateStagingEnabled() {
|
|
return (
|
|
!this.updateDisabledByPolicy &&
|
|
!this.updateDisabledByPackage &&
|
|
this.aus.canStageUpdates
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check for updates
|
|
*/
|
|
checkForUpdates() {
|
|
// Clear prefs that could prevent a user from discovering available updates.
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
|
|
}
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER);
|
|
}
|
|
this._setStatus(AppUpdater.STATUS.CHECKING);
|
|
this.checker.checkForUpdates(this._updateCheckListener, true);
|
|
// after checking, onCheckComplete() is called
|
|
}
|
|
|
|
/**
|
|
* Implements nsIUpdateCheckListener. The methods implemented by
|
|
* nsIUpdateCheckListener are in a different scope from nsIIncrementalDownload
|
|
* to make it clear which are used by each interface.
|
|
*/
|
|
get _updateCheckListener() {
|
|
if (!this.__updateCheckListener) {
|
|
this.__updateCheckListener = {
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
onCheckComplete: (aRequest, aUpdates) => {
|
|
this.update = this.aus.selectUpdate(aUpdates);
|
|
if (!this.update) {
|
|
this._setStatus(AppUpdater.STATUS.NO_UPDATES_FOUND);
|
|
return;
|
|
}
|
|
|
|
if (this.update.unsupported) {
|
|
this._setStatus(AppUpdater.STATUS.UNSUPPORTED_SYSTEM);
|
|
return;
|
|
}
|
|
|
|
if (!this.aus.canApplyUpdates) {
|
|
this._setStatus(AppUpdater.STATUS.MANUAL_UPDATE);
|
|
return;
|
|
}
|
|
|
|
if (!this.promiseAutoUpdateSetting) {
|
|
this.promiseAutoUpdateSetting = UpdateUtils.getAppUpdateAutoEnabled();
|
|
}
|
|
this.promiseAutoUpdateSetting.then(updateAuto => {
|
|
if (updateAuto && !this.aus.manualUpdateOnly) {
|
|
// automatically download and install
|
|
this.startDownload();
|
|
} else {
|
|
// ask
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOAD_AND_INSTALL);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
onError: (aRequest, aUpdate) => {
|
|
// Errors in the update check are treated as no updates found. If the
|
|
// update check fails repeatedly without a success the user will be
|
|
// notified with the normal app update user interface so this is safe.
|
|
this._setStatus(AppUpdater.STATUS.NO_UPDATES_FOUND);
|
|
},
|
|
|
|
/**
|
|
* See nsISupports.idl
|
|
*/
|
|
QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckListener"]),
|
|
};
|
|
}
|
|
return this.__updateCheckListener;
|
|
}
|
|
|
|
/**
|
|
* Sets the status to STAGING. The status will then be set again when the
|
|
* update finishes staging.
|
|
*/
|
|
_waitForUpdateToStage() {
|
|
if (!this.update) {
|
|
this.update = this.um.readyUpdate;
|
|
}
|
|
this.update.QueryInterface(Ci.nsIWritablePropertyBag);
|
|
this.update.setProperty("foregroundDownload", "true");
|
|
this._setStatus(AppUpdater.STATUS.STAGING);
|
|
this._awaitStagingComplete();
|
|
}
|
|
|
|
/**
|
|
* Starts the download of an update mar.
|
|
*/
|
|
startDownload() {
|
|
if (!this.update) {
|
|
this.update = this.um.downloadingUpdate;
|
|
}
|
|
this.update.QueryInterface(Ci.nsIWritablePropertyBag);
|
|
this.update.setProperty("foregroundDownload", "true");
|
|
|
|
let success = this.aus.downloadUpdate(this.update, false);
|
|
if (!success) {
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
|
|
return;
|
|
}
|
|
|
|
this._setupDownloadListener();
|
|
}
|
|
|
|
/**
|
|
* Starts tracking the download.
|
|
*/
|
|
_setupDownloadListener() {
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOADING);
|
|
this.aus.addDownloadListener(this);
|
|
}
|
|
|
|
/**
|
|
* See nsIRequestObserver.idl
|
|
*/
|
|
onStartRequest(aRequest) {}
|
|
|
|
/**
|
|
* See nsIRequestObserver.idl
|
|
*/
|
|
onStopRequest(aRequest, aStatusCode) {
|
|
switch (aStatusCode) {
|
|
case Cr.NS_ERROR_UNEXPECTED:
|
|
if (
|
|
this.update.selectedPatch.state == "download-failed" &&
|
|
(this.update.isCompleteUpdate || this.update.patchCount != 2)
|
|
) {
|
|
// Verification error of complete patch, informational text is held in
|
|
// the update object.
|
|
this.aus.removeDownloadListener(this);
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
|
|
break;
|
|
}
|
|
// Verification failed for a partial patch, complete patch is now
|
|
// downloading so return early and do NOT remove the download listener!
|
|
break;
|
|
case Cr.NS_BINDING_ABORTED:
|
|
// Do not remove UI listener since the user may resume downloading again.
|
|
break;
|
|
case Cr.NS_OK:
|
|
this.aus.removeDownloadListener(this);
|
|
if (this.updateStagingEnabled) {
|
|
// It could be that another instance was started during the download,
|
|
// and if that happened, then we actually should not advance to the
|
|
// STAGING status because the staging process isn't really happening
|
|
// until that instance exits (or we time out waiting).
|
|
if (this.aus.isOtherInstanceHandlingUpdates) {
|
|
this._setStatus(AppUpdater.OTHER_INSTANCE_HANDLING_UPDATES);
|
|
} else {
|
|
this._setStatus(AppUpdater.STATUS.STAGING);
|
|
}
|
|
// But we should register the staging observer in either case, because
|
|
// if we do time out waiting for the other instance to exit, then
|
|
// staging really will start at that point.
|
|
this._awaitStagingComplete();
|
|
} else {
|
|
this._awaitDownloadComplete();
|
|
}
|
|
break;
|
|
default:
|
|
this.aus.removeDownloadListener(this);
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIProgressEventSink.idl
|
|
*/
|
|
onStatus(aRequest, aStatus, aStatusArg) {}
|
|
|
|
/**
|
|
* See nsIProgressEventSink.idl
|
|
*/
|
|
onProgress(aRequest, aProgress, aProgressMax) {
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOADING, aProgress, aProgressMax);
|
|
}
|
|
|
|
/**
|
|
* This function registers an observer that watches for the download
|
|
* to complete. Once it does, it updates the status accordingly.
|
|
*/
|
|
_awaitDownloadComplete() {
|
|
let observer = (aSubject, aTopic, aData) => {
|
|
// Update the UI when the download is finished
|
|
this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
|
|
Services.obs.removeObserver(observer, "update-downloaded");
|
|
};
|
|
Services.obs.addObserver(observer, "update-downloaded");
|
|
}
|
|
|
|
/**
|
|
* This function registers an observer that watches for the staging process
|
|
* to complete. Once it does, it sets the status to either request that the
|
|
* user restarts to install the update on success, request that the user
|
|
* manually download and install the newer version, or automatically download
|
|
* a complete update if applicable.
|
|
*/
|
|
_awaitStagingComplete() {
|
|
let observer = (aSubject, aTopic, aData) => {
|
|
// Update the UI when the background updater is finished
|
|
switch (aTopic) {
|
|
case "update-staged":
|
|
let status = aData;
|
|
if (
|
|
status == "applied" ||
|
|
status == "applied-service" ||
|
|
status == "pending" ||
|
|
status == "pending-service" ||
|
|
status == "pending-elevate"
|
|
) {
|
|
// If the update is successfully applied, or if the updater has
|
|
// fallen back to non-staged updates, show the "Restart to Update"
|
|
// button.
|
|
this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
|
|
} else if (status == "failed") {
|
|
// Background update has failed, let's show the UI responsible for
|
|
// prompting the user to update manually.
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
|
|
} else if (status == "downloading") {
|
|
// We've fallen back to downloading the complete update because the
|
|
// partial update failed to get staged in the background.
|
|
// Therefore we need to keep our observer.
|
|
this._setupDownloadListener();
|
|
return;
|
|
}
|
|
break;
|
|
case "update-error":
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
|
|
break;
|
|
}
|
|
Services.obs.removeObserver(observer, "update-staged");
|
|
Services.obs.removeObserver(observer, "update-error");
|
|
};
|
|
Services.obs.addObserver(observer, "update-staged");
|
|
Services.obs.addObserver(observer, "update-error");
|
|
}
|
|
|
|
/**
|
|
* Stops the current check for updates and any ongoing download.
|
|
*/
|
|
stop() {
|
|
this.checker.stopCurrentCheck();
|
|
this.aus.removeDownloadListener(this);
|
|
}
|
|
|
|
/**
|
|
* {AppUpdater.STATUS} The status of the current check or update.
|
|
*/
|
|
get status() {
|
|
if (!this._status) {
|
|
if (!AppConstants.MOZ_UPDATER || this.updateDisabledByPackage) {
|
|
this._status = AppUpdater.STATUS.NO_UPDATER;
|
|
} else if (this.updateDisabledByPolicy) {
|
|
this._status = AppUpdater.STATUS.UPDATE_DISABLED_BY_POLICY;
|
|
} else if (this.isReadyForRestart) {
|
|
this._status = AppUpdater.STATUS.READY_FOR_RESTART;
|
|
} else if (this.aus.isOtherInstanceHandlingUpdates) {
|
|
this._status = AppUpdater.STATUS.OTHER_INSTANCE_HANDLING_UPDATES;
|
|
} else if (this.isDownloading) {
|
|
this._status = AppUpdater.STATUS.DOWNLOADING;
|
|
} else if (this.isStaging) {
|
|
this._status = AppUpdater.STATUS.STAGING;
|
|
} else {
|
|
this._status = AppUpdater.STATUS.NEVER_CHECKED;
|
|
}
|
|
}
|
|
return this._status;
|
|
}
|
|
|
|
/**
|
|
* Adds a listener function that will be called back on status changes as
|
|
* different stages of updates occur. The function will be called without
|
|
* arguments for most status changes; see the comments around the STATUS value
|
|
* definitions below. This is safe to call multiple times with the same
|
|
* function. It will be added only once.
|
|
*
|
|
* @param {function} listener
|
|
* The listener function to add.
|
|
*/
|
|
addListener(listener) {
|
|
this._listeners.add(listener);
|
|
}
|
|
|
|
/**
|
|
* Removes a listener. This is safe to call multiple times with the same
|
|
* function, or with a function that was never added.
|
|
*
|
|
* @param {function} listener
|
|
* The listener function to remove.
|
|
*/
|
|
removeListener(listener) {
|
|
this._listeners.delete(listener);
|
|
}
|
|
|
|
/**
|
|
* Sets the updater's current status and calls listeners.
|
|
*
|
|
* @param {AppUpdater.STATUS} status
|
|
* The new updater status.
|
|
* @param {*} listenerArgs
|
|
* Arguments to pass to listeners.
|
|
*/
|
|
_setStatus(status, ...listenerArgs) {
|
|
this._status = status;
|
|
for (let listener of this._listeners) {
|
|
listener(status, ...listenerArgs);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
observe(subject, topic, status) {
|
|
switch (topic) {
|
|
case "update-swap":
|
|
this._handleUpdateSwap();
|
|
break;
|
|
}
|
|
}
|
|
|
|
_handleUpdateSwap() {
|
|
// This function exists to deal with the fact that we support handling 2
|
|
// updates at once: a ready update and a downloading update. But AppUpdater
|
|
// only ever really considers a single update at a time.
|
|
// We see an update swap just when the downloading update has finished
|
|
// downloading and is being swapped into UpdateManager.readyUpdate. At this
|
|
// point, we are in one of two states. Either:
|
|
// a) The update that is being swapped in is the update that this
|
|
// AppUpdater has already been tracking, or
|
|
// b) We've been tracking the ready update. Now that the downloading
|
|
// update is about to be swapped into the place of the ready update, we
|
|
// need to switch over to tracking the new update.
|
|
if (
|
|
this._status == AppUpdater.STATUS.DOWNLOADING ||
|
|
this._status == AppUpdater.STATUS.STAGING
|
|
) {
|
|
// We are already tracking the correct update.
|
|
return;
|
|
}
|
|
|
|
if (this.updateStagingEnabled) {
|
|
this._setStatus(AppUpdater.STATUS.STAGING);
|
|
this._awaitStagingComplete();
|
|
} else {
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOADING);
|
|
this._awaitDownloadComplete();
|
|
}
|
|
}
|
|
}
|
|
|
|
AppUpdater.STATUS = {
|
|
// Updates are allowed and there's no downloaded or staged update, but the
|
|
// AppUpdater hasn't checked for updates yet, so it doesn't know more than
|
|
// that.
|
|
NEVER_CHECKED: 0,
|
|
|
|
// The updater isn't available (AppConstants.MOZ_UPDATER is falsey).
|
|
NO_UPDATER: 1,
|
|
|
|
// "appUpdate" is not allowed by policy.
|
|
UPDATE_DISABLED_BY_POLICY: 2,
|
|
|
|
// Another app instance is handling updates.
|
|
OTHER_INSTANCE_HANDLING_UPDATES: 3,
|
|
|
|
// There's an update, but it's not supported on this system.
|
|
UNSUPPORTED_SYSTEM: 4,
|
|
|
|
// The user must apply updates manually.
|
|
MANUAL_UPDATE: 5,
|
|
|
|
// The AppUpdater is checking for updates.
|
|
CHECKING: 6,
|
|
|
|
// The AppUpdater checked for updates and none were found.
|
|
NO_UPDATES_FOUND: 7,
|
|
|
|
// The AppUpdater is downloading an update. Listeners are notified of this
|
|
// status as a download starts. They are also notified on download progress,
|
|
// and in that case they are passed two arguments: the current download
|
|
// progress and the total download size.
|
|
DOWNLOADING: 8,
|
|
|
|
// The AppUpdater tried to download an update but it failed.
|
|
DOWNLOAD_FAILED: 9,
|
|
|
|
// There's an update available, but the user wants us to ask them to download
|
|
// and install it.
|
|
DOWNLOAD_AND_INSTALL: 10,
|
|
|
|
// An update is staging.
|
|
STAGING: 11,
|
|
|
|
// An update is downloaded and staged and will be applied on restart.
|
|
READY_FOR_RESTART: 12,
|
|
|
|
/**
|
|
* Is the given `status` a terminal state in the update state machine?
|
|
*
|
|
* A terminal state means that the `check()` method has completed.
|
|
*
|
|
* N.b.: `DOWNLOAD_AND_INSTALL` is not considered terminal because the normal
|
|
* flow is that Firefox will show UI prompting the user to install, and when
|
|
* the user interacts, the `check()` method will continue through the update
|
|
* state machine.
|
|
*
|
|
* @returns {boolean} `true` if `status` is terminal.
|
|
*/
|
|
isTerminalStatus(status) {
|
|
return ![
|
|
AppUpdater.STATUS.CHECKING,
|
|
AppUpdater.STATUS.DOWNLOAD_AND_INSTALL,
|
|
AppUpdater.STATUS.DOWNLOADING,
|
|
AppUpdater.STATUS.NEVER_CHECKED,
|
|
AppUpdater.STATUS.STAGING,
|
|
].includes(status);
|
|
},
|
|
|
|
/**
|
|
* Turn the given `status` into a string for debugging.
|
|
*
|
|
* @returns {?string} representation of given numerical `status`.
|
|
*/
|
|
debugStringFor(status) {
|
|
for (let [k, v] of Object.entries(AppUpdater.STATUS)) {
|
|
if (v == status) {
|
|
return k;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
};
|