Spaces:
Paused
Paused
| import { spawn, type ChildProcess } from "child_process"; | |
| import fs from "fs/promises"; | |
| import fsSync from "fs"; | |
| import path from "path"; | |
| import os from "os"; | |
| import { setToolStatus, getVersionManagerTool } from "@/lib/db/versionManager"; | |
| const DEFAULT_PORT = 8317; | |
| const GRACEFUL_TIMEOUT_MS = 5000; | |
| function defaultConfigDir(): string { | |
| return process.env.CLIPROXYAPI_CONFIG_DIR || path.join(os.homedir(), ".cli-proxy-api"); | |
| } | |
| async function writeConfig( | |
| configDir: string, | |
| port: number, | |
| overrides?: Record<string, unknown> | |
| ): Promise<string> { | |
| await fs.mkdir(configDir, { recursive: true }); | |
| const configPath = path.join(configDir, "config.yaml"); | |
| const config = `port: ${port} | |
| host: 127.0.0.1 | |
| log_level: warn | |
| `; | |
| await fs.writeFile(configPath, config); | |
| return configPath; | |
| } | |
| export async function startProcess( | |
| binaryPath: string, | |
| port?: number, | |
| configDir?: string | |
| ): Promise<{ pid: number; port: number }> { | |
| const existing = await getVersionManagerTool("cliproxyapi"); | |
| if (existing?.pid) { | |
| const alive = isProcessRunning(existing.pid); | |
| if (alive) return { pid: existing.pid, port: existing.port }; | |
| } | |
| const actualPort = port || DEFAULT_PORT; | |
| const actualConfigDir = configDir || defaultConfigDir(); | |
| await writeConfig(actualConfigDir, actualPort); | |
| const child = spawn(binaryPath, ["-c", path.join(actualConfigDir, "config.yaml")], { | |
| detached: false, | |
| stdio: ["ignore", "pipe", "pipe"], | |
| env: { ...process.env }, | |
| }); | |
| child.stdout?.on("data", () => {}); | |
| child.stderr?.on("data", () => {}); | |
| child.on("error", async (err) => { | |
| await setToolStatus("cliproxyapi", "error", undefined, err.message); | |
| }); | |
| child.on("exit", async (code) => { | |
| if (code !== 0 && code !== null) { | |
| await setToolStatus("cliproxyapi", "stopped", undefined, `Process exited with code ${code}`); | |
| } | |
| }); | |
| const pid = child.pid; | |
| await setToolStatus("cliproxyapi", "running", pid); | |
| return { pid, port: actualPort }; | |
| } | |
| export function isProcessRunning(pid: number): boolean { | |
| try { | |
| process.kill(pid, 0); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| export function stopProcess(pid: number): Promise<void> { | |
| return new Promise((resolve) => { | |
| if (!isProcessRunning(pid)) { | |
| resolve(); | |
| return; | |
| } | |
| try { | |
| process.kill(pid, "SIGTERM"); | |
| } catch { | |
| resolve(); | |
| return; | |
| } | |
| const timer = setTimeout(() => { | |
| try { | |
| process.kill(pid, "SIGKILL"); | |
| } catch {} | |
| clearInterval(check); | |
| resolve(); | |
| }, GRACEFUL_TIMEOUT_MS); | |
| const check = setInterval(() => { | |
| if (!isProcessRunning(pid)) { | |
| clearTimeout(timer); | |
| clearInterval(check); | |
| resolve(); | |
| } | |
| }, 200); | |
| }); | |
| } | |
| export async function restartProcess( | |
| binaryPath: string, | |
| port?: number, | |
| configDir?: string, | |
| currentPid?: number | null | |
| ): Promise<{ pid: number; port: number }> { | |
| if (currentPid) { | |
| await stopProcess(currentPid); | |
| await new Promise((r) => setTimeout(r, 500)); | |
| } | |
| return startProcess(binaryPath, port, configDir); | |
| } | |
| export async function getProcessInfo(pid: number): Promise<{ | |
| pid: number; | |
| alive: boolean; | |
| memoryUsage?: number; | |
| }> { | |
| if (!isProcessRunning(pid)) { | |
| return { pid, alive: false }; | |
| } | |
| try { | |
| if (process.platform === "linux" || process.platform === "android") { | |
| const statusFile = `/proc/${pid}/status`; | |
| const content = await fs.readFile(statusFile, "utf-8"); | |
| const match = content.match(/VmRSS:\s+(\d+)\s+kB/); | |
| if (match) { | |
| return { pid, alive: true, memoryUsage: parseInt(match[1], 10) * 1024 }; | |
| } | |
| } else if (process.platform === "darwin") { | |
| const { execFile } = await import("child_process"); | |
| const { promisify } = await import("util"); | |
| const execFileAsync = promisify(execFile); | |
| const { stdout } = await execFileAsync("ps", ["-o", "rss=", "-p", String(pid)]); | |
| const rssKb = parseInt(stdout.trim(), 10); | |
| if (!isNaN(rssKb)) { | |
| return { pid, alive: true, memoryUsage: rssKb * 1024 }; | |
| } | |
| } | |
| return { pid, alive: true }; | |
| } catch { | |
| return { pid, alive: true }; | |
| } | |
| } | |