@fynjs/run 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/LICENSE +201 -0
- package/README.md +720 -0
- package/bin/qrun.js +3 -0
- package/bin/xrun.js +42 -0
- package/cli/check-global.js +6 -0
- package/cli/ck.js +6 -0
- package/cli/cli-options.js +98 -0
- package/cli/config.js +8 -0
- package/cli/env.js +33 -0
- package/cli/npm-loader.js +56 -0
- package/cli/parse-cmd-args.js +156 -0
- package/cli/provider-packages.js +73 -0
- package/cli/search-up-task-file.js +92 -0
- package/cli/task-file.js +192 -0
- package/cli/ts-runner.js +51 -0
- package/cli/usage.js +6 -0
- package/cli/wrap-process.js +27 -0
- package/cli/xrun-main.js +425 -0
- package/cli/xrun.js +8 -0
- package/lib/chalk.js +11 -0
- package/lib/cli-context.js +145 -0
- package/lib/defaults.js +32 -0
- package/lib/gen-xqid.js +10 -0
- package/lib/index.js +12 -0
- package/lib/logger.js +105 -0
- package/lib/ns-order.js +76 -0
- package/lib/print-tasks/index.js +105 -0
- package/lib/reporters/console.js +133 -0
- package/lib/stringify.js +16 -0
- package/lib/util/parse-array.js +5 -0
- package/lib/util/update-env.js +16 -0
- package/lib/xqitem.js +70 -0
- package/lib/xqtor.js +814 -0
- package/lib/xqtree.js +41 -0
- package/lib/xrun-instance.js +33 -0
- package/lib/xrun.d.ts +89 -0
- package/lib/xrun.js +344 -0
- package/lib/xtask-spec.js +63 -0
- package/lib/xtasks.js +137 -0
- package/package.json +93 -0
package/lib/xqtor.js
ADDED
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import assert from "assert";
|
|
3
|
+
import defaults from "./defaults.js";
|
|
4
|
+
import Insync from "insync";
|
|
5
|
+
import XQItem from "./xqitem.js";
|
|
6
|
+
import { exec as exec } from "xsh";
|
|
7
|
+
import parseArray from "./util/parse-array.js";
|
|
8
|
+
import childProc from "child_process";
|
|
9
|
+
import updateEnv from "./util/update-env.js";
|
|
10
|
+
import { NixClap } from "@fynjs/cli-args";
|
|
11
|
+
|
|
12
|
+
//
|
|
13
|
+
// Kept lazy rather than a static import: a static import is hoisted to module load, which is
|
|
14
|
+
// exactly what the call site below avoids so fynpo bootstrap can build unwrap-npm-cmd first.
|
|
15
|
+
//
|
|
16
|
+
const lazyRequire = createRequire(import.meta.url);
|
|
17
|
+
|
|
18
|
+
const STAGE_FINALLY = "finally";
|
|
19
|
+
|
|
20
|
+
const isSerial = x => defaults.SERIAL_SIG.includes(x[0]);
|
|
21
|
+
const isConcurrent = x => defaults.CONCURRENT_SIG.includes(x[0]);
|
|
22
|
+
|
|
23
|
+
const isReadableStream = x => Boolean(x && x.pipe && x.on && x._readableState);
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* Task executor (XQtor) - After all the fuss, it comes down to this -- the core of xrun
|
|
27
|
+
* that handles the execution of tasks.
|
|
28
|
+
*
|
|
29
|
+
* Tasks are added to a stack using an array.
|
|
30
|
+
*
|
|
31
|
+
* - Each concurrent tasks array is executed with a new XQtor and Insync.parallel.
|
|
32
|
+
* - Serial tasks are all added to the top of the "stack" in reverse order
|
|
33
|
+
* - Some items are actions that cause more items to be created and pushed
|
|
34
|
+
* into the stack. Like looking up the task of a task name.
|
|
35
|
+
* - As each task is completed, the XQtor's next is invoked to pop items
|
|
36
|
+
* from the stack for processing.
|
|
37
|
+
* - A mark item is added to the stack before each task so when a mark
|
|
38
|
+
* is seen, error is checked and done event is emitted.
|
|
39
|
+
*
|
|
40
|
+
*/
|
|
41
|
+
class XQtor {
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this._tasks = options.tasks;
|
|
44
|
+
this._done = options.done;
|
|
45
|
+
this._xrun = options.xrun;
|
|
46
|
+
this.xqItems = [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
next(err, xqId) {
|
|
50
|
+
process.nextTick(() => this._next(err, xqId), 0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_next(err, xqId) {
|
|
54
|
+
if (err) {
|
|
55
|
+
const qItem = this._xrun.xqTree.item(xqId);
|
|
56
|
+
qItem.err = err;
|
|
57
|
+
this._xrun.fail(err);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (this.xqItems.length > 0) {
|
|
61
|
+
this.execute();
|
|
62
|
+
} else {
|
|
63
|
+
this._done(this._xrun.failed);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
execute() {
|
|
68
|
+
assert(this.xqItems.length > 0, "no next task");
|
|
69
|
+
|
|
70
|
+
const qItem = this.popItem();
|
|
71
|
+
|
|
72
|
+
if (qItem.mark) {
|
|
73
|
+
return this._processMark(qItem);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this._xrun._isStop || (this._xrun.failed && !qItem.isFinally && this._xrun.stopOnError)) {
|
|
77
|
+
return this.next(null, qItem.id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const value = qItem.value();
|
|
81
|
+
if (value === defaults.STOP_SYM) {
|
|
82
|
+
this._xrun.actStop();
|
|
83
|
+
return this.next(null, qItem.id);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const vtype = value.constructor.name;
|
|
87
|
+
if (vtype === "XTaskSpec") {
|
|
88
|
+
if (value.type === "exec") {
|
|
89
|
+
return this._shellXer(qItem, value, true);
|
|
90
|
+
} else if (value.type === "env") {
|
|
91
|
+
return this._envXer(qItem, value);
|
|
92
|
+
}
|
|
93
|
+
this._xrun.fail(
|
|
94
|
+
new Error(`Unable to process XTaskSpec type ${value.type} for task ${qItem.name}`)
|
|
95
|
+
);
|
|
96
|
+
} else if (vtype === "String") {
|
|
97
|
+
if (this._isAnonShell(value)) {
|
|
98
|
+
return this._shellXer(qItem, this._parseAnonShell(value), true);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return this._processLookup(qItem);
|
|
102
|
+
} else if (vtype === "Function" || vtype === "AsyncFunction") {
|
|
103
|
+
return this._functionXer(qItem, value);
|
|
104
|
+
} else if (vtype === "Array") {
|
|
105
|
+
return this._processArray(qItem, value);
|
|
106
|
+
} else if (value.item) {
|
|
107
|
+
const dep = value.item.dep;
|
|
108
|
+
if (dep && !qItem.xqDep) {
|
|
109
|
+
return this._processDep(qItem, dep);
|
|
110
|
+
} else {
|
|
111
|
+
return this._processTaskObject(qItem);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
this._xrun.fail(
|
|
115
|
+
new Error(`Unable to process task ${qItem.name} \
|
|
116
|
+
because value type ${vtype} is unknown and no value.item`)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return this.next(null, qItem.id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
pushMarkItem(qItem, callback) {
|
|
124
|
+
const x = this._makeXqMarkItem(qItem, callback);
|
|
125
|
+
this.pushItem(x);
|
|
126
|
+
return x;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pushItem(qItem) {
|
|
130
|
+
this.xqItems.push(qItem);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
popItem() {
|
|
134
|
+
return this.xqItems.pop();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_isAnonShell(value) {
|
|
138
|
+
let sig;
|
|
139
|
+
if ((sig = defaults.ANON_SHELL_OPT_SIG.find(x => value.startsWith(x)))) {
|
|
140
|
+
return { sig, opt: true };
|
|
141
|
+
}
|
|
142
|
+
if ((sig = defaults.ANON_SHELL_SIG.find(x => value.startsWith(x)))) {
|
|
143
|
+
return { sig, opt: false };
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_parseShellFlags(flags, value) {
|
|
149
|
+
const unknowns = [];
|
|
150
|
+
|
|
151
|
+
flags = flags.reduce((a, o) => {
|
|
152
|
+
const f = o.trim().toLowerCase();
|
|
153
|
+
if (defaults.SHELL_FLAGS.indexOf(f) < 0) {
|
|
154
|
+
unknowns.push(o);
|
|
155
|
+
} else {
|
|
156
|
+
a[f] = true;
|
|
157
|
+
}
|
|
158
|
+
return a;
|
|
159
|
+
}, {});
|
|
160
|
+
|
|
161
|
+
if (unknowns.length) {
|
|
162
|
+
throw new Error(`Unknown flag ${unknowns.join(",")} in shell task: ${value}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return flags;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_parseAnonShell(value) {
|
|
169
|
+
if (typeof value !== "string") return value;
|
|
170
|
+
|
|
171
|
+
let cmd = value;
|
|
172
|
+
let flags = {};
|
|
173
|
+
let error;
|
|
174
|
+
const anon = this._isAnonShell(value);
|
|
175
|
+
if (anon) {
|
|
176
|
+
const { sig, opt } = anon;
|
|
177
|
+
// support options like ~(tty,spawn,sync)$
|
|
178
|
+
|
|
179
|
+
if (opt) {
|
|
180
|
+
let ix;
|
|
181
|
+
const closeSig = defaults.ANON_SHELL_OPT_CLOSE_SIG.find(x => {
|
|
182
|
+
ix = value.indexOf(x, sig.length);
|
|
183
|
+
return ix > sig.length;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (closeSig) {
|
|
187
|
+
const so = value.substring(sig.length, ix);
|
|
188
|
+
try {
|
|
189
|
+
flags = this._parseShellFlags(so.split(","), value);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
error = err;
|
|
192
|
+
}
|
|
193
|
+
cmd = value.substr(ix + closeSig.length);
|
|
194
|
+
} else {
|
|
195
|
+
error = new Error(
|
|
196
|
+
`Missing ${defaults.ANON_SHELL_OPT_CLOSE_SIG[0]} in shell task: ${value}`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
cmd = value.substr(sig.length);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { flags, cmd, error };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
_isStrArray(value) {
|
|
208
|
+
return value.startsWith(defaults.STR_ARRAY_SIG);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
_parseStrArray(value) {
|
|
212
|
+
if (this._isStrArray(value)) {
|
|
213
|
+
return parseArray(value.substr(defaults.STR_ARRAY_SIG.length - 1));
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_processLookup(qItem) {
|
|
219
|
+
this._emit("execute", {
|
|
220
|
+
type: "lookup",
|
|
221
|
+
qItem
|
|
222
|
+
});
|
|
223
|
+
// lookup is sync; should not insert mark for it
|
|
224
|
+
try {
|
|
225
|
+
const found = qItem.lookup(this._tasks);
|
|
226
|
+
this.pushItem(qItem);
|
|
227
|
+
this.next(null, qItem.id);
|
|
228
|
+
if (found.search) {
|
|
229
|
+
this._xrun.emit("search", { qItem, found });
|
|
230
|
+
qItem.setNamespace(found.ns);
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error.optional) {
|
|
234
|
+
this._xrun.emit("not-found", error);
|
|
235
|
+
this.next();
|
|
236
|
+
} else {
|
|
237
|
+
this.next(error, qItem.id);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
_processDep(qItem, dep) {
|
|
243
|
+
this._emit("dep", {
|
|
244
|
+
qItem
|
|
245
|
+
});
|
|
246
|
+
qItem.xqDep = true; // item's dep has been processed
|
|
247
|
+
this.pushItem(qItem);
|
|
248
|
+
const di = this._createQItem(
|
|
249
|
+
{
|
|
250
|
+
name: qItem.name,
|
|
251
|
+
value: { top: true, depDee: qItem, item: { task: dep } }
|
|
252
|
+
},
|
|
253
|
+
this._xrun.xqTree.parent(qItem)
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
this.pushItem(di);
|
|
257
|
+
|
|
258
|
+
this.next(null, qItem.id);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
_processTaskObject(qItem) {
|
|
262
|
+
const value = qItem.value();
|
|
263
|
+
const stage = qItem.stage || "task";
|
|
264
|
+
const itemTask = value.item[stage] || value.item;
|
|
265
|
+
const type = itemTask.constructor.name;
|
|
266
|
+
if (type === "Array") {
|
|
267
|
+
return this._processArray(qItem, itemTask, value.top);
|
|
268
|
+
} else if (type === "Function" || type === "AsyncFunction") {
|
|
269
|
+
return this._functionXer(qItem, itemTask);
|
|
270
|
+
} else if (type === "String") {
|
|
271
|
+
const parsedArray = this._parseStrArray(itemTask);
|
|
272
|
+
if (parsedArray) return this._processArray(qItem, parsedArray, value.top);
|
|
273
|
+
return this._shellXer(qItem, itemTask);
|
|
274
|
+
} else if (type === "XTaskSpec") {
|
|
275
|
+
if (itemTask.type === "exec") {
|
|
276
|
+
return this._shellXer(qItem, itemTask);
|
|
277
|
+
} else if (itemTask.type === "env") {
|
|
278
|
+
return this._envXer(qItem, itemTask);
|
|
279
|
+
}
|
|
280
|
+
this._xrun.fail(
|
|
281
|
+
new Error(`Unable to process XTaskSpec type ${itemTask.type} for task ${qItem.name}`)
|
|
282
|
+
);
|
|
283
|
+
} else {
|
|
284
|
+
this._xrun.fail(new Error(`Task ${qItem.name} has unrecognize task value type ${type}`));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return this.next(null, qItem.id);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_processArray(qItem, tasks, top) {
|
|
291
|
+
// check first element for concurrent signature
|
|
292
|
+
// top level task arrays are automatically serial
|
|
293
|
+
// or check first element for serial signature
|
|
294
|
+
if (isConcurrent(tasks) || (!top && !isSerial(tasks))) {
|
|
295
|
+
this._concurrentArrayXer(qItem, tasks);
|
|
296
|
+
} else {
|
|
297
|
+
this._serialArrayXer(qItem, tasks);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
_serialArrayXer(qItem, tasks) {
|
|
302
|
+
if (isSerial(tasks)) {
|
|
303
|
+
tasks = tasks.slice(1);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this._emit("execute", {
|
|
307
|
+
type: "serial-arr",
|
|
308
|
+
qItem,
|
|
309
|
+
array: tasks
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
this.pushMarkItem(qItem);
|
|
313
|
+
this.xqItems = this.xqItems.concat(
|
|
314
|
+
tasks
|
|
315
|
+
.map(value => {
|
|
316
|
+
return this._createQItem(
|
|
317
|
+
this._resolveValueToQItemOptions(qItem.ns, `${qItem.name}.S`, value, "serial_child"),
|
|
318
|
+
qItem
|
|
319
|
+
);
|
|
320
|
+
})
|
|
321
|
+
.reverse()
|
|
322
|
+
);
|
|
323
|
+
this.next(null, qItem.id);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
_resolveValueToQItemOptions(ns, name, value, type) {
|
|
327
|
+
if (typeof value === "string" && !this._isAnonShell(value)) {
|
|
328
|
+
name = value;
|
|
329
|
+
value = undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const anon = typeof value === "function";
|
|
333
|
+
return {
|
|
334
|
+
ns,
|
|
335
|
+
name,
|
|
336
|
+
value,
|
|
337
|
+
anon,
|
|
338
|
+
type
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_concurrentArrayXer(qItem, tasks) {
|
|
343
|
+
if (isConcurrent(tasks)) {
|
|
344
|
+
tasks = tasks.slice(1);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
this._emit("execute", {
|
|
348
|
+
type: "concurrent-arr",
|
|
349
|
+
qItem,
|
|
350
|
+
array: tasks
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
this.pushMarkItem(qItem);
|
|
354
|
+
Insync.parallel(
|
|
355
|
+
tasks.map(value => cb => {
|
|
356
|
+
this._emit("spawn-async", { name: qItem.name });
|
|
357
|
+
const xqtor = new XQtor({
|
|
358
|
+
tasks: this._tasks,
|
|
359
|
+
xrun: this._xrun,
|
|
360
|
+
done: () => {
|
|
361
|
+
this._emit("done-async");
|
|
362
|
+
process.nextTick(() => cb());
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
const options = this._resolveValueToQItemOptions(
|
|
366
|
+
qItem.ns,
|
|
367
|
+
`${qItem.name}.C`,
|
|
368
|
+
value,
|
|
369
|
+
"concurrent_child"
|
|
370
|
+
);
|
|
371
|
+
const item = this._createQItem(options, qItem);
|
|
372
|
+
if (options.value === undefined) {
|
|
373
|
+
xqtor._processLookup(item);
|
|
374
|
+
} else {
|
|
375
|
+
xqtor.pushItem(item);
|
|
376
|
+
xqtor.next();
|
|
377
|
+
}
|
|
378
|
+
}),
|
|
379
|
+
() => this.next()
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
_envXer(qItem, cmdVal) {
|
|
384
|
+
this._emit("execute", {
|
|
385
|
+
type: "env",
|
|
386
|
+
qItem,
|
|
387
|
+
cmdVal
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
updateEnv(cmdVal.options.env, process.env, cmdVal.options.override);
|
|
391
|
+
return this.next();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
_shellXer(qItem, cmdVal, anon) {
|
|
395
|
+
cmdVal = this._parseAnonShell(cmdVal);
|
|
396
|
+
|
|
397
|
+
if (cmdVal.error) {
|
|
398
|
+
setTimeout(() => this.next(cmdVal.error, qItem.id), 0);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const { cmd } = cmdVal;
|
|
403
|
+
const xrunOptions = Object.assign({ delayRunMs: 0 }, cmdVal.xrun || cmdVal.xclap);
|
|
404
|
+
const options = Object.assign({}, cmdVal.options);
|
|
405
|
+
|
|
406
|
+
let { flags } = cmdVal;
|
|
407
|
+
|
|
408
|
+
if (typeof flags === "string") {
|
|
409
|
+
flags = this._parseShellFlags(flags.split(","), `XTaskSpec "${flags}"`);
|
|
410
|
+
} else if (Array.isArray(flags)) {
|
|
411
|
+
flags = this._parseShellFlags(flags);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const item = qItem.value().item || {};
|
|
415
|
+
|
|
416
|
+
Object.assign(flags, item.flags);
|
|
417
|
+
|
|
418
|
+
// Access CLI context through runner
|
|
419
|
+
const cliContext = this._xrun.getCliContext();
|
|
420
|
+
const itemArgv = qItem.argv;
|
|
421
|
+
const cliArgv = cliContext.getTaskArgv(qItem.name);
|
|
422
|
+
const remainingArgv =
|
|
423
|
+
(cliContext.isLastTask(qItem.name) && cliContext.getRemainingArgs()) || [];
|
|
424
|
+
|
|
425
|
+
// Build command with task-specific argv
|
|
426
|
+
const cmd2 = []
|
|
427
|
+
.concat(itemArgv.slice(1), cliArgv.slice(1), remainingArgv)
|
|
428
|
+
.map(x => (x && x.includes(" ") ? `"${x}"` : x))
|
|
429
|
+
.join(" ");
|
|
430
|
+
|
|
431
|
+
setTimeout(() => {
|
|
432
|
+
const execData = {
|
|
433
|
+
type: "shell",
|
|
434
|
+
anon,
|
|
435
|
+
qItem,
|
|
436
|
+
cmd: cmd + ((cmd2 && " " + cmd2) || ""),
|
|
437
|
+
cmd2,
|
|
438
|
+
flags,
|
|
439
|
+
cmdVal,
|
|
440
|
+
options,
|
|
441
|
+
item
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
this._emit("execute", execData);
|
|
445
|
+
this._doShellXer(execData);
|
|
446
|
+
}, xrunOptions.delayRunMs);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
_doShellXer({ /*anon,*/ qItem, cmd, flags, /*cmdVal,*/ options, item }) {
|
|
450
|
+
const itemOptions = Object.assign({}, item.options);
|
|
451
|
+
|
|
452
|
+
this.pushMarkItem(qItem);
|
|
453
|
+
const env = Object.assign(
|
|
454
|
+
flags.noenv ? {} : Object.assign({}, process.env),
|
|
455
|
+
itemOptions.env,
|
|
456
|
+
options.env
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
if (qItem.err) {
|
|
460
|
+
env.XCLAP_ERR = env.XRUN_ERR = qItem.err.message || "true";
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (this._xrun.failed || qItem.err) {
|
|
464
|
+
env.XCLAP_FAILED = env.XRUN_FAILED = "true";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let child;
|
|
468
|
+
|
|
469
|
+
const watch = { finish: false };
|
|
470
|
+
const done = err => {
|
|
471
|
+
// even if there's error, but if it's due to child being terminated with
|
|
472
|
+
// SIGTERM, then treat that as a normal exit.
|
|
473
|
+
if (this._isChildSigTerm(err, child) || this._isChildTerminated(child)) {
|
|
474
|
+
err = null;
|
|
475
|
+
}
|
|
476
|
+
//
|
|
477
|
+
// A spawned child can report both `error` and `close`, so `done` can be called twice.
|
|
478
|
+
// Guarding it is the point; reproducing that race on demand in a test is not something
|
|
479
|
+
// this suite can do deterministically.
|
|
480
|
+
//
|
|
481
|
+
/* istanbul ignore next */
|
|
482
|
+
if (watch.finish) return 0;
|
|
483
|
+
watch.finish = true;
|
|
484
|
+
return this.next(err, qItem.id);
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
let { tty, spawn, sync, npm } = flags;
|
|
488
|
+
if (npm) {
|
|
489
|
+
tty = spawn = true;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Lazy require unwrap-npm-cmd to avoid requiring it at module load time
|
|
493
|
+
// This ensures it's built by fynpo bootstrap before it's required
|
|
494
|
+
const { unwrapNpmCmd } = lazyRequire("unwrap-npm-cmd");
|
|
495
|
+
const cmd2 = unwrapNpmCmd(cmd, { path: env.PATH });
|
|
496
|
+
|
|
497
|
+
if (tty || spawn) {
|
|
498
|
+
const spawnOpts = { shell: true };
|
|
499
|
+
Object.assign(spawnOpts, itemOptions, options, { env });
|
|
500
|
+
if (tty) spawnOpts.stdio = "inherit";
|
|
501
|
+
|
|
502
|
+
if (sync) {
|
|
503
|
+
child = childProc.spawnSync(cmd2, spawnOpts);
|
|
504
|
+
if (child.error) {
|
|
505
|
+
done(child.error);
|
|
506
|
+
} else if (child.status === 0) {
|
|
507
|
+
done();
|
|
508
|
+
} else {
|
|
509
|
+
done(new Error(`cmd "${cmd}" exit code ${child.status}`));
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
child = childProc.spawn(cmd2, spawnOpts);
|
|
513
|
+
child.on("close", (code, signal) => {
|
|
514
|
+
if (code === 0 || signal === "SIGTERM") {
|
|
515
|
+
return done();
|
|
516
|
+
}
|
|
517
|
+
return done(new Error(`cmd "${cmd}" exit code ${code}`));
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
child = exec(Object.assign({ silent: false }, itemOptions, options, { env }), cmd2, done);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (!watch.finish) {
|
|
525
|
+
this._handleChildTask(child, cmd);
|
|
526
|
+
watch.cancel = () => {
|
|
527
|
+
this._xrun.killChildProcess(child);
|
|
528
|
+
done();
|
|
529
|
+
};
|
|
530
|
+
this._watchFailure(watch);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
_handleChildTask(child, cmd) {
|
|
535
|
+
if (child && child instanceof childProc.ChildProcess) {
|
|
536
|
+
const sym = Symbol(cmd);
|
|
537
|
+
this._xrun.addTaskChild(child, sym);
|
|
538
|
+
child.on("exit", () => {
|
|
539
|
+
this._xrun.removeTaskChild(child, sym);
|
|
540
|
+
});
|
|
541
|
+
return child;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
_processMoreFromFn(qItem, value, callback) {
|
|
546
|
+
let tof = typeof value;
|
|
547
|
+
|
|
548
|
+
if (tof === "string") {
|
|
549
|
+
const parsedArray = this._parseStrArray(value);
|
|
550
|
+
if (parsedArray) value = parsedArray;
|
|
551
|
+
tof = typeof value;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (
|
|
555
|
+
!value ||
|
|
556
|
+
(tof !== "string" &&
|
|
557
|
+
tof !== "function" &&
|
|
558
|
+
!Array.isArray(value) &&
|
|
559
|
+
value.constructor.name !== "XTaskSpec" &&
|
|
560
|
+
value !== defaults.STOP_SYM)
|
|
561
|
+
) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const qi = this._createQItem(
|
|
566
|
+
this._resolveValueToQItemOptions(qItem.ns, `${qItem.name}.fR`, value, "func_returned_child"),
|
|
567
|
+
qItem
|
|
568
|
+
);
|
|
569
|
+
if (callback) {
|
|
570
|
+
assert(
|
|
571
|
+
typeof callback === "function",
|
|
572
|
+
`${qItem.name} callback from function calling run is not a function`
|
|
573
|
+
);
|
|
574
|
+
this.pushMarkItem(qItem, callback);
|
|
575
|
+
}
|
|
576
|
+
this.pushItem(qi);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
_isChildSigTerm(err, child) {
|
|
580
|
+
return err && err.code === null && child && child.signalCode === "SIGTERM";
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
_isChildTerminated(child) {
|
|
584
|
+
return child && child[defaults.STOP_SYM];
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
_handleStream(val) {
|
|
588
|
+
if (!isReadableStream(val)) {
|
|
589
|
+
return val;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return new Promise((resolve, reject) => {
|
|
593
|
+
const handle = err => {
|
|
594
|
+
val.removeListener("end", handle);
|
|
595
|
+
val.removeListener("error", handle);
|
|
596
|
+
return err ? reject(err) : resolve();
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
val.once("close", handle);
|
|
600
|
+
val.once("error", handle);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
_functionXer(qItem, fn) {
|
|
605
|
+
this._emit("execute", {
|
|
606
|
+
type: "function",
|
|
607
|
+
anon: qItem.anon,
|
|
608
|
+
qItem
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
const watch = { finish: false, qItem };
|
|
612
|
+
const done = (err, value) => {
|
|
613
|
+
if (watch.finish) return;
|
|
614
|
+
watch.finish = true;
|
|
615
|
+
if (value) {
|
|
616
|
+
this._processMoreFromFn(qItem, value);
|
|
617
|
+
}
|
|
618
|
+
this.next(err, qItem.id);
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
watch.cancel = done;
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
this.pushMarkItem(qItem);
|
|
625
|
+
const run = (value, cb) => this._processMoreFromFn(qItem, value, cb);
|
|
626
|
+
const task = qItem.value();
|
|
627
|
+
|
|
628
|
+
// Access CLI context through runner
|
|
629
|
+
const cliContext = this._xrun.getCliContext();
|
|
630
|
+
let cliCmd = cliContext.getTaskCommand(qItem.name);
|
|
631
|
+
|
|
632
|
+
const cliParser = task.item?.cliParser || task.cliParser || {};
|
|
633
|
+
const itemArgv = qItem.argv;
|
|
634
|
+
const cliArgv = cliCmd.argv || [];
|
|
635
|
+
|
|
636
|
+
if (itemArgv.length > 1 || (cliParser && cliArgv.length > 1)) {
|
|
637
|
+
const argv = [qItem.name].concat(itemArgv.slice(1), cliArgv.slice(1));
|
|
638
|
+
const config = {
|
|
639
|
+
name: qItem.name,
|
|
640
|
+
allowUnknownOption:
|
|
641
|
+
cliParser.allowUnknownOption !== undefined ? cliParser.allowUnknownOption : true,
|
|
642
|
+
allowUnknownCommand: false,
|
|
643
|
+
noDefaultHandlers: true
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const argp = new NixClap(config)
|
|
647
|
+
.init(cliParser.options || {}, cliParser.commands || {})
|
|
648
|
+
.parse(argv, 1);
|
|
649
|
+
|
|
650
|
+
if (argp.errorNodes.length > 0) {
|
|
651
|
+
const unknownOptions = argp.errorNodes.reduce((acc, node) => {
|
|
652
|
+
acc.push(
|
|
653
|
+
...node.errors
|
|
654
|
+
.filter(e => e.message.includes("unknown CLI option"))
|
|
655
|
+
.map(e => e.data.name)
|
|
656
|
+
);
|
|
657
|
+
return acc;
|
|
658
|
+
}, []);
|
|
659
|
+
|
|
660
|
+
if (unknownOptions.length > 0) {
|
|
661
|
+
return done(
|
|
662
|
+
new Error(`Unknown options for task ${qItem.name}: ${unknownOptions.join(", ")}`)
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
cliCmd = argp.command;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const argv = cliCmd.argv || [];
|
|
671
|
+
|
|
672
|
+
const context = {
|
|
673
|
+
run,
|
|
674
|
+
argv,
|
|
675
|
+
cliCmd,
|
|
676
|
+
argp: cliCmd.jsonMeta,
|
|
677
|
+
args: cliCmd.argsList,
|
|
678
|
+
argOpts: cliCmd.opts,
|
|
679
|
+
err: qItem.err,
|
|
680
|
+
failed: this._xrun.failed
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
if (fn.length > 1) {
|
|
684
|
+
// takes two params, pass in context and done callback
|
|
685
|
+
this._watchFailure(watch);
|
|
686
|
+
return fn.call(context, context, done);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (fn.constructor.name !== "AsyncFunction" && fn.length > 0) {
|
|
690
|
+
// non async function that takes 1 param, check if it wants the context
|
|
691
|
+
// or a callback.
|
|
692
|
+
const fnStr = fn.toString().replace(/\s/g, "");
|
|
693
|
+
const matchParam = x => {
|
|
694
|
+
return (
|
|
695
|
+
fnStr.startsWith(`${x}=>`) ||
|
|
696
|
+
fnStr.match(new RegExp(`\\(${x}[^\\)]*\\)=>`)) ||
|
|
697
|
+
fnStr.match(new RegExp(`function[^\\(]*\\(${x}[^\\)]*\\){`)) ||
|
|
698
|
+
fnStr.match(new RegExp(`[^(]*\\(${x}[^\\)]*\\){`))
|
|
699
|
+
);
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const takeContext = ["ctx", "context"].find(matchParam);
|
|
703
|
+
|
|
704
|
+
if (!takeContext) {
|
|
705
|
+
// non async task function takes callback, expect async behavior
|
|
706
|
+
this._watchFailure(watch);
|
|
707
|
+
return fn.call(context, done);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// function takes context as first param
|
|
711
|
+
return done(null, fn.call(context, context));
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const x = this._handleStream(fn.call(context, context));
|
|
715
|
+
|
|
716
|
+
// handling a exec/spawn child from a function
|
|
717
|
+
const cmd = `ChildProcess of function task ${qItem.name}`;
|
|
718
|
+
const child = this._handleChildTask(x && x.child, cmd) || this._handleChildTask(x, cmd);
|
|
719
|
+
|
|
720
|
+
if (!x || typeof x.then !== "function") {
|
|
721
|
+
// assume no async behavior in task function
|
|
722
|
+
return done(null, x);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// x.then is a function, assume task function returned a promise
|
|
726
|
+
this._watchFailure(watch);
|
|
727
|
+
return x.then(
|
|
728
|
+
v => done(null, v),
|
|
729
|
+
err => {
|
|
730
|
+
// treat child process that got SIGTERM as exited normally
|
|
731
|
+
if (this._isChildSigTerm(err, child)) {
|
|
732
|
+
done();
|
|
733
|
+
} else {
|
|
734
|
+
done(err);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
);
|
|
738
|
+
} catch (err) {
|
|
739
|
+
return done(err);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
_watchFailure(watch) {
|
|
744
|
+
if (this._xrun.stopOnError !== "full") return undefined;
|
|
745
|
+
|
|
746
|
+
return setTimeout(() => {
|
|
747
|
+
if (watch.finish) return 0;
|
|
748
|
+
if (this._xrun.failed) {
|
|
749
|
+
this._xrun.emit("fail-cancel", watch);
|
|
750
|
+
return watch.cancel();
|
|
751
|
+
}
|
|
752
|
+
return this._watchFailure(watch);
|
|
753
|
+
}, 50).unref();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
_processMark(qItem) {
|
|
757
|
+
const value = qItem.value();
|
|
758
|
+
const xqItem = this._xrun.xqTree.item(value.xqId);
|
|
759
|
+
const endTime = Date.now();
|
|
760
|
+
const data = {
|
|
761
|
+
qItem,
|
|
762
|
+
xqItem,
|
|
763
|
+
elapse: endTime - value.startTime,
|
|
764
|
+
startTime: value.startTime,
|
|
765
|
+
endTime,
|
|
766
|
+
hrStartTime: value.hrStartTime,
|
|
767
|
+
hrElapse: process.hrtime(value.hrStartTime)
|
|
768
|
+
};
|
|
769
|
+
if (value.callback) {
|
|
770
|
+
value.callback(this._xrun.failed, data);
|
|
771
|
+
} else {
|
|
772
|
+
this._emit("done-item", data);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const xqValue = xqItem.value();
|
|
776
|
+
|
|
777
|
+
if (xqValue.item && xqValue.item[STAGE_FINALLY] && !xqItem.isFinally) {
|
|
778
|
+
xqItem.stage = STAGE_FINALLY;
|
|
779
|
+
xqItem.isFinally = true;
|
|
780
|
+
this.pushItem(xqItem);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
this.next();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
_makeXqMarkItem(qItem, callback) {
|
|
787
|
+
// do not add marking item to tree
|
|
788
|
+
const mark = new XQItem({
|
|
789
|
+
name: `mark_${qItem.name}`,
|
|
790
|
+
value: {
|
|
791
|
+
startTime: Date.now(),
|
|
792
|
+
hrStartTime: process.hrtime(),
|
|
793
|
+
callback,
|
|
794
|
+
xqId: qItem.id
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
mark.mark = true;
|
|
798
|
+
return mark;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
_emit(event, data) {
|
|
802
|
+
try {
|
|
803
|
+
this._xrun.emit(event, data);
|
|
804
|
+
} catch (err) {
|
|
805
|
+
this._xrun.fail(err);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
_createQItem(options, parent) {
|
|
810
|
+
return this._xrun.xqTree.create(options, parent);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export default XQtor;
|