199 lines · 7.3 KB · TypeScript
RawDownload
import {  ServerSelfUpdateError,  type ServerSelfUpdateCapability,  type ServerSelfUpdateInput,  type ServerSelfUpdateProgressStage,  type ServerSelfUpdateResult,} from "@t3tools/contracts";import { HostProcessExecutablePath } from "@t3tools/shared/hostProcess";import * as Context from "effect/Context";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path";import * as Ref from "effect/Ref"; import * as ServerConfig from "../config.ts";import * as ProcessRunner from "../processRunner.ts";import {  ensurePinnedRuntimeInstalled,  PinnedRuntimeInstallError,  PinnedRuntimePreflightBlockedError,} from "./pinnedRuntime.ts";import { decodeServicePreflightResult } from "./servicePreflight.ts";import * as ServiceLauncherClient from "./serviceLauncherClient.ts";import { isExactServiceVersion, SERVICE_LAUNCHER_PROTOCOL } from "./serviceProtocol.ts"; const PREFLIGHT_TIMEOUT = Duration.seconds(30); export function resolveServerSelfUpdateCapability(input: {  readonly desktopManaged: boolean;  readonly launcherManaged: boolean;}): ServerSelfUpdateCapability | null {  if (input.desktopManaged) return "desktop-managed" as const;  return input.launcherManaged ? ("boot-service" as const) : null;} export class ServerSelfUpdate extends Context.Service<  ServerSelfUpdate,  {    readonly update: (      input: ServerSelfUpdateInput,      reportProgress?: (stage: ServerSelfUpdateProgressStage) => Effect.Effect<void>,    ) => Effect.Effect<ServerSelfUpdateResult, ServerSelfUpdateError>;  }>()("t3/cloud/selfUpdate/ServerSelfUpdate") {} export const make = Effect.fn("cloud.server_self_update.make")(function* () {  const serverConfig = yield* ServerConfig.ServerConfig;  const launcher = yield* ServiceLauncherClient.ServiceLauncherClient;  const runner = yield* ProcessRunner.ProcessRunner;  const fs = yield* FileSystem.FileSystem;  const path = yield* Path.Path;  const execPath = yield* HostProcessExecutablePath;  const inFlight = yield* Ref.make(false);   const capability: ServerSelfUpdateCapability | null =    serverConfig.mode === "desktop" ? "desktop-managed" : launcher.managed ? "boot-service" : null;  const failWith = (reason: string, cause?: unknown) =>    cause === undefined      ? new ServerSelfUpdateError({ reason })      : new ServerSelfUpdateError({ reason, cause });   const update: ServerSelfUpdate["Service"]["update"] = Effect.fn(    "cloud.server_self_update.update",  )(function* (input, reportProgress = () => Effect.void) {    if (capability === "desktop-managed") {      return yield* failWith(        "This server is managed by the T3 Code desktop app on its machine; update the desktop app to update it.",      );    }    if (capability === null) {      return yield* failWith(        "Remote updates require the T3 Code background service. Run `t3 service install` on the server machine.",      );    }     const targetVersion = input.targetVersion.trim();    if (!isExactServiceVersion(targetVersion)) {      return yield* failWith(`'${targetVersion}' is not an exact t3 version.`);    }    if (yield* Ref.getAndSet(inFlight, true)) {      return yield* failWith("A server update is already in progress.");    }     return yield* Effect.gen(function* () {      yield* reportProgress("downloading");      const paths = yield* ensurePinnedRuntimeInstalled({        baseDir: serverConfig.baseDir,        version: targetVersion,        fs,        path,        runner,        validate: (runtime) =>          runner            .run({              command: execPath,              args: [                runtime.entryPath,                "__service-preflight",                "--database-path",                serverConfig.dbPath,                "--launcher-protocol",                String(SERVICE_LAUNCHER_PROTOCOL),              ],              timeout: PREFLIGHT_TIMEOUT,            })            .pipe(              Effect.mapError(                (cause) =>                  new PinnedRuntimeInstallError({                    step: "running the staged service preflight",                    cause,                  }),              ),              Effect.flatMap(                (                  result,                ): Effect.Effect<                  void,                  PinnedRuntimeInstallError | PinnedRuntimePreflightBlockedError                > => {                  if (result.code !== 0) {                    return Effect.fail(                      new PinnedRuntimeInstallError({                        step: "running the staged service preflight",                        exitCode: Number(result.code),                        stdoutLength: result.stdout.length,                        stderrLength: result.stderr.length,                      }),                    );                  }                  let parsed: unknown;                  try {                    parsed = JSON.parse(result.stdout.trim());                  } catch (cause) {                    return Effect.fail(                      new PinnedRuntimeInstallError({                        step: "decoding the staged service preflight",                        cause,                      }),                    );                  }                  const preflight = decodeServicePreflightResult(parsed);                  if (preflight === undefined || preflight.version !== targetVersion) {                    return Effect.fail(                      new PinnedRuntimeInstallError({                        step: "verifying the staged service preflight",                      }),                    );                  }                  return preflight.status === "ready"                    ? Effect.void                    : Effect.fail(                        new PinnedRuntimePreflightBlockedError({                          version: targetVersion,                          reason: preflight.reason,                        }),                      );                },              ),            ),      }).pipe(        Effect.mapError((error) =>          error._tag === "PinnedRuntimePreflightBlockedError"            ? failWith(error.reason, error)            : failWith(`Could not prepare t3@${targetVersion}.`, error),        ),      );       yield* reportProgress("installing");      const updateId = yield* launcher        .requestUpdate({ targetVersion, dbPath: serverConfig.dbPath })        .pipe(          Effect.mapError((error) =>            failWith(              error._tag === "ServiceLauncherRejectedError"                ? error.reason                : "Could not ask the service launcher to activate the prepared update.",              error,            ),          ),        );       yield* Effect.logInfo("Server update prepared; handing off to the service launcher.", {        updateId,        targetVersion,        runtimePath: paths.entryPath,      });      return { targetVersion, method: "boot-service" as const, updateId };    }).pipe(Effect.onError(() => Ref.set(inFlight, false)));  });   return ServerSelfUpdate.of({ update });}); export const layer = Layer.effect(ServerSelfUpdate, make()).pipe(  Layer.provide(ProcessRunner.layer),);