(
command: string,
opts?: ProcessOptions,
)
| 224 | } |
| 225 | |
| 226 | private async spawnProcess( |
| 227 | command: string, |
| 228 | opts?: ProcessOptions, |
| 229 | ): Promise<SpawnHandle> { |
| 230 | const exec = await this.container.exec({ |
| 231 | Cmd: ['sh', '-c', command], |
| 232 | AttachStdin: true, |
| 233 | AttachStdout: true, |
| 234 | AttachStderr: true, |
| 235 | WorkingDir: opts?.cwd ? this.abs(opts.cwd) : this.workdir, |
| 236 | Env: this.envArray(opts?.env), |
| 237 | }) |
| 238 | const stream = await exec.start({ hijack: true, stdin: true }) |
| 239 | const outPT = new PassThrough() |
| 240 | const errPT = new PassThrough() |
| 241 | this.docker.modem.demuxStream(stream, outPT, errPT) |
| 242 | /* |
| 243 | * Close the demuxed output streams when the hijacked exec stream finishes, |
| 244 | * so consumers iterating `stdout`/`stderr` (for await ... of) terminate. |
| 245 | * A normal EOF emits `end`, but a destroyed stream (e.g. from kill()) emits |
| 246 | * only `close` and never `end` — so we must also end the PassThroughs on |
| 247 | * `close`/`error`, or the consumer hangs forever waiting for the iterator to |
| 248 | * complete. `end()` is idempotent, so handling multiple events is safe. |
| 249 | */ |
| 250 | const endOutputs = (): void => { |
| 251 | outPT.end() |
| 252 | errPT.end() |
| 253 | } |
| 254 | stream.on('end', endOutputs) |
| 255 | stream.on('close', endOutputs) |
| 256 | stream.on('error', endOutputs) |
| 257 | if (opts?.signal) { |
| 258 | opts.signal.addEventListener('abort', () => stream.destroy(), { |
| 259 | once: true, |
| 260 | }) |
| 261 | } |
| 262 | |
| 263 | return { |
| 264 | pid: -1, // docker exec does not surface a host-visible pid |
| 265 | stdout: decodeStream(outPT), |
| 266 | stderr: decodeStream(errPT), |
| 267 | stdin: { |
| 268 | write: (data) => |
| 269 | new Promise<void>((resolve, reject) => { |
| 270 | stream.write(data, (err) => (err ? reject(err) : resolve())) |
| 271 | }), |
| 272 | end: () => { |
| 273 | stream.end() |
| 274 | return Promise.resolve() |
| 275 | }, |
| 276 | }, |
| 277 | wait: async () => { |
| 278 | await new Promise<void>((resolve) => { |
| 279 | if (outPT.readableEnded) { |
| 280 | resolve() |
| 281 | return |
| 282 | } |
| 283 | // Resolve on whichever of these fires first. A clean exit emits |
no test coverage detected