13
13
// limitations under the License.
14
14
15
15
import assert from 'node:assert'
16
- import { ChildProcess , spawn , StdioNull , StdioPipe } from 'node:child_process'
16
+ import { spawn , StdioNull , StdioPipe } from 'node:child_process'
17
17
import { AsyncLocalStorage , createHook } from 'node:async_hooks'
18
18
import { Readable , Writable } from 'node:stream'
19
19
import { inspect } from 'node:util'
20
20
import {
21
+ exec ,
22
+ buildCmd ,
21
23
chalk ,
22
24
which ,
23
25
type ChalkInstance ,
@@ -36,10 +38,10 @@ import {
36
38
quotePowerShell ,
37
39
} from './util.js'
38
40
39
- export type Shell = (
40
- pieces : TemplateStringsArray ,
41
- ... args : any [ ]
42
- ) => ProcessPromise
41
+ export interface Shell {
42
+ ( pieces : TemplateStringsArray , ... args : any [ ] ) : ProcessPromise
43
+ ( opts : Partial < Options > ) : Shell
44
+ }
43
45
44
46
const processCwd = Symbol ( 'processCwd' )
45
47
@@ -49,8 +51,10 @@ export interface Options {
49
51
verbose : boolean
50
52
env : NodeJS . ProcessEnv
51
53
shell : string | boolean
54
+ nothrow : boolean
52
55
prefix : string
53
56
quote : typeof quote
57
+ quiet : boolean
54
58
spawn : typeof spawn
55
59
log : typeof log
56
60
}
@@ -70,20 +74,22 @@ export const defaults: Options = {
70
74
verbose : true ,
71
75
env : process . env ,
72
76
shell : true ,
77
+ nothrow : false ,
78
+ quiet : false ,
73
79
prefix : '' ,
74
80
quote : ( ) => {
75
81
throw new Error ( 'No quote function is defined: https://ï.at/no-quote-func' )
76
82
} ,
77
83
spawn,
78
84
log,
79
85
}
80
-
86
+ const isWin = process . platform == 'win32'
81
87
try {
82
88
defaults . shell = which . sync ( 'bash' )
83
89
defaults . prefix = 'set -euo pipefail;'
84
90
defaults . quote = quote
85
91
} catch ( err ) {
86
- if ( process . platform == 'win32' ) {
92
+ if ( isWin ) {
87
93
try {
88
94
defaults . shell = which . sync ( 'powershell.exe' )
89
95
defaults . quote = quotePowerShell
@@ -97,25 +103,28 @@ function getStore() {
97
103
return storage . getStore ( ) || defaults
98
104
}
99
105
100
- export const $ = new Proxy < Shell & Options > (
106
+ export const $ : Shell & Options = new Proxy < Shell & Options > (
101
107
function ( pieces , ...args ) {
108
+ if ( ! Array . isArray ( pieces ) ) {
109
+ return function ( this : any , ...args : any ) {
110
+ const self = this
111
+ return within ( ( ) => {
112
+ return Object . assign ( $ , pieces ) . apply ( self , args )
113
+ } )
114
+ }
115
+ }
102
116
const from = new Error ( ) . stack ! . split ( / ^ \s * a t \s / m) [ 2 ] . trim ( )
103
117
if ( pieces . some ( ( p ) => p == undefined ) ) {
104
118
throw new Error ( `Malformed command at ${ from } ` )
105
119
}
106
120
let resolve : Resolve , reject : Resolve
107
121
const promise = new ProcessPromise ( ( ...args ) => ( [ resolve , reject ] = args ) )
108
- let cmd = pieces [ 0 ] ,
109
- i = 0
110
- while ( i < args . length ) {
111
- let s
112
- if ( Array . isArray ( args [ i ] ) ) {
113
- s = args [ i ] . map ( ( x : any ) => $ . quote ( substitute ( x ) ) ) . join ( ' ' )
114
- } else {
115
- s = $ . quote ( substitute ( args [ i ] ) )
116
- }
117
- cmd += s + pieces [ ++ i ]
118
- }
122
+ const cmd = buildCmd (
123
+ $ . quote ,
124
+ pieces as TemplateStringsArray ,
125
+ args
126
+ ) as string
127
+
119
128
promise . _bind ( cmd , from , resolve ! , reject ! , getStore ( ) )
120
129
// Postpone run to allow promise configuration.
121
130
setImmediate ( ( ) => promise . isHalted || promise . run ( ) )
@@ -145,20 +154,20 @@ type Resolve = (out: ProcessOutput) => void
145
154
type IO = StdioPipe | StdioNull
146
155
147
156
export class ProcessPromise extends Promise < ProcessOutput > {
148
- child ?: ChildProcess
149
157
private _command = ''
150
158
private _from = ''
151
159
private _resolve : Resolve = noop
152
160
private _reject : Resolve = noop
153
161
private _snapshot = getStore ( )
154
162
private _stdio : [ IO , IO , IO ] = [ 'inherit' , 'pipe' , 'pipe' ]
155
- private _nothrow = false
156
- private _quiet = false
163
+ private _nothrow ?: boolean
164
+ private _quiet ?: boolean
157
165
private _timeout ?: number
158
- private _timeoutSignal ?: string
166
+ private _timeoutSignal = 'SIGTERM'
159
167
private _resolved = false
160
168
private _halted = false
161
169
private _piped = false
170
+ private zurk : ReturnType < typeof exec > | null = null
162
171
_prerun = noop
163
172
_postrun = noop
164
173
@@ -178,80 +187,89 @@ export class ProcessPromise extends Promise<ProcessOutput> {
178
187
179
188
run ( ) : ProcessPromise {
180
189
const $ = this . _snapshot
190
+ const self = this
181
191
if ( this . child ) return this // The _run() can be called from a few places.
182
192
this . _prerun ( ) // In case $1.pipe($2), the $2 returned, and on $2._run() invoke $1._run().
193
+
183
194
$ . log ( {
184
195
kind : 'cmd' ,
185
196
cmd : this . _command ,
186
- verbose : $ . verbose && ! this . _quiet ,
197
+ verbose : self . isVerbose ( ) ,
187
198
} )
188
- this . child = $ . spawn ( $ . prefix + this . _command , {
199
+
200
+ this . zurk = exec ( {
201
+ cmd : $ . prefix + this . _command ,
189
202
cwd : $ . cwd ?? $ [ processCwd ] ,
190
203
shell : typeof $ . shell === 'string' ? $ . shell : true ,
191
- stdio : this . _stdio ,
192
- windowsHide : true ,
193
204
env : $ . env ,
205
+ spawn : $ . spawn ,
206
+ stdio : this . _stdio as any ,
207
+ sync : false ,
208
+ detached : ! isWin ,
209
+ run : ( cb ) => cb ( ) ,
210
+ on : {
211
+ start : ( ) => {
212
+ if ( self . _timeout ) {
213
+ const t = setTimeout (
214
+ ( ) => self . kill ( self . _timeoutSignal ) ,
215
+ self . _timeout
216
+ )
217
+ self . finally ( ( ) => clearTimeout ( t ) ) . catch ( noop )
218
+ }
219
+ } ,
220
+ stdout : ( data ) => {
221
+ // If process is piped, don't print output.
222
+ if ( self . _piped ) return
223
+ $ . log ( { kind : 'stdout' , data, verbose : self . isVerbose ( ) } )
224
+ } ,
225
+ stderr : ( data ) => {
226
+ // Stderr should be printed regardless of piping.
227
+ $ . log ( { kind : 'stderr' , data, verbose : self . isVerbose ( ) } )
228
+ } ,
229
+ end : ( { error, stdout, stderr, stdall, status, signal } ) => {
230
+ self . _resolved = true
231
+
232
+ if ( error ) {
233
+ const message = ProcessOutput . getErrorMessage ( error , self . _from )
234
+ // Should we enable this?
235
+ // (nothrow ? self._resolve : self._reject)(
236
+ self . _reject (
237
+ new ProcessOutput ( null , null , stdout , stderr , stdall , message )
238
+ )
239
+ } else {
240
+ const message = ProcessOutput . getExitMessage (
241
+ status ,
242
+ signal ,
243
+ stderr ,
244
+ self . _from
245
+ )
246
+ const output = new ProcessOutput (
247
+ status ,
248
+ signal ,
249
+ stdout ,
250
+ stderr ,
251
+ stdall ,
252
+ message
253
+ )
254
+ if ( status === 0 || ( self . _nothrow ?? $ . nothrow ) ) {
255
+ self . _resolve ( output )
256
+ } else {
257
+ self . _reject ( output )
258
+ }
259
+ }
260
+ } ,
261
+ } ,
194
262
} )
195
- this . child . on ( 'close' , ( code , signal ) => {
196
- let message = `exit code: ${ code } `
197
- if ( code != 0 || signal != null ) {
198
- message = `${ stderr || '\n' } at ${ this . _from } `
199
- message += `\n exit code: ${ code } ${
200
- exitCodeInfo ( code ) ? ' (' + exitCodeInfo ( code ) + ')' : ''
201
- } `
202
- if ( signal != null ) {
203
- message += `\n signal: ${ signal } `
204
- }
205
- }
206
- let output = new ProcessOutput (
207
- code ,
208
- signal ,
209
- stdout ,
210
- stderr ,
211
- combined ,
212
- message
213
- )
214
- if ( code === 0 || this . _nothrow ) {
215
- this . _resolve ( output )
216
- } else {
217
- this . _reject ( output )
218
- }
219
- this . _resolved = true
220
- } )
221
- this . child . on ( 'error' , ( err : NodeJS . ErrnoException ) => {
222
- const message =
223
- `${ err . message } \n` +
224
- ` errno: ${ err . errno } (${ errnoMessage ( err . errno ) } )\n` +
225
- ` code: ${ err . code } \n` +
226
- ` at ${ this . _from } `
227
- this . _reject (
228
- new ProcessOutput ( null , null , stdout , stderr , combined , message )
229
- )
230
- this . _resolved = true
231
- } )
232
- let stdout = '' ,
233
- stderr = '' ,
234
- combined = ''
235
- let onStdout = ( data : any ) => {
236
- $ . log ( { kind : 'stdout' , data, verbose : $ . verbose && ! this . _quiet } )
237
- stdout += data
238
- combined += data
239
- }
240
- let onStderr = ( data : any ) => {
241
- $ . log ( { kind : 'stderr' , data, verbose : $ . verbose && ! this . _quiet } )
242
- stderr += data
243
- combined += data
244
- }
245
- if ( ! this . _piped ) this . child . stdout ?. on ( 'data' , onStdout ) // If process is piped, don't collect or print output.
246
- this . child . stderr ?. on ( 'data' , onStderr ) // Stderr should be printed regardless of piping.
263
+
247
264
this . _postrun ( ) // In case $1.pipe($2), after both subprocesses are running, we can pipe $1.stdout to $2.stdin.
248
- if ( this . _timeout && this . _timeoutSignal ) {
249
- const t = setTimeout ( ( ) => this . kill ( this . _timeoutSignal ) , this . _timeout )
250
- this . finally ( ( ) => clearTimeout ( t ) ) . catch ( noop )
251
- }
265
+
252
266
return this
253
267
}
254
268
269
+ get child ( ) {
270
+ return this . zurk ?. child
271
+ }
272
+
255
273
get stdin ( ) : Writable {
256
274
this . stdio ( 'pipe' )
257
275
this . run ( )
@@ -340,14 +358,15 @@ export class ProcessPromise extends Promise<ProcessOutput> {
340
358
if ( ! this . child )
341
359
throw new Error ( 'Trying to kill a process without creating one.' )
342
360
if ( ! this . child . pid ) throw new Error ( 'The process pid is undefined.' )
361
+
343
362
let children = await psTree ( this . child . pid )
344
363
for ( const p of children ) {
345
364
try {
346
365
process . kill ( + p . PID , signal )
347
366
} catch ( e ) { }
348
367
}
349
368
try {
350
- process . kill ( this . child . pid , signal )
369
+ process . kill ( - this . child . pid , signal )
351
370
} catch ( e ) { }
352
371
}
353
372
@@ -366,6 +385,11 @@ export class ProcessPromise extends Promise<ProcessOutput> {
366
385
return this
367
386
}
368
387
388
+ isVerbose ( ) : boolean {
389
+ const { verbose, quiet } = this . _snapshot
390
+ return verbose && ! ( this . _quiet ?? quiet )
391
+ }
392
+
369
393
timeout ( d : Duration , signal = 'SIGTERM' ) : ProcessPromise {
370
394
this . _timeout = parseDuration ( d )
371
395
this . _timeoutSignal = signal
@@ -425,6 +449,35 @@ export class ProcessOutput extends Error {
425
449
return this . _signal
426
450
}
427
451
452
+ static getExitMessage (
453
+ code : number | null ,
454
+ signal : NodeJS . Signals | null ,
455
+ stderr : string ,
456
+ from : string
457
+ ) {
458
+ let message = `exit code: ${ code } `
459
+ if ( code != 0 || signal != null ) {
460
+ message = `${ stderr || '\n' } at ${ from } `
461
+ message += `\n exit code: ${ code } ${
462
+ exitCodeInfo ( code ) ? ' (' + exitCodeInfo ( code ) + ')' : ''
463
+ } `
464
+ if ( signal != null ) {
465
+ message += `\n signal: ${ signal } `
466
+ }
467
+ }
468
+
469
+ return message
470
+ }
471
+
472
+ static getErrorMessage ( err : NodeJS . ErrnoException , from : string ) {
473
+ return (
474
+ `${ err . message } \n` +
475
+ ` errno: ${ err . errno } (${ errnoMessage ( err . errno ) } )\n` +
476
+ ` code: ${ err . code } \n` +
477
+ ` at ${ from } `
478
+ )
479
+ }
480
+
428
481
[ inspect . custom ] ( ) {
429
482
let stringify = ( s : string , c : ChalkInstance ) =>
430
483
s . length === 0 ? "''" : c ( inspect ( s ) )
0 commit comments