@j-o-r/sh 0.0.2 → 0.0.4
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 +4 -10
- package/lib/sh.d.ts +222 -0
- package/lib/sh.js +1103 -0
- package/package.json +13 -8
- package/src/ProcessOutput.js +0 -113
- package/src/ProcessPromise.js +0 -341
- package/src/sh.js +0 -256
- package/src/utils.js +0 -422
package/lib/sh.js
ADDED
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
import { exec, spawn } from 'node:child_process';
|
|
4
|
+
import { inspect } from 'node:util';
|
|
5
|
+
|
|
6
|
+
// Copyright 2021 Google LLC
|
|
7
|
+
//
|
|
8
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
+
// you may not use this file except in compliance with the License.
|
|
10
|
+
// You may obtain a copy of the License at
|
|
11
|
+
//
|
|
12
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
//
|
|
14
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
15
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
+
// See the License for the specific language governing permissions and
|
|
18
|
+
// limitations under the License.
|
|
19
|
+
//
|
|
20
|
+
//
|
|
21
|
+
// Original Source: zx
|
|
22
|
+
// Link to Original Source: https://github.com/google/zx
|
|
23
|
+
// Reason for Using This Code:
|
|
24
|
+
// The core functionality of this code is highly beneficial. However, certain parts of the original code
|
|
25
|
+
// were overwriting the global namespace with core libraries and variables. This was causing conflicts with
|
|
26
|
+
// other packages (for instance, fetch) and introducing unexpected elements into my code base.
|
|
27
|
+
// Changes Made:
|
|
28
|
+
// - The code has been or is being reformatted to comply with ES2020 standards.
|
|
29
|
+
// - Some methods were added and existing ones were modified to enhance usability.
|
|
30
|
+
// - The namespace has been changed from '$' to 'SH'.
|
|
31
|
+
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Kills a process and all child processes of a given process ID in Linux/Posix.
|
|
35
|
+
* @param {number} processPid - The process ID.
|
|
36
|
+
* @param {string} signal - Signal to send.
|
|
37
|
+
* @retruns {Promise<number[]>} array with killed pid numbers
|
|
38
|
+
*/
|
|
39
|
+
function killProcesses(processPid, signal) {
|
|
40
|
+
const killed = [];
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
// Command to get child PIDs of the given process
|
|
43
|
+
const cmd = `pgrep -P ${processPid}`;
|
|
44
|
+
exec(cmd, (error, stdout, stderr) => {
|
|
45
|
+
if (error) {
|
|
46
|
+
reject(error);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (stderr) {
|
|
50
|
+
reject(new Error(stderr));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const pids = stdout.split(/\r?\n/).filter(pid => pid);
|
|
54
|
+
// Kill each child process
|
|
55
|
+
try {
|
|
56
|
+
for (const pid of pids) {
|
|
57
|
+
process.kill(parseInt(pid), signal);
|
|
58
|
+
killed.push(parseInt(pid));
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
reject(err);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Kill the parent process after all child processes have been killed
|
|
65
|
+
try {
|
|
66
|
+
process.kill(processPid, signal);
|
|
67
|
+
killed.push(processPid);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
reject(err);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
resolve(killed);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function noop() { }
|
|
77
|
+
function quote(arg) {
|
|
78
|
+
if (/^[a-z0-9/_.\-@:=]+$/i.test(arg) || arg === '') {
|
|
79
|
+
return arg;
|
|
80
|
+
}
|
|
81
|
+
return (`$'` +
|
|
82
|
+
arg
|
|
83
|
+
.replace(/\\/g, '\\\\')
|
|
84
|
+
.replace(/'/g, "\\'")
|
|
85
|
+
.replace(/\f/g, '\\f')
|
|
86
|
+
.replace(/\n/g, '\\n')
|
|
87
|
+
.replace(/\r/g, '\\r')
|
|
88
|
+
.replace(/\t/g, '\\t')
|
|
89
|
+
.replace(/\v/g, '\\v')
|
|
90
|
+
.replace(/\0/g, '\\0') +
|
|
91
|
+
`'`);
|
|
92
|
+
}
|
|
93
|
+
function quotePowerShell(arg) {
|
|
94
|
+
if (/^[a-z0-9/_.\-]+$/i.test(arg) || arg === '') {
|
|
95
|
+
return arg;
|
|
96
|
+
}
|
|
97
|
+
return `'` + arg.replace(/'/g, "''") + `'`;
|
|
98
|
+
}
|
|
99
|
+
function log(entry) {
|
|
100
|
+
switch (entry.kind) {
|
|
101
|
+
case 'cmd':
|
|
102
|
+
if (!entry.verbose) return;
|
|
103
|
+
process.stderr.write(formatCmd(entry.cmd));
|
|
104
|
+
break;
|
|
105
|
+
case 'stdout':
|
|
106
|
+
case 'stderr':
|
|
107
|
+
if (!entry.verbose) return;
|
|
108
|
+
process.stderr.write(entry.data);
|
|
109
|
+
break;
|
|
110
|
+
case 'retry':
|
|
111
|
+
if (!entry.verbose) return;
|
|
112
|
+
process.stderr.write(entry.error + '\n');
|
|
113
|
+
break;
|
|
114
|
+
case 'cd':
|
|
115
|
+
if (!entry.verbose) return;
|
|
116
|
+
process.stderr.write(`cd ${entry.dir}\n`);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function exitCodeInfo(exitCode) {
|
|
121
|
+
return {
|
|
122
|
+
2: 'Misuse of shell builtins',
|
|
123
|
+
126: 'Invoked command cannot execute',
|
|
124
|
+
127: 'Command not found',
|
|
125
|
+
128: 'Invalid exit argument',
|
|
126
|
+
129: 'Hangup',
|
|
127
|
+
130: 'Interrupt',
|
|
128
|
+
131: 'Quit and dump core',
|
|
129
|
+
132: 'Illegal instruction',
|
|
130
|
+
133: 'Trace/breakpoint trap',
|
|
131
|
+
134: 'Process aborted',
|
|
132
|
+
135: 'Bus error: "access to undefined portion of memory object"',
|
|
133
|
+
136: 'Floating point exception: "erroneous arithmetic operation"',
|
|
134
|
+
137: 'Kill (terminate immediately)',
|
|
135
|
+
138: 'User-defined 1',
|
|
136
|
+
139: 'Segmentation violation',
|
|
137
|
+
140: 'User-defined 2',
|
|
138
|
+
141: 'Write to pipe with no one reading',
|
|
139
|
+
142: 'Signal raised by alarm',
|
|
140
|
+
143: 'Termination (request to terminate)',
|
|
141
|
+
145: 'Child process terminated, stopped (or continued*)',
|
|
142
|
+
146: 'Continue if stopped',
|
|
143
|
+
147: 'Stop executing temporarily',
|
|
144
|
+
148: 'Terminal stop signal',
|
|
145
|
+
149: 'Background process attempting to read from tty ("in")',
|
|
146
|
+
150: 'Background process attempting to write to tty ("out")',
|
|
147
|
+
151: 'Urgent data available on socket',
|
|
148
|
+
152: 'CPU time limit exceeded',
|
|
149
|
+
153: 'File size limit exceeded',
|
|
150
|
+
154: 'Signal raised by timer counting virtual time: "virtual timer expired"',
|
|
151
|
+
155: 'Profiling timer expired',
|
|
152
|
+
157: 'Pollable event',
|
|
153
|
+
159: 'Bad syscall',
|
|
154
|
+
}[exitCode || -1];
|
|
155
|
+
}
|
|
156
|
+
function errnoMessage(errno) {
|
|
157
|
+
if (errno === undefined) {
|
|
158
|
+
return 'Unknown error';
|
|
159
|
+
}
|
|
160
|
+
return ({
|
|
161
|
+
0: 'Success',
|
|
162
|
+
1: 'Not super-user',
|
|
163
|
+
2: 'No such file or directory',
|
|
164
|
+
3: 'No such process',
|
|
165
|
+
4: 'Interrupted system call',
|
|
166
|
+
5: 'I/O error',
|
|
167
|
+
6: 'No such device or address',
|
|
168
|
+
7: 'Arg list too long',
|
|
169
|
+
8: 'Exec format error',
|
|
170
|
+
9: 'Bad file number',
|
|
171
|
+
10: 'No children',
|
|
172
|
+
11: 'No more processes',
|
|
173
|
+
12: 'Not enough core',
|
|
174
|
+
13: 'Permission denied',
|
|
175
|
+
14: 'Bad address',
|
|
176
|
+
15: 'Block device required',
|
|
177
|
+
16: 'Mount device busy',
|
|
178
|
+
17: 'File exists',
|
|
179
|
+
18: 'Cross-device link',
|
|
180
|
+
19: 'No such device',
|
|
181
|
+
20: 'Not a directory',
|
|
182
|
+
21: 'Is a directory',
|
|
183
|
+
22: 'Invalid argument',
|
|
184
|
+
23: 'Too many open files in system',
|
|
185
|
+
24: 'Too many open files',
|
|
186
|
+
25: 'Not a typewriter',
|
|
187
|
+
26: 'Text file busy',
|
|
188
|
+
27: 'File too large',
|
|
189
|
+
28: 'No space left on device',
|
|
190
|
+
29: 'Illegal seek',
|
|
191
|
+
30: 'Read only file system',
|
|
192
|
+
31: 'Too many links',
|
|
193
|
+
32: 'Broken pipe',
|
|
194
|
+
33: 'Math arg out of domain of func',
|
|
195
|
+
34: 'Math result not representable',
|
|
196
|
+
35: 'File locking deadlock error',
|
|
197
|
+
36: 'File or path name too long',
|
|
198
|
+
37: 'No record locks available',
|
|
199
|
+
38: 'Function not implemented',
|
|
200
|
+
39: 'Directory not empty',
|
|
201
|
+
40: 'Too many symbolic links',
|
|
202
|
+
42: 'No message of desired type',
|
|
203
|
+
43: 'Identifier removed',
|
|
204
|
+
44: 'Channel number out of range',
|
|
205
|
+
45: 'Level 2 not synchronized',
|
|
206
|
+
46: 'Level 3 halted',
|
|
207
|
+
47: 'Level 3 reset',
|
|
208
|
+
48: 'Link number out of range',
|
|
209
|
+
49: 'Protocol driver not attached',
|
|
210
|
+
50: 'No CSI structure available',
|
|
211
|
+
51: 'Level 2 halted',
|
|
212
|
+
52: 'Invalid exchange',
|
|
213
|
+
53: 'Invalid request descriptor',
|
|
214
|
+
54: 'Exchange full',
|
|
215
|
+
55: 'No anode',
|
|
216
|
+
56: 'Invalid request code',
|
|
217
|
+
57: 'Invalid slot',
|
|
218
|
+
59: 'Bad font file fmt',
|
|
219
|
+
60: 'Device not a stream',
|
|
220
|
+
61: 'No data (for no delay io)',
|
|
221
|
+
62: 'Timer expired',
|
|
222
|
+
63: 'Out of streams resources',
|
|
223
|
+
64: 'Machine is not on the network',
|
|
224
|
+
65: 'Package not installed',
|
|
225
|
+
66: 'The object is remote',
|
|
226
|
+
67: 'The link has been severed',
|
|
227
|
+
68: 'Advertise error',
|
|
228
|
+
69: 'Srmount error',
|
|
229
|
+
70: 'Communication error on send',
|
|
230
|
+
71: 'Protocol error',
|
|
231
|
+
72: 'Multihop attempted',
|
|
232
|
+
73: 'Cross mount point (not really error)',
|
|
233
|
+
74: 'Trying to read unreadable message',
|
|
234
|
+
75: 'Value too large for defined data type',
|
|
235
|
+
76: 'Given log. name not unique',
|
|
236
|
+
77: 'f.d. invalid for this operation',
|
|
237
|
+
78: 'Remote address changed',
|
|
238
|
+
79: 'Can access a needed shared lib',
|
|
239
|
+
80: 'Accessing a corrupted shared lib',
|
|
240
|
+
81: '.lib section in a.out corrupted',
|
|
241
|
+
82: 'Attempting to link in too many libs',
|
|
242
|
+
83: 'Attempting to exec a shared library',
|
|
243
|
+
84: 'Illegal byte sequence',
|
|
244
|
+
86: 'Streams pipe error',
|
|
245
|
+
87: 'Too many users',
|
|
246
|
+
88: 'Socket operation on non-socket',
|
|
247
|
+
89: 'Destination address required',
|
|
248
|
+
90: 'Message too long',
|
|
249
|
+
91: 'Protocol wrong type for socket',
|
|
250
|
+
92: 'Protocol not available',
|
|
251
|
+
93: 'Unknown protocol',
|
|
252
|
+
94: 'Socket type not supported',
|
|
253
|
+
95: 'Not supported',
|
|
254
|
+
96: 'Protocol family not supported',
|
|
255
|
+
97: 'Address family not supported by protocol family',
|
|
256
|
+
98: 'Address already in use',
|
|
257
|
+
99: 'Address not available',
|
|
258
|
+
100: 'Network interface is not configured',
|
|
259
|
+
101: 'Network is unreachable',
|
|
260
|
+
102: 'Connection reset by network',
|
|
261
|
+
103: 'Connection aborted',
|
|
262
|
+
104: 'Connection reset by peer',
|
|
263
|
+
105: 'No buffer space available',
|
|
264
|
+
106: 'Socket is already connected',
|
|
265
|
+
107: 'Socket is not connected',
|
|
266
|
+
108: "Can't send after socket shutdown",
|
|
267
|
+
109: 'Too many references',
|
|
268
|
+
110: 'Connection timed out',
|
|
269
|
+
111: 'Connection refused',
|
|
270
|
+
112: 'Host is down',
|
|
271
|
+
113: 'Host is unreachable',
|
|
272
|
+
114: 'Socket already connected',
|
|
273
|
+
115: 'Connection already in progress',
|
|
274
|
+
116: 'Stale file handle',
|
|
275
|
+
122: 'Quota exceeded',
|
|
276
|
+
123: 'No medium (in tape drive)',
|
|
277
|
+
125: 'Operation canceled',
|
|
278
|
+
130: 'Previous owner died',
|
|
279
|
+
131: 'State not recoverable',
|
|
280
|
+
}[-errno] || 'Unknown error');
|
|
281
|
+
}
|
|
282
|
+
function parseDuration(d) {
|
|
283
|
+
if (typeof d == 'number') {
|
|
284
|
+
if (isNaN(d) || d < 0)
|
|
285
|
+
throw new Error(`Invalid duration: "${d}".`);
|
|
286
|
+
return d;
|
|
287
|
+
}
|
|
288
|
+
else if (/\d+s/.test(d)) {
|
|
289
|
+
return +d.slice(0, -1) * 1000;
|
|
290
|
+
}
|
|
291
|
+
else if (/\d+ms/.test(d)) {
|
|
292
|
+
return +d.slice(0, -2);
|
|
293
|
+
}
|
|
294
|
+
throw new Error(`Unknown duration: "${d}".`);
|
|
295
|
+
}
|
|
296
|
+
function formatCmd(cmd) {
|
|
297
|
+
if (cmd == undefined)
|
|
298
|
+
return 'undefined';
|
|
299
|
+
const chars = [...cmd];
|
|
300
|
+
let out = '$ ';
|
|
301
|
+
let buf = '';
|
|
302
|
+
let ch;
|
|
303
|
+
let state = root;
|
|
304
|
+
while (state) {
|
|
305
|
+
ch = chars.shift() || 'EOF';
|
|
306
|
+
if (ch == '\n') {
|
|
307
|
+
out += style(state, buf) + '\n> ';
|
|
308
|
+
buf = '';
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const next = ch == 'EOF' ? undefined : state();
|
|
312
|
+
if (next != state) {
|
|
313
|
+
out += style(state, buf);
|
|
314
|
+
buf = '';
|
|
315
|
+
}
|
|
316
|
+
state = next == root ? next() : next;
|
|
317
|
+
buf += ch;
|
|
318
|
+
}
|
|
319
|
+
function style(state, s) {
|
|
320
|
+
// if (s == '')
|
|
321
|
+
// return '';
|
|
322
|
+
// if (reservedWords.includes(s)) {
|
|
323
|
+
// return chalk.cyanBright(s);
|
|
324
|
+
// }
|
|
325
|
+
// if (state == word && wordCount == 0) {
|
|
326
|
+
// wordCount++;
|
|
327
|
+
// return chalk.greenBright(s);
|
|
328
|
+
// }
|
|
329
|
+
// if (state == syntax) {
|
|
330
|
+
// wordCount = 0;
|
|
331
|
+
// return chalk.cyanBright(s);
|
|
332
|
+
// }
|
|
333
|
+
// if (state == dollar)
|
|
334
|
+
// return chalk.yellowBright(s);
|
|
335
|
+
// if (state?.name.startsWith('str'))
|
|
336
|
+
// return chalk.yellowBright(s);
|
|
337
|
+
return s;
|
|
338
|
+
}
|
|
339
|
+
function isSyntax(ch) {
|
|
340
|
+
return '()[]{}<>;:+|&='.includes(ch);
|
|
341
|
+
}
|
|
342
|
+
function root() {
|
|
343
|
+
if (/\s/.test(ch))
|
|
344
|
+
return space;
|
|
345
|
+
if (isSyntax(ch))
|
|
346
|
+
return syntax;
|
|
347
|
+
if (/[$]/.test(ch))
|
|
348
|
+
return dollar;
|
|
349
|
+
if (/["]/.test(ch))
|
|
350
|
+
return strDouble;
|
|
351
|
+
if (/[']/.test(ch))
|
|
352
|
+
return strSingle;
|
|
353
|
+
return word;
|
|
354
|
+
}
|
|
355
|
+
function space() {
|
|
356
|
+
if (/\s/.test(ch))
|
|
357
|
+
return space;
|
|
358
|
+
return root;
|
|
359
|
+
}
|
|
360
|
+
function word() {
|
|
361
|
+
if (/[0-9a-z/_.]/i.test(ch))
|
|
362
|
+
return word;
|
|
363
|
+
return root;
|
|
364
|
+
}
|
|
365
|
+
function syntax() {
|
|
366
|
+
if (isSyntax(ch))
|
|
367
|
+
return syntax;
|
|
368
|
+
return root;
|
|
369
|
+
}
|
|
370
|
+
function dollar() {
|
|
371
|
+
if (/[']/.test(ch))
|
|
372
|
+
return str;
|
|
373
|
+
return root;
|
|
374
|
+
}
|
|
375
|
+
function str() {
|
|
376
|
+
if (/[']/.test(ch))
|
|
377
|
+
return strEnd;
|
|
378
|
+
if (/[\\]/.test(ch))
|
|
379
|
+
return strBackslash;
|
|
380
|
+
return str;
|
|
381
|
+
}
|
|
382
|
+
function strBackslash() {
|
|
383
|
+
return strEscape;
|
|
384
|
+
}
|
|
385
|
+
function strEscape() {
|
|
386
|
+
return str;
|
|
387
|
+
}
|
|
388
|
+
function strDouble() {
|
|
389
|
+
if (/["]/.test(ch))
|
|
390
|
+
return strEnd;
|
|
391
|
+
return strDouble;
|
|
392
|
+
}
|
|
393
|
+
function strSingle() {
|
|
394
|
+
if (/[']/.test(ch))
|
|
395
|
+
return strEnd;
|
|
396
|
+
return strSingle;
|
|
397
|
+
}
|
|
398
|
+
function strEnd() {
|
|
399
|
+
return root;
|
|
400
|
+
}
|
|
401
|
+
return out + '\n';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Copyright 2021 Google LLC
|
|
405
|
+
//
|
|
406
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
407
|
+
// you may not use this file except in compliance with the License.
|
|
408
|
+
// You may obtain a copy of the License at
|
|
409
|
+
//
|
|
410
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
411
|
+
//
|
|
412
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
413
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
414
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
415
|
+
// See the License for the specific language governing permissions and
|
|
416
|
+
// limitations under the License.
|
|
417
|
+
//
|
|
418
|
+
//
|
|
419
|
+
// Original Source: zx
|
|
420
|
+
// Link to Original Source: https://github.com/google/zx
|
|
421
|
+
// Reason for Using This Code:
|
|
422
|
+
// The core functionality of this code is highly beneficial. However, certain parts of the original code
|
|
423
|
+
// were overwriting the global namespace with core libraries and variables. This was causing conflicts with
|
|
424
|
+
// other packages (for instance, fetch) and introducing unexpected elements into my code base.
|
|
425
|
+
// Changes Made:
|
|
426
|
+
// - The code has been or is being reformatted to comply with ES2020 standards.
|
|
427
|
+
// - Some methods were added and existing ones were modified to enhance usability.
|
|
428
|
+
// - The namespace has been changed from '$' to 'SH'.
|
|
429
|
+
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* The ProcessPromise returns a ProcessOutput even when it fails, by rejecting it.
|
|
434
|
+
* The extension of the Error class is implemented to ensure compatibility when an Error is expected upon rejection.
|
|
435
|
+
*/
|
|
436
|
+
class ProcessOutput extends Error {
|
|
437
|
+
#code = 0;
|
|
438
|
+
#signal;
|
|
439
|
+
#stdout = '';
|
|
440
|
+
#stderr = '';
|
|
441
|
+
#combined = '';
|
|
442
|
+
/**
|
|
443
|
+
* @param {number} code - exit code
|
|
444
|
+
* @param {string} signal - SIGTERM ...
|
|
445
|
+
* @param {string} stdout - std reponse string
|
|
446
|
+
* @param {string} stderr - error reponse string
|
|
447
|
+
* @param {string} combined - stderr + stdout
|
|
448
|
+
* @param {string} message - Error message
|
|
449
|
+
*/
|
|
450
|
+
constructor(code, signal, stdout = '', stderr = '', combined = '', message = '') {
|
|
451
|
+
super(message);
|
|
452
|
+
this.#code = code;
|
|
453
|
+
this.#signal = signal;
|
|
454
|
+
this.#stdout = stdout;
|
|
455
|
+
this.#stderr = stderr;
|
|
456
|
+
this.#combined = combined;
|
|
457
|
+
this.name = 'ProcessOutput';
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* This string represents the standard output (stdout) from the child process.
|
|
461
|
+
* @returns {string}
|
|
462
|
+
*/
|
|
463
|
+
get stdout() {
|
|
464
|
+
return this.#stdout;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* This string represents the error output (stderr) from the child process.
|
|
468
|
+
* @returns {string}
|
|
469
|
+
*/
|
|
470
|
+
get stderr() {
|
|
471
|
+
return this.#stderr;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* This is an internal method that is often invoked automatically
|
|
476
|
+
* by various JavaScript methods.
|
|
477
|
+
* It consolidates the entire output for completeness and facilitates further processing.
|
|
478
|
+
*
|
|
479
|
+
* @returns {string} The consolidated output as a
|
|
480
|
+
*/
|
|
481
|
+
toString() {
|
|
482
|
+
return this.#combined.trim();
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* This represents the exit code returned by the external process.
|
|
486
|
+
* @returns {number} The exit
|
|
487
|
+
*/
|
|
488
|
+
get exitCode() {
|
|
489
|
+
return this.#code;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* This represents the exit signal, for example, "SIGTERM", received from the child process.
|
|
493
|
+
* @returns {string} The exit signal from the child
|
|
494
|
+
*/
|
|
495
|
+
get signal() {
|
|
496
|
+
return this.#signal;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* This method is used for debugging purposes. It displays the current state of the object
|
|
500
|
+
* when passed to the console.log function.
|
|
501
|
+
*/
|
|
502
|
+
[inspect.custom]() {
|
|
503
|
+
let stringify = (s) => s.length === 0 ? "''" : inspect(s);
|
|
504
|
+
return `ProcessOutput {
|
|
505
|
+
stdout: ${stringify(this.stdout)},
|
|
506
|
+
stderr: ${stringify(this.stderr)},
|
|
507
|
+
signal: ${inspect(this.signal)},
|
|
508
|
+
exitCode: ${(this.exitCode)}${exitCodeInfo(this.exitCode)
|
|
509
|
+
? ' (' + exitCodeInfo(this.exitCode) + ')'
|
|
510
|
+
: ''}
|
|
511
|
+
}`;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Copyright 2021 Google LLC
|
|
516
|
+
//
|
|
517
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
518
|
+
// you may not use this file except in compliance with the License.
|
|
519
|
+
// You may obtain a copy of the License at
|
|
520
|
+
//
|
|
521
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
522
|
+
//
|
|
523
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
524
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
525
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
526
|
+
// See the License for the specific language governing permissions and
|
|
527
|
+
// limitations under the License.
|
|
528
|
+
//
|
|
529
|
+
//
|
|
530
|
+
// Original Source: zx
|
|
531
|
+
// Link to Original Source: https://github.com/google/zx
|
|
532
|
+
// Reason for Using This Code:
|
|
533
|
+
// The core functionality of this code is highly beneficial. However, certain parts of the original code
|
|
534
|
+
// were overwriting the global namespace with core libraries and variables. This was causing conflicts with
|
|
535
|
+
// other packages (for instance, fetch) and introducing unexpected elements into my code base.
|
|
536
|
+
// Changes Made:
|
|
537
|
+
// - The code has been or is being reformatted to comply with ES2020 standards.
|
|
538
|
+
// - Some methods were added and existing ones were modified to enhance usability.
|
|
539
|
+
// - The namespace has been changed from '$' to 'SH'.
|
|
540
|
+
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* @typedef {Function} resolver
|
|
545
|
+
* @param {ProcessOutput} value
|
|
546
|
+
*/
|
|
547
|
+
/**
|
|
548
|
+
* @typedef {Function} rejecter
|
|
549
|
+
* @param {ProcessOutput} value
|
|
550
|
+
*/
|
|
551
|
+
/**
|
|
552
|
+
* @typedef {Function} PromiseConstruct
|
|
553
|
+
* @param {resolver} resolve
|
|
554
|
+
* @param {rejecter} reject
|
|
555
|
+
*/
|
|
556
|
+
/**
|
|
557
|
+
* @typedef {('pipe' | 'ignore' | 'inherit' | number)} StdioOption
|
|
558
|
+
* @description Defines the stdio configuration for each of the standard streams.
|
|
559
|
+
*
|
|
560
|
+
* - 'pipe' creates a pipe between the child process and the parent process.
|
|
561
|
+
* The parent end of the pipe is exposed as a property on the `ChildProcess` object.
|
|
562
|
+
* - 'ignore' indicates that the child process's corresponding stdio file descriptor will be ignored.
|
|
563
|
+
* - 'inherit' passes the corresponding stdio stream to/from the child process.
|
|
564
|
+
* - Stream object to be used for the stdio stream.
|
|
565
|
+
* - Positive integer representing a file descriptor to be used for the stdio stream.
|
|
566
|
+
*/
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* @typedef {Array<StdioOption>|StdioOption} StdioOptions
|
|
570
|
+
* @description
|
|
571
|
+
* Configures the stdio streams for the child process. This can be an array or a single StdioOption.
|
|
572
|
+
*
|
|
573
|
+
* Array Form: Specify the configuration for [stdin, stdout, stderr].
|
|
574
|
+
* - If array length is more than 3, additional positions correspond to extra streams.
|
|
575
|
+
* Single Value: This value will be applied to stdin, stdout, and stderr.
|
|
576
|
+
*
|
|
577
|
+
* Examples:
|
|
578
|
+
* - ['pipe', 'pipe', 'ignore']: Pipe stdin and stdout, ignore stderr.
|
|
579
|
+
* - 'inherit': Inherit all stdio streams from the parent.
|
|
580
|
+
*/
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* class extends promise
|
|
584
|
+
*/
|
|
585
|
+
class ProcessPromise extends Promise {
|
|
586
|
+
#command = '';
|
|
587
|
+
#from = '';
|
|
588
|
+
/** @type {resolver} */
|
|
589
|
+
#resolve = () => { };
|
|
590
|
+
/** @type {rejecter} */
|
|
591
|
+
#reject = () => { };
|
|
592
|
+
#snapshot = {};
|
|
593
|
+
/** @type {StdioOptions} */
|
|
594
|
+
#stdio = ['inherit', 'pipe', 'pipe'];
|
|
595
|
+
#nothrow = false;
|
|
596
|
+
#quiet = false;
|
|
597
|
+
#resolved = false;
|
|
598
|
+
#halted = false;
|
|
599
|
+
#piped = false;
|
|
600
|
+
#prerun = noop;
|
|
601
|
+
#postrun = noop;
|
|
602
|
+
/**
|
|
603
|
+
* @param {PromiseConstruct} p - A function that takes two arguments, resolve and reject.
|
|
604
|
+
*/
|
|
605
|
+
constructor(p) {
|
|
606
|
+
// @ts-ignore
|
|
607
|
+
super(p);
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Set the environment
|
|
611
|
+
* and the
|
|
612
|
+
* @param {string} cmd - Command to execute
|
|
613
|
+
* @param {string} from - Position in the codfe where this is triggred from
|
|
614
|
+
* @param {function} resolve - Promise resolve method
|
|
615
|
+
* @param {function} reject - Reject method
|
|
616
|
+
* @param {object} options - Settings (options default)
|
|
617
|
+
*/
|
|
618
|
+
_bind(cmd, from, resolve, reject, options) {
|
|
619
|
+
this.#command = cmd;
|
|
620
|
+
this.#from = from;
|
|
621
|
+
this.#resolve = resolve;
|
|
622
|
+
this.#reject = reject;
|
|
623
|
+
this.#snapshot = { ...options };
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Run the promise
|
|
627
|
+
*/
|
|
628
|
+
run() {
|
|
629
|
+
const ENV = this.#snapshot;
|
|
630
|
+
if (this.child) return this; // The _run() can be called from a few places.
|
|
631
|
+
this.#prerun(); // In case $1.pipe($2), the $2 returned, and on $2._run() invoke $1._run().
|
|
632
|
+
log({
|
|
633
|
+
kind: 'cmd',
|
|
634
|
+
cmd: this.#command,
|
|
635
|
+
verbose: ENV.verbose && !this.#quiet,
|
|
636
|
+
});
|
|
637
|
+
const cwd = ENV['processCwd'];
|
|
638
|
+
const shell = ENV['shell'];
|
|
639
|
+
this.child = spawn(ENV.prefix, [this.#command], {
|
|
640
|
+
cwd,
|
|
641
|
+
shell,
|
|
642
|
+
// @ts-ignore
|
|
643
|
+
stdio: this.#stdio,
|
|
644
|
+
windowsHide: true,
|
|
645
|
+
env: ENV.env,
|
|
646
|
+
});
|
|
647
|
+
this.child.on('close', (code, signal) => {
|
|
648
|
+
let message = `exit code: ${code}`;
|
|
649
|
+
if (code != 0 || signal != null) {
|
|
650
|
+
message = `${stderr || '\n'} at ${this.#from}`;
|
|
651
|
+
message += `\n exit code: ${code}${exitCodeInfo(code) ? ' (' + exitCodeInfo(code) + ')' : ''}`;
|
|
652
|
+
if (signal != null) {
|
|
653
|
+
message += `\n signal: ${signal}`;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
let output = new ProcessOutput(code, signal, stdout, stderr, combined, message);
|
|
657
|
+
if (code === 0 || this.#nothrow) {
|
|
658
|
+
this.#resolve(output);
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
this.#reject(output);
|
|
662
|
+
}
|
|
663
|
+
this.#resolved = true;
|
|
664
|
+
});
|
|
665
|
+
this.child.on('error', (err) => {
|
|
666
|
+
const message = `${err.message}\n` +
|
|
667
|
+
// @ts-ignore
|
|
668
|
+
` errno: ${err.errno} (${errnoMessage(err.errno)})\n` +
|
|
669
|
+
// @ts-ignore
|
|
670
|
+
` code: ${err.code}\n` +
|
|
671
|
+
` at ${this.#from}`;
|
|
672
|
+
this.#reject(new ProcessOutput(null, null, stdout, stderr, combined, message));
|
|
673
|
+
this.#resolved = true;
|
|
674
|
+
});
|
|
675
|
+
let stdout = '', stderr = '', combined = '';
|
|
676
|
+
/** @param {Blob} data */
|
|
677
|
+
const onStdout = (data) => {
|
|
678
|
+
log({ kind: 'stdout', data, verbose: ENV.verbose && !this.#quiet });
|
|
679
|
+
stdout += data;
|
|
680
|
+
combined += data;
|
|
681
|
+
};
|
|
682
|
+
/** @param {Blob} data */
|
|
683
|
+
const onStderr = (data) => {
|
|
684
|
+
log({ kind: 'stderr', data, verbose: ENV.verbose && !this.#quiet });
|
|
685
|
+
stderr += data;
|
|
686
|
+
combined += data;
|
|
687
|
+
};
|
|
688
|
+
if (!this.#piped)
|
|
689
|
+
// @ts-ignore
|
|
690
|
+
this.child.stdout?.on('data', onStdout); // If process is piped, don't collect or print output.
|
|
691
|
+
// @ts-ignore
|
|
692
|
+
this.child.stderr?.on('data', onStderr); // Stderr should be printed regardless of piping.
|
|
693
|
+
this.#postrun(); // In case $1.pipe($2), after both subprocesses are running, we can pipe $1.stdout to $2.stdin.
|
|
694
|
+
if (this._timeout && this._timeoutSignal) {
|
|
695
|
+
const t = setTimeout(() => this.kill(this._timeoutSignal), this._timeout);
|
|
696
|
+
this.finally(() => clearTimeout(t)).catch(noop);
|
|
697
|
+
}
|
|
698
|
+
return this;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* stdin child stream
|
|
702
|
+
* @retruns {Writeable}
|
|
703
|
+
*/
|
|
704
|
+
get stdin() {
|
|
705
|
+
this.stdio('pipe');
|
|
706
|
+
this.run();
|
|
707
|
+
assert(this.child);
|
|
708
|
+
if (this.child.stdin == null)
|
|
709
|
+
throw new Error('The stdin of subprocess is null.');
|
|
710
|
+
return this.child.stdin;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* stdout child stream
|
|
714
|
+
* @retruns {Readable}
|
|
715
|
+
*/
|
|
716
|
+
get stdout() {
|
|
717
|
+
this.run();
|
|
718
|
+
assert(this.child);
|
|
719
|
+
if (this.child.stdout == null)
|
|
720
|
+
throw new Error('The stdout of subprocess is null.');
|
|
721
|
+
return this.child.stdout;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* stderr child stream
|
|
725
|
+
* @retruns {Readable}
|
|
726
|
+
*/
|
|
727
|
+
get stderr() {
|
|
728
|
+
this.run();
|
|
729
|
+
assert(this.child);
|
|
730
|
+
if (this.child.stderr == null)
|
|
731
|
+
throw new Error('The stderr of subprocess is null.');
|
|
732
|
+
return this.child.stderr;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* process exit code
|
|
736
|
+
* @returns {Promise<number>}
|
|
737
|
+
*/
|
|
738
|
+
get exitCode() {
|
|
739
|
+
return this.#then((p) => p.exitCode, (p) => p.exitCode);
|
|
740
|
+
}
|
|
741
|
+
#then(onfulfilled, onrejected) {
|
|
742
|
+
if (this.isHalted && !this.child) {
|
|
743
|
+
throw new Error('The process is halted!');
|
|
744
|
+
}
|
|
745
|
+
return super.then(onfulfilled, onrejected);
|
|
746
|
+
}
|
|
747
|
+
catch(onrejected) {
|
|
748
|
+
return super.catch(onrejected);
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Pipe the output to the input to the next Promise
|
|
752
|
+
* @example
|
|
753
|
+
* const res = await SH`ls -FLa`.pipe(SH`grep package.json`);
|
|
754
|
+
*/
|
|
755
|
+
pipe(dest) {
|
|
756
|
+
if (typeof dest == 'string')
|
|
757
|
+
throw new Error('The pipe() method does not take strings. Forgot SH?');
|
|
758
|
+
if (this.#resolved) {
|
|
759
|
+
if (dest instanceof ProcessPromise)
|
|
760
|
+
// @ts-ignore
|
|
761
|
+
dest.stdin.end(); // In case of piped stdin, we may want to close stdin of dest as well.
|
|
762
|
+
throw new Error("The pipe() method shouldn't be called after promise is already resolved!");
|
|
763
|
+
}
|
|
764
|
+
this.#piped = true;
|
|
765
|
+
if (dest instanceof ProcessPromise) {
|
|
766
|
+
dest.stdio('pipe');
|
|
767
|
+
dest._prerun = this.run.bind(this);
|
|
768
|
+
dest._postrun = () => {
|
|
769
|
+
if (!dest.child)
|
|
770
|
+
throw new Error('Access to stdin of pipe destination without creation a subprocess.');
|
|
771
|
+
// @ts-ignore
|
|
772
|
+
this.stdout.pipe(dest.stdin);
|
|
773
|
+
};
|
|
774
|
+
return dest;
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
// @ts-ignore
|
|
778
|
+
this._postrun = () => this.stdout.pipe(dest);
|
|
779
|
+
return this;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Send a KILL signal to the child process
|
|
784
|
+
* @returns {Promise<number[]>} the pid numbers that has been killed
|
|
785
|
+
*/
|
|
786
|
+
async kill(signal = 'SIGTERM') {
|
|
787
|
+
if (!this.child)
|
|
788
|
+
throw new Error('Trying to kill a process without creating one.');
|
|
789
|
+
if (!this.child.pid)
|
|
790
|
+
throw new Error('The process pid is undefined.');
|
|
791
|
+
|
|
792
|
+
return await killProcesses(this.child.pid, signal)
|
|
793
|
+
}
|
|
794
|
+
stdio(stdin, stdout = 'pipe', stderr = 'pipe') {
|
|
795
|
+
this.#stdio = [stdin, stdout, stderr];
|
|
796
|
+
return this;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Do not throw
|
|
800
|
+
*/
|
|
801
|
+
nothrow() {
|
|
802
|
+
this.#nothrow = true;
|
|
803
|
+
return this;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* supress log output
|
|
807
|
+
* SH.verbose = false; does the same
|
|
808
|
+
*/
|
|
809
|
+
quiet() {
|
|
810
|
+
this.#quiet = true;
|
|
811
|
+
return this;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Show log output in the console
|
|
815
|
+
*/
|
|
816
|
+
verbose() {
|
|
817
|
+
this._quiet = false;
|
|
818
|
+
return this;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Set a timeout to kill a process
|
|
822
|
+
*
|
|
823
|
+
* @param {string} d - 10s, 1000ms
|
|
824
|
+
* @param {string} [signal] - default "SIGTERM" Signal to send to kill the proces
|
|
825
|
+
*/
|
|
826
|
+
timeout(d, signal = 'SIGTERM') {
|
|
827
|
+
this._timeout = parseDuration(d);
|
|
828
|
+
this._timeoutSignal = signal;
|
|
829
|
+
return this;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* stop execution for the next step
|
|
833
|
+
*/
|
|
834
|
+
halt() {
|
|
835
|
+
this.#halted = true;
|
|
836
|
+
return this;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* @private
|
|
840
|
+
* Set a prerun action, internal use only
|
|
841
|
+
* @param {function} f
|
|
842
|
+
*/
|
|
843
|
+
set _prerun(f) {
|
|
844
|
+
// @ts-ignore
|
|
845
|
+
this.#prerun = f;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* @private
|
|
849
|
+
* Set a postrun action, internal use only
|
|
850
|
+
* @param {function} f
|
|
851
|
+
*/
|
|
852
|
+
set _postrun(f) {
|
|
853
|
+
// @ts-ignore
|
|
854
|
+
this.#postrun = f;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Is this promise halted?
|
|
858
|
+
* @returns {boolean}
|
|
859
|
+
*/
|
|
860
|
+
get isHalted() {
|
|
861
|
+
return this.#halted;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Copyright 2021 Google LLC
|
|
866
|
+
//
|
|
867
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
868
|
+
// you may not use this file except in compliance with the License.
|
|
869
|
+
// You may obtain a copy of the License at
|
|
870
|
+
//
|
|
871
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
872
|
+
//
|
|
873
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
874
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
875
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
876
|
+
// See the License for the specific language governing permissions and
|
|
877
|
+
// limitations under the License.
|
|
878
|
+
//
|
|
879
|
+
//
|
|
880
|
+
// Original Source: zx
|
|
881
|
+
// Link to Original Source: https://github.com/google/zx
|
|
882
|
+
// Reason for Using This Code:
|
|
883
|
+
// The core functionality of this code is highly beneficial. However, certain parts of the original code
|
|
884
|
+
// were overwriting the global namespace with core libraries and variables. This was causing conflicts with
|
|
885
|
+
// other packages (for instance, fetch) and introducing unexpected elements into my code base.
|
|
886
|
+
// Changes Made:
|
|
887
|
+
// - The code has been or is being reformatted to comply with ES2020 standards.
|
|
888
|
+
// - Some methods were added and existing ones were modified to enhance usability.
|
|
889
|
+
// - The namespace has been changed from '$' to 'SH'.
|
|
890
|
+
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
891
|
+
|
|
892
|
+
const storage = new AsyncLocalStorage();
|
|
893
|
+
|
|
894
|
+
const defaults = {
|
|
895
|
+
processCwd: '',
|
|
896
|
+
verbose: false,
|
|
897
|
+
env: {},
|
|
898
|
+
shell: 'bash',
|
|
899
|
+
prefix: '',
|
|
900
|
+
};
|
|
901
|
+
defaults.prefix = 'set -euo pipefail;/usr/bin/env';
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Escape CLI arguments
|
|
905
|
+
* @param {string[]} arg
|
|
906
|
+
* @retruns {string}
|
|
907
|
+
*/
|
|
908
|
+
const sanitizeArg = (arg) => {
|
|
909
|
+
const s = `${arg}`;
|
|
910
|
+
if (process.platform == 'win32') {
|
|
911
|
+
return quotePowerShell(s)
|
|
912
|
+
}
|
|
913
|
+
return quote(s);
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
const getStore = () => {
|
|
917
|
+
return storage.getStore() || defaults;
|
|
918
|
+
};
|
|
919
|
+
/**
|
|
920
|
+
* Creates a new ProcessPromise object that represents a command to be executed.
|
|
921
|
+
*
|
|
922
|
+
* @typedef {Function} Shell
|
|
923
|
+
* @property {function(Array, ...*): ProcessPromise} execute - The function to execute the command.
|
|
924
|
+
* @property {boolean} verbose - A property to control verbosity.
|
|
925
|
+
*
|
|
926
|
+
* @param {Array} pieces - An array of string literals from a template literal.
|
|
927
|
+
* @param {...*} args - The values to be interpolated into the string literals.
|
|
928
|
+
* @returns {ProcessPromise} A ProcessPromise object that represents the command.
|
|
929
|
+
* @throws {Error} Throws an error if any of the string literals in `pieces` is undefined.
|
|
930
|
+
*
|
|
931
|
+
* @example
|
|
932
|
+
* const command = await SH`echo 'Hello, world!'`;
|
|
933
|
+
*/
|
|
934
|
+
/** @type {Shell & { (pieces: TemplateStringsArray, ...args: any[]): ProcessPromise }} */
|
|
935
|
+
// @ts-ignore
|
|
936
|
+
const SH = new Proxy(function(pieces, ...args) {
|
|
937
|
+
const from = new Error().stack.split(/^\s*at\s/m)[2].trim();
|
|
938
|
+
if (pieces.some((p) => p == undefined)) {
|
|
939
|
+
throw new Error(`Malformed command at ${from}`);
|
|
940
|
+
}
|
|
941
|
+
let cmd = pieces[0], i = 0;
|
|
942
|
+
while (i < args.length) {
|
|
943
|
+
let s;
|
|
944
|
+
if (Array.isArray(args[i])) {
|
|
945
|
+
s = args[i].map((x) => sanitizeArg(x)).join(' ');
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
s = sanitizeArg(args[i]);
|
|
949
|
+
}
|
|
950
|
+
cmd += s + pieces[++i];
|
|
951
|
+
}
|
|
952
|
+
let resolve, reject;
|
|
953
|
+
|
|
954
|
+
const promise = new ProcessPromise((...args) => ([resolve, reject] = args));
|
|
955
|
+
// re-add the environment
|
|
956
|
+
defaults.processCwd = process.cwd();
|
|
957
|
+
defaults.env = process.env,
|
|
958
|
+
promise._bind(cmd, from, resolve, reject, getStore());
|
|
959
|
+
// Postpone run to allow promise configuration.
|
|
960
|
+
setImmediate(() => promise.isHalted || promise.run());
|
|
961
|
+
return promise;
|
|
962
|
+
}, {
|
|
963
|
+
// this will get and set from:
|
|
964
|
+
// defaults OR storage (@see within());
|
|
965
|
+
set(_, key, value) {
|
|
966
|
+
const target = key in Function.prototype ? _ : getStore();
|
|
967
|
+
Reflect.set(target, key, value);
|
|
968
|
+
return true;
|
|
969
|
+
},
|
|
970
|
+
get(_, key) {
|
|
971
|
+
const target = key in Function.prototype ? _ : getStore();
|
|
972
|
+
return Reflect.get(target, key);
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Create a async context in an sync block
|
|
978
|
+
* @param {function} callback - async function
|
|
979
|
+
* @example
|
|
980
|
+
* const p = within(async () => {
|
|
981
|
+
* const res = await Promise.all([
|
|
982
|
+
* SH`sleep 1; echo 1`,
|
|
983
|
+
* SH`sleep 2; echo 2`,
|
|
984
|
+
* sleep(2),
|
|
985
|
+
* SH`sleep 3; echo 3`
|
|
986
|
+
* ]);
|
|
987
|
+
*/
|
|
988
|
+
const within = (callback) => {
|
|
989
|
+
// @ts-ignore
|
|
990
|
+
return storage.run({ ...getStore() }, callback);
|
|
991
|
+
};
|
|
992
|
+
/**
|
|
993
|
+
* This function reads the standard input (stdin) for the current process.
|
|
994
|
+
* It is used to get piped content into a script.
|
|
995
|
+
* @example
|
|
996
|
+
* const content = await stdin();
|
|
997
|
+
*/
|
|
998
|
+
const stdin = async () => {
|
|
999
|
+
let buf = '';
|
|
1000
|
+
process.stdin.setEncoding('utf8');
|
|
1001
|
+
for await (const chunk of process.stdin) {
|
|
1002
|
+
buf += chunk;
|
|
1003
|
+
}
|
|
1004
|
+
return buf;
|
|
1005
|
+
};
|
|
1006
|
+
/**
|
|
1007
|
+
* This function retries a command a specified number of times.
|
|
1008
|
+
* @example
|
|
1009
|
+
* // Retry a command 3 times
|
|
1010
|
+
* const p = await retry(3, () => SH`curl -s https://flipwrsi`);
|
|
1011
|
+
*
|
|
1012
|
+
* // Retry a command 3 times with an interval of 1 second between each try
|
|
1013
|
+
* const p = await retry(3, '1s', () => SH`curl -s https://flipwrsi`);
|
|
1014
|
+
*
|
|
1015
|
+
* // Retry a command 3 times with irregular intervals using exponential backoff
|
|
1016
|
+
* const p = await retry(3, expBackoff(), () => SH`curl -s https://flipwrsi`);
|
|
1017
|
+
*/
|
|
1018
|
+
const retry = async (count, a, b) => {
|
|
1019
|
+
const total = count;
|
|
1020
|
+
let callback;
|
|
1021
|
+
let delayStatic = 0;
|
|
1022
|
+
let delayGen;
|
|
1023
|
+
// @ts-ignore
|
|
1024
|
+
const verbose = SH.verbose;
|
|
1025
|
+
if (typeof a == 'function') {
|
|
1026
|
+
callback = a;
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
if (typeof a == 'object') {
|
|
1030
|
+
delayGen = a;
|
|
1031
|
+
}
|
|
1032
|
+
else {
|
|
1033
|
+
delayStatic = parseDuration(a);
|
|
1034
|
+
}
|
|
1035
|
+
assert(b);
|
|
1036
|
+
callback = b;
|
|
1037
|
+
}
|
|
1038
|
+
let lastErr;
|
|
1039
|
+
let attempt = 0;
|
|
1040
|
+
while (count-- > 0) {
|
|
1041
|
+
attempt++;
|
|
1042
|
+
try {
|
|
1043
|
+
return await callback();
|
|
1044
|
+
}
|
|
1045
|
+
catch (err) {
|
|
1046
|
+
let delay = 0;
|
|
1047
|
+
if (delayStatic > 0)
|
|
1048
|
+
delay = delayStatic;
|
|
1049
|
+
if (delayGen) delay = delayGen.next().value;
|
|
1050
|
+
log({
|
|
1051
|
+
verbose,
|
|
1052
|
+
kind: 'retry',
|
|
1053
|
+
error: ' FAIL ' +
|
|
1054
|
+
` Attempt: ${attempt}${total == Infinity ? '' : `/${total}`}` +
|
|
1055
|
+
(delay > 0 ? `; next in ${delay}ms` : ''),
|
|
1056
|
+
});
|
|
1057
|
+
lastErr = err;
|
|
1058
|
+
if (count == 0)
|
|
1059
|
+
break;
|
|
1060
|
+
if (delay)
|
|
1061
|
+
await sleep(delay);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
throw lastErr;
|
|
1065
|
+
};
|
|
1066
|
+
/**
|
|
1067
|
+
* This function pauses or "sleeps" code execution for a specified duration.
|
|
1068
|
+
* @param {string|number} duration - The duration to pause execution for, e.g., '100ms' or '3s'.
|
|
1069
|
+
*
|
|
1070
|
+
* @example
|
|
1071
|
+
*
|
|
1072
|
+
* const res = await Promise.all([
|
|
1073
|
+
* SH`sleep 2; echo 2`, // Sleep for 2 seconds
|
|
1074
|
+
* sleep(2), // Sleep for 2 seconds
|
|
1075
|
+
* SH`sleep 3; echo 3` // Sleep for 3 seconds
|
|
1076
|
+
* ]);
|
|
1077
|
+
*/
|
|
1078
|
+
const sleep = (duration) => {
|
|
1079
|
+
return new Promise((resolve) => {
|
|
1080
|
+
setTimeout(resolve, parseDuration(duration));
|
|
1081
|
+
});
|
|
1082
|
+
};
|
|
1083
|
+
/**
|
|
1084
|
+
* Change working directory
|
|
1085
|
+
* @param {string} dir
|
|
1086
|
+
*/
|
|
1087
|
+
const cd = (dir) => {
|
|
1088
|
+
// @ts-ignore
|
|
1089
|
+
const verbose = SH.verbose;
|
|
1090
|
+
log({ kind: 'cd', dir, verbose });
|
|
1091
|
+
process.chdir(dir);
|
|
1092
|
+
};
|
|
1093
|
+
function* expBackoff(max = '60s', rand = '100ms') {
|
|
1094
|
+
const maxMs = parseDuration(max);
|
|
1095
|
+
const randMs = parseDuration(rand);
|
|
1096
|
+
let n = 1;
|
|
1097
|
+
while (true) {
|
|
1098
|
+
const ms = Math.floor(Math.random() * randMs);
|
|
1099
|
+
yield Math.min(2 ** n++, maxMs) + ms;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
export { SH, cd, expBackoff, retry, sleep, stdin, within };
|