mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 17:12:27 +03:00
feat(cliproxyapi): add version manager service, API routes, CLI Tools UI & Docker
Part 3 of CLIProxyAPI integration (#902). Depends on PR1 + PR2. - releaseChecker.ts: GitHub releases API with 5min cache - binaryManager.ts: download, SHA256 verify, install, symlink, rollback - processManager.ts: spawn, graceful stop (SIGTERM→SIGKILL with interval cleanup), restart - healthMonitor.ts: periodic /v1/models health checks - index.ts: orchestrator (install/start/stop/restart/checkUpdates/pin/rollback) - 6 API routes under /api/version-manager/ with Zod validation - CliproxyapiToolCard: CLI Tools page card - Docker: cliproxyapi profile with sidecar service + healthcheck - 78 unit tests across 5 test files
This commit is contained in:
155
src/lib/versionManager/processManager.ts
Normal file
155
src/lib/versionManager/processManager.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user