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(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(); const borrowing = new WeakMap(); 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(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 {} } } }