@stacksjs/rpx
Version:
A modern and smart reverse proxy.
152 lines (130 loc) • 4.4 kB
TypeScript
import type { ChildProcess } from 'node:child_process';
import type { StartOptions } from './types';
export declare interface ManagedProcess {
command: string
cwd: string
process: ChildProcess | null
env?: Record<string, string>
}
export declare class ProcessManager {
private processes: Map<string, ManagedProcess> = new Map()
private isShuttingDown = false
async startProcess(id: string, options: StartOptions, verbose?: boolean): Promise<void> {
if (this.processes.has(id)) {
debugLog('start', `Process ${id} is already running`, verbose)
return
}
const [cmd, ...args] = options.command.split(' ')
const cwd = options.cwd || process.cwd()
debugLog('start', `Starting process ${id}:`, verbose)
debugLog('start', ` Command: ${cmd} ${args.join(' ')}`, verbose)
debugLog('start', ` Working directory: ${cwd}`, verbose)
debugLog('start', ` Environment variables: ${JSON.stringify(options.env)}`, verbose)
const childProcess = spawn(cmd, args, {
cwd,
env: { ...process.env, ...options.env },
shell: true,
stdio: 'inherit',
})
this.processes.set(id, {
command: options.command,
cwd,
process: childProcess,
env: options.env,
})
return new Promise((resolve, reject) => {
childProcess.on('error', (err) => {
if (!this.isShuttingDown) {
debugLog('start', `Process ${id} failed to start: ${err}`, verbose)
this.processes.delete(id)
reject(err)
process.emit('SIGINT')
}
})
childProcess.on('exit', (code) => {
if (!this.isShuttingDown && code !== null && code !== 0) {
debugLog('start', `Process ${id} exited with code ${code}`, verbose)
this.processes.delete(id)
reject(new Error(`Process ${id} exited with code ${code}`))
process.emit('SIGINT')
}
})
if (verbose) {
childProcess.stdout?.on('data', (data) => {
debugLog('process', `[${id}] ${data.toString().trim()}`, true)
})
childProcess.stderr?.on('data', (data) => {
debugLog('process', `[${id}] ERR: ${data.toString().trim()}`, true)
})
}
setTimeout(() => {
if (!this.isShuttingDown && childProcess.killed) {
this.processes.delete(id)
reject(new Error(`Process ${id} was killed during startup`))
}
else {
debugLog('start', `Process ${id} started successfully`, verbose)
resolve()
}
}, 1000)
})
}
async stopProcess(id: string, verbose?: boolean): Promise<void> {
const managed = this.processes.get(id)
if (!managed?.process) {
debugLog('start', `No process found for ${id}`, verbose)
return
}
debugLog('start', `Stopping process ${id}`, verbose)
return new Promise((resolve) => {
if (!managed.process) {
resolve()
return
}
managed.process.once('exit', () => {
this.processes.delete(id)
debugLog('start', `Process ${id} stopped`, verbose)
resolve()
})
try {
managed.process.kill('SIGTERM')
setTimeout(() => {
if (managed.process) {
debugLog('start', `Force killing process ${id}`, verbose)
try {
managed.process.kill('SIGKILL')
}
catch (err) {
}
}
}, 3000)
}
catch (err) {
debugLog('start', `Error stopping process ${id}: ${err}`, verbose)
this.processes.delete(id)
resolve()
}
})
}
async stopAll(verbose?: boolean): Promise<void> {
if (this.isShuttingDown) {
debugLog('start', 'Already shutting down, skipping duplicate stopAll call', verbose)
return
}
this.isShuttingDown = true
debugLog('start', 'Stopping all processes', verbose)
const promises = Array.from(this.processes.keys()).map(id =>
this.stopProcess(id, verbose).catch((err) => {
log.error(`Failed to stop process ${id}:`, err)
}),
)
await Promise.allSettled(promises)
this.processes.clear()
this.isShuttingDown = false
}
isRunning(id: string): boolean {
const managed = this.processes.get(id)
return !!managed?.process && !managed.process.killed
}
}
export declare const processManager: ProcessManager;