UNPKG

@adpt/cli

Version:
260 lines 9.8 kB
"use strict"; /* * Copyright 2019 Unbounded Systems, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const utils_1 = require("@adpt/utils"); const listr_1 = tslib_1.__importDefault(require("@unboundedsystems/listr")); const debug_1 = tslib_1.__importDefault(require("debug")); const p_defer_1 = tslib_1.__importDefault(require("p-defer")); const ts_custom_error_1 = require("ts-custom-error"); const debug = debug_1.default("adapt:cli:tasks"); const debugDetail = debug_1.default("adapt:detail:cli:tasks"); // Show status for ALL tasks, including ones marked trivial const debugTrivial = debug_1.default("adapt:detail:cli:status"); class DynamicTaskFailed extends ts_custom_error_1.CustomError { } exports.DynamicTaskFailed = DynamicTaskFailed; function addDynamicTask(listr, // The listr where this dynamic task will be added listrId, // The task ID that corresponds to this listr msgClient, taskDef) { const { onCompleteRoot } = taskDef, rootTask = tslib_1.__rest(taskDef, ["onCompleteRoot"]); const registry = new TaskRegistry(listr, listrId); // Listen for all task events for all tasks in the task hierarchy under taskDef.id msgClient.task.on(`task:*:${taskDef.id}:**`, (event, status, from) => { debug(`Dynamic task event ${event} (${from}) ${status || ""}`); if (event === utils_1.TaskEvent.Created) { if (typeof status !== "string") throw new utils_1.InternalError(`Task create status is not string`); const taskInfo = utils_1.parseTaskInfo(status); createTask(registry, { id: from, title: taskInfo.description, trivial: taskInfo.trivial, }); } else { updateTask(registry, from, event, status); } }); createTask(registry, Object.assign({ onComplete: onCompleteRoot }, rootTask)); } exports.addDynamicTask = addDynamicTask; function parent(id) { const lastColon = id.lastIndexOf(":"); return lastColon > 0 ? id.slice(0, lastColon) : ""; } function updateStoredEvent(taskStatus, event) { switch (event) { case undefined: return taskStatus.event; case utils_1.TaskEvent.ChildGroup: case utils_1.TaskEvent.Status: case utils_1.TaskEvent.Description: // Filter out TaskEvents that are not TaskStates break; default: taskStatus.event = event; } return event; } function updateStoredStatus(taskStatus, event, status) { switch (event) { case utils_1.TaskEvent.ChildGroup: case utils_1.TaskEvent.Description: case utils_1.TaskEvent.Complete: break; default: if (status == null) status = taskStatus.status; else taskStatus.status = status; } return status; } function updateTask(registry, id, event, status) { const taskStatus = registry.get(id); if (!taskStatus) throw new Error(`Task ${id} got event ${event} but was never created`); const task = taskStatus.task; event = updateStoredEvent(taskStatus, event); status = updateStoredStatus(taskStatus, event, status); switch (event) { case utils_1.TaskEvent.Complete: if (task && status) task.title = `${task.title} (${status})`; if (!taskStatus.settled) taskStatus.dPromise.resolve(); break; case utils_1.TaskEvent.Skipped: if (task) task.skip(status || ""); if (!taskStatus.settled) taskStatus.dPromise.resolve(); break; case utils_1.TaskEvent.Failed: const err = new DynamicTaskFailed(status); if (taskStatus.childGroup) { debugDetail("Warning: Child group task Failed and no child has failed: " + `status: ${taskStatus.status}` + `settled: ${taskStatus.settled}`); } if (!taskStatus.settled) taskStatus.dPromise.reject(err); else if (taskStatus.task) taskStatus.task.report(err); else throw new Error(`Internal error: dynamic task ${id} cannot report error ${err}`); break; case utils_1.TaskEvent.Status: if (task) task.output = status; break; case utils_1.TaskEvent.Description: if (task && status) task.title = status; break; case utils_1.TaskEvent.Created: case utils_1.TaskEvent.Started: break; case utils_1.TaskEvent.ChildGroup: if (!status) throw new Error(`Received event ChildGroup without status`); let opts; try { opts = JSON.parse(status); } catch (err) { throw new Error(`Received ChildGroup event but could not parse options`); } taskStatus.childGroup = new listr_1.default({ concurrent: !opts.serial }); break; default: return utils_1.badTaskEvent(event); } } class TaskRegistry { constructor(rootListr, rootListrId) { this.rootListr = rootListr; this.rootListrId = rootListrId; this.tasks = new Map(); } get(id) { return this.tasks.get(id); } set(id, tStatus) { this.tasks.set(id, tStatus); } getParent(child) { const parentId = parent(child); if (parentId === this.rootListrId) return undefined; const parentStatus = this.get(parentId); if (!parentStatus) throw new Error(`Can't find parent status for ${parentId} for child ${child}`); return parentStatus; } getParentListr(child) { const parentStatus = this.getParent(child); if (!parentStatus) return this.rootListr; if (!parentStatus.childGroup) throw new Error(`Parent ${parent(child)} has no task group`); return parentStatus.childGroup; } } const createTaskDefaults = { adoptable: false, runOnChildTask: 1, trivial: false, }; function createTask(registry, taskDef) { const def = Object.assign({}, createTaskDefaults, taskDef); const { id, title, initiate, adoptable, onComplete, runOnChildTask, trivial } = def; if (!Number.isInteger(runOnChildTask) || runOnChildTask < 1) { throw new Error(`createTask: runOnChildTask must be an integer >= 1`); } const tStatus = registry.get(id); if (tStatus) { if (!tStatus.adoptable) throw new Error(`Task ${id} has already been created`); return; } else { const newStatus = { id, event: utils_1.TaskState.Created, status: undefined, adoptable, dPromise: p_defer_1.default(), settled: false, childTasksNeeded: runOnChildTask, trivial, }; // Record when the promise has been settled const settled = () => newStatus.settled = true; newStatus.dPromise.promise.then(settled, settled); registry.set(id, newStatus); } if (trivial && !debugTrivial.enabled) return; const returnHolder = {}; const listrTask = { title, task: async (ctx, task) => { const cur = registry.get(id); if (!cur) throw new Error(`Cannot find task ${id}`); if (cur.task) throw new Error(`Task ${id} already has a task object`); cur.task = task; updateTask(registry, id); if (initiate) { returnHolder.value = initiate(ctx, task).catch((err) => { cur.dPromise.reject(err); throw err; }); } return cur.dPromise.promise; }, }; listrTask.onComplete = async (ctx, task, err) => { if (onComplete) return onComplete(ctx, task, err, returnHolder.value); if (err) throw err; if (returnHolder.value) await returnHolder.value; }; registry.getParentListr(id).add(listrTask); // Allow the task group to run once there's a task in the group const parentStat = registry.getParent(id); if (parentStat && parentStat.childGroup && parentStat.childTasksNeeded > 0) { if (--parentStat.childTasksNeeded === 0) { parentStat.dPromise.resolve(parentStat.childGroup); } } } function waitForInitiate(err, initiatePromise) { // Ignore errors generated by dynamic task failed messages, but allow // others through. if (err && !(err instanceof DynamicTaskFailed)) { throw err; } if (initiatePromise === undefined) { // This happens if the initiate function throws, but then err should // have been set...? throw new Error(`Internal error: initiatePromise is null`); } return initiatePromise; } exports.waitForInitiate = waitForInitiate; //# sourceMappingURL=dynamic_task_mgr.js.map