@adpt/cli
Version:
AdaptJS command line interface
260 lines • 9.8 kB
JavaScript
;
/*
* 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