244 lines · 8.2 KB · TypeScript
RawDownload
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 PlatformError from "effect/PlatformError";import * as Schema from "effect/Schema";import * as Option from "effect/Option";import * as Semaphore from "effect/Semaphore"; import * as ProcessRunner from "../processRunner.ts"; /** * A pinned runtime is an exact `t3@<version>` npm-installed into * <baseDir>/runtime/versions/<version>. The boot service points its unit or * launch agent 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 update can construct separate layers. Serialize// the complete install transaction across every caller 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.TaggedError<PinnedRuntimeInstallError>()(  "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}).`;  }} export class PinnedRuntimePreflightBlockedError extends Schema.TaggedError<PinnedRuntimePreflightBlockedError>()(  "PinnedRuntimePreflightBlockedError",  {    version: Schema.String,    reason: Schema.String,  },) {  override get message(): string {    return this.reason;  }} /** * Installs `t3@<version>` 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. */interface PinnedRuntimeInstallInput {  readonly baseDir: string;  readonly version: string;  readonly fs: FileSystem.FileSystem;  readonly path: Path.Path;  readonly runner: ProcessRunner.ProcessRunner["Service"];  readonly validate: (    paths: PinnedRuntimePaths,  ) => Effect.Effect<void, PinnedRuntimeInstallError | PinnedRuntimePreflightBlockedError>;} const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")(function* (  input: PinnedRuntimeInstallInput,) {  const { fs, runner } = input;  const paths = pinnedRuntimePaths(input.path, input.baseDir, input.version);  const [versionDirExists, entryExists, sentinel] = yield* Effect.all([    fs.exists(paths.versionDir),    fs.exists(paths.entryPath),    fs.readFileString(paths.sentinelPath).pipe(Effect.option),  ]).pipe(    Effect.mapError(      (cause) => new PinnedRuntimeInstallError({ step: "checking the pinned runtime", cause }),    ),  );  const alreadyPinned =    entryExists && Option.isSome(sentinel) && sentinel.value.trim() === input.version;  if (alreadyPinned) {    yield* input.validate(paths);    return paths;  }  if (versionDirExists) {    yield* fs.remove(paths.versionDir, { recursive: true, force: true }).pipe(      Effect.mapError(        (cause) =>          new PinnedRuntimeInstallError({            step: "removing an incomplete pinned runtime",            cause,          }),      ),    );  }   const versionsDir = input.path.dirname(paths.versionDir);  yield* fs.makeDirectory(versionsDir, { recursive: true }).pipe(    Effect.mapError(      (cause) =>        new PinnedRuntimeInstallError({          step: "preparing the pinned runtime directory",          cause,        }),    ),  );  const stagingDir = yield* fs    .makeTempDirectory({      directory: versionsDir,      prefix: ".staging-",    })    .pipe(      Effect.mapError(        (cause) =>          new PinnedRuntimeInstallError({            step: "preparing the pinned runtime directory",            cause,          }),      ),    );  const stagingPaths: PinnedRuntimePaths = {    versionDir: stagingDir,    entryPath: input.path.join(stagingDir, "node_modules", "t3", "dist", "bin.mjs"),    sentinelPath: input.path.join(stagingDir, ".install-complete"),  };   return yield* Effect.gen(function* () {    const installStep = "installing the pinned t3 runtime (this can take a few minutes)";    const installArgs = [      "install",      "--prefix",      stagingDir,      "--no-fund",      "--no-audit",      `t3@${input.version}`,    ];    yield* runner      .run({        command: "npm",        args: installArgs,        // Native dependencies may compile from source on slower machines.        timeout: PINNED_RUNTIME_INSTALL_TIMEOUT,      })      .pipe(        Effect.catchTags({          ProcessSpawnError: (error) =>            error.cause instanceof PlatformError.PlatformError &&            error.cause.reason._tag === "NotFound"              ? // pnpm-managed Node installations do not include npm. Keep npm                // installation semantics for the pinned runtime and native builds.                runner.run({                  command: "pnpm",                  args: ["--package=npm@11", "dlx", "npm", ...installArgs],                  timeout: PINNED_RUNTIME_INSTALL_TIMEOUT,                })              : Effect.fail(error),        }),        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,            }),        ),      );     yield* input.validate(stagingPaths);    yield* fs      .writeFileString(stagingPaths.sentinelPath, `${input.version}\n`)      .pipe(        Effect.mapError(          (cause) =>            new PinnedRuntimeInstallError({ step: "recording the completed install", cause }),        ),      );    const published = yield* fs.rename(stagingDir, paths.versionDir).pipe(      Effect.as(true),      Effect.catch((cause) =>        Effect.all([          fs.exists(paths.entryPath),          fs.readFileString(paths.sentinelPath).pipe(Effect.option),        ]).pipe(          Effect.mapError(            (checkCause) =>              new PinnedRuntimeInstallError({                step: "checking a concurrently published pinned runtime",                cause: checkCause,              }),          ),          Effect.flatMap(([publishedEntryExists, publishedSentinel]) =>            publishedEntryExists &&            Option.isSome(publishedSentinel) &&            publishedSentinel.value.trim() === input.version              ? Effect.succeed(false)              : Effect.fail(                  new PinnedRuntimeInstallError({                    step: "publishing the pinned runtime",                    cause,                  }),                ),          ),        ),      ),    );    if (!published) yield* input.validate(paths);    return paths;  }).pipe(    Effect.ensuring(fs.remove(stagingDir, { recursive: true, force: true }).pipe(Effect.ignore)),  );}); export const ensurePinnedRuntimeInstalled = (input: PinnedRuntimeInstallInput) =>  pinnedRuntimeInstallLock.withPermit(installPinnedRuntime(input));