diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/normandy/actions/AddonRolloutAction.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/normandy/actions/AddonRolloutAction.sys.mjs')
-rw-r--r-- | toolkit/components/normandy/actions/AddonRolloutAction.sys.mjs | 241 |
1 files changed, 241 insertions, 0 deletions
diff --git a/toolkit/components/normandy/actions/AddonRolloutAction.sys.mjs b/toolkit/components/normandy/actions/AddonRolloutAction.sys.mjs new file mode 100644 index 0000000000..f669aaaf4f --- /dev/null +++ b/toolkit/components/normandy/actions/AddonRolloutAction.sys.mjs @@ -0,0 +1,241 @@ +/* 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/. */ + +import { BaseAction } from "resource://normandy/actions/BaseAction.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ActionSchemas: "resource://normandy/actions/schemas/index.sys.mjs", + AddonRollouts: "resource://normandy/lib/AddonRollouts.sys.mjs", + NormandyAddonManager: "resource://normandy/lib/NormandyAddonManager.sys.mjs", + NormandyApi: "resource://normandy/lib/NormandyApi.sys.mjs", + NormandyUtils: "resource://normandy/lib/NormandyUtils.sys.mjs", + TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs", + TelemetryEvents: "resource://normandy/lib/TelemetryEvents.sys.mjs", +}); + +class AddonRolloutError extends Error { + /** + * @param {string} slug + * @param {object} extra Extra details to include when reporting the error to telemetry. + * @param {string} extra.reason The specific reason for the failure. + */ + constructor(slug, extra) { + let message; + let { reason } = extra; + switch (reason) { + case "conflict": { + message = "an existing rollout already exists for this add-on"; + break; + } + case "addon-id-changed": { + message = "upgrade add-on ID does not match installed add-on ID"; + break; + } + case "upgrade-required": { + message = "a newer version of the add-on is already installed"; + break; + } + case "download-failure": { + message = "the add-on failed to download"; + break; + } + case "metadata-mismatch": { + message = "the server metadata does not match the downloaded add-on"; + break; + } + case "install-failure": { + message = "the add-on failed to install"; + break; + } + default: { + throw new Error(`Unexpected AddonRolloutError reason: ${reason}`); + } + } + super(`Cannot install add-on for rollout (${slug}): ${message}.`); + this.slug = slug; + this.extra = extra; + } +} + +export class AddonRolloutAction extends BaseAction { + get schema() { + return lazy.ActionSchemas["addon-rollout"]; + } + + async _run(recipe) { + const { extensionApiId, slug } = recipe.arguments; + + const existingRollout = await lazy.AddonRollouts.get(slug); + const eventName = existingRollout ? "update" : "enroll"; + const extensionDetails = await lazy.NormandyApi.fetchExtensionDetails( + extensionApiId + ); + let enrollmentId = existingRollout + ? existingRollout.enrollmentId + : undefined; + + // Check if the existing rollout matches the current rollout + if ( + existingRollout && + existingRollout.addonId === extensionDetails.extension_id + ) { + const versionCompare = Services.vc.compare( + existingRollout.addonVersion, + extensionDetails.version + ); + + if (versionCompare === 0) { + return; // Do nothing + } + } + + const createError = (reason, extra) => { + return new AddonRolloutError(slug, { + ...extra, + reason, + }); + }; + + // Check for a conflict (addon already installed by another rollout) + const activeRollouts = await lazy.AddonRollouts.getAllActive(); + const conflictingRollout = activeRollouts.find( + rollout => + rollout.slug !== slug && + rollout.addonId === extensionDetails.extension_id + ); + if (conflictingRollout) { + const conflictError = createError("conflict", { + addonId: conflictingRollout.addonId, + conflictingSlug: conflictingRollout.slug, + enrollmentId: + conflictingRollout.enrollmentId || + lazy.TelemetryEvents.NO_ENROLLMENT_ID_MARKER, + }); + this.reportError(conflictError, "enrollFailed"); + throw conflictError; + } + + const onInstallStarted = (install, installDeferred) => { + const existingAddon = install.existingAddon; + + if (existingRollout && existingRollout.addonId !== install.addon.id) { + installDeferred.reject( + createError("addon-id-changed", { + enrollmentId: + enrollmentId || lazy.TelemetryEvents.NO_ENROLLMENT_ID_MARKER, + }) + ); + return false; // cancel the upgrade, the add-on ID has changed + } + + if ( + existingAddon && + Services.vc.compare(existingAddon.version, install.addon.version) > 0 + ) { + installDeferred.reject( + createError("upgrade-required", { + enrollmentId: + enrollmentId || lazy.TelemetryEvents.NO_ENROLLMENT_ID_MARKER, + }) + ); + return false; // cancel the installation, must be an upgrade + } + + return true; + }; + + const applyNormandyChanges = async install => { + const details = { + addonId: install.addon.id, + addonVersion: install.addon.version, + extensionApiId, + xpiUrl: extensionDetails.xpi, + xpiHash: extensionDetails.hash, + xpiHashAlgorithm: extensionDetails.hash_algorithm, + }; + + if (existingRollout) { + await lazy.AddonRollouts.update({ + ...existingRollout, + ...details, + }); + } else { + enrollmentId = lazy.NormandyUtils.generateUuid(); + await lazy.AddonRollouts.add({ + recipeId: recipe.id, + state: lazy.AddonRollouts.STATE_ACTIVE, + slug, + enrollmentId: + enrollmentId || lazy.TelemetryEvents.NO_ENROLLMENT_ID_MARKER, + ...details, + }); + } + }; + + const undoNormandyChanges = async () => { + if (existingRollout) { + await lazy.AddonRollouts.update(existingRollout); + } else { + await lazy.AddonRollouts.delete(recipe.id); + } + }; + + const [installedId, installedVersion] = + await lazy.NormandyAddonManager.downloadAndInstall({ + createError, + extensionDetails, + applyNormandyChanges, + undoNormandyChanges, + onInstallStarted, + reportError: error => this.reportError(error, `${eventName}Failed`), + }); + + if (existingRollout) { + this.log.debug(`Updated addon rollout ${slug}`); + } else { + this.log.debug(`Enrolled in addon rollout ${slug}`); + lazy.TelemetryEnvironment.setExperimentActive( + slug, + lazy.AddonRollouts.STATE_ACTIVE, + { + type: "normandy-addonrollout", + } + ); + } + + // All done, report success to Telemetry + lazy.TelemetryEvents.sendEvent(eventName, "addon_rollout", slug, { + addonId: installedId, + addonVersion: installedVersion, + enrollmentId: + enrollmentId || lazy.TelemetryEvents.NO_ENROLLMENT_ID_MARKER, + }); + } + + reportError(error, eventName) { + if (error instanceof AddonRolloutError) { + // One of our known errors. Report it nicely to telemetry + lazy.TelemetryEvents.sendEvent( + eventName, + "addon_rollout", + error.slug, + error.extra + ); + } else { + /* + * Some unknown error. Add some helpful details, and report it to + * telemetry. The actual stack trace and error message could possibly + * contain PII, so we don't include them here. Instead include some + * information that should still be helpful, and is less likely to be + * unsafe. + */ + const safeErrorMessage = `${error.fileName}:${error.lineNumber}:${error.columnNumber} ${error.name}`; + lazy.TelemetryEvents.sendEvent(eventName, "addon_rollout", error.slug, { + reason: safeErrorMessage.slice(0, 80), // max length is 80 chars + }); + } + } +} |