Bug 1727820 - Implement nsIUpdateChecker API change r=bhearsum

Note that, for the most part, this isn't meant to change behavior. It is just meant to eliminate race conditions and fix some bugs. However, a tiny bit of behavior has been changed.

Previously, the fallback error codes used to potentially populate nsIUpdate.statusText were 200 in Checker.onError (corresponding to "Update XML file malformed") and 404 in Checker.onLoad (corresponding to "Update XML file not found"). These really seem backwards to me. Especially in the Checker.onLoad case, where we basically use that as the fallback if we fail to parse the update XML. So the codes have effectively been reversed in this patch.

Differential Revision: https://phabricator.services.mozilla.com/D159302
This commit is contained in:
Kirk Steuber 2022-10-25 18:10:21 +00:00
parent 065730ec74
commit edd4998dd1
3 changed files with 415 additions and 234 deletions

View file

@ -41,3 +41,5 @@ check_error-2152398920=Proxy server connection refused
check_error-2153390069=Server certificate has expired (please adjust your system clock to the correct date and time if it is incorrect)
check_error-verification_failed=The integrity of the update could not be verified
check_error-move_failed=Failed to prepare the update for installation
check_error-update_url_not_available=Update URL not available
check_error-connection_aborted=Connection aborted

View file

@ -1063,6 +1063,24 @@ function getUpdateFile(pathArray) {
return file;
}
/**
* This function is designed to let us slightly clean up the mapping between
* strings and error codes. So that instead of having:
* check_error-2147500036=Connection aborted
* check_error-2152398850=Connection aborted
* We can have:
* check_error-connection_aborted=Connection aborted
* And map both of those error codes to it.
*/
function maybeMapErrorCode(code) {
switch (code) {
case Cr.NS_BINDING_ABORTED:
case Cr.NS_ERROR_ABORT:
return "connection_aborted";
}
return code;
}
/**
* Returns human readable status text from the updates.properties bundle
* based on an error code
@ -1074,6 +1092,8 @@ function getUpdateFile(pathArray) {
* @return A human readable status text string
*/
function getStatusTextFromCode(code, defaultCode) {
code = maybeMapErrorCode(code);
let reason;
try {
reason = lazy.gUpdateBundle.GetStringFromName("check_error-" + code);
@ -1081,6 +1101,8 @@ function getStatusTextFromCode(code, defaultCode) {
"getStatusTextFromCode - transfer error: " + reason + ", code: " + code
);
} catch (e) {
defaultCode = maybeMapErrorCode(defaultCode);
// Use the default reason
reason = lazy.gUpdateBundle.GetStringFromName("check_error-" + defaultCode);
LOG(
@ -2442,15 +2464,6 @@ Update.prototype = {
]),
};
const UpdateServiceFactory = {
_instance: null,
createInstance(iid) {
return this._instance == null
? (this._instance = new UpdateService())
: this._instance;
},
};
/**
* UpdateService
* A Service for managing the discovery and installation of software updates.
@ -4513,23 +4526,45 @@ UpdateManager.prototype = {
};
/**
* Checker
* Checks for new Updates
* @constructor
* CheckerService
* Provides an interface for checking for new updates. When more checks are
* made while an equivalent check is already in-progress, they will be coalesced
* into a single update check request.
*/
function Checker() {}
Checker.prototype = {
/**
* The XMLHttpRequest object that performs the connection.
*/
_request: null,
class CheckerService {
#nextUpdateCheckId = 1;
// Most of the update checking data is looked up via a "request key". This
// allows us to lookup the request key for a particular check id, since
// multiple checks can correspond to a single request.
// When a check is cancelled or completed, it will be removed from this
// object.
#requestKeyByCheckId = {};
// This object will relate request keys to update check data objects. The
// format of the update check data objects is defined by
// #makeUpdateCheckDataObject, below.
// When an update request is cancelled (by all of the corresponding update
// checks being cancelled) or completed, its key will be removed from this
// object.
#updateCheckData = {};
#makeUpdateCheckDataObject(type, promise) {
return { type, promise, request: null };
}
/**
* The nsIUpdateCheckListener callback
* Indicates whether the passed parameter is one of the valid enumerated
* values that indicates a type of update check.
*/
_callback: null,
#validUpdateCheckType(checkType) {
return [
Ci.nsIUpdateChecker.BACKGROUND_CHECK,
Ci.nsIUpdateChecker.FOREGROUND_CHECK,
].includes(checkType);
}
_getCanMigrate: function UC__getCanMigrate() {
#getCanMigrate() {
if (AppConstants.platform != "win") {
return false;
}
@ -4563,7 +4598,10 @@ Checker.prototype = {
// The Never registry key value allows configuring a system to never migrate
// any of the installations.
if (regValHKCU === 1 || regValHKLM === 1) {
LOG("Checker:_getCanMigrate - all installations should not be migrated");
LOG(
"CheckerService:#getCanMigrate - all installations should not be " +
"migrated"
);
return false;
}
@ -4584,27 +4622,36 @@ Checker.prototype = {
// name then the installation has already been migrated once or the system
// was configured to not migrate that installation.
if (regValHKCU === 1 || regValHKLM === 1) {
LOG("Checker:_getCanMigrate - this installation should not be migrated");
LOG(
"CheckerService:#getCanMigrate - this installation should not be " +
"migrated"
);
return false;
}
// When the registry value is 0 for the installation directory path value
// name then the installation has updated to Firefox 56 and can be migrated.
if (regValHKCU === 0 || regValHKLM === 0) {
LOG("Checker:_getCanMigrate - this installation can be migrated");
LOG("CheckerService:#getCanMigrate - this installation can be migrated");
return true;
}
LOG("Checker:_getCanMigrate - no registry entries for this installation");
LOG(
"CheckerService:#getCanMigrate - no registry entries for this " +
"installation"
);
return false;
},
}
/**
* The URL of the update service XML file to connect to that contains details
* about available updates.
* See nsIUpdateService.idl
*/
getUpdateURL: async function UC_getUpdateURL(force) {
this._forced = force;
async getUpdateURL(checkType) {
LOG("CheckerService:getUpdateURL - checkType: " + checkType);
if (!this.#validUpdateCheckType(checkType)) {
LOG("CheckerService:getUpdateURL - Invalid checkType");
throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
}
let url = Services.appinfo.updateURL;
let updatePin;
@ -4625,17 +4672,17 @@ Checker.prototype = {
}
if (!url) {
LOG("Checker:getUpdateURL - update URL not defined");
LOG("CheckerService:getUpdateURL - update URL not defined");
return null;
}
url = await lazy.UpdateUtils.formatUpdateURL(url);
if (force) {
if (checkType == Ci.nsIUpdateChecker.FOREGROUND_CHECK) {
url += (url.includes("?") ? "&" : "?") + "force=1";
}
if (this._getCanMigrate()) {
if (this.#getCanMigrate()) {
url += (url.includes("?") ? "&" : "?") + "mig64=1";
}
@ -4646,158 +4693,194 @@ Checker.prototype = {
encodeURIComponent(updatePin);
}
LOG("Checker:getUpdateURL - update URL: " + url);
LOG("CheckerService:getUpdateURL - update URL: " + url);
return url;
},
}
/**
* See nsIUpdateService.idl
*/
checkForUpdates: function UC_checkForUpdates(listener, force) {
LOG("Checker: checkForUpdates, force: " + force);
if (!listener) {
throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
checkForUpdates(checkType) {
LOG("CheckerService:checkForUpdates - checkType: " + checkType);
if (!this.#validUpdateCheckType(checkType)) {
LOG("CheckerService:checkForUpdates - Invalid checkType");
throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
}
let UpdateServiceInstance = UpdateServiceFactory.createInstance();
// |force| can override |canCheckForUpdates| since |force| indicates a
// manual update check. But nothing should override enterprise policies.
if (UpdateServiceInstance.disabled) {
LOG("Checker: checkForUpdates, disabled by policy");
return;
let checkId = this.#nextUpdateCheckId;
this.#nextUpdateCheckId += 1;
// `checkType == FOREGROUND_CHECK`` can override `canCheckForUpdates`. But
// nothing should override enterprise policies.
if (lazy.AUS.disabled) {
LOG("CheckerService:checkForUpdates - disabled by policy");
return this.#getChecksNotAllowedObject(checkId);
}
if (!UpdateServiceInstance.canCheckForUpdates && !force) {
return;
if (
checkType == Ci.nsIUpdateChecker.BACKGROUND_CHECK &&
!lazy.AUS.canCheckForUpdates
) {
LOG("CheckerService:checkForUpdates - !canCheckForUpdates");
return this.#getChecksNotAllowedObject(checkId);
}
waitForOtherInstances()
.then(() => this.getUpdateURL(force))
.then(url => {
if (!url) {
return;
}
// We want to combine simultaneous requests, but only ones that are
// equivalent. If, say, one of them uses the force parameter and one
// doesn't, we want those two requests to remain separate. This key will
// allow us to map equivalent requests together. It is also the key that we
// use to lookup the update check data in this.#updateCheckData.
let requestKey = checkType;
// It's possible that another check was kicked off and that request sent
// while we were waiting for other instances to exit here; if the other
// instances were closed and also the other check was started during the
// same interval between polls, then here we could now be about to start
// a second overlapping check, which should not happen. So make sure we
// don't have a request already active before we start a new one.
if (this._request) {
LOG(
"Checker: checkForUpdates: check request already active, aborting"
);
return;
}
this._request = new XMLHttpRequest();
this._request.open("GET", url, true);
this._request.channel.notificationCallbacks = new lazy.CertUtils.BadCertHandler(
false
);
// Prevent the request from reading from the cache.
this._request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
// Prevent the request from writing to the cache.
this._request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
// Disable cutting edge features, like TLS 1.3, where middleboxes might brick us
this._request.channel.QueryInterface(
Ci.nsIHttpChannelInternal
).beConservative = true;
this._request.overrideMimeType("text/xml");
// The Cache-Control header is only interpreted by proxies and the
// final destination. It does not help if a resource is already
// cached locally.
this._request.setRequestHeader("Cache-Control", "no-cache");
// HTTP/1.0 servers might not implement Cache-Control and
// might only implement Pragma: no-cache
this._request.setRequestHeader("Pragma", "no-cache");
var self = this;
this._request.addEventListener("error", function(event) {
self.onError(event);
});
this._request.addEventListener("load", function(event) {
self.onLoad(event);
});
LOG("Checker:checkForUpdates - sending request to: " + url);
this._request.send(null);
this._callback = listener;
});
},
/**
* Returns an array of nsIUpdate objects discovered by the update check.
* @throws if the XML document element node name is not updates.
*/
get _updates() {
var updatesElement = this._request.responseXML.documentElement;
if (!updatesElement) {
LOG("Checker:_updates get - empty updates document?!");
return [];
}
if (updatesElement.nodeName != "updates") {
LOG("Checker:_updates get - unexpected node name!");
throw new Error(
"Unexpected node name, expected: updates, got: " +
updatesElement.nodeName
if (requestKey in this.#updateCheckData) {
LOG(
`CheckerService:checkForUpdates - Connecting check id ${checkId} to ` +
`existing check request.`
);
} else {
LOG(
`CheckerService:checkForUpdates - Making new check request for check ` +
`id ${checkId}.`
);
this.#updateCheckData[requestKey] = this.#makeUpdateCheckDataObject(
checkType,
this.#updateCheck(checkType, requestKey)
);
}
var updates = [];
for (var i = 0; i < updatesElement.childNodes.length; ++i) {
var updateElement = updatesElement.childNodes.item(i);
if (
updateElement.nodeType != updateElement.ELEMENT_NODE ||
updateElement.localName != "update"
) {
continue;
}
this.#requestKeyByCheckId[checkId] = requestKey;
let update;
try {
update = new Update(updateElement);
} catch (e) {
LOG("Checker:_updates get - invalid <update/>, ignoring...");
continue;
}
update.serviceURL = this._request.responseURL;
update.channel = lazy.UpdateUtils.UpdateChannel;
updates.push(update);
}
return {
id: checkId,
result: this.#updateCheckData[requestKey].promise,
QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheck"]),
};
}
return updates;
},
#getChecksNotAllowedObject(checkId) {
return {
id: checkId,
result: Promise.resolve(
Object.freeze({
checksAllowed: false,
succeeded: false,
request: null,
updates: [],
QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckResult"]),
})
),
QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheck"]),
};
}
/**
* Returns the status code for the XMLHttpRequest
*/
_getChannelStatus: function UC__getChannelStatus(request) {
var status = 0;
async #updateCheck(checkType, requestKey) {
await waitForOtherInstances();
let url;
try {
status = request.status;
} catch (e) {}
url = await this.getUpdateURL(checkType);
} catch (ex) {}
if (status == 0) {
status = request.channel.QueryInterface(Ci.nsIRequest).status;
if (!url) {
LOG("CheckerService:#updateCheck - !url");
return this.#getCheckFailedObject("update_url_not_available");
}
return status;
},
_isHttpStatusCode: function UC__isHttpStatusCode(status) {
return status >= 100 && status <= 599;
},
let request = new XMLHttpRequest();
request.open("GET", url, true);
request.channel.notificationCallbacks = new lazy.CertUtils.BadCertHandler(
false
);
// Prevent the request from reading from the cache.
request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
// Prevent the request from writing to the cache.
request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
// Disable cutting edge features, like TLS 1.3, where middleboxes might
// brick us
request.channel.QueryInterface(
Ci.nsIHttpChannelInternal
).beConservative = true;
/**
* The XMLHttpRequest succeeded and the document was loaded.
* @param event
* The Event for the load
*/
onLoad: function UC_onLoad(event) {
LOG("Checker:onLoad - request completed downloading document");
request.overrideMimeType("text/xml");
// The Cache-Control header is only interpreted by proxies and the
// final destination. It does not help if a resource is already
// cached locally.
request.setRequestHeader("Cache-Control", "no-cache");
// HTTP/1.0 servers might not implement Cache-Control and
// might only implement Pragma: no-cache
request.setRequestHeader("Pragma", "no-cache");
const UPDATE_CHECK_LOAD_SUCCESS = 1;
const UPDATE_CHECK_LOAD_ERROR = 2;
const UPDATE_CHECK_CANCELLED = 3;
let result = await new Promise(resolve => {
// It's important that nothing potentially asynchronous happens between
// checking if the request has been cancelled and starting the request.
// If an update check cancellation happens before dispatching the request
// and we end up dispatching it anyways, we will never call cancel on the
// request later and the cancellation effectively won't happen.
if (!(requestKey in this.#updateCheckData)) {
LOG(
"CheckerService:#updateCheck - check was cancelled before request " +
"was able to start"
);
resolve(UPDATE_CHECK_CANCELLED);
return;
}
let onLoad = event => {
request.removeEventListener("load", onLoad);
LOG("CheckerService:#updateCheck - request got 'load' event");
resolve(UPDATE_CHECK_LOAD_SUCCESS);
};
request.addEventListener("load", onLoad);
let onError = event => {
request.removeEventListener("error", onLoad);
LOG("CheckerService:#updateCheck - request got 'error' event");
resolve(UPDATE_CHECK_LOAD_ERROR);
};
request.addEventListener("error", onError);
LOG("CheckerService:#updateCheck - sending request to: " + url);
request.send(null);
this.#updateCheckData[requestKey].request = request;
});
// Remove all entries for this request key. This marks the request and the
// associated check ids as no longer in-progress.
delete this.#updateCheckData[requestKey];
for (const checkId of Object.keys(this.#requestKeyByCheckId)) {
if (this.#requestKeyByCheckId[checkId] == requestKey) {
delete this.#requestKeyByCheckId[checkId];
}
}
if (result == UPDATE_CHECK_CANCELLED) {
return this.#getCheckFailedObject(Cr.NS_BINDING_ABORTED);
}
if (result == UPDATE_CHECK_LOAD_ERROR) {
let status = this.#getChannelStatus(request);
LOG("CheckerService:#updateCheck - Failed. request.status: " + status);
// Set MitM pref.
try {
let secInfo = request.channel.securityInfo;
if (secInfo.serverCert && secInfo.serverCert.issuerName) {
Services.prefs.setStringPref(
"security.pki.mitm_canary_issuer",
secInfo.serverCert.issuerName
);
}
} catch (e) {
LOG("CheckerService:#updateCheck - Getting secInfo failed.");
}
return this.#getCheckFailedObject(status, 404, request);
}
LOG("CheckerService:#updateCheck - request completed downloading document");
Services.prefs.clearUserPref("security.pki.mitm_canary_issuer");
// Check whether there is a mitm, i.e. check whether the root cert is
// built-in or not.
@ -4820,99 +4903,195 @@ Checker.prototype = {
}
}
} catch (e) {
LOG("Checker:onLoad - Getting sslStatus failed.");
LOG("CheckerService:#updateCheck - Getting sslStatus failed.");
}
let updates;
try {
// Analyze the resulting DOM and determine the set of updates.
var updates = this._updates;
LOG("Checker:onLoad - number of updates available: " + updates.length);
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
}
// Tell the callback about the updates
this._callback.onCheckComplete(event.target, updates);
updates = this.#parseUpdates(request);
} catch (e) {
LOG(
"Checker:onLoad - there was a problem checking for updates. " +
"Exception: " +
"CheckerService:#updateCheck - there was a problem checking for " +
"updates. Exception: " +
e
);
var request = event.target;
var status = this._getChannelStatus(request);
LOG("Checker:onLoad - request.status: " + status);
var update = new Update(null);
update.errorCode = status;
update.statusText = getStatusTextFromCode(status, 404);
if (this._isHttpStatusCode(status)) {
update.errorCode = HTTP_ERROR_OFFSET + status;
}
this._callback.onError(request, update);
let status = this.#getChannelStatus(request);
// If we can't find an error string specific to this status code,
// just use the 200 message from above, which means everything
// "looks" fine but there was probably an XML error or a bogus file.
return this.#getCheckFailedObject(status, 200, request);
}
this._callback = null;
this._request = null;
},
LOG(
"CheckerService:#updateCheck - number of updates available: " +
updates.length
);
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
}
return Object.freeze({
checksAllowed: true,
succeeded: true,
request,
updates,
QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckResult"]),
});
}
/**
* There was an error of some kind during the XMLHttpRequest
* @param event
* The Event for the error
* @param errorCode
* The error code to include in the return value. If possible, we
* will get the update status text based on this error code.
* @param defaultCode
* Optional. The error code to use to get the status text if there
* isn't status text available for `errorCode`.
* @param request
* The XMLHttpRequest used to check for updates. Or null, if one was
* never constructed.
* @returns An nsIUpdateCheckResult object indicating an error, using the
* error data passed to this function.
*/
onError: function UC_onError(event) {
var request = event.target;
var status = this._getChannelStatus(request);
LOG("Checker:onError - request.status: " + status);
#getCheckFailedObject(
errorCode,
defaultCode = Cr.NS_BINDING_FAILED,
request = null
) {
let update = new Update(null);
update.errorCode = errorCode;
update.statusText = getStatusTextFromCode(errorCode, defaultCode);
// Set MitM pref.
try {
var secInfo = request.channel.securityInfo;
if (secInfo.serverCert && secInfo.serverCert.issuerName) {
Services.prefs.setStringPref(
"security.pki.mitm_canary_issuer",
secInfo.serverCert.issuerName
);
}
} catch (e) {
LOG("Checker:onError - Getting secInfo failed.");
}
// If we can't find an error string specific to this status code,
// just use the 200 message from above, which means everything
// "looks" fine but there was probably an XML error or a bogus file.
var update = new Update(null);
update.errorCode = status;
update.statusText = getStatusTextFromCode(status, 200);
if (status == Cr.NS_ERROR_OFFLINE) {
if (errorCode == Cr.NS_ERROR_OFFLINE) {
// We use a separate constant here because nsIUpdate.errorCode is signed
update.errorCode = NETWORK_ERROR_OFFLINE;
} else if (this._isHttpStatusCode(status)) {
update.errorCode = HTTP_ERROR_OFFSET + status;
} else if (this.#isHttpStatusCode(errorCode)) {
update.errorCode = HTTP_ERROR_OFFSET + errorCode;
}
this._callback.onError(request, update);
this._request = null;
},
return Object.freeze({
checksAllowed: true,
succeeded: false,
request,
updates: [update],
QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckResult"]),
});
}
/**
* Returns the status code for the XMLHttpRequest
*/
#getChannelStatus(request) {
var status = 0;
try {
status = request.status;
} catch (e) {}
if (status == 0) {
status = request.channel.QueryInterface(Ci.nsIRequest).status;
}
return status;
}
#isHttpStatusCode(status) {
return status >= 100 && status <= 599;
}
/**
* @param request
* The XMLHttpRequest that successfully loaded the update XML.
* @returns An array of 0 or more nsIUpdate objects describing the available
* updates.
* @throws If the XML document element node name is not updates.
*/
#parseUpdates(request) {
let updatesElement = request.responseXML.documentElement;
if (!updatesElement) {
LOG("CheckerService:#parseUpdates - empty updates document?!");
return [];
}
if (updatesElement.nodeName != "updates") {
LOG("CheckerService:#parseUpdates - unexpected node name!");
throw new Error(
"Unexpected node name, expected: updates, got: " +
updatesElement.nodeName
);
}
let updates = [];
for (const updateElement of updatesElement.childNodes) {
if (
updateElement.nodeType != updateElement.ELEMENT_NODE ||
updateElement.localName != "update"
) {
continue;
}
let update;
try {
update = new Update(updateElement);
} catch (e) {
LOG("CheckerService:#parseUpdates - invalid <update/>, ignoring...");
continue;
}
update.serviceURL = request.responseURL;
update.channel = lazy.UpdateUtils.UpdateChannel;
updates.push(update);
}
return updates;
}
/**
* See nsIUpdateService.idl
*/
stopCurrentCheck: function UC_stopCurrentCheck() {
// Always stop the current check
if (this._request) {
this._request.abort();
stopCheck(checkId) {
if (!(checkId in this.#requestKeyByCheckId)) {
LOG(`CheckerService:stopCheck - Non-existent check id ${checkId}`);
return;
}
this._callback = null;
},
LOG(`CheckerService:stopCheck - Cancelling check id ${checkId}`);
let requestKey = this.#requestKeyByCheckId[checkId];
delete this.#requestKeyByCheckId[checkId];
if (Object.values(this.#requestKeyByCheckId).includes(requestKey)) {
LOG(
`CheckerService:stopCheck - Not actually cancelling request because ` +
`other check id's depend on it.`
);
} else {
LOG(
`CheckerService:stopCheck - This is the last check using this ` +
`request. Cancelling the request now.`
);
let request = this.#updateCheckData[requestKey].request;
delete this.#updateCheckData[requestKey];
if (request) {
LOG(`CheckerService:stopCheck - Aborting XMLHttpRequest`);
request.abort();
} else {
LOG(
`CheckerService:stopCheck - Not aborting XMLHttpRequest. It ` +
`doesn't appear to have started yet.`
);
}
}
}
classID: Components.ID("{898CDC9B-E43F-422F-9CC4-2F6291B415A3}"),
QueryInterface: ChromeUtils.generateQI(["nsIUpdateChecker"]),
};
/**
* See nsIUpdateService.idl
*/
stopAllChecks() {
LOG("CheckerService:stopAllChecks - stopping all checks.");
for (const checkId of Object.keys(this.#requestKeyByCheckId)) {
this.stopCheck(checkId);
}
}
classID = Components.ID("{898CDC9B-E43F-422F-9CC4-2F6291B415A3}");
QueryInterface = ChromeUtils.generateQI(["nsIUpdateChecker"]);
}
/**
* Manages the download of updates
@ -6544,4 +6723,4 @@ class RestartOnLastWindowClosed {
// eslint-disable-next-line no-unused-vars
let restartOnLastWindowClosed = new RestartOnLastWindowClosed();
var EXPORTED_SYMBOLS = ["UpdateService", "Checker", "UpdateManager"];
var EXPORTED_SYMBOLS = ["UpdateService", "CheckerService", "UpdateManager"];

View file

@ -23,7 +23,7 @@ Classes = [
'cid': '{898CDC9B-E43F-422F-9CC4-2F6291B415A3}',
'contract_ids': ['@mozilla.org/updates/update-checker;1'],
'jsm': 'resource://gre/modules/UpdateService.jsm',
'constructor': 'Checker',
'constructor': 'CheckerService',
'singleton': True,
},