fs.realpath throws EISDIR on Windows drive roots (e.g. R:\), but canonicalProjectDir in launch/presence.ts and launch/client.ts only recovered ENOENT, aborting startup. Both now fall back to path.resolve() on EISDIR, matching the existing ENOENT handling. Fixes #5708
83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import * as fs from "node:fs/promises";
|
|
import * as path from "node:path";
|
|
import { isEisdir, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
|
|
import { daemonRuntimeDir } from "./paths";
|
|
|
|
const CLIENTS_DIR = "clients";
|
|
|
|
/** Handle keeping one omp process registered in a project daemon scope. */
|
|
export interface DaemonProjectPresence {
|
|
close(): Promise<void>;
|
|
}
|
|
|
|
async function canonicalProjectDir(projectDir: string): Promise<string> {
|
|
const resolved = path.resolve(projectDir);
|
|
try {
|
|
return await fs.realpath(resolved);
|
|
} catch (error) {
|
|
if (isEnoent(error) || isEisdir(error)) return resolved;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/** Register this omp process so project daemons survive while it remains alive. */
|
|
export async function registerDaemonProjectPresence(
|
|
projectDir: string,
|
|
runtimeOverride?: string,
|
|
): Promise<DaemonProjectPresence> {
|
|
const canonical = await canonicalProjectDir(projectDir);
|
|
const runtimeDir = runtimeOverride ?? daemonRuntimeDir(canonical);
|
|
const clientsDir = path.join(runtimeDir, CLIENTS_DIR);
|
|
await fs.mkdir(clientsDir, { recursive: true, mode: 0o700 });
|
|
const id = `${process.pid}-${crypto.randomUUID()}`;
|
|
const presencePath = path.join(clientsDir, `${id}.json`);
|
|
await Bun.write(presencePath, JSON.stringify({ pid: process.pid, id, projectDir: canonical }));
|
|
await fs.chmod(presencePath, 0o600);
|
|
let closed = false;
|
|
const close = async (): Promise<void> => {
|
|
if (closed) return;
|
|
closed = true;
|
|
cancelCleanup();
|
|
await fs.rm(presencePath, { force: true });
|
|
};
|
|
const cancelCleanup = postmortem.register(`daemon-presence:${id}`, () => close());
|
|
return { close };
|
|
}
|
|
|
|
/** Return whether a registered omp process in this runtime directory is still alive. */
|
|
export async function hasLiveDaemonProjectPresence(runtimeDir: string): Promise<boolean> {
|
|
const clientsDir = path.join(runtimeDir, CLIENTS_DIR);
|
|
let entries: string[];
|
|
try {
|
|
entries = await fs.readdir(clientsDir);
|
|
} catch (error) {
|
|
if (isEnoent(error)) return false;
|
|
throw error;
|
|
}
|
|
let live = false;
|
|
for (const entry of entries) {
|
|
const presencePath = path.join(clientsDir, entry);
|
|
try {
|
|
const decoded: unknown = await Bun.file(presencePath).json();
|
|
if (
|
|
typeof decoded !== "object" ||
|
|
decoded === null ||
|
|
!("pid" in decoded) ||
|
|
typeof decoded.pid !== "number"
|
|
) {
|
|
await fs.rm(presencePath, { force: true });
|
|
continue;
|
|
}
|
|
try {
|
|
process.kill(decoded.pid, 0);
|
|
live = true;
|
|
} catch {
|
|
await fs.rm(presencePath, { force: true });
|
|
}
|
|
} catch (error) {
|
|
if (!isEnoent(error)) await fs.rm(presencePath, { force: true });
|
|
}
|
|
}
|
|
return live;
|
|
}
|