* Run a command with the given arguments, transparently forwarding stdin/stdout/stderr. * Also collects combined stdout+stderr output for error pattern detection. * * The child process is spawned with `cwd` set to `process.env.GH_AW_ENGINE_CWD` when * available, falling back to `process.env.GITH
({ command, args, attempt, log, logArgs, env })
| 66 | * @returns {Promise<{exitCode: number, output: string, hasOutput: boolean, durationMs: number}>} |
| 67 | */ |
| 68 | function runProcess({ command, args, attempt, log, logArgs, env }) { |
| 69 | return new Promise(resolve => { |
| 70 | const startTime = Date.now(); |
| 71 | // Guard against the promise being settled more than once. On some systems Node |
| 72 | // emits 'close' after 'error' (or vice-versa); only the first terminal event should |
| 73 | // log and resolve so callers receive a deterministic result. |
| 74 | let settled = false; |
| 75 | /** @param {{exitCode: number, output: string, hasOutput: boolean, durationMs: number}} result */ |
| 76 | function settle(result) { |
| 77 | if (settled) return; |
| 78 | settled = true; |
| 79 | resolve(result); |
| 80 | } |
| 81 | |
| 82 | const argsForLog = logArgs ?? args; |
| 83 | log(`attempt ${attempt + 1}: spawning: ${command} ${argsForLog.join(" ").substring(0, 200)}`); |
| 84 | |
| 85 | const child = spawn(command, args, { |
| 86 | stdio: ["inherit", "pipe", "pipe"], |
| 87 | env: env ?? process.env, |
| 88 | cwd: process.env.GH_AW_ENGINE_CWD || process.env.GITHUB_WORKSPACE || undefined, |
| 89 | }); |
| 90 | |
| 91 | log(`attempt ${attempt + 1}: process started (pid=${child.pid ?? "unknown"})`); |
| 92 | |
| 93 | let collectedOutput = ""; |
| 94 | let hasOutput = false; |
| 95 | let stdoutBytes = 0; |
| 96 | let stderrBytes = 0; |
| 97 | |
| 98 | child.stdout.on( |
| 99 | "data", |
| 100 | /** @param {Buffer} data */ data => { |
| 101 | hasOutput = true; |
| 102 | stdoutBytes += data.length; |
| 103 | collectedOutput += data.toString(); |
| 104 | process.stdout.write(data); |
| 105 | } |
| 106 | ); |
| 107 | |
| 108 | child.stderr.on( |
| 109 | "data", |
| 110 | /** @param {Buffer} data */ data => { |
| 111 | hasOutput = true; |
| 112 | stderrBytes += data.length; |
| 113 | collectedOutput += data.toString(); |
| 114 | process.stderr.write(data); |
| 115 | } |
| 116 | ); |
| 117 | |
| 118 | child.on("exit", (code, signal) => { |
| 119 | log(`attempt ${attempt + 1}: process exit event` + ` exitCode=${code ?? 1}` + (signal ? ` signal=${signal}` : "")); |
| 120 | }); |
| 121 | |
| 122 | // Resolve on 'close', not 'exit', to ensure stdio streams are fully drained. |
| 123 | child.on("close", (code, signal) => { |
| 124 | const durationMs = Date.now() - startTime; |
| 125 | const exitCode = code ?? 1; |
no test coverage detected