UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

1 lines • 9.94 kB
{"version":3,"file":"process-registry.cjs","names":["join","homedir","existsSync","readFileSync","spawnSync","Effect","access"],"sources":["../src/process-registry.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { access } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { Effect } from \"effect\";\n\nexport type ProcessRole = \"standalone\" | \"workspace-parent\" | \"workspace-child\";\n\nexport interface PidEntry {\n pid: number;\n configDir: string;\n parentPid?: number;\n role: ProcessRole;\n ports: Record<string, number>;\n childPids?: number[];\n budget?: { min: number; max: number };\n // Lease seam (ADR 0012 §6): entries are leases on shared resources, keyed\n // by what they hold; refcount-ready for the shared-plugin broker.\n leaseKey?: string;\n refcount?: number;\n startedAt: number;\n // Identity of the claimed pid captured at registration time (ADR 0012\n // amendment): PIDs are reused across container restarts, so a bare pid\n // cannot prove a claim live. `processStartTime` is stable per process\n // generation — a mismatch proves the recorded session is dead.\n processStart?: string;\n description: string;\n}\n\nfunction getRegistryDir(): string {\n return join(homedir(), \".cache\", \"everything-dev\");\n}\n\nfunction ensureRegistryDir(): void {\n const path = getRegistryPath();\n const dir = join(path, \"..\");\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n}\n\nexport function readRegistry(): PidEntry[] {\n const path = getRegistryPath();\n if (!existsSync(path)) return [];\n try {\n const raw = JSON.parse(readFileSync(path, \"utf-8\"));\n if (!Array.isArray(raw)) return [];\n return raw.filter(\n (entry): entry is PidEntry =>\n entry &&\n typeof entry === \"object\" &&\n typeof entry.pid === \"number\" &&\n typeof entry.configDir === \"string\" &&\n typeof entry.role === \"string\",\n );\n } catch {\n return [];\n }\n}\n\nexport function isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code !== \"ESRCH\";\n }\n}\n\n/**\n * Identity of a process generation, stable across PID reuse: Linux reads the\n * kernel's starttime tick from /proc, macOS falls back to `ps -o lstart=`.\n * Null when the generation cannot be determined (the caller then treats the\n * claim as unverifiable and keeps legacy behavior).\n */\nexport function processStartTime(pid: number): string | null {\n if (pid <= 1) return null;\n try {\n const stat = readFileSync(`/proc/${pid}/stat`, \"utf-8\");\n const close = stat.lastIndexOf(\")\");\n const fields = stat.slice(close + 2).split(\" \");\n const starttime = fields[19];\n return starttime ? `proc:${starttime}` : null;\n } catch {\n try {\n const out = spawnSync(\"ps\", [\"-o\", \"lstart=\", \"-p\", String(pid)], { timeout: 5000 });\n const text = out.stdout?.toString().trim();\n return text ? `ps:${text}` : null;\n } catch {\n return null;\n }\n }\n}\n\nexport function isEntryLive(entry: PidEntry): boolean {\n if (entry.pid <= 1) return false;\n if (!existsSync(entry.configDir)) return false;\n if (!isPidAlive(entry.pid)) return false;\n if (entry.processStart !== undefined) {\n return processStartTime(entry.pid) === entry.processStart;\n }\n return true;\n}\n\nexport function pruneDead(entries: PidEntry[]): PidEntry[] {\n return entries.filter(isEntryLive);\n}\n\nexport function pruneDeadEffect(entries: PidEntry[]): Effect.Effect<PidEntry[]> {\n return Effect.forEach(\n entries,\n (entry) => {\n if (entry.pid <= 1) return Effect.succeed(null);\n return Effect.gen(function* () {\n const dirExists = yield* Effect.promise(() =>\n access(entry.configDir)\n .then(() => true)\n .catch(() => false),\n );\n if (!dirExists) return null;\n if (!isPidAlive(entry.pid)) return null;\n if (entry.processStart !== undefined) {\n const current = yield* Effect.promise(() => Promise.resolve(processStartTime(entry.pid)));\n if (current !== entry.processStart) return null;\n }\n return entry;\n });\n },\n { concurrency: \"unbounded\" },\n ).pipe(Effect.map((results) => results.filter((e): e is PidEntry => e !== null)));\n}\n\nexport function writeRegistry(entries: PidEntry[]): void {\n ensureRegistryDir();\n const path = getRegistryPath();\n const tmpPath = `${path}.tmp`;\n writeFileSync(tmpPath, `${JSON.stringify(entries, null, 2)}\\n`);\n renameSync(tmpPath, path);\n}\n\nexport function registerStandalone(entry: Omit<PidEntry, \"role\">): PidEntry {\n const live = pruneDead(readRegistry());\n const full: PidEntry = {\n ...entry,\n role: \"standalone\",\n processStart: processStartTime(entry.pid) ?? entry.processStart,\n };\n const withoutSelf = live.filter((existing) => existing.pid !== entry.pid);\n withoutSelf.push(full);\n writeRegistry(withoutSelf);\n return full;\n}\n\nexport function registerEntry(entry: PidEntry): void {\n const live = pruneDead(readRegistry());\n const withoutSelf = live.filter((existing) => existing.pid !== entry.pid);\n withoutSelf.push({\n ...entry,\n processStart: processStartTime(entry.pid) ?? entry.processStart,\n });\n writeRegistry(withoutSelf);\n}\n\nexport function updateChildPids(pid: number, childPids: number[]): void {\n const live = pruneDead(readRegistry());\n const entry = live.find((existing) => existing.pid === pid);\n if (!entry) return;\n entry.childPids = childPids;\n writeRegistry(live);\n}\n\nexport function unregisterPid(pid: number): void {\n const entries = readRegistry();\n const next = pruneDead(entries).filter((entry) => entry.pid !== pid);\n if (next.length === entries.length) return;\n writeRegistry(next);\n}\n\nexport function removeRegistryFile(): void {\n const path = getRegistryPath();\n if (existsSync(path)) {\n rmSync(path, { force: true });\n }\n}\n\nexport function getRegistryPath(): string {\n return process.env.BO_PID_REGISTRY_PATH ?? join(getRegistryDir(), \"pids.json\");\n}\n\nexport function claimedPorts(): Set<number> {\n const out = new Set<number>(claimedPortOwners().keys());\n return out;\n}\n\n/** Port → the live registry entry claiming it, for occupancy diagnostics. */\nexport function claimedPortOwners(): Map<number, PidEntry> {\n const live = pruneDead(readRegistry());\n const out = new Map<number, PidEntry>();\n for (const entry of live) {\n for (const port of Object.values(entry.ports)) {\n if (typeof port === \"number\") out.set(port, entry);\n }\n }\n return out;\n}\n"],"mappings":"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"}