@j-o-r/sh 0.0.1
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.txt +202 -0
- package/README.md +133 -0
- package/package.json +48 -0
- package/src/ProcessOutput.js +113 -0
- package/src/ProcessPromise.js +323 -0
- package/src/sh.js +261 -0
- package/src/utils.js +381 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// Copyright 2021 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
//
|
|
16
|
+
// Original Source: zx
|
|
17
|
+
// Link to Original Source: https://github.com/google/zx
|
|
18
|
+
// Reason for Using This Code:
|
|
19
|
+
// The core functionality of this code is highly beneficial. However, certain parts of the original code
|
|
20
|
+
// were overwriting the global namespace with core libraries and variables. This was causing conflicts with
|
|
21
|
+
// other packages (for instance, fetch) and introducing unexpected elements into my code base.
|
|
22
|
+
// Changes Made:
|
|
23
|
+
// - The code has been or is being reformatted to comply with ES2020 standards.
|
|
24
|
+
// - Some methods were added and existing ones were modified to enhance usability.
|
|
25
|
+
// - The namespace has been changed from '$' to 'SH'.
|
|
26
|
+
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
27
|
+
|
|
28
|
+
import { spawn } from 'node:child_process';
|
|
29
|
+
import assert from 'node:assert';
|
|
30
|
+
import { log, errnoMessage, exitCodeInfo, noop, parseDuration, psTree } from './utils.js';
|
|
31
|
+
import ProcessOutput from './ProcessOutput.js';
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Function} resolver
|
|
34
|
+
* @param {ProcessOutput} value
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Function} rejecter
|
|
38
|
+
* @param {ProcessOutput} value
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {Function} PromiseConstruct
|
|
42
|
+
* @param {resolver} resolve
|
|
43
|
+
* @param {rejecter} reject
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
class ProcessPromise extends Promise {
|
|
47
|
+
#command = '';
|
|
48
|
+
#from = '';
|
|
49
|
+
/** @type {resolver} */
|
|
50
|
+
#resolve = () => { };
|
|
51
|
+
/** @type {rejecter} */
|
|
52
|
+
#reject = () => { };
|
|
53
|
+
#snapshot = {};
|
|
54
|
+
#stdio = ['inherit', 'pipe', 'pipe'];
|
|
55
|
+
#nothrow = false;
|
|
56
|
+
#quiet = false;
|
|
57
|
+
#resolved = false;
|
|
58
|
+
#halted = false;
|
|
59
|
+
#piped = false;
|
|
60
|
+
#prerun = noop;
|
|
61
|
+
#postrun = noop;
|
|
62
|
+
/**
|
|
63
|
+
* @param {PromiseConstruct} p - A function that takes two arguments, resolve and reject.
|
|
64
|
+
*/
|
|
65
|
+
constructor(p) {
|
|
66
|
+
super(p)
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Set the environment
|
|
70
|
+
* and the
|
|
71
|
+
* @param {string} cmd - Command to execute
|
|
72
|
+
* @param {string} from - Position in the codfe where this is triggred from
|
|
73
|
+
* @param {function} resolve - Promise resolve method
|
|
74
|
+
* @param {function} reject - Reject method
|
|
75
|
+
* @param {object} options - Settings (options default)
|
|
76
|
+
*/
|
|
77
|
+
_bind(cmd, from, resolve, reject, options) {
|
|
78
|
+
this.#command = cmd;
|
|
79
|
+
this.#from = from;
|
|
80
|
+
this.#resolve = resolve;
|
|
81
|
+
this.#reject = reject;
|
|
82
|
+
this.#snapshot = { ...options };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Run the promise
|
|
86
|
+
*/
|
|
87
|
+
run() {
|
|
88
|
+
const ENV = this.#snapshot;
|
|
89
|
+
if (this.child) return this; // The _run() can be called from a few places.
|
|
90
|
+
this.#prerun(); // In case $1.pipe($2), the $2 returned, and on $2._run() invoke $1._run().
|
|
91
|
+
log({
|
|
92
|
+
kind: 'cmd',
|
|
93
|
+
cmd: this.#command,
|
|
94
|
+
verbose: ENV.verbose && !this.#quiet,
|
|
95
|
+
});
|
|
96
|
+
const cwd = ENV['processCwd'];
|
|
97
|
+
this.child = spawn(ENV.prefix + this.#command, {
|
|
98
|
+
cwd,
|
|
99
|
+
// cwd: $.cwd ?? $[processCwd],
|
|
100
|
+
shell: typeof ENV.shell === 'string' ? ENV.shell : true,
|
|
101
|
+
stdio: this.#stdio,
|
|
102
|
+
windowsHide: true,
|
|
103
|
+
env: ENV.env,
|
|
104
|
+
});
|
|
105
|
+
this.child.on('close', (code, signal) => {
|
|
106
|
+
let message = `exit code: ${code}`;
|
|
107
|
+
if (code != 0 || signal != null) {
|
|
108
|
+
message = `${stderr || '\n'} at ${this.#from}`;
|
|
109
|
+
message += `\n exit code: ${code}${exitCodeInfo(code) ? ' (' + exitCodeInfo(code) + ')' : ''}`;
|
|
110
|
+
if (signal != null) {
|
|
111
|
+
message += `\n signal: ${signal}`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
let output = new ProcessOutput(code, signal, stdout, stderr, combined, message);
|
|
115
|
+
if (code === 0 || this.#nothrow) {
|
|
116
|
+
this.#resolve(output);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.#reject(output);
|
|
120
|
+
}
|
|
121
|
+
this.#resolved = true;
|
|
122
|
+
});
|
|
123
|
+
this.child.on('error', (err) => {
|
|
124
|
+
const message = `${err.message}\n` +
|
|
125
|
+
` errno: ${err.errno} (${errnoMessage(err.errno)})\n` +
|
|
126
|
+
` code: ${err.code}\n` +
|
|
127
|
+
` at ${this.#from}`;
|
|
128
|
+
this.#reject(new ProcessOutput(null, null, stdout, stderr, combined, message));
|
|
129
|
+
this.#resolved = true;
|
|
130
|
+
});
|
|
131
|
+
let stdout = '', stderr = '', combined = '';
|
|
132
|
+
/** @param {Blob} data */
|
|
133
|
+
const onStdout = (data) => {
|
|
134
|
+
log({ kind: 'stdout', data, verbose: ENV.verbose && !this.#quiet });
|
|
135
|
+
stdout += data;
|
|
136
|
+
combined += data;
|
|
137
|
+
};
|
|
138
|
+
/** @param {Blob} data */
|
|
139
|
+
const onStderr = (data) => {
|
|
140
|
+
log({ kind: 'stderr', data, verbose: ENV.verbose && !this.#quiet });
|
|
141
|
+
stderr += data;
|
|
142
|
+
combined += data;
|
|
143
|
+
};
|
|
144
|
+
if (!this.#piped)
|
|
145
|
+
this.child.stdout?.on('data', onStdout); // If process is piped, don't collect or print output.
|
|
146
|
+
this.child.stderr?.on('data', onStderr); // Stderr should be printed regardless of piping.
|
|
147
|
+
this.#postrun(); // In case $1.pipe($2), after both subprocesses are running, we can pipe $1.stdout to $2.stdin.
|
|
148
|
+
if (this._timeout && this._timeoutSignal) {
|
|
149
|
+
const t = setTimeout(() => this.kill(this._timeoutSignal), this._timeout);
|
|
150
|
+
this.finally(() => clearTimeout(t)).catch(noop);
|
|
151
|
+
}
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* stdin child stream
|
|
156
|
+
* @retruns {Writeable}
|
|
157
|
+
*/
|
|
158
|
+
get stdin() {
|
|
159
|
+
this.stdio('pipe');
|
|
160
|
+
this.run();
|
|
161
|
+
assert(this.child);
|
|
162
|
+
if (this.child.stdin == null)
|
|
163
|
+
throw new Error('The stdin of subprocess is null.');
|
|
164
|
+
return this.child.stdin;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* stdout child stream
|
|
168
|
+
* @retruns {Readable}
|
|
169
|
+
*/
|
|
170
|
+
get stdout() {
|
|
171
|
+
this.run();
|
|
172
|
+
assert(this.child);
|
|
173
|
+
if (this.child.stdout == null)
|
|
174
|
+
throw new Error('The stdout of subprocess is null.');
|
|
175
|
+
return this.child.stdout;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* stderr child stream
|
|
179
|
+
* @retruns {Readable}
|
|
180
|
+
*/
|
|
181
|
+
get stderr() {
|
|
182
|
+
this.run();
|
|
183
|
+
assert(this.child);
|
|
184
|
+
if (this.child.stderr == null)
|
|
185
|
+
throw new Error('The stderr of subprocess is null.');
|
|
186
|
+
return this.child.stderr;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* process exit code
|
|
190
|
+
* @returns {Promise<number>}
|
|
191
|
+
*/
|
|
192
|
+
get exitCode() {
|
|
193
|
+
return this.then((p) => p.exitCode, (p) => p.exitCode);
|
|
194
|
+
}
|
|
195
|
+
then(onfulfilled, onrejected) {
|
|
196
|
+
if (this.isHalted && !this.child) {
|
|
197
|
+
throw new Error('The process is halted!');
|
|
198
|
+
}
|
|
199
|
+
return super.then(onfulfilled, onrejected);
|
|
200
|
+
}
|
|
201
|
+
catch(onrejected) {
|
|
202
|
+
return super.catch(onrejected);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Pipe the output to the input to the next Promise
|
|
206
|
+
* @example
|
|
207
|
+
* const res = await SH`ls -FLa`.pipe(SH`grep package.json`);
|
|
208
|
+
*/
|
|
209
|
+
pipe(dest) {
|
|
210
|
+
if (typeof dest == 'string')
|
|
211
|
+
throw new Error('The pipe() method does not take strings. Forgot SH?');
|
|
212
|
+
if (this.#resolved) {
|
|
213
|
+
if (dest instanceof ProcessPromise)
|
|
214
|
+
dest.stdin.end(); // In case of piped stdin, we may want to close stdin of dest as well.
|
|
215
|
+
throw new Error("The pipe() method shouldn't be called after promise is already resolved!");
|
|
216
|
+
}
|
|
217
|
+
this.#piped = true;
|
|
218
|
+
if (dest instanceof ProcessPromise) {
|
|
219
|
+
dest.stdio('pipe');
|
|
220
|
+
dest._prerun = this.run.bind(this);
|
|
221
|
+
dest._postrun = () => {
|
|
222
|
+
if (!dest.child)
|
|
223
|
+
throw new Error('Access to stdin of pipe destination without creation a subprocess.');
|
|
224
|
+
this.stdout.pipe(dest.stdin);
|
|
225
|
+
};
|
|
226
|
+
return dest;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
this._postrun = () => this.stdout.pipe(dest);
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Send a KILL signal to the child process
|
|
235
|
+
*/
|
|
236
|
+
async kill(signal = 'SIGTERM') {
|
|
237
|
+
if (!this.child)
|
|
238
|
+
throw new Error('Trying to kill a process without creating one.');
|
|
239
|
+
if (!this.child.pid)
|
|
240
|
+
throw new Error('The process pid is undefined.');
|
|
241
|
+
let children = await psTree(this.child.pid);
|
|
242
|
+
for (const p of children) {
|
|
243
|
+
try {
|
|
244
|
+
process.kill(+p.PID, signal);
|
|
245
|
+
}
|
|
246
|
+
catch (e) { }
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
process.kill(this.child.pid, signal);
|
|
250
|
+
}
|
|
251
|
+
catch (e) { }
|
|
252
|
+
}
|
|
253
|
+
stdio(stdin, stdout = 'pipe', stderr = 'pipe') {
|
|
254
|
+
this.#stdio = [stdin, stdout, stderr];
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Do not throw
|
|
259
|
+
*/
|
|
260
|
+
nothrow() {
|
|
261
|
+
this.#nothrow = true;
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* supress log output
|
|
266
|
+
* SH.verbose = false; does the same
|
|
267
|
+
*/
|
|
268
|
+
quiet() {
|
|
269
|
+
this.#quiet = true;
|
|
270
|
+
return this;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Show log output in the console
|
|
274
|
+
*/
|
|
275
|
+
verbose() {
|
|
276
|
+
this._quiet = false;
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Set a timeout to kill a process
|
|
281
|
+
*
|
|
282
|
+
* @param {string} d - 10s, 1000ms
|
|
283
|
+
* @param {string} [signal] - default "SIGTERM" Signal to send to kill the proces
|
|
284
|
+
*/
|
|
285
|
+
timeout(d, signal = 'SIGTERM') {
|
|
286
|
+
this._timeout = parseDuration(d);
|
|
287
|
+
this._timeoutSignal = signal;
|
|
288
|
+
return this;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* stop execution for the next step
|
|
292
|
+
*/
|
|
293
|
+
halt() {
|
|
294
|
+
this.#halted = true;
|
|
295
|
+
return this;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* @private
|
|
299
|
+
* Set a prerun action, internal use only
|
|
300
|
+
* @param {function} f
|
|
301
|
+
*/
|
|
302
|
+
set _prerun(f) {
|
|
303
|
+
// @ts-ignore
|
|
304
|
+
this.#prerun = f;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* @private
|
|
308
|
+
* Set a postrun action, internal use only
|
|
309
|
+
* @param {function} f
|
|
310
|
+
*/
|
|
311
|
+
set _postrun(f) {
|
|
312
|
+
// @ts-ignore
|
|
313
|
+
this.#postrun = f;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Is this promise halted?
|
|
317
|
+
* @returns {boolean}
|
|
318
|
+
*/
|
|
319
|
+
get isHalted() {
|
|
320
|
+
return this.#halted;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
export default ProcessPromise;
|
package/src/sh.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Copyright 2021 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
//
|
|
16
|
+
// Original Source: zx
|
|
17
|
+
// Link to Original Source: https://github.com/google/zx
|
|
18
|
+
// Reason for Using This Code:
|
|
19
|
+
// The core functionality of this code is highly beneficial. However, certain parts of the original code
|
|
20
|
+
// were overwriting the global namespace with core libraries and variables. This was causing conflicts with
|
|
21
|
+
// other packages (for instance, fetch) and introducing unexpected elements into my code base.
|
|
22
|
+
// Changes Made:
|
|
23
|
+
// - The code has been or is being reformatted to comply with ES2020 standards.
|
|
24
|
+
// - Some methods were added and existing ones were modified to enhance usability.
|
|
25
|
+
// - The namespace has been changed from '$' to 'SH'.
|
|
26
|
+
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
27
|
+
|
|
28
|
+
import assert from 'node:assert';
|
|
29
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
30
|
+
import which from 'which';
|
|
31
|
+
import { log, parseDuration, quote, quotePowerShell, } from './utils.js';
|
|
32
|
+
import ProcessPromise from './ProcessPromise.js';
|
|
33
|
+
// const processCwd = Symbol('processCwd');
|
|
34
|
+
const storage = new AsyncLocalStorage();
|
|
35
|
+
|
|
36
|
+
const defaults = {
|
|
37
|
+
processCwd: '',
|
|
38
|
+
verbose: false,
|
|
39
|
+
env: {},
|
|
40
|
+
shell: '',
|
|
41
|
+
prefix: '',
|
|
42
|
+
};
|
|
43
|
+
if (process.platform == 'win32') {
|
|
44
|
+
defaults.shell = which.sync('powershell.exe');
|
|
45
|
+
} else {
|
|
46
|
+
defaults.shell = which.sync('bash');
|
|
47
|
+
defaults.prefix = 'set -euo pipefail;';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Escape CLI arguments
|
|
51
|
+
* @retruns {string}
|
|
52
|
+
*/
|
|
53
|
+
const sanitizeArg = (arg) => {
|
|
54
|
+
const s = `${arg}`;
|
|
55
|
+
if (process.platform == 'win32') {
|
|
56
|
+
return quotePowerShell(s)
|
|
57
|
+
}
|
|
58
|
+
return quote(s);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const getStore = () => {
|
|
62
|
+
return storage.getStore() || defaults;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates a new ProcessPromise object that represents a command to be executed.
|
|
67
|
+
*
|
|
68
|
+
* @typedef {Function} Shell
|
|
69
|
+
* @type {function}
|
|
70
|
+
* @param {Array} pieces - An array of string literals from a template literal.
|
|
71
|
+
* @param {...*} args - The values to be interpolated into the string literals.
|
|
72
|
+
* @returns {ProcessPromise} A ProcessPromise object that represents the command.
|
|
73
|
+
* @throws {Error} Throws an error if any of the string literals in `pieces` is undefined.
|
|
74
|
+
*
|
|
75
|
+
* @property {boolean} verbose
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* const command = await SH`echo 'Hello, world!'`;
|
|
79
|
+
*/
|
|
80
|
+
/** @type {Shell} */
|
|
81
|
+
const SH = new Proxy(function(pieces, ...args) {
|
|
82
|
+
const from = new Error().stack.split(/^\s*at\s/m)[2].trim();
|
|
83
|
+
if (pieces.some((p) => p == undefined)) {
|
|
84
|
+
throw new Error(`Malformed command at ${from}`);
|
|
85
|
+
}
|
|
86
|
+
let cmd = pieces[0], i = 0;
|
|
87
|
+
while (i < args.length) {
|
|
88
|
+
let s;
|
|
89
|
+
if (Array.isArray(args[i])) {
|
|
90
|
+
s = args[i].map((x) => sanitizeArg(x)).join(' ');
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
s = sanitizeArg(args[i]);
|
|
94
|
+
}
|
|
95
|
+
cmd += s + pieces[++i];
|
|
96
|
+
}
|
|
97
|
+
let resolve, reject;
|
|
98
|
+
|
|
99
|
+
const promise = new ProcessPromise((...args) => ([resolve, reject] = args));
|
|
100
|
+
// re-add the environment
|
|
101
|
+
defaults.processCwd = process.cwd();
|
|
102
|
+
defaults.env = process.env,
|
|
103
|
+
promise._bind(cmd, from, resolve, reject, getStore());
|
|
104
|
+
// Postpone run to allow promise configuration.
|
|
105
|
+
setImmediate(() => promise.isHalted || promise.run());
|
|
106
|
+
return promise;
|
|
107
|
+
}, {
|
|
108
|
+
// this will get and set from:
|
|
109
|
+
// defaults OR storage (@see within());
|
|
110
|
+
set(_, key, value) {
|
|
111
|
+
const target = key in Function.prototype ? _ : getStore();
|
|
112
|
+
Reflect.set(target, key, value);
|
|
113
|
+
return true;
|
|
114
|
+
},
|
|
115
|
+
get(_, key) {
|
|
116
|
+
const target = key in Function.prototype ? _ : getStore();
|
|
117
|
+
return Reflect.get(target, key);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create a async context in an sync block
|
|
123
|
+
* @param {function} callback - async function
|
|
124
|
+
* @example
|
|
125
|
+
* const p = within(async () => {
|
|
126
|
+
* const res = await Promise.all([
|
|
127
|
+
* SH`sleep 1; echo 1`,
|
|
128
|
+
* SH`sleep 2; echo 2`,
|
|
129
|
+
* sleep(2),
|
|
130
|
+
* SH`sleep 3; echo 3`
|
|
131
|
+
* ]);
|
|
132
|
+
*/
|
|
133
|
+
const within = (callback) => {
|
|
134
|
+
// @ts-ignore
|
|
135
|
+
return storage.run({ ...getStore() }, callback);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* This function reads the standard input (stdin) for the current process.
|
|
139
|
+
* It is used to get piped content into a script.
|
|
140
|
+
* @example
|
|
141
|
+
* const content = await stdin();
|
|
142
|
+
*/
|
|
143
|
+
const stdin = async () => {
|
|
144
|
+
let buf = '';
|
|
145
|
+
process.stdin.setEncoding('utf8');
|
|
146
|
+
for await (const chunk of process.stdin) {
|
|
147
|
+
buf += chunk;
|
|
148
|
+
}
|
|
149
|
+
return buf;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* This function retries a command a specified number of times.
|
|
153
|
+
* @example
|
|
154
|
+
* // Retry a command 3 times
|
|
155
|
+
* const p = await retry(3, () => SH`curl -s https://flipwrsi`);
|
|
156
|
+
*
|
|
157
|
+
* // Retry a command 3 times with an interval of 1 second between each try
|
|
158
|
+
* const p = await retry(3, '1s', () => SH`curl -s https://flipwrsi`);
|
|
159
|
+
*
|
|
160
|
+
* // Retry a command 3 times with irregular intervals using exponential backoff
|
|
161
|
+
* const p = await retry(3, expBackoff(), () => SH`curl -s https://flipwrsi`);
|
|
162
|
+
*/
|
|
163
|
+
const retry = async (count, a, b) => {
|
|
164
|
+
const total = count;
|
|
165
|
+
let callback;
|
|
166
|
+
let delayStatic = 0;
|
|
167
|
+
let delayGen;
|
|
168
|
+
// @ts-ignore
|
|
169
|
+
const verbose = SH.verbose;
|
|
170
|
+
if (typeof a == 'function') {
|
|
171
|
+
callback = a;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
if (typeof a == 'object') {
|
|
175
|
+
delayGen = a;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
delayStatic = parseDuration(a);
|
|
179
|
+
}
|
|
180
|
+
// console.log(assert(b));
|
|
181
|
+
assert(b);
|
|
182
|
+
callback = b;
|
|
183
|
+
}
|
|
184
|
+
let lastErr;
|
|
185
|
+
let attempt = 0;
|
|
186
|
+
while (count-- > 0) {
|
|
187
|
+
attempt++;
|
|
188
|
+
try {
|
|
189
|
+
return await callback();
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
let delay = 0;
|
|
193
|
+
if (delayStatic > 0)
|
|
194
|
+
delay = delayStatic;
|
|
195
|
+
if (delayGen) delay = delayGen.next().value;
|
|
196
|
+
log({
|
|
197
|
+
verbose,
|
|
198
|
+
kind: 'retry',
|
|
199
|
+
error: ' FAIL ' +
|
|
200
|
+
` Attempt: ${attempt}${total == Infinity ? '' : `/${total}`}` +
|
|
201
|
+
(delay > 0 ? `; next in ${delay}ms` : ''),
|
|
202
|
+
});
|
|
203
|
+
lastErr = err;
|
|
204
|
+
if (count == 0)
|
|
205
|
+
break;
|
|
206
|
+
if (delay)
|
|
207
|
+
await sleep(delay);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
throw lastErr;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* This function pauses or "sleeps" code execution for a specified duration.
|
|
214
|
+
* @param {string} duration - The duration to pause execution for, e.g., '100ms' or '3s'.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
*
|
|
218
|
+
* const res = await Promise.all([
|
|
219
|
+
* SH`sleep 2; echo 2`, // Sleep for 2 seconds
|
|
220
|
+
* sleep(2), // Sleep for 2 seconds
|
|
221
|
+
* SH`sleep 3; echo 3` // Sleep for 3 seconds
|
|
222
|
+
* ]);
|
|
223
|
+
*/
|
|
224
|
+
const sleep = (duration) => {
|
|
225
|
+
return new Promise((resolve) => {
|
|
226
|
+
setTimeout(resolve, parseDuration(duration));
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Change working directory
|
|
231
|
+
* @param {string} dir
|
|
232
|
+
*/
|
|
233
|
+
const cd = (dir) => {
|
|
234
|
+
// @ts-ignore
|
|
235
|
+
const verbose = SH.verbose;
|
|
236
|
+
log({ kind: 'cd', dir, verbose });
|
|
237
|
+
process.chdir(dir);
|
|
238
|
+
}
|
|
239
|
+
function* expBackoff(max = '60s', rand = '100ms') {
|
|
240
|
+
const maxMs = parseDuration(max);
|
|
241
|
+
const randMs = parseDuration(rand);
|
|
242
|
+
let n = 1;
|
|
243
|
+
while (true) {
|
|
244
|
+
const ms = Math.floor(Math.random() * randMs);
|
|
245
|
+
yield Math.min(2 ** n++, maxMs) + ms;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// const syncCwd = () => {
|
|
249
|
+
// if (SH['processCwd'] != process.cwd())
|
|
250
|
+
// process.chdir(SH['processCwd']);
|
|
251
|
+
// }
|
|
252
|
+
export {
|
|
253
|
+
/** @type {Shell} */
|
|
254
|
+
SH,
|
|
255
|
+
cd,
|
|
256
|
+
sleep,
|
|
257
|
+
retry,
|
|
258
|
+
stdin,
|
|
259
|
+
within,
|
|
260
|
+
expBackoff,
|
|
261
|
+
}
|