@pixlcore/xycli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +543 -0
- package/docs/help.md +2587 -0
- package/docs/screenshots.md +219 -0
- package/docs/sync.md +920 -0
- package/docs/tutorial.md +204 -0
- package/index.js +444 -0
- package/lib/alerts.js +735 -0
- package/lib/apikey.js +425 -0
- package/lib/buckets.js +607 -0
- package/lib/categories.js +323 -0
- package/lib/channels.js +290 -0
- package/lib/config.js +86 -0
- package/lib/dashboard.js +237 -0
- package/lib/doc.js +131 -0
- package/lib/events.js +1314 -0
- package/lib/groups.js +1038 -0
- package/lib/jobs.js +1254 -0
- package/lib/log.js +125 -0
- package/lib/marketplace.js +386 -0
- package/lib/monitors.js +412 -0
- package/lib/plugins.js +491 -0
- package/lib/secrets.js +441 -0
- package/lib/servers.js +1502 -0
- package/lib/snapshots.js +498 -0
- package/lib/sync.js +963 -0
- package/lib/system.js +612 -0
- package/lib/tags.js +250 -0
- package/lib/tickets.js +681 -0
- package/lib/transfer.js +447 -0
- package/lib/utils.js +2745 -0
- package/lib/webhook.js +414 -0
- package/logo.webp +0 -0
- package/package.json +38 -0
- package/test/README.md +97 -0
- package/test/api-validation.test.js +170 -0
- package/test/categories.test.js +246 -0
- package/test/channels.test.js +209 -0
- package/test/config.test.js +96 -0
- package/test/doc.test.js +84 -0
- package/test/groups.test.js +211 -0
- package/test/help.test.js +26 -0
- package/test/helpers/common.js +127 -0
- package/test/helpers/pagination-preload.cjs +18 -0
- package/test/log.test.js +103 -0
- package/test/markdown.test.js +61 -0
- package/test/marketplace.test.js +229 -0
- package/test/monitors.test.js +227 -0
- package/test/pagination.test.js +138 -0
- package/test/plugins.test.js +351 -0
- package/test/secrets.test.js +271 -0
- package/test/servers.test.js +303 -0
- package/test/snapshots.test.js +167 -0
- package/test/sync-unit.test.js +446 -0
- package/test/sync.test.js +542 -0
- package/test/system.test.js +94 -0
- package/test/tags.test.js +230 -0
- package/test/tickets.test.js +261 -0
- package/test/toast.test.js +73 -0
- package/test/transfer-apikey.test.js +96 -0
- package/test/transfer-workflow.test.js +122 -0
- package/test/transfer.test.js +314 -0
- package/test/webhook.test.js +332 -0
- package/test.js +49 -0
package/lib/jobs.js
ADDED
|
@@ -0,0 +1,1254 @@
|
|
|
1
|
+
// Job Search Layer
|
|
2
|
+
|
|
3
|
+
const cli = require('pixl-cli');
|
|
4
|
+
const Tools = cli.Tools;
|
|
5
|
+
const chalk = cli.chalk;
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
|
|
9
|
+
async cmd_jobs() {
|
|
10
|
+
// alias for cmd_search_jobs
|
|
11
|
+
await this.cmd_search_jobs();
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
async cmd_job() {
|
|
15
|
+
// router, e.g. `xy job ID`
|
|
16
|
+
var cmd = this.args.other.shift();
|
|
17
|
+
switch (cmd) {
|
|
18
|
+
case 'search': await this.cmd_search_jobs(); break;
|
|
19
|
+
case 'get': await this.cmd_get_job(); break;
|
|
20
|
+
case 'log': await this.cmd_get_job_log(); break;
|
|
21
|
+
case 'stream': await this.cmd_stream_job(); break;
|
|
22
|
+
|
|
23
|
+
case 'run': await this.cmd_run_job_again(); break;
|
|
24
|
+
case 'resume': await this.cmd_resume_job(); break;
|
|
25
|
+
case 'abort': await this.cmd_abort_job(); break;
|
|
26
|
+
case 'delete': await this.cmd_delete_job(); break;
|
|
27
|
+
|
|
28
|
+
default:
|
|
29
|
+
// assume cmd_get_job, and arg was the id
|
|
30
|
+
this.args.other.unshift(cmd);
|
|
31
|
+
await this.cmd_get_job();
|
|
32
|
+
break;
|
|
33
|
+
} // switch cmd
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async cmd_search_jobs() {
|
|
37
|
+
// search completed jobs
|
|
38
|
+
this.prepSearchArgs();
|
|
39
|
+
await this.getMultiple();
|
|
40
|
+
|
|
41
|
+
// args become search criteria, other[0] becomes raw query
|
|
42
|
+
if (this.args.other.length) this.args.query = this.args.other.join(' ');
|
|
43
|
+
delete this.args.other;
|
|
44
|
+
var is_filtered = !!Tools.firstKey(this.args);
|
|
45
|
+
|
|
46
|
+
// active jobs
|
|
47
|
+
if (!is_filtered) this.printActiveJobs({
|
|
48
|
+
rows: Object.values(this.activeJobs).sort( function(a, b) {
|
|
49
|
+
// keep workflow parent jobs directly above their sub-jobs
|
|
50
|
+
if (b.workflow && (b.workflow.job == a.id)) return -1;
|
|
51
|
+
if (a.workflow && (a.workflow.job == b.id)) return 1;
|
|
52
|
+
return (a.started < b.started) ? 1 : -1;
|
|
53
|
+
} )
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// upcoming jobs
|
|
57
|
+
if (this.args.upcoming || (this.args.query === "upcoming")) {
|
|
58
|
+
await this.printUpcomingJobs();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// job search
|
|
63
|
+
if (this.args.event) {
|
|
64
|
+
var event = this.findObjectFuzzy( this.events, { id: this.args.event, title: this.args.event }, 1 );
|
|
65
|
+
if (!event) return this.die("Could not find event based on your criteria: " + this.args.event);
|
|
66
|
+
this.args.event = event.id;
|
|
67
|
+
}
|
|
68
|
+
if (this.args.category) {
|
|
69
|
+
var category = this.findObjectFuzzy( this.categories, { id: this.args.category, title: this.args.category }, 1 );
|
|
70
|
+
if (!category) return this.die("Could not find category based on your criteria: " + this.args.category);
|
|
71
|
+
this.args.category = category.id;
|
|
72
|
+
}
|
|
73
|
+
if (this.args.plugin && !this.args.plugin.match(/^_/)) {
|
|
74
|
+
var plugin = this.findObjectFuzzy( this.plugins, { id: this.args.plugin, title: this.args.plugin }, 1 );
|
|
75
|
+
if (!plugin) return this.die("Could not find plugin based on your criteria: " + this.args.plugin);
|
|
76
|
+
this.args.plugin = plugin.id;
|
|
77
|
+
}
|
|
78
|
+
if (this.args.server) {
|
|
79
|
+
var server = this.findObjectFuzzy( Object.values(this.servers), { id: this.args.server, title: this.args.server, hostname: this.args.server, ip: this.args.server }, 1 );
|
|
80
|
+
if (!server) return this.die("Could not find server based on your criteria: " + this.args.server);
|
|
81
|
+
this.args.server = server.id;
|
|
82
|
+
}
|
|
83
|
+
if (this.args.group) {
|
|
84
|
+
var group = this.findObjectFuzzy( this.groups, { id: this.args.group, title: this.args.group }, 1 );
|
|
85
|
+
if (!group) return this.die("Could not find group based on your criteria: " + this.args.group);
|
|
86
|
+
this.args.groups = group.id;
|
|
87
|
+
delete this.args.group;
|
|
88
|
+
}
|
|
89
|
+
if (this.args.tag && !this.args.tag.match(/^_/)) {
|
|
90
|
+
var tag = this.findObjectFuzzy( this.tags, { id: this.args.tag, title: this.args.tag }, 1 );
|
|
91
|
+
if (!tag) return this.die("Could not find tag based on your criteria: " + this.args.tag);
|
|
92
|
+
this.args.tags = tag.id;
|
|
93
|
+
delete this.args.tag;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.args.success) {
|
|
97
|
+
this.args.tags = '_success';
|
|
98
|
+
delete this.args.success;
|
|
99
|
+
}
|
|
100
|
+
else if (this.args.error) {
|
|
101
|
+
this.args.tags = '_error';
|
|
102
|
+
delete this.args.error;
|
|
103
|
+
}
|
|
104
|
+
else if (this.args.files) {
|
|
105
|
+
this.args.tags = '_files';
|
|
106
|
+
delete this.args.files;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (this.args.warning) {
|
|
110
|
+
this.args.code = 'warning';
|
|
111
|
+
delete this.args.warning;
|
|
112
|
+
}
|
|
113
|
+
else if (this.args.critical) {
|
|
114
|
+
this.args.code = 'critical';
|
|
115
|
+
delete this.args.critical;
|
|
116
|
+
}
|
|
117
|
+
else if (this.args.abort) {
|
|
118
|
+
this.args.code = 'abort';
|
|
119
|
+
delete this.args.abort;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (this.args.workflow || this.args.workflows) {
|
|
123
|
+
this.args.plugin = '_workflow';
|
|
124
|
+
delete this.args.workflow;
|
|
125
|
+
delete this.args.workflows;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
var date_range = this.args.date || '';
|
|
129
|
+
delete this.args.date;
|
|
130
|
+
|
|
131
|
+
var query = this.args.query || '';
|
|
132
|
+
delete this.args.query;
|
|
133
|
+
query += ' ' + Object.keys(this.args).map( key => `${key}:${this.args[key]}` ).join(' ');
|
|
134
|
+
|
|
135
|
+
// date (keyword or raw expression)
|
|
136
|
+
if (date_range) {
|
|
137
|
+
var criteron = this.getDateRangeQuery('date', date_range) || date_range;
|
|
138
|
+
query += ' ' + criteron;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await this.printCompletedJobs({
|
|
142
|
+
title: is_filtered ? 'Job Search Results' : 'All Completed Jobs',
|
|
143
|
+
query: query.trim()
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
this.printSuggestedCommands({
|
|
147
|
+
"Show successful jobs": "xy jobs --success",
|
|
148
|
+
"Show failed jobs": "xy jobs --error",
|
|
149
|
+
"Show workflows": "xy jobs --workflows",
|
|
150
|
+
"Specific category": "xy jobs --category ID_OR_TITLE",
|
|
151
|
+
"Completed today": "xy jobs --date today",
|
|
152
|
+
"Job details": "xy job JOB_ID_HERE"
|
|
153
|
+
// "Get help": "xy jobs --help"
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
async cmd_stream_job() {
|
|
158
|
+
// stream job progress and print result
|
|
159
|
+
if (this.args.other && this.args.other.length && !this.args.id) {
|
|
160
|
+
this.args.id = this.args.other.shift();
|
|
161
|
+
}
|
|
162
|
+
if (!this.args.id) this.die("Missing required 'id' argument.");
|
|
163
|
+
|
|
164
|
+
// load everything
|
|
165
|
+
await this.getMultiple();
|
|
166
|
+
|
|
167
|
+
// load job directly, as it may be queued (not in AJ)
|
|
168
|
+
var { err, data } = await this.api.getJob({ id: this.args.id, remove: 'timelines' });
|
|
169
|
+
if (err) this.die(err);
|
|
170
|
+
if (!data.job) this.die("Job not found: " + this.args.id);
|
|
171
|
+
var job = data.job;
|
|
172
|
+
|
|
173
|
+
// check if job is still active
|
|
174
|
+
if (job.final) {
|
|
175
|
+
// job must have completed, hop over to job details
|
|
176
|
+
return await this.cmd_get_job();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// print def list of job summary data
|
|
180
|
+
this.printBoxList({
|
|
181
|
+
title: "Job Summary",
|
|
182
|
+
rows: [
|
|
183
|
+
[ 'Job ID', this.color('theme').bold( this.getNiceJob(job) ) ],
|
|
184
|
+
[ 'Event', this.getNiceJobEvent(job) ],
|
|
185
|
+
[ 'Category', this.getNiceCategory(job.category) + ` ${gray('(' + job.category + ')')}` ],
|
|
186
|
+
[ 'Plugin', this.getNicePlugin(job.plugin) + (job.plugin != '_workflow' ? ` ${gray('(' + job.plugin + ')')}` : '') ],
|
|
187
|
+
(job.type != 'workflow') ? [ 'Targets', this.getNiceTargets(job.targets) ] : null,
|
|
188
|
+
(job.type != 'workflow') ? [ 'Server', this.getNiceServer(job.server) + ` ${gray('(' + job.server + ')')}` ] : null,
|
|
189
|
+
(job.type != 'workflow') ? [ 'Algorithm', this.getNiceAlgo(job.algo) ] : null,
|
|
190
|
+
[ 'Source', this.getNiceJobSource(job) ],
|
|
191
|
+
(job.workflow && job.workflow.job) ? [ 'Parent Workflow', this.getNiceJob(job.workflow.job) ] : null,
|
|
192
|
+
(job.parent && job.parent.job) ? [ 'Linked Job', this.getNiceJob(job.parent.job) ] : null,
|
|
193
|
+
[ 'Started', this.getRelativeDateTime(job.started, true) ]
|
|
194
|
+
]
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// println( "\n " + this.color('theme').bold("Streaming Job: ") + '#' + this.args.id );
|
|
198
|
+
print("\n");
|
|
199
|
+
|
|
200
|
+
// print meta log up to current
|
|
201
|
+
(job.activity || []).forEach( row => {
|
|
202
|
+
if (!row.msg) return;
|
|
203
|
+
if (!row.server) row.server = 'n/a';
|
|
204
|
+
cli.println( green('[' + this.getNiceServer(String(row.server).replace(/^m:/, '')) + '] ') + gray( String(row.msg).trim() ) );
|
|
205
|
+
} );
|
|
206
|
+
|
|
207
|
+
cli.progress.start();
|
|
208
|
+
var job = {};
|
|
209
|
+
var { err } = await this.api.streamJob({ id: this.args.id, output: true, meta: true }, data => {
|
|
210
|
+
// called repeatedly for each streaming job update
|
|
211
|
+
if ((data.type === 'output') && ('text' in data) && (typeof(data.text) == 'string')) {
|
|
212
|
+
if (!data.text.match(/\n$/)) data.text += "\n";
|
|
213
|
+
cli.print( data.text );
|
|
214
|
+
}
|
|
215
|
+
else if ((data.type === 'meta') && data.row && data.row.msg) {
|
|
216
|
+
if (!data.row.server) data.row.server = 'n/a';
|
|
217
|
+
cli.println( green('[' + this.getNiceServer(String(data.row.server).replace(/^m:/, '')) + '] ') + gray( String(data.row.msg).trim() ) );
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
Tools.mergeHashInto( job, data );
|
|
221
|
+
cli.progress.update({
|
|
222
|
+
amount: job.progress || 0,
|
|
223
|
+
text: '[' + (job.status || this.getNiceJobState(job)) + ']'
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
if (err) this.die(err);
|
|
228
|
+
cli.progress.end();
|
|
229
|
+
|
|
230
|
+
verboseln( "\n Stream is complete." );
|
|
231
|
+
|
|
232
|
+
// this.printJobCompletionBanner(job);
|
|
233
|
+
// delete this.activeJobs[this.args.id];
|
|
234
|
+
await this.cmd_get_job();
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
async cmd_get_job() {
|
|
238
|
+
// get job progress or result
|
|
239
|
+
if (this.args.other && this.args.other.length && !this.args.id) {
|
|
240
|
+
this.args.id = this.args.other.shift();
|
|
241
|
+
}
|
|
242
|
+
if (!this.args.id) this.die("Missing required 'id' argument.");
|
|
243
|
+
|
|
244
|
+
// load everything
|
|
245
|
+
await this.getMultiple();
|
|
246
|
+
|
|
247
|
+
// load job data
|
|
248
|
+
var { err, data } = await this.api.getJob({ id: this.args.id, remove: 'timelines' });
|
|
249
|
+
if (err) this.die(err);
|
|
250
|
+
if (!data.job) this.die("Job not found: " + this.args.id);
|
|
251
|
+
var job = data.job;
|
|
252
|
+
|
|
253
|
+
if (!job.final) {
|
|
254
|
+
// job is still in progress, hop over to cmd_stream_job
|
|
255
|
+
return await this.cmd_stream_job();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (this.format.match(/json/)) {
|
|
259
|
+
return this.jsonOutput(job);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.printJobCompletionBanner(job);
|
|
263
|
+
|
|
264
|
+
// The Job result code may be a number, a string such as "warning", or a
|
|
265
|
+
// falsey success value. Normalize a missing code to zero for display.
|
|
266
|
+
var result_code = ('code' in job) ? job.code : 0;
|
|
267
|
+
var result_args = this.getJobResultArgs(job);
|
|
268
|
+
var description = job.description || (
|
|
269
|
+
result_code ?
|
|
270
|
+
'Unknown Error (no description provided).' :
|
|
271
|
+
'Job completed successfully.'
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Summarize retry lineage. retry_count is zero or absent for the initial
|
|
275
|
+
// attempt, and retry_prev points to the immediately preceding attempt.
|
|
276
|
+
var nice_attempt = 'Initial';
|
|
277
|
+
if (job.retry_count) {
|
|
278
|
+
nice_attempt = 'Retry #' + Tools.commify(job.retry_count);
|
|
279
|
+
if (job.retry_prev) {
|
|
280
|
+
nice_attempt += gray(' (Previous: ') +
|
|
281
|
+
this.getNiceJob(job.retry_prev) + gray(')');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// CPU is expressed in percentage points
|
|
286
|
+
var has_cpu = !!(job.cpu && job.cpu.count && ('total' in job.cpu));
|
|
287
|
+
var avg_cpu = has_cpu ?
|
|
288
|
+
Math.round(job.cpu.total / job.cpu.count) : 0;
|
|
289
|
+
|
|
290
|
+
// Memory samples are expressed in bytes.
|
|
291
|
+
var has_mem = !!(job.mem && job.mem.count && ('total' in job.mem));
|
|
292
|
+
var avg_mem = has_mem ?
|
|
293
|
+
Math.floor(job.mem.total / job.mem.count) : 0;
|
|
294
|
+
|
|
295
|
+
this.printBoxList({
|
|
296
|
+
title: job.test ? "Test Summary" : "Job Summary",
|
|
297
|
+
rows: [
|
|
298
|
+
[ 'Job ID', this.color('theme').bold(this.getNiceJob(job)) ],
|
|
299
|
+
[ 'Event', this.getNiceJobEvent(job) ],
|
|
300
|
+
[ 'Category', this.getNiceCategory(job.category) + ` ${gray('(' + job.category + ')')}` ],
|
|
301
|
+
[ 'Plugin', this.getNicePlugin(job.plugin) + (job.plugin != '_workflow' ? ` ${gray('(' + job.plugin + ')')}` : '') ],
|
|
302
|
+
[ 'Targets', this.getNiceTargets(job.targets) ],
|
|
303
|
+
job.algo ? [ 'Algorithm', this.getNiceAlgo(job.algo) ] : null,
|
|
304
|
+
job.server ? [ 'Server', this.getNiceServer(job.server) + ` ${gray('(' + job.server + ')')}` ] : null,
|
|
305
|
+
|
|
306
|
+
// [ 'Result', this.getNiceJobResult(job) ],
|
|
307
|
+
// [ 'Result Code', this.color(result_args.color).bold(String(result_code)) ],
|
|
308
|
+
// [ 'Description', this.color(result_args.color)(description) ],
|
|
309
|
+
|
|
310
|
+
[ 'Source', this.getNiceJobSource(job) ],
|
|
311
|
+
(job.workflow && job.workflow.job) ? [ 'Parent Workflow', this.getNiceJob(job.workflow.job) ] : null,
|
|
312
|
+
(job.parent && job.parent.job) ? [ 'Linked Job', this.getNiceJob(job.parent.job) ] : null,
|
|
313
|
+
[ 'Attempt', nice_attempt ],
|
|
314
|
+
|
|
315
|
+
[ 'Started', this.getRelativeDateTime(job.started, true) ],
|
|
316
|
+
[ 'Completed', this.getRelativeDateTime(job.completed, true) ],
|
|
317
|
+
[ 'Elapsed', this.getNiceJobElapsedTime(job) ],
|
|
318
|
+
|
|
319
|
+
has_cpu ? [ 'Avg CPU', Tools.pct(avg_cpu, 100) ] : null,
|
|
320
|
+
has_mem ? [ 'Avg Memory', Tools.getTextFromBytes(avg_mem) ] : null,
|
|
321
|
+
(job.type != 'workflow') ? [ 'Job Output', Tools.getTextFromBytes(job.log_file_size || 0) ] : null,
|
|
322
|
+
|
|
323
|
+
[ 'Tags', this.getNiceTagList(job.tags || []) ]
|
|
324
|
+
]
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// workflow jobs
|
|
328
|
+
if (job.type == 'workflow') {
|
|
329
|
+
this.printWorkflowJobs(job);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// actions
|
|
333
|
+
this.printJobActions(job);
|
|
334
|
+
|
|
335
|
+
// limits
|
|
336
|
+
this.printJobLimits(job);
|
|
337
|
+
|
|
338
|
+
// Related alerts, snapshots, and tickets share the requested page settings.
|
|
339
|
+
this.prepSearchArgs();
|
|
340
|
+
|
|
341
|
+
// alerts
|
|
342
|
+
await this.printAlertInvocations({
|
|
343
|
+
title: 'Alert Invocations',
|
|
344
|
+
query: 'jobs:' + job.id,
|
|
345
|
+
hide_empty: true
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// snapshots
|
|
349
|
+
await this.printSnapshots({
|
|
350
|
+
title: 'Server Snapshots',
|
|
351
|
+
query: 'jobs:' + job.id,
|
|
352
|
+
hide_empty: true
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// tickets
|
|
356
|
+
await this.printTickets({
|
|
357
|
+
title: 'Job Tickets',
|
|
358
|
+
ids: job.tickets || [],
|
|
359
|
+
hide_empty: true
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// additional jobs
|
|
363
|
+
await this.printAdditionalJobs({
|
|
364
|
+
title: 'Additional Jobs',
|
|
365
|
+
jobs: job.jobs || [],
|
|
366
|
+
hide_empty: true
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// user params
|
|
370
|
+
this.printParamValues({
|
|
371
|
+
title: `User Parameters`,
|
|
372
|
+
fields: job.fields || [],
|
|
373
|
+
values: job.params || {}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// plugin params
|
|
377
|
+
if (job.type != 'workflow') {
|
|
378
|
+
var plugin = Tools.findObject( this.plugins, { id: job.plugin } ) || { title: "Plugin" };
|
|
379
|
+
this.printParamValues({
|
|
380
|
+
title: `${plugin.title} Parameters`,
|
|
381
|
+
fields: plugin.params || [],
|
|
382
|
+
values: job.params || {}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// user content
|
|
387
|
+
if (job.table && Array.isArray(job.table.header) && Array.isArray(job.table.rows)) {
|
|
388
|
+
var table_header = job.table.header.map( function(value) {
|
|
389
|
+
return (value == null) ? '' : String(value);
|
|
390
|
+
} );
|
|
391
|
+
|
|
392
|
+
var table_rows = job.table.rows.map( function(row) {
|
|
393
|
+
return row.map( function(value) {
|
|
394
|
+
if (value == null) return '';
|
|
395
|
+
if (typeof(value) == 'object') return JSON.stringify(value);
|
|
396
|
+
return String(value);
|
|
397
|
+
} );
|
|
398
|
+
} );
|
|
399
|
+
|
|
400
|
+
this.printBoxTable({
|
|
401
|
+
title: job.table.title || 'User Table',
|
|
402
|
+
header: table_header,
|
|
403
|
+
rows: table_rows
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
if (job.table.caption) {
|
|
407
|
+
println( " " + gray(job.table.caption) );
|
|
408
|
+
}
|
|
409
|
+
} // job.table
|
|
410
|
+
|
|
411
|
+
// job.html
|
|
412
|
+
if (job.html && job.html.content) {
|
|
413
|
+
println( "\n " + this.color('theme').bold( String( job.html.title || "Custom Content" ).toUpperCase() ) );
|
|
414
|
+
|
|
415
|
+
this.printNiceHTML( job.html.content );
|
|
416
|
+
|
|
417
|
+
if (job.table.caption) {
|
|
418
|
+
println( " " + gray(job.table.caption) );
|
|
419
|
+
}
|
|
420
|
+
} // job.html
|
|
421
|
+
|
|
422
|
+
// perf metrics
|
|
423
|
+
if (job.perf) {
|
|
424
|
+
println( "\n " + this.color('theme').bold( 'PERFORMANCE METRICS' ) );
|
|
425
|
+
var disp_value = JSON.stringify(job.perf, null, "\t").trim().split(/\n/).map( line => ' ' + line ).join("\n");
|
|
426
|
+
println( this.highlight(disp_value, { language: 'json', ignoreIllegals: true }) );
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// files
|
|
430
|
+
this.printJobFiles(job);
|
|
431
|
+
|
|
432
|
+
// data
|
|
433
|
+
this.printJobData( job.input?.data, "Input Data" );
|
|
434
|
+
this.printJobData( job.data, "Output Data" );
|
|
435
|
+
|
|
436
|
+
// job output
|
|
437
|
+
this.printJobOutput(job);
|
|
438
|
+
|
|
439
|
+
// meta log
|
|
440
|
+
this.printMetaLog(job);
|
|
441
|
+
|
|
442
|
+
// suggested commands
|
|
443
|
+
var has_output = !!(job.output || job.log_file_size);
|
|
444
|
+
var has_event = !!(job.event && Tools.findObject(this.events, { id: job.event }));
|
|
445
|
+
|
|
446
|
+
this.printSuggestedCommands({
|
|
447
|
+
"Show details": this.verbose ? "" : `xy job ${job.id} --verbose`,
|
|
448
|
+
"View job output": has_output ? `xy job log ${job.id}` : "",
|
|
449
|
+
"Download job output": has_output ? `xy job log ${job.id} --download ${job.id}.log` : "",
|
|
450
|
+
"Parent workflow": (job.workflow && job.workflow.job) ? `xy job ${job.workflow.job}` : "",
|
|
451
|
+
"Linked job": (job.parent && job.parent.job) ? `xy job ${job.parent.job}` : "",
|
|
452
|
+
"Previous attempt": job.retry_prev ? `xy job ${job.retry_prev}` : "",
|
|
453
|
+
"Event details": has_event ? `xy event ${job.event}` : "",
|
|
454
|
+
"Run event again": has_event ? `xy run ${job.event}` : ""
|
|
455
|
+
});
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
async cmd_get_job_log() {
|
|
459
|
+
// print raw job log
|
|
460
|
+
// alias: xy job log JOB_ID
|
|
461
|
+
if (this.args.other && this.args.other.length && !this.args.id) {
|
|
462
|
+
this.args.id = this.args.other.shift();
|
|
463
|
+
}
|
|
464
|
+
if (!this.args.id) this.die("Missing required 'id' argument.");
|
|
465
|
+
|
|
466
|
+
println( "\n " + this.color('theme').bold( 'JOB OUTPUT: #' + this.args.id ) );
|
|
467
|
+
if (this.args.download) println( " " + gray("Downloading to file: ") + this.args.download );
|
|
468
|
+
else println("");
|
|
469
|
+
|
|
470
|
+
var download = this.args.download || this.getOutputStream();
|
|
471
|
+
var { err } = await this.api.getJobLog({ id: this.args.id }, { download });
|
|
472
|
+
if (err) this.die(err);
|
|
473
|
+
|
|
474
|
+
if (this.args.download) println( " " + gray("Download complete." ) );
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
async cmd_run_job_again() {
|
|
478
|
+
// run completed job again
|
|
479
|
+
if (this.args.other && this.args.other.length && !this.args.id) {
|
|
480
|
+
this.args.id = this.args.other.shift();
|
|
481
|
+
}
|
|
482
|
+
if (!this.args.id) this.die("Missing required 'id' argument.");
|
|
483
|
+
|
|
484
|
+
var do_follow = this.args.follow;
|
|
485
|
+
delete this.args.follow;
|
|
486
|
+
|
|
487
|
+
// load everything
|
|
488
|
+
await this.getMultiple();
|
|
489
|
+
|
|
490
|
+
// load job data
|
|
491
|
+
var { err, data } = await this.api.getJob({ id: this.args.id, remove: 'timelines' });
|
|
492
|
+
if (err) this.die(err);
|
|
493
|
+
if (!data.job) this.die("Job not found: " + this.args.id);
|
|
494
|
+
var job = data.job;
|
|
495
|
+
|
|
496
|
+
if (!job.final) this.die("Job is still in progress.");
|
|
497
|
+
if (!job.event) this.die("Cannot rerun adhoc jobs (need to rerun the workflow).");
|
|
498
|
+
|
|
499
|
+
if (job.workflow && (job.type == 'adhoc')) {
|
|
500
|
+
return this.die("Adhoc workflow jobs cannot run independently. Please rerun the parent workflow job instead.");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// clense new job of previous running context
|
|
504
|
+
var new_job = Tools.copyHash(job, true);
|
|
505
|
+
for (var key in new_job) {
|
|
506
|
+
if (!key.match(/^(type|event|category|plugin|targets|algo|workflow|input|params|parent|source|username|api_key|icon|label|test|retry_count|tags|now)$/)) delete new_job[key];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// run_event API expects event ID in "id" prop
|
|
510
|
+
new_job.id = job.event;
|
|
511
|
+
|
|
512
|
+
// locate original event
|
|
513
|
+
var event = Tools.findObject( this.events, { id: job.event } );
|
|
514
|
+
if (!event) this.die("The original event could not be found: " + job.event);
|
|
515
|
+
|
|
516
|
+
// we must reset actions and limits to the event versions, as inherited ones are added server-side
|
|
517
|
+
new_job.actions = Tools.copyHash( event.actions || [], true );
|
|
518
|
+
new_job.limits = Tools.copyHash( event.limits || [], true );
|
|
519
|
+
|
|
520
|
+
// remove workflow running context (state, etc.)
|
|
521
|
+
if (new_job.workflow) {
|
|
522
|
+
for (var key in new_job.workflow) {
|
|
523
|
+
if (!key.match(/^(nodes|connections|start)$/)) delete new_job.workflow[key];
|
|
524
|
+
}
|
|
525
|
+
if (!Tools.numKeys(new_job.workflow)) delete new_job.workflow;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// remove system tags from new job
|
|
529
|
+
// FUTURE: tags that were added dynamically during the previous job run will be included in the new job
|
|
530
|
+
new_job.tags = (new_job.tags || []).filter( function(tag) { return !tag.match(/^_/); } );
|
|
531
|
+
|
|
532
|
+
var data = await this.callStandardAPI('runEvent', new_job, {
|
|
533
|
+
text: 'Running job again...'
|
|
534
|
+
});
|
|
535
|
+
if (this.dry) return;
|
|
536
|
+
|
|
537
|
+
this.toast('✅', 'green', "Successfully launched job: #" + data.id );
|
|
538
|
+
|
|
539
|
+
if (do_follow) {
|
|
540
|
+
this.gotMultiple = false;
|
|
541
|
+
this.args = { id: data.id };
|
|
542
|
+
await this.cmd_stream_job();
|
|
543
|
+
}
|
|
544
|
+
else if (!this.verbose) {
|
|
545
|
+
println( "\n " + green.bold("Watch job live: ") + bold("xy job " + data.id) );
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
|
|
549
|
+
async cmd_resume_job() {
|
|
550
|
+
// resume suspended job
|
|
551
|
+
// xy job resume JOB_ID --params.foo "bar"
|
|
552
|
+
// xy job resume JOB_ID --input.data.bar "baz"
|
|
553
|
+
if (this.args.other && this.args.other.length && !this.args.id) {
|
|
554
|
+
this.args.id = this.args.other.shift();
|
|
555
|
+
}
|
|
556
|
+
if (!this.args.id) this.die("Missing required 'id' argument.");
|
|
557
|
+
delete this.args.other;
|
|
558
|
+
|
|
559
|
+
// load everything
|
|
560
|
+
await this.getMultiple();
|
|
561
|
+
|
|
562
|
+
// resolve dot paths in args
|
|
563
|
+
this.mergeDotArgs( this.args, this.args );
|
|
564
|
+
|
|
565
|
+
// load job directly, as it may be queued (not in AJ)
|
|
566
|
+
var { err, data } = await this.api.getJob({ id: this.args.id, remove: 'timelines' });
|
|
567
|
+
if (err) this.die(err);
|
|
568
|
+
if (!data.job) this.die("Job not found: " + this.args.id);
|
|
569
|
+
var job = data.job;
|
|
570
|
+
|
|
571
|
+
// check if job is still active
|
|
572
|
+
if (job.final) this.die("Job is already completed.");
|
|
573
|
+
if (!job.suspended) this.die("Job is not currently suspended.");
|
|
574
|
+
|
|
575
|
+
var data = await this.callStandardAPI('resumeJob', this.args, {
|
|
576
|
+
text: 'Resuming job...'
|
|
577
|
+
});
|
|
578
|
+
if (this.dry) return;
|
|
579
|
+
|
|
580
|
+
this.toast('✅', 'green', "Successfully resumed job: #" + this.args.id );
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
async cmd_abort_job() {
|
|
584
|
+
// abort active job
|
|
585
|
+
// xy job abort JOB_ID
|
|
586
|
+
if (this.args.other && this.args.other.length && !this.args.id) {
|
|
587
|
+
this.args.id = this.args.other.shift();
|
|
588
|
+
}
|
|
589
|
+
if (!this.args.id) this.die("Missing required 'id' argument.");
|
|
590
|
+
delete this.args.other;
|
|
591
|
+
|
|
592
|
+
// load everything
|
|
593
|
+
await this.getMultiple();
|
|
594
|
+
|
|
595
|
+
// load job directly, as it may be queued (not in AJ)
|
|
596
|
+
var { err, data } = await this.api.getJob({ id: this.args.id, remove: 'timelines' });
|
|
597
|
+
if (err) this.die(err);
|
|
598
|
+
if (!data.job) this.die("Job not found: " + this.args.id);
|
|
599
|
+
var job = data.job;
|
|
600
|
+
|
|
601
|
+
// check if job is still active
|
|
602
|
+
if (job.final) this.die("Job is already completed.");
|
|
603
|
+
|
|
604
|
+
this.printMutationSummary({
|
|
605
|
+
title: 'Abort Job',
|
|
606
|
+
rows: [
|
|
607
|
+
[ 'Job ID', this.color('theme').bold(this.getNiceJob(job)) ],
|
|
608
|
+
[ 'Event', this.getNiceJobEvent(job) ]
|
|
609
|
+
]
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
var data = await this.callStandardAPI('abortJob', this.args, {
|
|
613
|
+
text: 'Aborting job...'
|
|
614
|
+
});
|
|
615
|
+
if (this.dry) return;
|
|
616
|
+
|
|
617
|
+
this.toast('✅', 'green', "Successfully aborted job: #" + this.args.id );
|
|
618
|
+
},
|
|
619
|
+
|
|
620
|
+
async cmd_delete_job() {
|
|
621
|
+
// delete completed job
|
|
622
|
+
// xy job delete JOB_ID --confirm
|
|
623
|
+
if (this.args.other && this.args.other.length && !this.args.id) {
|
|
624
|
+
this.args.id = this.args.other.shift();
|
|
625
|
+
}
|
|
626
|
+
if (!this.args.id) this.die("Missing required 'id' argument.");
|
|
627
|
+
delete this.args.other;
|
|
628
|
+
|
|
629
|
+
// load everything
|
|
630
|
+
await this.getMultiple();
|
|
631
|
+
|
|
632
|
+
// load job directly
|
|
633
|
+
var { err, data } = await this.api.getJob({ id: this.args.id, remove: 'timelines' });
|
|
634
|
+
if (err) this.die(err);
|
|
635
|
+
if (!data.job) this.die("Job not found: " + this.args.id);
|
|
636
|
+
var job = data.job;
|
|
637
|
+
|
|
638
|
+
// check if job is still active
|
|
639
|
+
if (!job.final) this.die("Cannot delete active job.");
|
|
640
|
+
|
|
641
|
+
this.printMutationSummary({
|
|
642
|
+
title: 'Delete Job',
|
|
643
|
+
rows: [
|
|
644
|
+
[ 'Job ID', this.color('theme').bold(this.getNiceJob(job)) ],
|
|
645
|
+
[ 'Event', this.getNiceJobEvent(job) ]
|
|
646
|
+
]
|
|
647
|
+
});
|
|
648
|
+
if (this.args.confirm !== true) {
|
|
649
|
+
this.toast('⚠️', 'orange', "Please confirm the delete by adding '--confirm'.");
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
delete this.args.confirm;
|
|
653
|
+
|
|
654
|
+
var data = await this.callStandardAPI('deleteJob', this.args, {
|
|
655
|
+
text: 'Deleting job...'
|
|
656
|
+
});
|
|
657
|
+
if (this.dry) return;
|
|
658
|
+
|
|
659
|
+
this.toast('✅', 'green', "Successfully deleted job: #" + this.args.id );
|
|
660
|
+
},
|
|
661
|
+
|
|
662
|
+
printJobData(data, title) {
|
|
663
|
+
// print job input or output data in verbose mode
|
|
664
|
+
if (!data) return;
|
|
665
|
+
|
|
666
|
+
println( "\n " + this.color('theme').bold( String(title).toUpperCase() ) );
|
|
667
|
+
if (!this.verbose) {
|
|
668
|
+
println( " " + gray("(Shown in verbose mode)") );
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
var disp_value = JSON.stringify(data, null, "\t").trim().split(/\n/).map( line => ' ' + line ).join("\n");
|
|
673
|
+
println( this.highlight(disp_value, { language: 'json', ignoreIllegals: true }) );
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
printJobOutput(job) {
|
|
677
|
+
// print job output in verbose mode
|
|
678
|
+
if (!job.output && !job.log_file_size) return; // zero output
|
|
679
|
+
|
|
680
|
+
println( "\n " + this.color('theme').bold( 'JOB OUTPUT (' + Tools.getTextFromBytes(job.log_file_size) + ')' ) );
|
|
681
|
+
|
|
682
|
+
if (!job.output && job.log_file_size) {
|
|
683
|
+
// too large, requires separate API call
|
|
684
|
+
println( " " + gray("View using: ") + bold("xy job log " + this.args.id) );
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (!this.verbose) {
|
|
689
|
+
println( " " + gray("(Shown in verbose mode)") );
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
println( "\n" + job.output.trim() );
|
|
694
|
+
},
|
|
695
|
+
|
|
696
|
+
printMetaLog(job) {
|
|
697
|
+
// Print the chronological job metadata activity log.
|
|
698
|
+
var activity = job.activity || [];
|
|
699
|
+
if (!activity.length) return;
|
|
700
|
+
|
|
701
|
+
var is_workflow = (job.type == 'workflow');
|
|
702
|
+
var title = is_workflow ? 'WORKFLOW LOG' : 'METADATA LOG';
|
|
703
|
+
|
|
704
|
+
// Keep potentially noisy metadata behind verbose mode.
|
|
705
|
+
if (!this.verbose) {
|
|
706
|
+
println( "\n " + this.color('theme').bold(title) );
|
|
707
|
+
println( " " + gray("(Shown in verbose mode)") );
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
var rows = activity.map( row => {
|
|
712
|
+
var nice_timestamp = row.epoch ? this.getRelativeDateTime(row.epoch, true) : '-';
|
|
713
|
+
var nice_server = '-';
|
|
714
|
+
|
|
715
|
+
if (row.server) {
|
|
716
|
+
var server_id = String(row.server);
|
|
717
|
+
|
|
718
|
+
// Conductor rows use the special "m:hostname" notation.
|
|
719
|
+
if (server_id.match(/^\w+$/)) nice_server = this.getNiceServer(server_id);
|
|
720
|
+
else nice_server = server_id.replace(/^m\:/, '');
|
|
721
|
+
}
|
|
722
|
+
else if (row.username) {
|
|
723
|
+
nice_server = row.username;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Meta-log messages should be single-line table cells.
|
|
727
|
+
var message = String(row.msg || '').replace(/\s+/g, ' ').trim();
|
|
728
|
+
var is_warning = message.match(/^WARNING\:/);
|
|
729
|
+
|
|
730
|
+
// Match the web UI by highlighting IDs and state names.
|
|
731
|
+
var nice_message = message
|
|
732
|
+
.replace(/\#(\w+)/g, function(match) {
|
|
733
|
+
return cyan.bold(match);
|
|
734
|
+
})
|
|
735
|
+
.replace(/\{(.+?)\}/g, function(match, state) {
|
|
736
|
+
return cyan(state);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
if (is_warning) {
|
|
740
|
+
nice_message = nice_message.replace(/^WARNING\:/, yellow.bold('WARNING:'));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (!is_workflow) {
|
|
744
|
+
return [
|
|
745
|
+
nice_timestamp,
|
|
746
|
+
nice_server,
|
|
747
|
+
nice_message
|
|
748
|
+
];
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Workflow meta rows may also identify the associated node.
|
|
752
|
+
var nice_node_id = '-';
|
|
753
|
+
var nice_node_type = '-';
|
|
754
|
+
|
|
755
|
+
if (row.node) {
|
|
756
|
+
nice_node_id = gray('#' + row.node);
|
|
757
|
+
|
|
758
|
+
var workflow = job.workflow || {};
|
|
759
|
+
var nodes = workflow.nodes || [];
|
|
760
|
+
var node = Tools.findObject( nodes, { id: row.node } );
|
|
761
|
+
|
|
762
|
+
if (node) {
|
|
763
|
+
var node_types = (this.config.ui && this.config.ui.workflow_node_types) || [];
|
|
764
|
+
var type_def = Tools.findObject( node_types, { id: node.type } );
|
|
765
|
+
|
|
766
|
+
nice_node_type = type_def ? type_def.title : Tools.ucfirst(node.type || 'Unknown');
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return [
|
|
771
|
+
nice_timestamp,
|
|
772
|
+
nice_server,
|
|
773
|
+
nice_node_id,
|
|
774
|
+
nice_node_type,
|
|
775
|
+
nice_message
|
|
776
|
+
];
|
|
777
|
+
} );
|
|
778
|
+
|
|
779
|
+
this.printBoxTable({
|
|
780
|
+
title: title,
|
|
781
|
+
header: is_workflow ? [ 'Date / Time', 'Server', 'Node ID', 'Type', 'Message' ] : [ 'Date / Time', 'Server', 'Message' ],
|
|
782
|
+
rows: rows
|
|
783
|
+
});
|
|
784
|
+
},
|
|
785
|
+
|
|
786
|
+
async printAdditionalJobs(opts) {
|
|
787
|
+
// print additional jobs for job (with reason column)
|
|
788
|
+
if (!opts) opts = {};
|
|
789
|
+
|
|
790
|
+
var rows = [];
|
|
791
|
+
var list = { length: 0 };
|
|
792
|
+
|
|
793
|
+
if (!opts.jobs || !opts.jobs.length) return;
|
|
794
|
+
var ids = opts.jobs.map( stub => stub.id );
|
|
795
|
+
|
|
796
|
+
cli.progress.start();
|
|
797
|
+
var { err, data } = await this.api.getJobs({
|
|
798
|
+
ids: ids
|
|
799
|
+
});
|
|
800
|
+
if (err) this.die(err);
|
|
801
|
+
cli.progress.end();
|
|
802
|
+
|
|
803
|
+
var rows = data.jobs || [];
|
|
804
|
+
list = { length: rows.length };
|
|
805
|
+
|
|
806
|
+
// Embedded detail screens should omit the section when there are no associated tickets.
|
|
807
|
+
if (opts.hide_empty && !rows.length) return;
|
|
808
|
+
|
|
809
|
+
rows.forEach( function(row, idx) {
|
|
810
|
+
row.reason = opts.jobs[idx].reason;
|
|
811
|
+
} );
|
|
812
|
+
|
|
813
|
+
opts.title = opts.title || 'Additional Jobs';
|
|
814
|
+
opts.header = [ 'Job ID', 'Reason', 'Event', 'Category', 'Plugin', 'Server', 'Result' ];
|
|
815
|
+
opts.rows = rows.map( job => {
|
|
816
|
+
if (!job) return false;
|
|
817
|
+
return [
|
|
818
|
+
this.color('theme').bold( this.getNiceJob(job) ),
|
|
819
|
+
Tools.ucfirst(job.reason),
|
|
820
|
+
this.getNiceJobEvent(job, true),
|
|
821
|
+
this.getNiceCategory(job.category, true),
|
|
822
|
+
this.getNicePlugin(job.plugin, true),
|
|
823
|
+
this.getNiceServer(job.server, true),
|
|
824
|
+
// this.getNiceJobSource(job),
|
|
825
|
+
// this.getNiceTagList( job.tags, false ),
|
|
826
|
+
// this.getRelativeDateTime( job.completed, true ),
|
|
827
|
+
// this.getNiceJobElapsedTime(job, true, true),
|
|
828
|
+
this.getNiceJobResult(job)
|
|
829
|
+
];
|
|
830
|
+
} );
|
|
831
|
+
opts.list = list;
|
|
832
|
+
|
|
833
|
+
this.printBoxTable(opts);
|
|
834
|
+
},
|
|
835
|
+
|
|
836
|
+
printJobCompletionBanner(job) {
|
|
837
|
+
// print toast for job completion
|
|
838
|
+
var disp = this.getJobResultArgs(job);
|
|
839
|
+
|
|
840
|
+
if (job.code) {
|
|
841
|
+
if (!job.description) job.description = 'Unknown Error (no description provided).';
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
if (!job.description) job.description = 'Job completed successfully.';
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
this.toast(disp.icon, disp.color, job.description );
|
|
848
|
+
},
|
|
849
|
+
|
|
850
|
+
printWorkflowJobs(job) {
|
|
851
|
+
// Render all completed sub-jobs and controller nodes belonging to a completed workflow.
|
|
852
|
+
var workflow = job.workflow || {};
|
|
853
|
+
var nodes = workflow.nodes || [];
|
|
854
|
+
var states = workflow.state || {};
|
|
855
|
+
var workflow_jobs = workflow.jobs || {};
|
|
856
|
+
var items = [];
|
|
857
|
+
|
|
858
|
+
// Controller nodes do not launch real jobs, so construct synthetic rows from their workflow state.
|
|
859
|
+
nodes.filter( node => node.type == 'controller' ).forEach( function(node) {
|
|
860
|
+
var state = states[node.id];
|
|
861
|
+
if (!state || !state.started) return;
|
|
862
|
+
if (!state.completed && !state.error) return;
|
|
863
|
+
|
|
864
|
+
items.push({
|
|
865
|
+
type: 'controller',
|
|
866
|
+
node: node,
|
|
867
|
+
state: state,
|
|
868
|
+
sort_epoch: state.started // for table sort
|
|
869
|
+
});
|
|
870
|
+
} );
|
|
871
|
+
|
|
872
|
+
// Reconstruct job-compatible objects from the compact completed-job stubs.
|
|
873
|
+
// Each workflow node may have launched multiple jobs.
|
|
874
|
+
for (var node_id in workflow_jobs) {
|
|
875
|
+
var node = Tools.findObject( nodes, { id: node_id } );
|
|
876
|
+
if (!node) continue;
|
|
877
|
+
|
|
878
|
+
var node_data = node.data || {};
|
|
879
|
+
var event = node_data.event ?
|
|
880
|
+
Tools.findObject( this.events, { id: node_data.event } ) : null;
|
|
881
|
+
|
|
882
|
+
(workflow_jobs[node_id] || []).forEach( function(job_stub) {
|
|
883
|
+
var sub_job = {
|
|
884
|
+
...job_stub,
|
|
885
|
+
state: 'complete',
|
|
886
|
+
final: true,
|
|
887
|
+
workflow: {
|
|
888
|
+
node: node_id,
|
|
889
|
+
job: job.id
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
if (event) {
|
|
894
|
+
// Standard event node.
|
|
895
|
+
sub_job.event = event.id;
|
|
896
|
+
sub_job.category = event.category;
|
|
897
|
+
sub_job.plugin = event.plugin;
|
|
898
|
+
sub_job.type = event.type;
|
|
899
|
+
sub_job.plabel = node_data.label;
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
// Ad-hoc job node.
|
|
903
|
+
sub_job.type = 'adhoc';
|
|
904
|
+
sub_job.category = node_data.category;
|
|
905
|
+
sub_job.plugin = node_data.plugin;
|
|
906
|
+
sub_job.plabel = node_data.label;
|
|
907
|
+
sub_job.picon = node_data.icon;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
items.push({
|
|
911
|
+
type: 'job',
|
|
912
|
+
job: sub_job,
|
|
913
|
+
sort_epoch: sub_job.completed || 0
|
|
914
|
+
});
|
|
915
|
+
} );
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Match the web UI by displaying completed workflow activity in chronological order.
|
|
919
|
+
items.sort( function(a, b) {
|
|
920
|
+
return a.sort_epoch - b.sort_epoch;
|
|
921
|
+
} );
|
|
922
|
+
|
|
923
|
+
var rows = items.map( item => {
|
|
924
|
+
if (item.type == 'controller') {
|
|
925
|
+
var node = item.node;
|
|
926
|
+
var state = item.state;
|
|
927
|
+
var node_data = node.data || {};
|
|
928
|
+
var controller_id = node_data.controller || 'controller';
|
|
929
|
+
var controller_menu = (this.config.ui && this.config.ui.workflow_controller_type_menu) || [];
|
|
930
|
+
var controller_def = Tools.findObject( controller_menu, { id: controller_id } ) || { title: Tools.ucfirst(controller_id) };
|
|
931
|
+
|
|
932
|
+
// Failed controller states do not currently receive a completed
|
|
933
|
+
// timestamp, so use the parent workflow completion as their end.
|
|
934
|
+
var completed = state.completed || job.completed || this.epoch;
|
|
935
|
+
var elapsed = this.getNiceJobElapsedTime({
|
|
936
|
+
started: state.started,
|
|
937
|
+
completed: completed
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
var result = state.error ?
|
|
941
|
+
this.color('red').bold('Error') :
|
|
942
|
+
this.color('blue').bold('Complete');
|
|
943
|
+
|
|
944
|
+
return [
|
|
945
|
+
this.color('theme').bold(this.getNiceWorkflowNode(node)),
|
|
946
|
+
controller_def.title,
|
|
947
|
+
gray('(Controller)'),
|
|
948
|
+
'-',
|
|
949
|
+
result,
|
|
950
|
+
elapsed
|
|
951
|
+
];
|
|
952
|
+
} // controller
|
|
953
|
+
|
|
954
|
+
var sub_job = item.job;
|
|
955
|
+
var result = sub_job.deleted ?
|
|
956
|
+
gray('Deleted (' + this.getJobResultArgs(sub_job).text + ')') :
|
|
957
|
+
this.getNiceJobResult(sub_job);
|
|
958
|
+
|
|
959
|
+
return [
|
|
960
|
+
this.color('theme').bold(this.getNiceJob(sub_job)),
|
|
961
|
+
this.getNiceJobEvent(sub_job),
|
|
962
|
+
this.getNiceCategory(sub_job.category),
|
|
963
|
+
this.getNiceServer(sub_job.server),
|
|
964
|
+
result,
|
|
965
|
+
this.getNiceJobElapsedTime(sub_job)
|
|
966
|
+
];
|
|
967
|
+
} ); // foreach row
|
|
968
|
+
|
|
969
|
+
this.printBoxTable({
|
|
970
|
+
title: 'Workflow Jobs',
|
|
971
|
+
header: [
|
|
972
|
+
'Job / Node ID',
|
|
973
|
+
'Event / Plugin',
|
|
974
|
+
'Category',
|
|
975
|
+
'Server',
|
|
976
|
+
'Result',
|
|
977
|
+
'Elapsed'
|
|
978
|
+
],
|
|
979
|
+
rows: rows
|
|
980
|
+
});
|
|
981
|
+
},
|
|
982
|
+
|
|
983
|
+
printJobFiles(job) {
|
|
984
|
+
// Combine input and output files into one table, while preserving their source.
|
|
985
|
+
var files = [];
|
|
986
|
+
|
|
987
|
+
if (job.input && job.input.files) {
|
|
988
|
+
files = files.concat( job.input.files.map( function(file) {
|
|
989
|
+
return { ...file, source: 'input' };
|
|
990
|
+
} ) );
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (job.files) {
|
|
994
|
+
files = files.concat( job.files.map( function(file) {
|
|
995
|
+
return { ...file, source: 'output' };
|
|
996
|
+
} ) );
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Match the web UI by omitting malformed or incomplete file records.
|
|
1000
|
+
files = files.filter( function(file) {
|
|
1001
|
+
return !!file.filename;
|
|
1002
|
+
} );
|
|
1003
|
+
|
|
1004
|
+
// The web UI hides the entire section when the job has no files.
|
|
1005
|
+
if (!files.length) return;
|
|
1006
|
+
|
|
1007
|
+
var rows = files.map( file => {
|
|
1008
|
+
var nice_source = '';
|
|
1009
|
+
|
|
1010
|
+
if (file.source == 'input') {
|
|
1011
|
+
if (file.bucket) nice_source = this.getNiceBucket(file.bucket);
|
|
1012
|
+
else if (file.plugin) nice_source = this.getNicePlugin(file.plugin);
|
|
1013
|
+
else if (file.ticket) nice_source = 'Ticket ' + file.ticket;
|
|
1014
|
+
else if (file.magic) nice_source = 'Magic Link';
|
|
1015
|
+
else if (file.username) nice_source = file.username;
|
|
1016
|
+
else nice_source = 'Input';
|
|
1017
|
+
}
|
|
1018
|
+
else {
|
|
1019
|
+
nice_source = 'Output';
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
return [
|
|
1023
|
+
cli.emoji('📄') + ' ' + bold(file.filename),
|
|
1024
|
+
Tools.getTextFromBytes(file.size || 0),
|
|
1025
|
+
file.date ? this.getRelativeDateTime(file.date, true) : gray('(Unknown)'),
|
|
1026
|
+
nice_source,
|
|
1027
|
+
this.getNiceJob({ id: file.job || job.id }),
|
|
1028
|
+
file.server ? this.getNiceServer(file.server) : gray('(None)')
|
|
1029
|
+
];
|
|
1030
|
+
} );
|
|
1031
|
+
|
|
1032
|
+
this.printBoxTable({
|
|
1033
|
+
title: 'Job Files',
|
|
1034
|
+
header: [ 'Filename', 'Size', 'Modified', 'Source', 'Job', 'Server' ],
|
|
1035
|
+
rows: rows
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
// In verbose mode, print full file URLs beneath the table.
|
|
1039
|
+
if (this.verbose) {
|
|
1040
|
+
var base_url = String(this.config.base_url || '').replace(/\/+$/, '');
|
|
1041
|
+
|
|
1042
|
+
var linked_files = files.filter( function(file) {
|
|
1043
|
+
return !!file.path;
|
|
1044
|
+
} );
|
|
1045
|
+
|
|
1046
|
+
if (linked_files.length) {
|
|
1047
|
+
println( "\n " + this.color('theme').bold('FILE URLS') );
|
|
1048
|
+
|
|
1049
|
+
linked_files.forEach( function(file) {
|
|
1050
|
+
var file_path = String(file.path).replace(/^\/+/, '');
|
|
1051
|
+
var url = base_url + '/' + file_path;
|
|
1052
|
+
|
|
1053
|
+
println( ' - ' + url );
|
|
1054
|
+
} );
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
|
|
1059
|
+
getNiceWorkflowNode(node) {
|
|
1060
|
+
// Get a terminal-friendly icon for any workflow node type.
|
|
1061
|
+
if (!node) return '(None)';
|
|
1062
|
+
|
|
1063
|
+
var node_icons = {
|
|
1064
|
+
trigger: '🚀',
|
|
1065
|
+
event: '📅',
|
|
1066
|
+
job: '🔌',
|
|
1067
|
+
action: '⚡',
|
|
1068
|
+
limit: '🚧',
|
|
1069
|
+
controller: '🎛️',
|
|
1070
|
+
note: '📝'
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
// Controller nodes get more specific icons based on their behavior.
|
|
1074
|
+
var controller_icons = {
|
|
1075
|
+
decision: '❓',
|
|
1076
|
+
join: '🔗',
|
|
1077
|
+
multiplex: '📡',
|
|
1078
|
+
repeat: '🔁',
|
|
1079
|
+
split: '🔀',
|
|
1080
|
+
wait: '⏳'
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
var icon = node_icons[node.type] || '🧩';
|
|
1084
|
+
|
|
1085
|
+
if (node.type == 'controller') {
|
|
1086
|
+
var controller_type = node.data && node.data.controller;
|
|
1087
|
+
icon = controller_icons[controller_type] || icon;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
return cli.emoji(icon) + ' ' + (node.id || '(Unknown)');
|
|
1091
|
+
},
|
|
1092
|
+
|
|
1093
|
+
printJobActions(job) {
|
|
1094
|
+
// Only display actions that actually fired and are not hidden.
|
|
1095
|
+
var actions = (job.actions || []).filter( function(action) {
|
|
1096
|
+
return !!(action.date && !action.hidden);
|
|
1097
|
+
} );
|
|
1098
|
+
|
|
1099
|
+
var workflow = job.workflow;
|
|
1100
|
+
|
|
1101
|
+
// Workflow action nodes store their execution results in the workflow
|
|
1102
|
+
// state object instead of the parent job's normal actions array.
|
|
1103
|
+
if (workflow && workflow.state && workflow.nodes) {
|
|
1104
|
+
workflow.nodes.filter( function(node) {
|
|
1105
|
+
return node.type == 'action';
|
|
1106
|
+
} ).forEach( function(node) {
|
|
1107
|
+
var state = workflow.state[node.id];
|
|
1108
|
+
if (!state || !state.date) return;
|
|
1109
|
+
|
|
1110
|
+
// Copy the state so we do not mutate the original job object.
|
|
1111
|
+
actions.push({
|
|
1112
|
+
...state,
|
|
1113
|
+
source: 'workflow'
|
|
1114
|
+
});
|
|
1115
|
+
} );
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Match the web UI by displaying actions chronologically.
|
|
1119
|
+
actions.sort( function(a, b) {
|
|
1120
|
+
return a.date - b.date;
|
|
1121
|
+
} );
|
|
1122
|
+
|
|
1123
|
+
// The web UI hides this entire section when no actions fired.
|
|
1124
|
+
if (!actions.length) return;
|
|
1125
|
+
|
|
1126
|
+
var rows = actions.map( item => {
|
|
1127
|
+
var disp = this.getJobActionDisplayArgs(item);
|
|
1128
|
+
var source = String(item.source || 'event').toLowerCase();
|
|
1129
|
+
var nice_source = Tools.ucfirst(source);
|
|
1130
|
+
|
|
1131
|
+
if (item.count > 1) {
|
|
1132
|
+
nice_source += ' (x' + Tools.commify(item.count) + ')';
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Preserve millisecond precision for quick actions, then switch to the
|
|
1136
|
+
// normal duration formatter for actions lasting one second or longer.
|
|
1137
|
+
var elapsed_ms = Math.max(0, Math.floor(item.elapsed_ms || 0));
|
|
1138
|
+
var nice_elapsed = (elapsed_ms < 1000) ?
|
|
1139
|
+
Tools.commify(elapsed_ms) + ' ms' :
|
|
1140
|
+
Tools.getTextFromSeconds(elapsed_ms / 1000, true, true);
|
|
1141
|
+
|
|
1142
|
+
var nice_result = '';
|
|
1143
|
+
if (item.code) {
|
|
1144
|
+
nice_result =
|
|
1145
|
+
cli.emoji('🛑') + ' ' + this.color('red').bold('Error');
|
|
1146
|
+
|
|
1147
|
+
// The web UI puts this behind a details link. Show it inline in
|
|
1148
|
+
// the CLI so an action failure remains immediately actionable.
|
|
1149
|
+
if (item.description) {
|
|
1150
|
+
var error_text = String(item.description)
|
|
1151
|
+
.replace(/\s+/g, ' ')
|
|
1152
|
+
.trim();
|
|
1153
|
+
|
|
1154
|
+
nice_result += gray(' (' + error_text + ')');
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
else {
|
|
1158
|
+
nice_result =
|
|
1159
|
+
cli.emoji('✅') + ' ' + this.color('green').bold('Success');
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
return [
|
|
1163
|
+
this.color(disp.condition.color).bold(disp.condition.title),
|
|
1164
|
+
(disp.type || Tools.ucfirst(item.type || 'Unknown')),
|
|
1165
|
+
nice_source,
|
|
1166
|
+
disp.desc || 'n/a',
|
|
1167
|
+
this.getRelativeDateTime(item.date, true),
|
|
1168
|
+
nice_result,
|
|
1169
|
+
nice_elapsed
|
|
1170
|
+
];
|
|
1171
|
+
} );
|
|
1172
|
+
|
|
1173
|
+
this.printBoxTable({
|
|
1174
|
+
title: 'Job Actions',
|
|
1175
|
+
header: [
|
|
1176
|
+
'Condition',
|
|
1177
|
+
'Type',
|
|
1178
|
+
'Source',
|
|
1179
|
+
'Description',
|
|
1180
|
+
'Date / Time',
|
|
1181
|
+
'Result',
|
|
1182
|
+
'Elapsed'
|
|
1183
|
+
],
|
|
1184
|
+
rows: rows
|
|
1185
|
+
});
|
|
1186
|
+
},
|
|
1187
|
+
|
|
1188
|
+
printJobLimits(job) {
|
|
1189
|
+
// Only display limits that actually triggered and are not hidden.
|
|
1190
|
+
var limits = (job.limits || []).filter( function(limit) {
|
|
1191
|
+
return !!(limit.date && !limit.hidden);
|
|
1192
|
+
} );
|
|
1193
|
+
|
|
1194
|
+
// The web UI hides this entire section when no limits were exceeded.
|
|
1195
|
+
if (!limits.length) return;
|
|
1196
|
+
|
|
1197
|
+
// Limits should normally already be chronological, but explicitly sort
|
|
1198
|
+
// them in case their configuration order differs from trigger order.
|
|
1199
|
+
limits.sort( function(a, b) {
|
|
1200
|
+
return a.date - b.date;
|
|
1201
|
+
} );
|
|
1202
|
+
|
|
1203
|
+
var rows = limits.map( item => {
|
|
1204
|
+
var disp = this.getResLimitDisplayArgs(item);
|
|
1205
|
+
var source = String(item.source || 'event').toLowerCase();
|
|
1206
|
+
var nice_type = (disp.nice_title || Tools.ucfirst(item.type || 'Unknown'));
|
|
1207
|
+
var nice_source = Tools.ucfirst(source);
|
|
1208
|
+
|
|
1209
|
+
// The triggered-limit message describes what was actually exceeded.
|
|
1210
|
+
// Fall back to the configured limit description for older records.
|
|
1211
|
+
var nice_description =
|
|
1212
|
+
item.msg || disp.nice_desc || 'n/a';
|
|
1213
|
+
|
|
1214
|
+
var nice_result = '';
|
|
1215
|
+
if (item.code) {
|
|
1216
|
+
nice_result =
|
|
1217
|
+
cli.emoji('🛑') + ' ' + this.color('red').bold('Error');
|
|
1218
|
+
|
|
1219
|
+
if (item.description) {
|
|
1220
|
+
var error_text = String(item.description)
|
|
1221
|
+
.replace(/\s+/g, ' ')
|
|
1222
|
+
.trim();
|
|
1223
|
+
|
|
1224
|
+
nice_result += gray(' (' + error_text + ')');
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
else {
|
|
1228
|
+
nice_result =
|
|
1229
|
+
cli.emoji('✅') + ' ' + this.color('green').bold('OK');
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
return [
|
|
1233
|
+
this.color('theme').bold(nice_type),
|
|
1234
|
+
nice_source,
|
|
1235
|
+
nice_description,
|
|
1236
|
+
this.getRelativeDateTime(item.date, true),
|
|
1237
|
+
nice_result
|
|
1238
|
+
];
|
|
1239
|
+
} );
|
|
1240
|
+
|
|
1241
|
+
this.printBoxTable({
|
|
1242
|
+
title: 'Job Limits Exceeded',
|
|
1243
|
+
header: [
|
|
1244
|
+
'Limit Type',
|
|
1245
|
+
'Source',
|
|
1246
|
+
'Description',
|
|
1247
|
+
'Date / Time',
|
|
1248
|
+
'Result'
|
|
1249
|
+
],
|
|
1250
|
+
rows: rows
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
};
|