@universal-packages/sub-process
v2.3.0
Published
Process encapsulation for different exec technics
Readme
Sub Process
Sub process encapsulation for different exec techniques.
Install
npm install @universal-packages/sub-processUsage
SubProcess class
The SubProcess class extends BaseRunner to provide a unified API for executing system processes with different engines. It handles process lifecycle, stream capture, and provides event-driven monitoring.
import { SubProcess } from '@universal-packages/sub-process'
const subProcess = new SubProcess({
command: 'echo "Hello World"',
args: ['--verbose'],
env: { NODE_ENV: 'production' },
captureStreams: true
})
await subProcess.run()
console.log(subProcess.stdout) // "Hello World"
console.log(subProcess.exitCode) // 0Constructor constructor
new SubProcess(options: SubProcessOptions)SubProcessOptions
Extends BaseRunnerOptions with the following additional options:
commandstringrequired Command to run. Can include arguments as part of the command string.argsstring[](optional) Additional arguments to pass to the command. These will be appended to any arguments already present in the command string.captureStreamsboolean(default:false) Whether to capture stdout and stderr streams. When enabled, the output will be available through thestdoutandstderrproperties.engineEngineInterface | 'spawn' | 'exec' | 'fork' | 'test'(default:'spawn') Instance of the engine to be used to execute the process or a string identifying the engine adapter.'spawn': Uses Node.js child_process.spawn (default)'exec': Uses Node.js child_process.exec'fork': Uses Node.js child_process.fork'test': Uses a test engine for unit testing
engineOptionsRecord<string, any>(optional) Options to pass to the engine if resolved as adapter.envRecord<string, string>(optional) Environment variables to pass to the process. These will be merged with the current process environment.inputstring | Buffer | string[] | Buffer[] | Readable(optional) Input to pass to the process stdin automatically during the process lifecycle. When provided, all input is made available immediately when the process starts. For manual input control during execution, omit this option and usepushInput()andcloseInput()methods instead. Useful when a process requires user input like yes/no questions or configuration input.workingDirectorystring(optional) Working directory to run the process in. Defaults to the current working directory.processIndexnumber(optional) Process index to be used for the process. This is useful when you want to identify the process in an orchestration.
Instance Methods
In addition to BaseRunner methods, SubProcess provides:
kill(signal?: NodeJS.Signals | number) async
Kills the process if it is running. Optionally specify a signal to send to the process.
// Kill with default signal
await subProcess.kill()
// Kill with specific signal
await subProcess.kill('SIGTERM')
await subProcess.kill(9) // SIGKILLpushInput(input: string | Buffer | string[] | Buffer[])
Sends input chunks to the running process stdin. This method is used when you want to provide input manually during the process execution rather than providing all input upfront through options.
const subProcess = new SubProcess({
command: 'node -e "process.stdin.on(\'data\', d => console.log(d.toString()))"',
captureStreams: true
})
await subProcess.run()
// Send input chunks manually
subProcess.pushInput('First chunk\n')
subProcess.pushInput('Second chunk\n')
subProcess.closeInput() // Signal end of inputcloseInput()
Closes the stdin stream to signal that no more input will be provided. This should be called after all input chunks have been sent via pushInput().
Instance Properties
In addition to BaseRunner properties, SubProcess provides:
stdout string
String containing the stdout output of the process. Only available when captureStreams option is enabled.
const subProcess = new SubProcess({
command: 'echo "Hello"',
captureStreams: true
})
await subProcess.run()
console.log(subProcess.stdout) // "Hello\n"stderr string
String containing the stderr output of the process. Only available when captureStreams option is enabled.
exitCode number
Exit code of the process. 0 indicates success, non-zero values indicate errors.
signal string | number
Signal that killed the process, if applicable.
processId number
Process ID of the running or completed process.
processIndex number
Process index of the running or completed process.
Events
SubProcess extends BaseRunner events with additional process-specific events:
stdout: Emitted when the process writes to stdout
subProcess.on('stdout', (event) => {
console.log('Output:', event.payload.data)
})stderr: Emitted when the process writes to stderr
subProcess.on('stderr', (event) => {
console.log('Error output:', event.payload.data)
})Engine System
SubProcess supports different execution engines for various use cases:
Creating a Custom Engine
import { EngineInterface } from '@universal-packages/sub-process'
import MyEngine from './MyEngine'
const subProcess = new SubProcess({ engine: new MyEngine() })Engine Process Implementation
You need to implement an engine process representation by extending the EngineProcess class to provide a way to control your custom process.
import { EngineProcess } from '@universal-packages/sub-process'
export default class MyEngineProcess extends EngineProcess {
killObject(signal?: NodeJS.Signals | number): void {
this.object.sendKillSignal(signal)
}
}Engine Implementation
The run method of the engine will be called with the command, args, input, env, and working directory to execute the process and return an EngineProcess instance.
import { EngineInterface } from '@universal-packages/sub-process'
export default class MyEngine implements EngineInterface {
constructor(options?: any) {
// Options passed through the adapter sub-system
}
async prepare(): Promise<void> {
// Initialize any connections or resources using options
}
async release(): Promise<void> {
// Release any resources or close any connections
}
async run(command: string, args: string[], input: Readable, env: Record<string, string>, workingDirectory?: string): Promise<EngineProcess> {
const myExecutableObject = myExecutionMethod.exec(command, args, input, env, workingDirectory)
const engineProcess = new MyEngineProcess(myExecutableObject.processId, myExecutableObject)
// Set up event handlers for the process
myExecutableObject.on('data', (data) => engineProcess.emit('stdout', data))
myExecutableObject.on('error', (data) => engineProcess.emit('stderr', data))
myExecutableObject.on('exit', (code) => {
if (code === 0) {
engineProcess.emit('success')
} else {
engineProcess.emit('failure', code)
}
})
return engineProcess
}
}EngineInterface
If you are using TypeScript, implement the EngineInterface in your class to ensure the correct implementation.
import { EngineInterface } from '@universal-packages/sub-process'
export default class MyEngine implements EngineInterface {
prepare?(): Promise<void> {
// Optional preparation logic
}
release?(): Promise<void> {
// Optional cleanup logic
}
run(command: string, args: string[], input: Readable, env: Record<string, string>, workingDirectory?: string): EngineProcess | Promise<EngineProcess> {
// Required implementation
}
}Usage Examples
Basic Command Execution
import { SubProcess } from '@universal-packages/sub-process'
const subProcess = new SubProcess({
command: 'ls -la',
captureStreams: true
})
await subProcess.run()
console.log(subProcess.stdout)Environment Variables
const subProcess = new SubProcess({
command: 'node -e "console.log(process.env.MY_VAR)"',
env: { MY_VAR: 'Hello World' },
captureStreams: true
})
await subProcess.run()
console.log(subProcess.stdout) // "Hello World"Process Input
SubProcess supports two methods for providing input to processes:
Automatic Input (via options)
When you provide input through the input option, all input is automatically provided to the process during its lifecycle:
const subProcess = new SubProcess({
command: 'node -e "process.stdin.on(\'data\', d => console.log(d.toString()))"',
input: 'Hello from input',
captureStreams: true
})
await subProcess.run()
// All input is provided automatically when the process startsManual Input (via methods)
When you need to control input timing or provide input dynamically, omit the input option and use pushInput() and closeInput() methods:
const subProcess = new SubProcess({
command: "node -e \"let data = ''; process.stdin.on('data', d => { data += d; }); process.stdin.on('end', () => console.log('Received:', data));\"",
captureStreams: true
})
// Start the process
subProcess.run()
// Send input chunks manually during execution
setTimeout(() => subProcess.pushInput('First chunk\n'), 100)
setTimeout(() => subProcess.pushInput('Second chunk\n'), 200)
setTimeout(() => subProcess.closeInput(), 300) // Signal end of inputWorking Directory
const subProcess = new SubProcess({
command: 'pwd',
workingDirectory: '/tmp',
captureStreams: true
})
await subProcess.run()
console.log(subProcess.stdout) // "/tmp"Event Monitoring
const subProcess = new SubProcess({
command: 'ping -c 3 google.com',
captureStreams: true
})
subProcess.on('stdout', (event) => {
console.log('Real-time output:', event.payload.data)
})
subProcess.on('succeeded', () => {
console.log('Ping completed successfully')
})
await subProcess.run()Error Handling
const subProcess = new SubProcess({
command: 'exit 1',
captureStreams: true
})
try {
await subProcess.run()
} catch (error) {
console.log('Exit code:', subProcess.exitCode) // 1
console.log('Error message:', error.message)
}Different Engines
// Spawn engine (default)
const spawnProcess = new SubProcess({
command: 'echo "Hello"',
engine: 'spawn'
})
// Exec engine for shell commands
const execProcess = new SubProcess({
command: 'echo "Current directory: $(pwd)"',
engine: 'exec'
})
// Fork engine for Node.js scripts
const forkProcess = new SubProcess({
command: 'node',
args: ['-e', 'console.log("Fork engine")'],
engine: 'fork'
})Typescript
This library is developed in TypeScript and shipped fully typed.
Contributing
The development of this library happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving this library.
