1import * as _ from 'lodash-es' 2import { spawn } from 'child_process' 8// When running under ptnode, write full output to its log file even when isQuiet 9let ptnodeLogStream: fs.WriteStream | null = null 10const getPtnodeLogStream = () => { 11 const logPath = process.env.PTNODE_LOG_PATH 12 if (!logPath) return null 13 if (!ptnodeLogStream) { 14 ptnodeLogStream = fs.createWriteStream(logPath, {flags: 'a'}) 16 return ptnodeLogStream 19export const liveSpawn = async ({cmd, cwd, isQuiet, quiet, onDataFnc, noOutCmd, printCmd, timeoutAfterSec, signal}: { 20 cmd: string, cwd?: string, isQuiet?: boolean, quiet?: boolean 21 onDataFnc?: (data: string) => string | undefined 24 timeoutAfterSec?: number 28 // isQuiet:true may truncate stdout for some cmds (eg kubectl logs). Use eptKubeCli/livePtySpawn instead. 29 if (!signal && timeoutAfterSec) { 30 signal = AbortSignal.timeout(timeoutAfterSec * 1000) 34 const displayCmd = (printCmd || cmd).replaceAll(`${ptDir}/`, '') 36 console.log(displayCmd) 39 const ptnodeLog = isQuiet ? getPtnodeLogStream() : null 40 if (ptnodeLog && !noOutCmd) { 41 ptnodeLog.write(`$ ${displayCmd}\n`) 44 // detached: true creates a new process group so we can kill all child processes together 45 // IMPORTANT: cleanup relies on process.kill(-pid) working - if this fails, zombie processes accumulate 46 const spawnObj = spawn(cmd, {shell: true, detached: true, cwd}) 48 signal?.addEventListener('abort', (...args) => { 54 // Kill entire process group (negative pid) to ensure all child processes are killed 55 // EPERM/ESRCH = process already exited or group doesn't exist - expected during cleanup races 56 try { process.kill(-spawnObj.pid!, 'SIGKILL') } catch (e) { 57 const err = e as NodeJS.ErrnoException 58 if (err.code !== 'EPERM' && err.code !== 'ESRCH') 59 console.error('liveSpawn kill failed:', {pid: spawnObj.pid, cmd: cmd.slice(0, 80), error: e}) 66 const mkOnData = (stdoutErrS: 'stdout' | 'stderr') => { 67 const spawnIo = spawnObj[stdoutErrS] 68 spawnIo.setEncoding('utf8') 69 const procIo = process[stdoutErrS] 70 spawnIo.on('data', (data) => { 72 const ret = onDataFnc(data) 73 if (ret !== undefined) { 78 if (stdoutErrS === 'stdout') stdout += data 82 procIo.write(data.toString()) 85 ptnodeLog.write(data.toString()) 92 return await new Promise<{exitCode: number | null, stdout: string, stderr: string, isSuccess: boolean}>((resolv) => { 93 spawnObj.on('exit', (exitCode) => { 95 const isSuccess = exitCode === 0 96 resolv({exitCode, stdout, stderr, isSuccess}) 99 signal?.addEventListener('abort', () => { 101 resolv({exitCode: 123, stdout, stderr, isSuccess: false})