Files
ThothII/backend/src/workspaces/workspace-fs-at.ts
T

110 lines
8.4 KiB
TypeScript

import { createRequire } from "node:module";
import { closeSync, openSync } from "node:fs";
import { spawn } from "node:child_process";
import type { WorkspaceFsAtBindingV1, NativeWorkspaceFsAtHandleV1, NativeWorkspaceFsAtStatV1, NativeWorkspaceFsAtComponentV1 } from "../native/workspace-fs-at-binding.js";
const require = createRequire(import.meta.url);
const binding = require("../../native/workspace-fs-at/build/Release/workspace_fs_at.node") as WorkspaceFsAtBindingV1 & {
withFd<T>(handle: NativeWorkspaceFsAtHandleV1, action: (fd: number) => T): T;
duplicateForChildStdio(writer: NativeWorkspaceFsAtHandleV1, root: NativeWorkspaceFsAtHandleV1, writerFd: number, rootFd: number): void;
};
export interface WorkspaceFsAtStatV1 extends NativeWorkspaceFsAtStatV1 {}
export type LockFileName = "writer.lock" | "session-readers.lock";
export type WorkspaceFlockKindV1 = "shared" | "exclusive";
export type WorkspaceFlockWaitV1 = "blocking" | "nonblocking";
function component(value: string): NativeWorkspaceFsAtComponentV1 {
if (typeof value !== "string" || value.length === 0 || Buffer.byteLength(value, "utf8") > 255 || value !== value.trim() || value === "." || value === ".." || value.includes("/") || value.includes("\0")) throw new Error("invalid path component");
return value as NativeWorkspaceFsAtComponentV1;
}
function normalizeError(error: unknown): Error {
if (error instanceof Error) return error;
return new Error(String(error));
}
const INTERNAL = Symbol("workspace-fs-at-owned");
const rawHandles = new WeakMap<object, NativeWorkspaceFsAtHandleV1>();
const borrowing = new WeakMap<object, number>();
abstract class Owned {
private live = true;
private borrowing = 0;
protected constructor(raw: NativeWorkspaceFsAtHandleV1, readonly opened: WorkspaceFsAtStatV1, token: symbol) { if (token !== INTERNAL) throw new TypeError("private workspace descriptor"); rawHandles.set(this, raw); borrowing.set(this, 0); }
stat(): WorkspaceFsAtStatV1 { if (!this.live) throw new Error("workspace descriptor is closed"); return this.opened; }
close(): void {
if (!this.live) return;
if ((borrowing.get(this) ?? 0) !== 0) throw Object.assign(new Error("workspace descriptor is borrowed"), { code: "ERR_WORKSPACE_FS_AT_BORROWED" });
this.live = false;
try { binding.close(rawHandles.get(this)!); } catch (error) { throw normalizeError(error); }
}
}
export class OwnedWorkspaceFsAtDirectory extends Owned {
private constructor(raw: NativeWorkspaceFsAtHandleV1, stat: WorkspaceFsAtStatV1, token: symbol) { super(raw, stat, token); }
static [INTERNAL](raw: NativeWorkspaceFsAtHandleV1, stat: WorkspaceFsAtStatV1): OwnedWorkspaceFsAtDirectory { return new OwnedWorkspaceFsAtDirectory(raw, stat, INTERNAL); }
}
export class OwnedWorkspaceFsAtRegularFile extends Owned {
private constructor(raw: NativeWorkspaceFsAtHandleV1, stat: WorkspaceFsAtStatV1, token: symbol) { super(raw, stat, token); }
static [INTERNAL](raw: NativeWorkspaceFsAtHandleV1, stat: WorkspaceFsAtStatV1): OwnedWorkspaceFsAtRegularFile { return new OwnedWorkspaceFsAtRegularFile(raw, stat, INTERNAL); }
}
function wrapDirectory(result: {handle: NativeWorkspaceFsAtHandleV1; openedStat: WorkspaceFsAtStatV1}): OwnedWorkspaceFsAtDirectory {
try { if ((result.openedStat.mode & 0o170000) !== 0o040000) throw new Error("not a directory"); return OwnedWorkspaceFsAtDirectory[INTERNAL](result.handle, result.openedStat); }
catch (error) { try { binding.close(result.handle); } catch { /* preserve conversion error */ } throw error; }
}
function wrapLock(result: {handle: NativeWorkspaceFsAtHandleV1; openedStat: WorkspaceFsAtStatV1}): OwnedWorkspaceFsAtRegularFile {
try {
const st = result.openedStat;
if ((st.mode & 0o170000) !== 0o100000 || (st.mode & 0o777) !== 0o600 || st.uid !== (process.getuid?.() ?? st.uid) || st.nlink !== 1n) throw new Error("invalid lock identity");
return OwnedWorkspaceFsAtRegularFile[INTERNAL](result.handle, st);
} catch (error) { try { binding.close(result.handle); } catch { /* preserve conversion error */ } throw error; }
}
function rawDirectory(value: OwnedWorkspaceFsAtDirectory): NativeWorkspaceFsAtHandleV1 { if (!rawHandles.has(value)) throw new Error("workspace descriptor is closed"); return rawHandles.get(value)!; }
function withLockFd<T>(value: OwnedWorkspaceFsAtRegularFile, action: (fd: number) => T): T {
if (!rawHandles.has(value)) throw new Error("workspace descriptor is closed");
const count = borrowing.get(value) ?? 0; borrowing.set(value, count + 1);
try { return binding.withFd(rawHandles.get(value)!, action as (fd: number) => void) as T; } finally { borrowing.set(value, count); }
}
function anchoredDirectoryPath(value: OwnedWorkspaceFsAtDirectory): string {
if (!rawHandles.has(value)) throw new Error("workspace descriptor is closed");
const count = borrowing.get(value) ?? 0; borrowing.set(value, count + 1);
try { return binding.withFd(rawHandles.get(value)!, fd => `${process.platform === "darwin" ? "/dev/fd" : "/proc/self/fd"}/${fd}`); }
finally { borrowing.set(value, count); }
}
export class WorkspaceFsAtV1 {
openRoot(): OwnedWorkspaceFsAtDirectory { return wrapDirectory(binding.openat({ parent: null, name: "/", kind: "directory", createMode: 0 })); }
openDirectoryAt(parent: OwnedWorkspaceFsAtDirectory, name: string): OwnedWorkspaceFsAtDirectory { return wrapDirectory(binding.openat({ parent: rawDirectory(parent), name: component(name), kind: "directory", createMode: 0 })); }
openOrCreateLockAt(parent: OwnedWorkspaceFsAtDirectory, name: LockFileName, mode: 0o600): OwnedWorkspaceFsAtRegularFile { if ((name !== "writer.lock" && name !== "session-readers.lock") || mode !== 0o600) throw new Error("invalid lock"); return wrapLock(binding.openat({ parent: rawDirectory(parent), name: component(name), kind: "regular_lock", createMode: 0o600 })); }
mkdirAt(parent: OwnedWorkspaceFsAtDirectory, name: string, mode: 0o700): void { binding.mkdirat(rawDirectory(parent), component(name), mode); }
statAtNoFollow(parent: OwnedWorkspaceFsAtDirectory, name: string): WorkspaceFsAtStatV1 { return binding.fstatat(rawDirectory(parent), component(name)); }
fsyncDirectory(directory: OwnedWorkspaceFsAtDirectory): void { binding.fsyncDirectory(rawDirectory(directory)); }
/** Returns a proc-fd path tied to the retained directory open description. */
anchoredDirectoryPath(directory: OwnedWorkspaceFsAtDirectory): string { return anchoredDirectoryPath(directory); }
flockOwnedLock(owned: OwnedWorkspaceFsAtRegularFile, kind: WorkspaceFlockKindV1, wait: WorkspaceFlockWaitV1): void {
const fsExt: { flockSync(fd: number, operation: string): void } = require("fs-ext");
const operation = kind === "shared" ? (wait === "blocking" ? "sh" : "shnb") : (wait === "blocking" ? "ex" : "exnb");
withLockFd(owned, fd => fsExt.flockSync(fd, operation));
}
fcntlOwnedLock(owned: OwnedWorkspaceFsAtRegularFile, operation: "probe-exclusive-nonblocking" | "hold-exclusive" | "unlock"): "held" | "available" { if (!rawHandles.has(owned)) throw new Error("workspace descriptor is closed"); return binding.fcntl(rawHandles.get(owned)!, operation); }
}
/** Internal child boundary. It deliberately returns a ChildProcess result, never an FD. */
export function spawnChildFromOwnedCapability(
writer: OwnedWorkspaceFsAtRegularFile,
root: OwnedWorkspaceFsAtDirectory,
executable: string,
args: readonly string[],
environment: NodeJS.ProcessEnv = process.env,
): Promise<{ exitCode: number; stdout: Uint8Array; stderr: Uint8Array }> {
if (!rawHandles.has(writer) || !rawHandles.has(root)) throw new Error("preprocessing_conflict");
const writerRaw = rawHandles.get(writer)!; const rootRaw = rawHandles.get(root)!;
const opened: number[] = [];
try {
const writerFd = openSync("/dev/null", "r"); opened.push(writerFd);
const rootFd = openSync("/dev/null", "r"); opened.push(rootFd);
binding.duplicateForChildStdio(writerRaw, rootRaw, writerFd, rootFd);
const child = spawn(executable, [...args], { stdio: ["ignore", "pipe", "pipe", writerFd, rootFd], env: { ...environment, THOTH_WORKSPACE_CAPABILITY_REQUIRED: "1" } });
const out: Buffer[] = []; const err: Buffer[] = [];
child.stdout?.on("data", (chunk: Buffer) => out.push(chunk)); child.stderr?.on("data", (chunk: Buffer) => err.push(chunk));
return new Promise((resolve, reject) => {
child.once("error", reject); child.once("close", code => resolve({ exitCode: code ?? 1, stdout: Buffer.concat(out), stderr: Buffer.concat(err) }));
});
} finally { for (const fd of opened) { try { closeSync(fd); } catch {} } }
}