import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as Semaphore from "effect/Semaphore"; import * as ProcessRunner from "../processRunner.ts"; /** * A pinned runtime is an exact `t3@` npm-installed into * /runtime/versions/. The boot service points its systemd * unit here, and server self-update installs the target version here before * switching over — never `npx t3`, whose cache is ephemeral and whose * registry fetch at boot would make startup depend on the network. */ const PINNED_RUNTIME_DIR = "runtime"; const PINNED_RUNTIME_INSTALL_TIMEOUT = Duration.minutes(10); // Boot-service setup and remote self-update share this module but can be // constructed in separate layers. Serialize the complete check/install/ // sentinel transaction across all callers in this process. const pinnedRuntimeInstallLock = Semaphore.makeUnsafe(1); export interface PinnedRuntimePaths { readonly versionDir: string; readonly entryPath: string; readonly sentinelPath: string; } export function pinnedRuntimePaths( path: Path.Path, baseDir: string, version: string, ): PinnedRuntimePaths { const versionDir = path.join(baseDir, PINNED_RUNTIME_DIR, "versions", version); return { versionDir, entryPath: path.join(versionDir, "node_modules", "t3", "dist", "bin.mjs"), sentinelPath: path.join(versionDir, ".install-complete"), }; } export class PinnedRuntimeInstallError extends Schema.TaggedErrorClass()( "PinnedRuntimeInstallError", { step: Schema.String, exitCode: Schema.optional(Schema.Number), stdoutLength: Schema.optional(Schema.Number), stderrLength: Schema.optional(Schema.Number), cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { return this.exitCode === undefined ? `Pinned runtime install failed while ${this.step}.` : `Pinned runtime install failed while ${this.step} (exit code ${this.exitCode}).`; } } /** * Installs `t3@` into the pinned runtime directory unless a complete * install is already there, and returns its paths. The sentinel is written * only after npm exits 0; checking the entry file alone is not enough — npm * extracts files before running native builds (node-pty), so a killed * install leaves a plausible-looking but broken tree behind. */ export const ensurePinnedRuntimeInstalled = Effect.fn("cloud.pinned_runtime.ensure_installed")( function* (input: { readonly baseDir: string; readonly version: string; readonly fs: FileSystem.FileSystem; readonly path: Path.Path; readonly runner: ProcessRunner.ProcessRunner["Service"]; }) { const { fs, runner } = input; const paths = pinnedRuntimePaths(input.path, input.baseDir, input.version); return yield* pinnedRuntimeInstallLock.withPermit( Effect.gen(function* () { const alreadyPinned = yield* Effect.all([ fs.exists(paths.sentinelPath), fs.exists(paths.entryPath), ]).pipe( Effect.map(([sentinelExists, entryExists]) => sentinelExists && entryExists), Effect.mapError( (cause) => new PinnedRuntimeInstallError({ step: "checking the pinned runtime", cause }), ), ); if (alreadyPinned) { return paths; } yield* fs.remove(paths.versionDir, { recursive: true, force: true }).pipe( Effect.andThen(fs.makeDirectory(paths.versionDir, { recursive: true })), Effect.mapError( (cause) => new PinnedRuntimeInstallError({ step: "preparing the pinned runtime directory", cause, }), ), ); const installStep = "installing the pinned t3 runtime (this can take a few minutes)"; yield* runner .run({ command: "npm", args: [ "install", "--prefix", paths.versionDir, "--no-fund", "--no-audit", `t3@${input.version}`, ], // Native deps (node-pty) can compile from source on slow boxes; the // ProcessRunner default of 60s would kill a healthy install. timeout: PINNED_RUNTIME_INSTALL_TIMEOUT, }) .pipe( Effect.mapError((cause) => new PinnedRuntimeInstallError({ step: installStep, cause })), Effect.filterOrFail( (result) => result.code === 0, (result) => new PinnedRuntimeInstallError({ step: installStep, exitCode: Number(result.code), stdoutLength: result.stdout.length, stderrLength: result.stderr.length, }), ), Effect.tapError(() => fs.remove(paths.versionDir, { recursive: true, force: true }).pipe(Effect.ignore), ), ); yield* fs .writeFileString(paths.sentinelPath, `${input.version}\n`) .pipe( Effect.mapError( (cause) => new PinnedRuntimeInstallError({ step: "recording the completed install", cause }), ), ); return paths; }), ); }, ); /** Removes one pinned runtime while holding the same process-wide lock used * by install/check/sentinel work, so cleanup cannot race another caller that * is materializing or reusing the runtime tree. */ export const removePinnedRuntimeInstallation = Effect.fn("cloud.pinned_runtime.remove")( function* (input: { readonly baseDir: string; readonly version: string; readonly fs: FileSystem.FileSystem; readonly path: Path.Path; }) { const paths = pinnedRuntimePaths(input.path, input.baseDir, input.version); yield* pinnedRuntimeInstallLock.withPermit( input.fs .remove(paths.versionDir, { recursive: true, force: true }) .pipe( Effect.mapError( (cause) => new PinnedRuntimeInstallError({ step: "removing the pinned runtime", cause }), ), ), ); }, );