169 lines
5.0 KiB
TypeScript
169 lines
5.0 KiB
TypeScript
import { mkdirSync, openSync, readFileSync, readlinkSync, realpathSync, rmSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
const action = process.argv[2];
|
|
const root = process.cwd();
|
|
const repoRoot = resolve(root, "../..");
|
|
const stateDir = resolve(root, ".state/tasktree-native");
|
|
const pidPath = resolve(stateDir, "supervisor.pid");
|
|
const logPath = resolve(stateDir, "supervisor.log");
|
|
const servicesLogPath = resolve(stateDir, "services.log");
|
|
|
|
function readPid(): number | null {
|
|
try {
|
|
const pid = Number(readFileSync(pidPath, "utf8").trim());
|
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function isRunning(pid: number | null): boolean {
|
|
if (!pid) return false;
|
|
try {
|
|
process.kill(pid, 0);
|
|
try {
|
|
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
const state = stat.slice(stat.lastIndexOf(")") + 2).split(" ")[0];
|
|
if (state === "Z") return false;
|
|
} catch {
|
|
// Non-Linux hosts do not expose /proc; the signal probe remains authoritative there.
|
|
}
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function canonicalPath(path: string): string {
|
|
try {
|
|
return realpathSync(path);
|
|
} catch {
|
|
return resolve(path);
|
|
}
|
|
}
|
|
|
|
function runningProcessCwd(pid: number | null): string | null {
|
|
if (!pid) return null;
|
|
try {
|
|
return canonicalPath(readlinkSync(`/proc/${pid}/cwd`));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function gitCommit(workspace: string | null): string | null {
|
|
if (!workspace) return null;
|
|
const result = Bun.spawnSync(["git", "-C", workspace, "rev-parse", "HEAD"], {
|
|
stdout: "pipe",
|
|
stderr: "ignore"
|
|
});
|
|
if (result.exitCode !== 0) return null;
|
|
return result.stdout.toString().trim() || null;
|
|
}
|
|
|
|
async function waitForExit(pid: number): Promise<boolean> {
|
|
const deadline = Date.now() + 15000;
|
|
while (Date.now() < deadline) {
|
|
if (!isRunning(pid)) return true;
|
|
await Bun.sleep(25);
|
|
}
|
|
return !isRunning(pid);
|
|
}
|
|
|
|
mkdirSync(stateDir, { recursive: true });
|
|
const pid = readPid();
|
|
|
|
if (action === "start") {
|
|
if (isRunning(pid)) {
|
|
console.log(JSON.stringify({ ok: true, action, status: "already-running", pid, logPath }));
|
|
process.exit(0);
|
|
}
|
|
const requiredEnvironment = ["TASKTREE_NATIVE_DATABASE_ENV_FILE", "TASKTREE_TEMPORAL_ADDRESS"] as const;
|
|
const missingEnvironment = requiredEnvironment.filter((key) => !process.env[key]?.trim());
|
|
if (missingEnvironment.length > 0) {
|
|
console.error(JSON.stringify({
|
|
ok: false,
|
|
action,
|
|
status: "preflight-failed",
|
|
error: "missing-required-environment",
|
|
missingEnvironment,
|
|
valuesPrinted: false
|
|
}));
|
|
process.exit(1);
|
|
}
|
|
rmSync(pidPath, { force: true });
|
|
const logFd = openSync(logPath, "a");
|
|
const child = Bun.spawn([process.execPath, "scripts/tasktree-native-supervisor.ts"], {
|
|
cwd: root,
|
|
env: process.env,
|
|
stdin: "ignore",
|
|
stdout: logFd,
|
|
stderr: logFd,
|
|
detached: true
|
|
});
|
|
child.unref();
|
|
await Bun.write(pidPath, `${child.pid}\n`);
|
|
await Bun.sleep(150);
|
|
if (!isRunning(child.pid)) {
|
|
rmSync(pidPath, { force: true });
|
|
console.error(JSON.stringify({
|
|
ok: false,
|
|
action,
|
|
status: "start-failed",
|
|
error: "supervisor-exited-before-ready",
|
|
logPath,
|
|
valuesPrinted: false
|
|
}));
|
|
process.exit(1);
|
|
}
|
|
console.log(JSON.stringify({ ok: true, action, status: "started", pid: child.pid, logPath }));
|
|
} else if (action === "stop") {
|
|
if (!isRunning(pid)) {
|
|
rmSync(pidPath, { force: true });
|
|
console.log(JSON.stringify({ ok: true, action, status: "stopped" }));
|
|
} else {
|
|
process.kill(pid!, "SIGTERM");
|
|
if (!await waitForExit(pid!)) {
|
|
console.error(JSON.stringify({ ok: false, action, status: "stop-timeout", pid }));
|
|
process.exit(1);
|
|
}
|
|
rmSync(pidPath, { force: true });
|
|
console.log(JSON.stringify({ ok: true, action, status: "stopped", pid }));
|
|
}
|
|
} else if (action === "status") {
|
|
const running = isRunning(pid);
|
|
const processCwd = runningProcessCwd(running ? pid : null);
|
|
const sourceWorkspace = processCwd ? canonicalPath(resolve(processCwd, "../..")) : null;
|
|
const expectedProcessCwd = canonicalPath(root);
|
|
const expectedSourceWorkspace = canonicalPath(repoRoot);
|
|
console.log(JSON.stringify({
|
|
ok: running,
|
|
action,
|
|
status: running ? "running" : "stopped",
|
|
pid: running ? pid : null,
|
|
logPath,
|
|
servicesLogPath,
|
|
provenance: {
|
|
expectedSourceWorkspace,
|
|
sourceWorkspace,
|
|
processCwd,
|
|
commit: gitCommit(sourceWorkspace),
|
|
workspaceMatches: sourceWorkspace === expectedSourceWorkspace,
|
|
processCwdMatches: processCwd === expectedProcessCwd
|
|
}
|
|
}));
|
|
process.exit(running ? 0 : 1);
|
|
} else if (action === "logs") {
|
|
for (const path of [logPath, servicesLogPath]) {
|
|
try {
|
|
console.log(readFileSync(path, "utf8").trimEnd().split("\n").slice(-80).join("\n"));
|
|
} catch {
|
|
// A log is optional until its process has started.
|
|
}
|
|
}
|
|
} else {
|
|
console.error("usage: bun run scripts/tasktree-native-service.ts <start|stop|status|logs>");
|
|
process.exit(2);
|
|
}
|