fix: harden native workspace root capabilities
This commit is contained in:
@@ -15,6 +15,8 @@ import {
|
||||
type CanonicalWorkspaceId,
|
||||
type WorkspaceLockRootIdentityV1,
|
||||
Revision40,
|
||||
workspaceRootInternals,
|
||||
makeBorrowedRootLease,
|
||||
} from "./workspace-lock-root-lease.js";
|
||||
|
||||
export interface ArtifactIdentity { readonly kind: string; readonly digest: string; readonly bytes: number; }
|
||||
@@ -72,14 +74,14 @@ async function durableJson(path: string, value: unknown): Promise<void> {
|
||||
}
|
||||
|
||||
export class PreprocessingStateStore {
|
||||
private readonly rootLease: Pick<VerifiedWorkspaceLockRootLease, "assertLive" | "anchoredPath">;
|
||||
private readonly rootLease: { assertLive(): void; anchoredPath(): string };
|
||||
constructor(rootLease: VerifiedWorkspaceLockRootLease | string) {
|
||||
if (typeof rootLease === "string") {
|
||||
let st: ReturnType<typeof lstatSync>;
|
||||
try { st = lstatSync(rootLease); } catch { throw fail(); }
|
||||
if (!st.isDirectory() || st.isSymbolicLink()) throw fail();
|
||||
this.rootLease = { assertLive: () => { const current = lstatSync(rootLease); if (!current.isDirectory() || current.isSymbolicLink()) throw fail(); }, anchoredPath: () => rootLease };
|
||||
} else this.rootLease = rootLease;
|
||||
} else this.rootLease = { assertLive: () => workspaceRootInternals.assertLive(rootLease), anchoredPath: () => { throw fail(); } };
|
||||
}
|
||||
private root(): string { this.rootLease.assertLive(); return this.rootLease.anchoredPath(); }
|
||||
private paths(input: { runId: string }) {
|
||||
@@ -158,13 +160,13 @@ export class WorkspaceWriterLockCapability {
|
||||
private live = true; private settled = false; private readerExclusive = false; private spawnActive = false; private poisoned = false;
|
||||
private constructor(readonly workspaceId: CanonicalWorkspaceId, readonly rootIdentity: WorkspaceLockRootIdentityV1, private readonly root: VerifiedWorkspaceLockRootLease, private readonly writer: WorkspaceRootLock) {}
|
||||
private assertLive(): void { if (!this.live || this.settled || this.poisoned) throw fail(); }
|
||||
assertWriterPath(): void { this.assertLive(); this.root.assertLive(); this.writer.assertPath(); }
|
||||
assertWriterPath(): void { this.assertLive(); workspaceRootInternals.assertLive(this.root); this.writer.assertPath(); }
|
||||
invalidateForSettlement(): void { this.settled = true; }
|
||||
static [INTERNAL_STATE](id: CanonicalWorkspaceId, identity: WorkspaceLockRootIdentityV1, root: VerifiedWorkspaceLockRootLease, writer: WorkspaceRootLock) { return new WorkspaceWriterLockCapability(id, identity, root, writer); }
|
||||
async runUnderSessionReadersExclusive<T>(action: (lease: BorrowedWorkspaceSessionReadersExclusiveLockLease) => Promise<T>): Promise<T> {
|
||||
this.assertLive(); if (this.readerExclusive || this.spawnActive) throw fail(); this.readerExclusive = true;
|
||||
let lock: WorkspaceRootLock;
|
||||
try { lock = await this.root.acquireSessionReadersExclusive(); } catch { this.readerExclusive = false; this.poisoned = true; throw fail(); }
|
||||
try { lock = await workspaceRootInternals.acquireReadersExclusive(this.root); } catch { this.readerExclusive = false; this.poisoned = true; throw fail(); }
|
||||
const borrowed = BorrowedWorkspaceSessionReadersExclusiveLockLease[INTERNAL_STATE](this.workspaceId, this.rootIdentity);
|
||||
try { return await action(borrowed); } catch (error) { throw error; }
|
||||
finally {
|
||||
@@ -182,7 +184,7 @@ export class WorkspaceWriterLockCapability {
|
||||
try {
|
||||
const configPath = cfg.path; let argv: string[];
|
||||
switch (request.kind) { case "dwh_preprocess": argv = ["-m", "tht.cli", "preprocess", "dwh", "--steps", request.stage, "--json", "-c", configPath]; break; case "schema_preprocess": argv = ["-m", "tht.cli", "schema", request.stage === "fk_suggest" ? "suggest-fks" : request.stage === "fk_check" ? "check" : "index", "--json", "-c", configPath]; break; case "evidence_preprocess": argv = ["-m", "tht.cli", "preprocess", "evidence", "--json", "-c", configPath]; break; default: throw fail(); }
|
||||
return await this.root.spawnChild(this.writer, process.env.THT_PYTHON ?? "python3", argv, { ...process.env, THOTH_WORKSPACE_ID: this.workspaceId, THOTH_WORKSPACE_REVISION: request.revision, THOTH_WORKSPACE_DEVICE: String(this.rootIdentity.device), THOTH_WORKSPACE_INODE: String(this.rootIdentity.inode) });
|
||||
return await workspaceRootInternals.spawn(this.root, this.writer, process.env.THT_PYTHON ?? "python3", argv, { ...process.env, THOTH_WORKSPACE_ID: this.workspaceId, THOTH_WORKSPACE_REVISION: request.revision, THOTH_WORKSPACE_DEVICE: String(this.rootIdentity.device), THOTH_WORKSPACE_INODE: String(this.rootIdentity.inode) });
|
||||
} finally { this.spawnActive = false; }
|
||||
}
|
||||
async close(): Promise<void> { if (!this.live) return; if (this.readerExclusive || this.spawnActive) throw fail(); this.live = false; let error: unknown; try { this.writer.close(); } catch (e) { error = e; } try { await this.root.close(); } catch (e) { error ??= e; } if (error) throw fail(); }
|
||||
@@ -199,7 +201,7 @@ export class OrderedWorkspaceWriterCapabilitySet {
|
||||
static [INTERNAL_STATE](caps: Map<CanonicalWorkspaceId, WorkspaceWriterLockCapability>) { return new OrderedWorkspaceWriterCapabilitySet(caps); }
|
||||
invalidate(): void { this.live = false; for (const cap of this.caps.values()) cap.invalidateForSettlement(); }
|
||||
get workspaceIds(): readonly CanonicalWorkspaceId[] { if (!this.live) throw fail(); return [...this.caps.keys()]; }
|
||||
async forWorkspace<T>(workspaceId: CanonicalWorkspaceId, action: (lease: OrderedWorkspaceCapability) => Promise<T>): Promise<T> { if (!this.live) throw fail(); const cap = this.caps.get(workspaceId); if (!cap) throw fail(); return action({ workspaceId, rootLease: BorrowedVerifiedWorkspaceLockRootLease.make(cap.rootIdentity, () => { if (!this.live) throw fail(); }), writerCapability: cap }); }
|
||||
async forWorkspace<T>(workspaceId: CanonicalWorkspaceId, action: (lease: OrderedWorkspaceCapability) => Promise<T>): Promise<T> { if (!this.live) throw fail(); const cap = this.caps.get(workspaceId); if (!cap) throw fail(); return action({ workspaceId, rootLease: makeBorrowedRootLease(cap.rootIdentity, () => { if (!this.live) throw fail(); }), writerCapability: cap }); }
|
||||
async forEachWorkspace<T>(action: (lease: OrderedWorkspaceCapability) => Promise<T>): Promise<readonly T[]> { return Promise.all(this.workspaceIds.map(id => this.forWorkspace(id, action))); }
|
||||
}
|
||||
export async function runUnderOrderedWorkspaceWriterLocks<T>(rootLeases: readonly VerifiedWorkspaceLockRootLease[], action: (capabilities: OrderedWorkspaceWriterCapabilitySet) => Promise<T>): Promise<T> {
|
||||
@@ -210,7 +212,7 @@ export async function runUnderOrderedWorkspaceWriterLocks<T>(rootLeases: readonl
|
||||
const root = source.transfer();
|
||||
let writer: WorkspaceRootLock | undefined;
|
||||
try {
|
||||
writer = await root.acquireWriterLock();
|
||||
writer = await workspaceRootInternals.acquireWriter(root);
|
||||
caps.push(makeWriterCapability(root.identity.workspaceId, root.identity, root, writer));
|
||||
} catch (error) {
|
||||
try { writer?.close(); } catch {}
|
||||
|
||||
Reference in New Issue
Block a user