UNPKG

@stacksjs/rpx

Version:

A modern and smart reverse proxy.

152 lines (130 loc) 4.4 kB
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;