115 lines
3.1 KiB
TypeScript
115 lines
3.1 KiB
TypeScript
import { spawn, execSync, type ChildProcess } from "child_process";
|
|
import { BLOG_ROOT } from "./config";
|
|
|
|
class HugoManager {
|
|
private process: ChildProcess | null = null;
|
|
private ready = false;
|
|
private starting = false;
|
|
private healthTimer: NodeJS.Timeout | null = null;
|
|
|
|
async start(): Promise<{ ok: boolean; alreadyRunning?: boolean }> {
|
|
if (this.process && this.ready) {
|
|
return { ok: true, alreadyRunning: true };
|
|
}
|
|
if (this.starting) {
|
|
return { ok: false };
|
|
}
|
|
|
|
this.starting = true;
|
|
this.ready = false;
|
|
|
|
this.process = spawn("hugo", ["server", "--bind", "0.0.0.0", "--port", "1313", "--disableFastRender", "--noHTTPCache"], {
|
|
cwd: BLOG_ROOT,
|
|
shell: true,
|
|
stdio: "pipe",
|
|
windowsHide: true,
|
|
});
|
|
|
|
this.process.stdout?.on("data", (d: Buffer) => process.stdout.write(`[hugo] ${d}`));
|
|
this.process.stderr?.on("data", (d: Buffer) => process.stderr.write(`[hugo] ${d}`));
|
|
|
|
this.process.on("exit", (code) => {
|
|
console.log(`[hugo] exited ${code}`);
|
|
this.process = null;
|
|
this.ready = false;
|
|
this.starting = false;
|
|
});
|
|
|
|
// 等待 Hugo 就绪(最多 15 秒)
|
|
const ok = await this.waitForReady(15000);
|
|
this.starting = false;
|
|
if (ok) {
|
|
this.ready = true;
|
|
this.startHealthCheck();
|
|
console.log("[hugo] ready on 0.0.0.0:1313");
|
|
}
|
|
return { ok };
|
|
}
|
|
|
|
stop(): void {
|
|
this.stopHealthCheck();
|
|
if (!this.process) return;
|
|
try {
|
|
const pid = this.process.pid;
|
|
if (process.platform === "win32" && pid) {
|
|
execSync(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
|
|
} else {
|
|
this.process.kill();
|
|
}
|
|
} catch (_e) { /* nothing to kill */ }
|
|
this.process = null;
|
|
this.ready = false;
|
|
this.starting = false;
|
|
}
|
|
|
|
status(): { running: boolean; starting: boolean } {
|
|
return { running: !!(this.process && this.ready), starting: this.starting };
|
|
}
|
|
|
|
async ensureRunning(): Promise<boolean> {
|
|
if (this.process && this.ready) return true;
|
|
const result = await this.start();
|
|
return result.ok;
|
|
}
|
|
|
|
private async waitForReady(timeoutMs: number): Promise<boolean> {
|
|
const start = Date.now();
|
|
while (Date.now() - start < timeoutMs) {
|
|
if (await this.ping()) return true;
|
|
await new Promise((r) => setTimeout(r, 1500));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private async ping(): Promise<boolean> {
|
|
try {
|
|
const res = await fetch("http://127.0.0.1:1313", { signal: AbortSignal.timeout(1500) });
|
|
return res.ok || res.status < 500;
|
|
} catch (_e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private startHealthCheck(): void {
|
|
this.stopHealthCheck();
|
|
this.healthTimer = setInterval(async () => {
|
|
if (!this.process) return;
|
|
const alive = await this.ping();
|
|
if (!alive) {
|
|
console.warn("[hugo] health check failed, marking as not ready");
|
|
this.ready = false;
|
|
this.stopHealthCheck();
|
|
}
|
|
}, 30_000);
|
|
}
|
|
|
|
private stopHealthCheck(): void {
|
|
if (this.healthTimer) {
|
|
clearInterval(this.healthTimer);
|
|
this.healthTimer = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const hugo = new HugoManager();
|