packages/@aws-cdk-testing/cli-integ/lib/process.ts (87 lines of code) (raw):
import * as child from 'child_process';
import type { Readable, Writable } from 'stream';
import * as pty from 'node-pty';
/**
* IProcess provides an interface to work with a subprocess.
*/
export interface IProcess {
/**
* Register a callback to be invoked when a chunk is written to stdout.
*/
onStdout(callback: (chunk: Buffer) => void): void;
/**
* Register a callback to be invoked when a chunk is written to stderr.
*/
onStderr(callback: (chunk: Buffer) => void): void;
/**
* Register a callback to be invoked when the process exists.
*/
onExit(callback: (exitCode: number) => void): void;
/**
* Register a callback to be invoked if the process failed to start.
*/
onError(callback: (error: Error) => void): void;
/**
* Write the process stdin stream.
*/
writeStdin(data: string): void;
/**
* Singal that no more data will be written to stdin. In non tty process you must
* call this method to make sure the process exits.
*
* @param delay - optional delay in milliseconds before the signal is sent.
*
*/
endStdin(delay?: number): void;
}
export class Process {
/**
* Spawn a process with a TTY attached.
*/
public static spawnTTY(command: string, args: string[], options: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions = {}): IProcess {
const process = pty.spawn(command, args, {
name: 'xterm-color',
...options,
});
return new PtyProcess(process);
}
/**
* Spawn a process without a forcing a TTY.
*/
public static spawn(command: string, args: string[], options: child.SpawnOptions = {}): IProcess {
const process = child.spawn(command, args, {
shell: true,
stdio: ['ignore', 'pipe', 'pipe'],
...options,
});
return new NonPtyProcess(process);
}
}
class PtyProcess implements IProcess {
public constructor(private readonly process: pty.IPty) {
}
public endStdin(_?: number): void {
// not needed because all streams are the same in tty.
}
public onError(_: (error: Error) => void): void {
// not needed because the pty.spawn will simply fail in this case.
}
public onStdout(callback: (chunk: Buffer) => void): void {
this.process.onData((e) => callback(Buffer.from(e)));
}
public onStderr(_callback: (chunk: Buffer) => void): void {
// https://github.com/microsoft/node-pty/issues/71
throw new Error('Cannot register callback for \'stderr\'. A tty does not have separate output and error channels');
}
public onExit(callback: (exitCode: number) => void): void {
this.process.onExit((e) => {
callback(e.exitCode);
});
}
public writeStdin(data: string): void {
// in a pty all streams are the same
this.process.write(data);
}
}
class NonPtyProcess implements IProcess {
public constructor(private readonly process: child.ChildProcess) {
}
public onError(callback: (error: Error) => void): void {
this.process.once('error', callback);
}
public onStdout(callback: (chunk: Buffer) => void): void {
this.assertDefined('stdout', this.process.stdout);
this.process.stdout.on('data', callback);
}
public onStderr(callback: (chunk: Buffer) => void): void {
this.assertDefined('stderr', this.process.stderr);
this.process.stderr.on('data', callback);
}
public onExit(callback: (exitCode: number) => void): void {
this.process.on('close', callback);
}
public writeStdin(content: string): void {
this.assertDefined('stdin', this.process.stdin);
this.process.stdin.write(content);
}
public endStdin(delay?: number): void {
if (this.process.stdin == null) {
throw new Error('No stdin defined for process');
}
if (delay) {
setTimeout(() => this.process.stdin!.end(), delay);
} else {
this.process.stdin!.end();
}
}
public assertDefined(name: 'stdin' | 'stdout' | 'stderr', stream?: Readable | Writable | undefined | null): asserts stream {
if (stream == null) {
throw new Error(`No ${name} defined for child process`);
}
}
}