| 203 | * using proc = execAsync("nohup bash -c ...", { shell: getBashPath() }); |
| 204 | */ |
| 205 | export function execAsync(command: string, options?: ExecAsyncOptions): DisposableExec { |
| 206 | // Child processes inherit process.env automatically, which includes |
| 207 | // the enriched PATH set by initShellEnv() at startup |
| 208 | const child = exec(command, { shell: options?.shell }); |
| 209 | const promise = new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { |
| 210 | let stdout = ""; |
| 211 | let stderr = ""; |
| 212 | let exitCode: number | null = null; |
| 213 | let exitSignal: string | null = null; |
| 214 | |
| 215 | child.stdout?.on("data", (data) => { |
| 216 | stdout += data; |
| 217 | }); |
| 218 | child.stderr?.on("data", (data) => { |
| 219 | stderr += data; |
| 220 | }); |
| 221 | |
| 222 | // Use 'close' event instead of 'exit' - close fires after all stdio streams are closed |
| 223 | // This ensures we've received all buffered output before resolving/rejecting |
| 224 | child.on("exit", (code, signal) => { |
| 225 | exitCode = code; |
| 226 | exitSignal = signal; |
| 227 | }); |
| 228 | |
| 229 | child.on("close", () => { |
| 230 | // Only resolve if process exited cleanly (code 0, no signal) |
| 231 | if (exitCode === 0 && exitSignal === null) { |
| 232 | resolve({ stdout, stderr }); |
| 233 | } else { |
| 234 | // Include stderr in error message for better debugging |
| 235 | const errorMsg = |
| 236 | stderr.trim() || |
| 237 | (exitSignal |
| 238 | ? `Command killed by signal ${exitSignal}` |
| 239 | : `Command failed with exit code ${exitCode ?? "unknown"}`); |
| 240 | const error = new Error(errorMsg) as Error & { |
| 241 | code: number | null; |
| 242 | signal: string | null; |
| 243 | stdout: string; |
| 244 | stderr: string; |
| 245 | }; |
| 246 | error.code = exitCode; |
| 247 | error.signal = exitSignal; |
| 248 | error.stdout = stdout; |
| 249 | error.stderr = stderr; |
| 250 | reject(error); |
| 251 | } |
| 252 | }); |
| 253 | |
| 254 | child.on("error", reject); |
| 255 | }); |
| 256 | |
| 257 | return new DisposableExec(promise, child); |
| 258 | } |
| 259 | |
| 260 | /** |
| 261 | * Options for execFileAsync. |