Spaces:
Build error
Build error
| import { spawn } from "node:child_process"; | |
| import { EventEmitter } from "node:events"; | |
| export class ProcessManager { | |
| constructor(maxLogLines = 2000) { | |
| this.maxLogLines = maxLogLines; | |
| this.runners = new Map(); | |
| this.events = new EventEmitter(); | |
| } | |
| get(projectId) { | |
| return this.runners.get(projectId) || null; | |
| } | |
| getLogs(projectId) { | |
| const runner = this.get(projectId); | |
| return runner ? runner.logs : []; | |
| } | |
| isRunning(projectId) { | |
| const runner = this.get(projectId); | |
| return Boolean(runner && runner.process && !runner.process.killed); | |
| } | |
| appendLog(projectId, chunk, stream = "stdout") { | |
| const runner = this.get(projectId); | |
| if (!runner) { | |
| return; | |
| } | |
| const text = String(chunk); | |
| const lines = text.split(/\r?\n/).filter((line, index, array) => line.length > 0 || index < array.length - 1); | |
| for (const line of lines) { | |
| runner.logs.push({ | |
| timestamp: new Date().toISOString(), | |
| stream, | |
| line | |
| }); | |
| } | |
| if (runner.logs.length > this.maxLogLines) { | |
| runner.logs.splice(0, runner.logs.length - this.maxLogLines); | |
| } | |
| this.events.emit(`log:${projectId}`, { | |
| type: "log", | |
| data: text, | |
| stream, | |
| timestamp: new Date().toISOString() | |
| }); | |
| } | |
| run(projectId, options) { | |
| this.stop(projectId); | |
| const child = spawn(options.command, { | |
| cwd: options.cwd, | |
| shell: true, | |
| env: { | |
| ...process.env, | |
| FORCE_COLOR: "1" | |
| } | |
| }); | |
| const runner = { | |
| process: child, | |
| logs: [], | |
| command: options.command, | |
| cwd: options.cwd, | |
| startedAt: new Date().toISOString(), | |
| status: "running" | |
| }; | |
| this.runners.set(projectId, runner); | |
| this.events.emit(`status:${projectId}`, { | |
| type: "status", | |
| status: "running", | |
| command: options.command, | |
| cwd: options.cwd, | |
| timestamp: runner.startedAt | |
| }); | |
| child.stdout.on("data", (chunk) => { | |
| this.appendLog(projectId, chunk, "stdout"); | |
| }); | |
| child.stderr.on("data", (chunk) => { | |
| this.appendLog(projectId, chunk, "stderr"); | |
| }); | |
| child.on("close", (code, signal) => { | |
| const current = this.get(projectId); | |
| if (!current) { | |
| return; | |
| } | |
| current.status = "stopped"; | |
| current.exitedAt = new Date().toISOString(); | |
| current.exitCode = code; | |
| current.signal = signal; | |
| this.events.emit(`status:${projectId}`, { | |
| type: "status", | |
| status: "stopped", | |
| exitCode: code, | |
| signal, | |
| timestamp: current.exitedAt | |
| }); | |
| }); | |
| child.on("error", (error) => { | |
| this.appendLog(projectId, error.message, "stderr"); | |
| }); | |
| return runner; | |
| } | |
| stop(projectId) { | |
| const runner = this.get(projectId); | |
| if (!runner || !runner.process) { | |
| return false; | |
| } | |
| if (!runner.process.killed) { | |
| runner.process.kill("SIGTERM"); | |
| } | |
| runner.status = "stopping"; | |
| this.events.emit(`status:${projectId}`, { | |
| type: "status", | |
| status: "stopping", | |
| timestamp: new Date().toISOString() | |
| }); | |
| return true; | |
| } | |
| } | |