Files
tubestation/toolkit/components/normandy/lib/ActionsManager.jsm
2018-04-19 15:37:11 -07:00

156 lines
5.2 KiB
JavaScript

ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://normandy/lib/LogManager.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
ActionSandboxManager: "resource://normandy/lib/ActionSandboxManager.jsm",
NormandyApi: "resource://normandy/lib/NormandyApi.jsm",
Uptake: "resource://normandy/lib/Uptake.jsm",
ConsoleLogAction: "resource://normandy/actions/ConsoleLogAction.jsm",
PreferenceRolloutAction: "resource://normandy/actions/PreferenceRolloutAction.jsm",
PreferenceRollbackAction: "resource://normandy/actions/PreferenceRollbackAction.jsm",
});
var EXPORTED_SYMBOLS = ["ActionsManager"];
const log = LogManager.getLogger("recipe-runner");
/**
* A class to manage the actions that recipes can use in Normandy.
*
* This includes both remote and local actions. Remote actions
* implementations are fetched from the Normandy server; their
* lifecycles are managed by `normandy/lib/ActionSandboxManager.jsm`.
* Local actions have their implementations packaged in the Normandy
* client, and manage their lifecycles internally.
*/
class ActionsManager {
constructor() {
this.finalized = false;
this.remoteActionSandboxes = {};
this.localActions = {
"console-log": new ConsoleLogAction(),
"preference-rollout": new PreferenceRolloutAction(),
"preference-rollback": new PreferenceRollbackAction(),
};
}
async fetchRemoteActions() {
const actions = await NormandyApi.fetchActions();
for (const action of actions) {
// Skip actions with local implementations
if (action.name in this.localActions) {
continue;
}
try {
const implementation = await NormandyApi.fetchImplementation(action);
const sandbox = new ActionSandboxManager(implementation);
sandbox.addHold("ActionsManager");
this.remoteActionSandboxes[action.name] = sandbox;
} catch (err) {
log.warn(`Could not fetch implementation for ${action.name}: ${err}`);
let status;
if (/NetworkError/.test(err)) {
status = Uptake.ACTION_NETWORK_ERROR;
} else {
status = Uptake.ACTION_SERVER_ERROR;
}
Uptake.reportAction(action.name, status);
}
}
const actionNames = Object.keys(this.remoteActionSandboxes);
log.debug(`Fetched ${actionNames.length} actions from the server: ${actionNames.join(", ")}`);
}
async preExecution() {
// Local actions run pre-execution hooks implicitly
for (const [actionName, manager] of Object.entries(this.remoteActionSandboxes)) {
try {
await manager.runAsyncCallback("preExecution");
manager.disabled = false;
} catch (err) {
log.error(`Could not run pre-execution hook for ${actionName}:`, err.message);
manager.disabled = true;
Uptake.reportAction(actionName, Uptake.ACTION_PRE_EXECUTION_ERROR);
}
}
}
async runRecipe(recipe) {
let actionName = recipe.action;
if (actionName in this.localActions) {
log.info(`Executing recipe "${recipe.name}" (action=${recipe.action})`);
const action = this.localActions[actionName];
await action.runRecipe(recipe);
} else if (actionName in this.remoteActionSandboxes) {
let status;
const manager = this.remoteActionSandboxes[recipe.action];
if (manager.disabled) {
log.warn(
`Skipping recipe ${recipe.name} because ${recipe.action} failed during pre-execution.`
);
status = Uptake.RECIPE_ACTION_DISABLED;
} else {
try {
log.info(`Executing recipe "${recipe.name}" (action=${recipe.action})`);
await manager.runAsyncCallback("action", recipe);
status = Uptake.RECIPE_SUCCESS;
} catch (e) {
e.message = `Could not execute recipe ${recipe.name}: ${e.message}`;
Cu.reportError(e);
status = Uptake.RECIPE_EXECUTION_ERROR;
}
}
Uptake.reportRecipe(recipe.id, status);
} else {
log.error(
`Could not execute recipe ${recipe.name}:`,
`Action ${recipe.action} is either missing or invalid.`
);
Uptake.reportRecipe(recipe.id, Uptake.RECIPE_INVALID_ACTION);
}
}
async finalize() {
if (this.finalized) {
throw new Error("ActionsManager has already been finalized");
}
this.finalized = true;
// Finalize local actions
for (const action of Object.values(this.localActions)) {
action.finalize();
}
// Run post-execution hooks for remote actions
for (const [actionName, manager] of Object.entries(this.remoteActionSandboxes)) {
// Skip if pre-execution failed.
if (manager.disabled) {
log.info(`Skipping post-execution hook for ${actionName} due to earlier failure.`);
continue;
}
try {
await manager.runAsyncCallback("postExecution");
Uptake.reportAction(actionName, Uptake.ACTION_SUCCESS);
} catch (err) {
log.info(`Could not run post-execution hook for ${actionName}:`, err.message);
Uptake.reportAction(actionName, Uptake.ACTION_POST_EXECUTION_ERROR);
}
}
// Nuke sandboxes
Object.values(this.remoteActionSandboxes)
.forEach(manager => manager.removeHold("ActionsManager"));
}
}