Backed out 7 changesets (bug 1942424) for causing frequent crashes. a=backout
[gecko.git] / toolkit / components / normandy / actions / AddonRolloutAction.sys.mjs
blobb6167737ac6edbccd5fcbefe80e5a072120281af
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/. */
5 import { BaseAction } from "resource://normandy/actions/BaseAction.sys.mjs";
7 const lazy = {};
9 ChromeUtils.defineESModuleGetters(lazy, {
10   ActionSchemas: "resource://normandy/actions/schemas/index.sys.mjs",
11   AddonRollouts: "resource://normandy/lib/AddonRollouts.sys.mjs",
12   NormandyAddonManager: "resource://normandy/lib/NormandyAddonManager.sys.mjs",
13   NormandyApi: "resource://normandy/lib/NormandyApi.sys.mjs",
14   TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
15   TelemetryEvents: "resource://normandy/lib/TelemetryEvents.sys.mjs",
16 });
18 class AddonRolloutError extends Error {
19   /**
20    * @param {string} slug
21    * @param {object} extra Extra details to include when reporting the error to telemetry.
22    * @param {string} extra.reason The specific reason for the failure.
23    */
24   constructor(slug, extra) {
25     let message;
26     let { reason } = extra;
27     switch (reason) {
28       case "conflict": {
29         message = "an existing rollout already exists for this add-on";
30         break;
31       }
32       case "addon-id-changed": {
33         message = "upgrade add-on ID does not match installed add-on ID";
34         break;
35       }
36       case "upgrade-required": {
37         message = "a newer version of the add-on is already installed";
38         break;
39       }
40       case "download-failure": {
41         message = "the add-on failed to download";
42         break;
43       }
44       case "metadata-mismatch": {
45         message = "the server metadata does not match the downloaded add-on";
46         break;
47       }
48       case "install-failure": {
49         message = "the add-on failed to install";
50         break;
51       }
52       default: {
53         throw new Error(`Unexpected AddonRolloutError reason: ${reason}`);
54       }
55     }
56     super(`Cannot install add-on for rollout (${slug}): ${message}.`);
57     this.slug = slug;
58     this.extra = extra;
59   }
62 export class AddonRolloutAction extends BaseAction {
63   get schema() {
64     return lazy.ActionSchemas["addon-rollout"];
65   }
67   async _run(recipe) {
68     const { extensionApiId, slug } = recipe.arguments;
70     const existingRollout = await lazy.AddonRollouts.get(slug);
71     const eventName = existingRollout ? "update" : "enroll";
72     const extensionDetails =
73       await lazy.NormandyApi.fetchExtensionDetails(extensionApiId);
75     // Check if the existing rollout matches the current rollout
76     if (
77       existingRollout &&
78       existingRollout.addonId === extensionDetails.extension_id
79     ) {
80       const versionCompare = Services.vc.compare(
81         existingRollout.addonVersion,
82         extensionDetails.version
83       );
85       if (versionCompare === 0) {
86         return; // Do nothing
87       }
88     }
90     const createError = (reason, extra = {}) => {
91       return new AddonRolloutError(slug, {
92         ...extra,
93         reason,
94       });
95     };
97     // Check for a conflict (addon already installed by another rollout)
98     const activeRollouts = await lazy.AddonRollouts.getAllActive();
99     const conflictingRollout = activeRollouts.find(
100       rollout =>
101         rollout.slug !== slug &&
102         rollout.addonId === extensionDetails.extension_id
103     );
104     if (conflictingRollout) {
105       const conflictError = createError("conflict", {
106         addonId: conflictingRollout.addonId,
107         conflictingSlug: conflictingRollout.slug,
108       });
109       this.reportError(conflictError, "enrollFailed");
110       throw conflictError;
111     }
113     const onInstallStarted = (install, installDeferred) => {
114       const existingAddon = install.existingAddon;
116       if (existingRollout && existingRollout.addonId !== install.addon.id) {
117         installDeferred.reject(createError("addon-id-changed"));
118         return false; // cancel the upgrade, the add-on ID has changed
119       }
121       if (
122         existingAddon &&
123         Services.vc.compare(existingAddon.version, install.addon.version) > 0
124       ) {
125         installDeferred.reject(createError("upgrade-required"));
126         return false; // cancel the installation, must be an upgrade
127       }
129       return true;
130     };
132     const applyNormandyChanges = async install => {
133       const details = {
134         addonId: install.addon.id,
135         addonVersion: install.addon.version,
136         extensionApiId,
137         xpiUrl: extensionDetails.xpi,
138         xpiHash: extensionDetails.hash,
139         xpiHashAlgorithm: extensionDetails.hash_algorithm,
140       };
142       if (existingRollout) {
143         await lazy.AddonRollouts.update({
144           ...existingRollout,
145           ...details,
146         });
147       } else {
148         await lazy.AddonRollouts.add({
149           recipeId: recipe.id,
150           state: lazy.AddonRollouts.STATE_ACTIVE,
151           slug,
152           ...details,
153         });
154       }
155     };
157     const undoNormandyChanges = async () => {
158       if (existingRollout) {
159         await lazy.AddonRollouts.update(existingRollout);
160       } else {
161         await lazy.AddonRollouts.delete(recipe.id);
162       }
163     };
165     const [installedId, installedVersion] =
166       await lazy.NormandyAddonManager.downloadAndInstall({
167         createError,
168         extensionDetails,
169         applyNormandyChanges,
170         undoNormandyChanges,
171         onInstallStarted,
172         reportError: error => this.reportError(error, `${eventName}Failed`),
173       });
175     if (existingRollout) {
176       this.log.debug(`Updated addon rollout ${slug}`);
177     } else {
178       this.log.debug(`Enrolled in addon rollout ${slug}`);
179       lazy.TelemetryEnvironment.setExperimentActive(
180         slug,
181         lazy.AddonRollouts.STATE_ACTIVE,
182         {
183           type: "normandy-addonrollout",
184         }
185       );
186     }
188     // All done, report success to Telemetry
189     lazy.TelemetryEvents.sendEvent(eventName, "addon_rollout", slug, {
190       addonId: installedId,
191       addonVersion: installedVersion,
192     });
193   }
195   reportError(error, eventName) {
196     if (error instanceof AddonRolloutError) {
197       // One of our known errors. Report it nicely to telemetry
198       lazy.TelemetryEvents.sendEvent(
199         eventName,
200         "addon_rollout",
201         error.slug,
202         error.extra
203       );
204     } else {
205       /*
206        * Some unknown error. Add some helpful details, and report it to
207        * telemetry. The actual stack trace and error message could possibly
208        * contain PII, so we don't include them here. Instead include some
209        * information that should still be helpful, and is less likely to be
210        * unsafe.
211        */
212       const safeErrorMessage = `${error.fileName}:${error.lineNumber}:${error.columnNumber} ${error.name}`;
213       lazy.TelemetryEvents.sendEvent(eventName, "addon_rollout", error.slug, {
214         reason: safeErrorMessage.slice(0, 80), // max length is 80 chars
215       });
216     }
217   }