427 lines · 14.8 KB · TypeScript
RawDownload
import { HostProcessExecutablePath, HostProcessPlatform } from "@t3tools/shared/hostProcess";import * as Config from "effect/Config";import * as Context from "effect/Context";import * as DateTime from "effect/DateTime";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 Option from "effect/Option";import * as Path from "effect/Path";import * as Schema from "effect/Schema"; import * as ProcessRunner from "../processRunner.ts";import {  ensurePinnedRuntimeInstalled,  pinnedRuntimePaths,  PinnedRuntimeInstallError,} from "./pinnedRuntime.ts";import {  SERVICE_LAUNCHER_FILE,  SERVICE_LAUNCHER_PROTOCOL,  SERVICE_STATE_FILE,  parseServiceState,  serviceStateHasPendingUpdate,  type ServiceState,} from "./serviceProtocol.ts"; const BOOT_SERVICE_NAME = "t3code";export const BOOT_SERVICE_UNIT_FILE = `${BOOT_SERVICE_NAME}.service`;export const BOOT_SERVICE_UNIT_ENV = "T3_BOOT_SERVICE_UNIT"; /** systemd expands `%` specifiers, including in unquoted append-log paths. */export function escapeSystemdSpecifiers(value: string): string {  return value.replaceAll("%", "%%");} export function quoteSystemdValue(value: string): string {  const escaped = escapeSystemdSpecifiers(value);  return /[\s"'\\]/.test(escaped)    ? `"${escaped.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`    : escaped;} export interface BootServicePlan {  readonly nodePath: string;  readonly launcherPath: string;  readonly baseDir: string;  readonly logPath: string;  readonly unitPath: string;} /** Pure renderer: service units cannot rely on the user's shell or PATH. */export function renderBootServiceUnit(plan: BootServicePlan): string {  // The user manager has no reliable network-online target; server networking retries itself.  return [    "[Unit]",    "Description=T3 Code server",    "StartLimitIntervalSec=300",    "StartLimitBurst=5",    "",    "[Service]",    "Type=simple",    "WorkingDirectory=%h",    `Environment=T3CODE_HOME=${quoteSystemdValue(plan.baseDir)}`,    `Environment=${BOOT_SERVICE_UNIT_ENV}=${BOOT_SERVICE_UNIT_FILE}`,    `ExecStart=${quoteSystemdValue(plan.nodePath)} ${quoteSystemdValue(plan.launcherPath)}`,    // Let the launcher mark an explicit stop before it signals the server.    // systemd still SIGKILLs the whole cgroup if graceful shutdown times out.    "KillMode=mixed",    // Agent tool calls run as children of the server, so they share this cgroup.    // With the systemd default of OOMPolicy=stop, the kernel killing one greedy    // child stops the whole unit: the server, every live agent, and the user's    // connection. Keep running and let Restart=always cover the main process.    "OOMPolicy=continue",    "Restart=always",    "RestartSec=5",    `StandardOutput=append:${escapeSystemdSpecifiers(plan.logPath)}`,    `StandardError=append:${escapeSystemdSpecifiers(plan.logPath)}`,    "",    "[Install]",    "WantedBy=default.target",    "",  ].join("\n");} export class BootServiceUnsupportedError extends Schema.TaggedErrorClass<BootServiceUnsupportedError>()(  "BootServiceUnsupportedError",  { platform: Schema.String },) {  override get message(): string {    return `Background setup currently supports Linux with systemd; this machine reports '${this.platform}'.`;  }} export class BootServiceCommandError extends Schema.TaggedErrorClass<BootServiceCommandError>()(  "BootServiceCommandError",  {    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      ? `Background setup failed while ${this.step}.`      : `Background setup failed while ${this.step} (exit code ${this.exitCode}).`;  }} export class BootServiceInstallError extends Schema.TaggedErrorClass<BootServiceInstallError>()(  "BootServiceInstallError",  { cause: Schema.Defect() },) {  override get message(): string {    return "Could not set up the T3 Code background service.";  }} export class BootServiceUpdatePendingError extends Schema.TaggedErrorClass<BootServiceUpdatePendingError>()(  "BootServiceUpdatePendingError",  {},) {  override get message(): string {    return "A remote server update is still pending. Wait for it to finish, then retry.";  }} export type BootServiceError =  | BootServiceUnsupportedError  | BootServiceCommandError  | BootServiceInstallError  | BootServiceUpdatePendingError; export interface BootServiceStatus {  readonly supported: boolean;  readonly installed: boolean;  readonly current: boolean;  readonly unitPath: string;  readonly logPath: string;} export class BootService extends Context.Service<  BootService,  {    readonly install: Effect.Effect<BootServicePlan, BootServiceError>;    readonly uninstall: Effect.Effect<boolean, BootServiceError>;    readonly status: Effect.Effect<BootServiceStatus, BootServiceError>;  }>()("t3/cloud/bootService") {} export interface BootServiceHost {  readonly execPath: string;  readonly launcherSourcePath?: string;} export const make = Effect.fn("cloud.boot_service.make")(function* (input: {  readonly baseDir: string;  readonly logsDir: string;  readonly cliVersion: string;  readonly host?: BootServiceHost;}) {  const hostExecPath = yield* HostProcessExecutablePath;  const platform = yield* HostProcessPlatform;  const homeDir = yield* Config.string("HOME").pipe(Config.withDefault(""));  const fs = yield* FileSystem.FileSystem;  const path = yield* Path.Path;  const runner = yield* ProcessRunner.ProcessRunner;  const host = input.host ?? { execPath: hostExecPath };   const unitDir = path.join(homeDir, ".config", "systemd", "user");  const unitPath = path.join(unitDir, BOOT_SERVICE_UNIT_FILE);  const logPath = path.join(input.logsDir, "boot-service.log");  const launcherPath = path.join(input.baseDir, "runtime", SERVICE_LAUNCHER_FILE);  const statePath = path.join(input.baseDir, "runtime", SERVICE_STATE_FILE);  const runtimePaths = pinnedRuntimePaths(path, input.baseDir, input.cliVersion);  const launcherSourcePath =    host.launcherSourcePath ??    path.join(path.dirname(runtimePaths.entryPath), SERVICE_LAUNCHER_FILE);  const writeDurably = (filePath: string, contents: string) =>    Effect.scoped(      Effect.gen(function* () {        const directory = path.dirname(filePath);        yield* fs.makeDirectory(directory, { recursive: true });        const tempPath = yield* fs.makeTempFileScoped({ directory, prefix: ".service-write-" });        yield* fs.writeFileString(tempPath, contents, { mode: 0o600 });        yield* (yield* fs.open(tempPath, { flag: "r" })).sync;        yield* fs.rename(tempPath, filePath);        yield* (yield* fs.open(directory, { flag: "r" })).sync;      }),    ).pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));  const plan: BootServicePlan = {    nodePath: host.execPath,    launcherPath,    baseDir: input.baseDir,    logPath,    unitPath,  };   const requireSystemdLinux = Effect.gen(function* () {    if (platform !== "linux" || homeDir === "") {      return yield* new BootServiceUnsupportedError({ platform });    }  });   const runStep = Effect.fn("cloud.boot_service.run_step")(function* (    step: string,    command: string,    args: ReadonlyArray<string>,    options?: { readonly timeout?: Duration.Input },  ) {    return yield* runner.run({ command, args, timeout: options?.timeout }).pipe(      Effect.mapError((cause) => new BootServiceCommandError({ step, cause })),      Effect.filterOrFail(        (result) => result.code === 0,        (result) =>          new BootServiceCommandError({            step,            exitCode: Number(result.code),            stdoutLength: result.stdout.length,            stderrLength: result.stderr.length,          }),      ),      Effect.tapError((error) =>        DateTime.now.pipe(          Effect.flatMap((now) =>            fs.writeFileString(logPath, `${DateTime.formatIso(now)} ${error.message}\n`, {              flag: "a",            }),          ),          Effect.ignore,        ),      ),    );  });   const install: BootService["Service"]["install"] = Effect.gen(function* () {    yield* requireSystemdLinux;    yield* fs      .makeDirectory(input.logsDir, { recursive: true })      .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));     // Prepare every immutable artifact before stopping the installed unit.    yield* ensurePinnedRuntimeInstalled({      baseDir: input.baseDir,      version: input.cliVersion,      fs,      path,      runner,      validate: (runtime) =>        runner          .run({            command: host.execPath,            args: [runtime.entryPath, "--version"],            timeout: Duration.seconds(30),          })          .pipe(            Effect.mapError(              (cause) =>                new PinnedRuntimeInstallError({                  step: "verifying the pinned t3 runtime",                  cause,                }),            ),            Effect.flatMap((result) => {              const reportedVersion = /\bv(\S+)\s*$/.exec(result.stdout)?.[1];              return result.code === 0 && reportedVersion === input.cliVersion                ? Effect.void                : Effect.fail(                    new PinnedRuntimeInstallError({                      step: "verifying the pinned t3 runtime",                      exitCode: Number(result.code),                      stdoutLength: result.stdout.length,                      stderrLength: result.stderr.length,                    }),                  );            }),          ),    }).pipe(      Effect.mapError((error) =>        error._tag === "PinnedRuntimeInstallError"          ? new BootServiceCommandError({              step: error.step,              exitCode: error.exitCode,              stdoutLength: error.stdoutLength,              stderrLength: error.stderrLength,              cause: error,            })          : new BootServiceInstallError({ cause: error }),      ),    );    const launcherSource = yield* fs      .readFileString(launcherSourcePath)      .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));     const installed = yield* fs      .exists(unitPath)      .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));    if (installed) {      yield* runStep("stopping the installed service", "systemctl", [        "--user",        "stop",        BOOT_SERVICE_UNIT_FILE,      ]);    }     yield* Effect.gen(function* () {      if (installed) {        const previousStateText = yield* fs.readFileString(statePath).pipe(Effect.option);        if (          Option.isSome(previousStateText) &&          serviceStateHasPendingUpdate(previousStateText.value)        ) {          return yield* new BootServiceUpdatePendingError();        }      }      yield* fs        .makeDirectory(unitDir, { recursive: true })        .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));      yield* writeDurably(launcherPath, launcherSource);      yield* writeDurably(        statePath,        // @effect-diagnostics-next-line preferSchemaOverJson:off - fixed launcher-owned document.        `${JSON.stringify(          {            protocol: SERVICE_LAUNCHER_PROTOCOL,            activeVersion: input.cliVersion,          } satisfies ServiceState,          null,          2,        )}\n`,      );      yield* writeDurably(unitPath, renderBootServiceUnit(plan));       yield* runStep("reloading systemd user units", "systemctl", ["--user", "daemon-reload"]);      yield* runStep("enabling the service", "systemctl", [        "--user",        "enable",        BOOT_SERVICE_UNIT_FILE,      ]);      yield* runStep("enabling lingering for this user", "loginctl", ["enable-linger"]);      // Start last. No administrative state write occurs after this succeeds.      yield* runStep("starting the service", "systemctl", [        "--user",        "restart",        BOOT_SERVICE_UNIT_FILE,      ]);    }).pipe(      Effect.tapError(() =>        installed          ? runStep("restarting the service after a failed update", "systemctl", [              "--user",              "restart",              BOOT_SERVICE_UNIT_FILE,            ]).pipe(Effect.ignore)          : Effect.void,      ),    );    return plan;  }).pipe(Effect.withSpan("cloud.boot_service.install"));   const uninstall: BootService["Service"]["uninstall"] = Effect.gen(function* () {    yield* requireSystemdLinux;    if (      !(yield* fs        .exists(unitPath)        .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause }))))    )      return false;    yield* runStep("stopping the service", "systemctl", [      "--user",      "disable",      "--now",      BOOT_SERVICE_UNIT_FILE,    ]);    yield* fs      .remove(unitPath)      .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));    yield* runStep("reloading systemd user units", "systemctl", ["--user", "daemon-reload"]);    return true;  }).pipe(Effect.withSpan("cloud.boot_service.uninstall"));   const status: BootService["Service"]["status"] = Effect.gen(function* () {    if (platform !== "linux" || homeDir === "") {      return { supported: false, installed: false, current: false, unitPath, logPath };    }    if (!(yield* fs.exists(unitPath))) {      return { supported: true, installed: false, current: false, unitPath, logPath };    }    const [unit, launcherExists, runtimeEntryExists, runtimeSentinel, stateText] =      yield* Effect.all([        fs.readFileString(unitPath),        fs.exists(launcherPath),        fs.exists(runtimePaths.entryPath),        fs.readFileString(runtimePaths.sentinelPath).pipe(Effect.option),        fs.readFileString(statePath).pipe(Effect.option),      ]);    const state = Option.isSome(stateText) ? parseServiceState(stateText.value) : undefined;    return {      supported: true,      installed: true,      current:        unit === renderBootServiceUnit(plan) &&        launcherExists &&        runtimeEntryExists &&        Option.isSome(runtimeSentinel) &&        runtimeSentinel.value.trim() === input.cliVersion &&        state?.activeVersion === input.cliVersion &&        state?.update?.status !== "pending",      unitPath,      logPath,    };  }).pipe(    Effect.mapError((cause) => new BootServiceInstallError({ cause })),    Effect.withSpan("cloud.boot_service.status"),  );   return BootService.of({ install, uninstall, status });}); export const layer = (input: {  readonly baseDir: string;  readonly logsDir: string;  readonly cliVersion: string;  readonly host?: BootServiceHost;}) => Layer.effect(BootService, make(input));