Add real-time verbose command streaming via stderr
This commit is contained in:
parent
10e98e2dc5
commit
18887b9419
75
src/run.ts
75
src/run.ts
|
|
@ -29,12 +29,23 @@ type RunOptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const SENTINEL_PREFIX = "__SHOUT_SENTINEL_"
|
const SENTINEL_PREFIX = "__SHOUT_SENTINEL_"
|
||||||
|
const VERBOSE_MARKER = "__SHOUT_CMD_"
|
||||||
|
|
||||||
function buildScript(commands: Command[], sentinel: string): string {
|
function buildScript(commands: Command[], sentinel: string, verbose: boolean): string {
|
||||||
const lines: string[] = ["exec 2>&1"]
|
const lines: string[] = []
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
// Save original stderr to fd 3 before merging stderr into stdout
|
||||||
|
lines.push("exec 3>&2 2>&1")
|
||||||
|
} else {
|
||||||
|
lines.push("exec 2>&1")
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < commands.length; i++) {
|
for (let i = 0; i < commands.length; i++) {
|
||||||
const cmd = commands[i]!
|
const cmd = commands[i]!
|
||||||
|
if (verbose) {
|
||||||
|
lines.push(`printf '${VERBOSE_MARKER}${i}\\n' >&3`)
|
||||||
|
}
|
||||||
lines.push(cmd.command)
|
lines.push(cmd.command)
|
||||||
// Sentinel: printf to avoid echo interpretation issues
|
// Sentinel: printf to avoid echo interpretation issues
|
||||||
// Format: __SHOUT_SENTINEL_<exitcode>_<index>__
|
// Format: __SHOUT_SENTINEL_<exitcode>_<index>__
|
||||||
|
|
@ -111,6 +122,36 @@ function escapeRegex(s: string): string {
|
||||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function streamVerboseMarkers(
|
||||||
|
stderr: ReadableStream<Uint8Array>,
|
||||||
|
commands: Command[],
|
||||||
|
onCommand: (cmd: Command) => void,
|
||||||
|
): void {
|
||||||
|
const reader = stderr.getReader()
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
let buffer = ""
|
||||||
|
|
||||||
|
const pump = (): void => {
|
||||||
|
reader.read().then(({ done, value }) => {
|
||||||
|
if (done) return
|
||||||
|
if (value) buffer += decoder.decode(value, { stream: true })
|
||||||
|
let nlIdx: number
|
||||||
|
while ((nlIdx = buffer.indexOf("\n")) !== -1) {
|
||||||
|
const line = buffer.slice(0, nlIdx)
|
||||||
|
buffer = buffer.slice(nlIdx + 1)
|
||||||
|
if (line.startsWith(VERBOSE_MARKER)) {
|
||||||
|
const i = parseInt(line.slice(VERBOSE_MARKER.length), 10)
|
||||||
|
if (i >= 0 && i < commands.length) {
|
||||||
|
onCommand(commands[i]!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pump()
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
pump()
|
||||||
|
}
|
||||||
|
|
||||||
export async function runFile(
|
export async function runFile(
|
||||||
file: ShoutFile,
|
file: ShoutFile,
|
||||||
options: RunOptions,
|
options: RunOptions,
|
||||||
|
|
@ -122,7 +163,8 @@ export async function runFile(
|
||||||
}
|
}
|
||||||
|
|
||||||
const sentinel = SENTINEL_PREFIX
|
const sentinel = SENTINEL_PREFIX
|
||||||
const script = buildScript(file.commands, sentinel)
|
const verbose = options.verbose && !!options.onCommand
|
||||||
|
const script = buildScript(file.commands, sentinel, verbose)
|
||||||
|
|
||||||
const env: Record<string, string> = options.cleanEnv
|
const env: Record<string, string> = options.cleanEnv
|
||||||
? {}
|
? {}
|
||||||
|
|
@ -155,11 +197,19 @@ export async function runFile(
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (verbose) {
|
||||||
|
// Stream stderr for verbose command markers before writing script
|
||||||
|
streamVerboseMarkers(proc.stderr, file.commands, options.onCommand!)
|
||||||
|
}
|
||||||
|
|
||||||
proc.stdin.write(script)
|
proc.stdin.write(script)
|
||||||
proc.stdin.end()
|
proc.stdin.end()
|
||||||
|
|
||||||
const stdout = await readWithTimeout(proc.stdout, options.timeout * file.commands.length)
|
const totalTimeout = options.timeout * file.commands.length
|
||||||
const stderr = await readWithTimeout(proc.stderr, 1000).catch(() => "")
|
const stdout = await readWithTimeout(proc.stdout, totalTimeout)
|
||||||
|
if (!verbose) {
|
||||||
|
await readWithTimeout(proc.stderr, 1000).catch(() => "")
|
||||||
|
}
|
||||||
|
|
||||||
await proc.exited
|
await proc.exited
|
||||||
|
|
||||||
|
|
@ -169,16 +219,11 @@ export async function runFile(
|
||||||
file.commands.length,
|
file.commands.length,
|
||||||
)
|
)
|
||||||
|
|
||||||
const results: CommandResult[] = file.commands.map((cmd, i) => {
|
const results: CommandResult[] = file.commands.map((cmd, i) => ({
|
||||||
if (options.verbose && options.onCommand) {
|
command: cmd,
|
||||||
options.onCommand(cmd)
|
actual: outputs[i] ?? [],
|
||||||
}
|
exitCode: exitCodes[i] ?? 1,
|
||||||
return {
|
}))
|
||||||
command: cmd,
|
|
||||||
actual: outputs[i] ?? [],
|
|
||||||
exitCode: exitCodes[i] ?? 1,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return { file, results, tmpDir }
|
return { file, results, tmpDir }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user