Skip to content

Commit aeec7ae

Browse files
authored
refactor: separate core (google#733)
* chore: inject zurk * chore: handle timeouts via zurk * chore: override zurk quote * chore: use zurk response to build ProcessOutput * chore: linting * chore: use buildCmd from zurk * feat: provide preset api closes google#600 * docs: add some comments * build: move zurk to vendor bundle * chore: move zurk to dev deps * feat: process quiet as preset option * refactor: use zurk/spawn * chore: linting * build: update esbuild
1 parent be0d674 commit aeec7ae

File tree

7 files changed

+367
-431
lines changed

7 files changed

+367
-431
lines changed

package-lock.json

+215-341
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
},
5656
"optionalDependencies": {
5757
"@types/fs-extra": "^11.0.4",
58-
"@types/node": ">=20.11.19"
58+
"@types/node": ">=20.11.28"
5959
},
6060
"devDependencies": {
6161
"@stryker-mutator/core": "^6.4.2",
@@ -67,9 +67,9 @@
6767
"c8": "^7.13.0",
6868
"chalk": "^5.3.0",
6969
"dts-bundle-generator": "^9.3.1",
70-
"esbuild": "^0.20.1",
70+
"esbuild": "^0.20.2",
7171
"esbuild-node-externals": "^1.13.0",
72-
"esbuild-plugin-entry-chunks": "^0.1.8",
72+
"esbuild-plugin-entry-chunks": "^0.1.11",
7373
"fs-extra": "^11.2.0",
7474
"fx": "*",
7575
"globby": "^14.0.1",
@@ -82,7 +82,8 @@
8282
"typescript": "^5.0.4",
8383
"webpod": "^0",
8484
"which": "^3.0.0",
85-
"yaml": "^2.3.4"
85+
"yaml": "^2.3.4",
86+
"zurk": "^0.0.27"
8687
},
8788
"publishConfig": {
8889
"registry": "https://wombat-dressing-room.appspot.com"

scripts/build-dts.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const entry = {
3737
'@types/minimist',
3838
'@types/ps-tree',
3939
'@types/which',
40+
'zurk',
4041
], // args['external-inlines'],
4142
},
4243
output: {

src/core.ts

+137-84
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
// limitations under the License.
1414

1515
import assert from 'node:assert'
16-
import { ChildProcess, spawn, StdioNull, StdioPipe } from 'node:child_process'
16+
import { spawn, StdioNull, StdioPipe } from 'node:child_process'
1717
import { AsyncLocalStorage, createHook } from 'node:async_hooks'
1818
import { Readable, Writable } from 'node:stream'
1919
import { inspect } from 'node:util'
2020
import {
21+
exec,
22+
buildCmd,
2123
chalk,
2224
which,
2325
type ChalkInstance,
@@ -36,10 +38,10 @@ import {
3638
quotePowerShell,
3739
} from './util.js'
3840

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+
}
4345

4446
const processCwd = Symbol('processCwd')
4547

@@ -49,8 +51,10 @@ export interface Options {
4951
verbose: boolean
5052
env: NodeJS.ProcessEnv
5153
shell: string | boolean
54+
nothrow: boolean
5255
prefix: string
5356
quote: typeof quote
57+
quiet: boolean
5458
spawn: typeof spawn
5559
log: typeof log
5660
}
@@ -70,20 +74,22 @@ export const defaults: Options = {
7074
verbose: true,
7175
env: process.env,
7276
shell: true,
77+
nothrow: false,
78+
quiet: false,
7379
prefix: '',
7480
quote: () => {
7581
throw new Error('No quote function is defined: https://ï.at/no-quote-func')
7682
},
7783
spawn,
7884
log,
7985
}
80-
86+
const isWin = process.platform == 'win32'
8187
try {
8288
defaults.shell = which.sync('bash')
8389
defaults.prefix = 'set -euo pipefail;'
8490
defaults.quote = quote
8591
} catch (err) {
86-
if (process.platform == 'win32') {
92+
if (isWin) {
8793
try {
8894
defaults.shell = which.sync('powershell.exe')
8995
defaults.quote = quotePowerShell
@@ -97,25 +103,28 @@ function getStore() {
97103
return storage.getStore() || defaults
98104
}
99105

100-
export const $ = new Proxy<Shell & Options>(
106+
export const $: Shell & Options = new Proxy<Shell & Options>(
101107
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+
}
102116
const from = new Error().stack!.split(/^\s*at\s/m)[2].trim()
103117
if (pieces.some((p) => p == undefined)) {
104118
throw new Error(`Malformed command at ${from}`)
105119
}
106120
let resolve: Resolve, reject: Resolve
107121
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+
119128
promise._bind(cmd, from, resolve!, reject!, getStore())
120129
// Postpone run to allow promise configuration.
121130
setImmediate(() => promise.isHalted || promise.run())
@@ -145,20 +154,20 @@ type Resolve = (out: ProcessOutput) => void
145154
type IO = StdioPipe | StdioNull
146155

147156
export class ProcessPromise extends Promise<ProcessOutput> {
148-
child?: ChildProcess
149157
private _command = ''
150158
private _from = ''
151159
private _resolve: Resolve = noop
152160
private _reject: Resolve = noop
153161
private _snapshot = getStore()
154162
private _stdio: [IO, IO, IO] = ['inherit', 'pipe', 'pipe']
155-
private _nothrow = false
156-
private _quiet = false
163+
private _nothrow?: boolean
164+
private _quiet?: boolean
157165
private _timeout?: number
158-
private _timeoutSignal?: string
166+
private _timeoutSignal = 'SIGTERM'
159167
private _resolved = false
160168
private _halted = false
161169
private _piped = false
170+
private zurk: ReturnType<typeof exec> | null = null
162171
_prerun = noop
163172
_postrun = noop
164173

@@ -178,80 +187,89 @@ export class ProcessPromise extends Promise<ProcessOutput> {
178187

179188
run(): ProcessPromise {
180189
const $ = this._snapshot
190+
const self = this
181191
if (this.child) return this // The _run() can be called from a few places.
182192
this._prerun() // In case $1.pipe($2), the $2 returned, and on $2._run() invoke $1._run().
193+
183194
$.log({
184195
kind: 'cmd',
185196
cmd: this._command,
186-
verbose: $.verbose && !this._quiet,
197+
verbose: self.isVerbose(),
187198
})
188-
this.child = $.spawn($.prefix + this._command, {
199+
200+
this.zurk = exec({
201+
cmd: $.prefix + this._command,
189202
cwd: $.cwd ?? $[processCwd],
190203
shell: typeof $.shell === 'string' ? $.shell : true,
191-
stdio: this._stdio,
192-
windowsHide: true,
193204
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+
},
194262
})
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+
247264
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+
252266
return this
253267
}
254268

269+
get child() {
270+
return this.zurk?.child
271+
}
272+
255273
get stdin(): Writable {
256274
this.stdio('pipe')
257275
this.run()
@@ -340,14 +358,15 @@ export class ProcessPromise extends Promise<ProcessOutput> {
340358
if (!this.child)
341359
throw new Error('Trying to kill a process without creating one.')
342360
if (!this.child.pid) throw new Error('The process pid is undefined.')
361+
343362
let children = await psTree(this.child.pid)
344363
for (const p of children) {
345364
try {
346365
process.kill(+p.PID, signal)
347366
} catch (e) {}
348367
}
349368
try {
350-
process.kill(this.child.pid, signal)
369+
process.kill(-this.child.pid, signal)
351370
} catch (e) {}
352371
}
353372

@@ -366,6 +385,11 @@ export class ProcessPromise extends Promise<ProcessOutput> {
366385
return this
367386
}
368387

388+
isVerbose(): boolean {
389+
const { verbose, quiet } = this._snapshot
390+
return verbose && !(this._quiet ?? quiet)
391+
}
392+
369393
timeout(d: Duration, signal = 'SIGTERM'): ProcessPromise {
370394
this._timeout = parseDuration(d)
371395
this._timeoutSignal = signal
@@ -425,6 +449,35 @@ export class ProcessOutput extends Error {
425449
return this._signal
426450
}
427451

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+
428481
[inspect.custom]() {
429482
let stringify = (s: string, c: ChalkInstance) =>
430483
s.length === 0 ? "''" : c(inspect(s))

0 commit comments

Comments
 (0)