commit 3b08e71bb5bef3f2ad259f0ab86f93aef85cd149
parent 38ad18e48ec3aba49a6e6a523d0e167c5654f783
Author: Mike Conley <mconley@mozilla.com>
Date: Wed, 1 Oct 2025 18:39:30 +0000
Bug 1991490 - Cancel in-process XPIInstalls on quit-application-granted. r=rpl
Normally, the network:offline-about-to-go-offline topic would satisfy here - however,
in cases where we have an AsyncShutdown blocker that's preventing us from reaching
network:offline-about-to-go-offline AND that blocker is awaiting on an XPIInstall,
the quit-application-granted topic can help break that circular dependency.
Differential Revision: https://phabricator.services.mozilla.com/D267029
Diffstat:
2 files changed, 68 insertions(+), 7 deletions(-)
diff --git a/toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs b/toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs
@@ -99,6 +99,8 @@ const PREF_XPI_WEAK_SIGNATURES_ALLOWED =
const PREF_SELECTED_THEME = "extensions.activeThemeID";
const TOOLKIT_ID = "toolkit@mozilla.org";
+const TOPIC_GOING_OFFLINE = "network:offline-about-to-go-offline";
+const TOPIC_QUIT_GRANTED = "quit-application-granted";
ChromeUtils.defineLazyGetter(lazy, "MOZ_UNSIGNED_SCOPES", () => {
let result = 0;
@@ -2424,9 +2426,11 @@ var DownloadAddonInstall = class extends AddonInstall {
}
}
- observe() {
- // Network is going offline
- this.cancel();
+ observe(_subject, topic, _data) {
+ if (topic == TOPIC_GOING_OFFLINE || topic == TOPIC_QUIT_GRANTED) {
+ // Network is going offline, or we're shutting down
+ this.cancel();
+ }
}
/**
@@ -2515,7 +2519,8 @@ var DownloadAddonInstall = class extends AddonInstall {
}
this.channel.asyncOpen(listener);
- Services.obs.addObserver(this, "network:offline-about-to-go-offline");
+ Services.obs.addObserver(this, TOPIC_GOING_OFFLINE);
+ Services.obs.addObserver(this, TOPIC_QUIT_GRANTED);
} catch (e) {
logger.warn(
"Failed to start download for addon " + this.sourceURI.spec,
@@ -2632,7 +2637,8 @@ var DownloadAddonInstall = class extends AddonInstall {
this.stream.close();
this.channel = null;
this.badCerthandler = null;
- Services.obs.removeObserver(this, "network:offline-about-to-go-offline");
+ Services.obs.removeObserver(this, TOPIC_GOING_OFFLINE);
+ Services.obs.removeObserver(this, TOPIC_QUIT_GRANTED);
let crypto = this.crypto;
this.crypto = null;
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js b/toolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js
@@ -22,6 +22,9 @@ class TestListener {
}
onStartRequest(request) {
+ if (this.listener.onStartRequest) {
+ this.listener.onStartRequest(request);
+ }
this.origListener.onStartRequest(request);
}
@@ -37,7 +40,7 @@ function startListener(listener) {
let observer = {
observe(subject) {
let channel = subject.QueryInterface(Ci.nsIHttpChannel);
- if (channel.URI.spec === "http://example.com/addons/test.xpi") {
+ if (channel.URI.spec.endsWith("/addons/test.xpi")) {
let channelListener = new TestListener(listener);
channelListener.origListener = subject
.QueryInterface(Ci.nsITraceableChannel)
@@ -49,7 +52,7 @@ function startListener(listener) {
Services.obs.addObserver(observer, "http-on-modify-request");
}
-add_task(async function setup() {
+add_setup(async function setup() {
let xpi = AddonTestUtils.createTempWebExtensionFile({
manifest: {
name: "Test",
@@ -109,3 +112,55 @@ add_task(async function test_install_user_cancelled() {
"wait for temp file to be removed"
);
});
+
+add_task(async function test_install_cancelled_on_quit_or_offline() {
+ const TOPICS = [
+ "quit-application-granted",
+ "network:offline-about-to-go-offline",
+ ];
+
+ const server = AddonTestUtils.createHttpServer({
+ hosts: ["slow.example.com"],
+ });
+ let pendingResponses = [];
+ server.registerPathHandler("/addons/test.xpi", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/x-xpinstall");
+ pendingResponses.push(response);
+ response.processAsync();
+ response.write("Here are some bytes");
+ });
+ const cleanupResponses = () => {
+ pendingResponses.forEach(res => res.finish());
+ pendingResponses = [];
+ };
+ registerCleanupFunction(cleanupResponses);
+
+ for (let topic of TOPICS) {
+ info(`Verify DownloadAddonInstall cancelled on topic ${topic}`);
+
+ let url = "http://slow.example.com/addons/test.xpi";
+ let install = await AddonManager.getInstallForURL(url, {
+ name: "Test",
+ version: "1.0",
+ });
+ let downloadStarted = new Promise(resolve => {
+ startListener({
+ onStartRequest() {
+ resolve();
+ },
+ });
+ });
+ let installPromise = install.install();
+ await downloadStarted;
+ equal(install.state, AddonManager.STATE_DOWNLOADING, "install was started");
+ Services.obs.notifyObservers(null, topic);
+ await Assert.rejects(
+ installPromise,
+ /Install failed: onDownloadCancelled/,
+ "Got the expected install onDownloadCancelled failure"
+ );
+ equal(install.state, AddonManager.STATE_CANCELLED, "install was cancelled");
+ cleanupResponses();
+ }
+});