import * as Context from "effect/Context"; import * as Config from "effect/Config"; 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 { HostProcessArguments, HostProcessExecutablePath, HostProcessPlatform, } from "@t3tools/shared/hostProcess"; import * as ProcessRunner from "../processRunner.ts"; import { ensurePinnedRuntimeInstalled, pinnedRuntimePaths } from "./pinnedRuntime.ts"; /** * Installs T3 Code as a per-user boot service. Linux-only for now: systemd * user unit + loginctl enable-linger. The service runs a stable or pinned * runtime — never an ephemeral `npx t3` cache whose eviction could break * startup. */ 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"; const EPHEMERAL_CACHE_SEGMENTS = [ "/_npx/", // npx "\\_npx\\", "/pnpm/dlx/", // pnpm dlx (~/.cache/pnpm/dlx and $PNPM_HOME/.pnpm/dlx) "/.pnpm/dlx/", "/.bun/install/cache/", // bunx ]; /** * `npx t3` (and pnpm dlx / bunx) run out of ephemeral package-manager * caches that can be evicted at any time — a boot service must never point * there. Global installs, repo checkouts, and the pinned runtime below are * all stable. */ export function isEphemeralCacheEntry(entryPath: string): boolean { return EPHEMERAL_CACHE_SEGMENTS.some((segment) => entryPath.includes(segment)); } /** * systemd expands `%` specifiers in most directive values, including the * `append:` file paths, which take the rest of the line literally and must * NOT be quoted. */ export function escapeSystemdSpecifiers(value: string): string { return value.replaceAll("%", "%%"); } /** * systemd word-splits ExecStart and Environment values and expands `%` * specifiers, so paths with spaces or percents must be quoted and escaped. */ export function quoteSystemdValue(value: string): string { const escaped = escapeSystemdSpecifiers(value); return /[\s"'\\]/.test(escaped) ? `"${escaped.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"` : escaped; } export interface BootServicePlan { /** Absolute path of the node binary running this CLI. */ readonly nodePath: string; /** Absolute path of the pinned t3 entry point the unit will run. */ readonly t3EntryPath: string; readonly baseDir: string; readonly logPath: string; readonly unitPath: string; } /** * Pure so it is testable byte-for-byte. systemd user units run with a * minimal environment: every path must be absolute, and the service must * not rely on PATH, nvm shims, or shell profiles. Failures land in * `logPath` because `systemctl --user` failures are otherwise invisible. */ export function renderBootServiceUnit(plan: BootServicePlan): string { // No After=network-online.target: it does not exist in the systemd *user* // manager, so ordering on it is silently ignored. The server retries its // relay connection, and Restart=always covers early-boot failures. return [ "[Unit]", "Description=T3 Code server", // Give up after 5 crashes in 5 minutes so a persistently broken install // (deleted runtime, broken workspace) stops instead of restarting every // 5s forever and growing the unrotated append log without bound. "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.t3EntryPath)} serve`, "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", { 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", { 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", { cause: Schema.Defect() }, ) { override get message(): string { return "Could not set up the T3 Code background service."; } } export type BootServiceError = | BootServiceUnsupportedError | BootServiceCommandError | BootServiceInstallError; export interface BootServiceStatus { readonly supported: boolean; readonly installed: boolean; /** False when the installed unit no longer matches what install would write. */ readonly current: boolean; readonly unitPath: string; readonly logPath: string; } export class BootService extends Context.Service< BootService, { /** Installs the pinned runtime + unit, enables linger, starts the service. */ readonly install: Effect.Effect; /** * Stops and removes the unit; leaves the pinned runtime for reuse. * Returns whether a unit was actually removed. */ readonly uninstall: Effect.Effect; readonly status: Effect.Effect; } >()("t3/cloud/bootService") {} export interface BootServiceHost { readonly execPath: string; readonly cliEntryPath: 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 hostArguments = yield* HostProcessArguments; const host = input.host ?? { execPath: hostExecPath, // When running the packed CLI this is dist/bin.mjs; when stable (global // install, repo checkout) the boot service runs this same artifact. cliEntryPath: hostArguments[1] ?? "", }; 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 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 runtimePaths = pinnedRuntimePaths(path, input.baseDir, input.cliVersion); 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, 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, ), ), ); }); /** * Ensures plannedEntryPath exists before the unit points at it. A stable * install (global bin, repo checkout) is used as-is; an ephemeral cache * entry is replaced by `npm install --prefix`-ing the exact running * version into /runtime/versions/. A real install (not a copy * of bin.mjs) because t3 ships native deps like node-pty. */ const ensurePinnedRuntime = Effect.gen(function* () { if (!isEphemeralCacheEntry(host.cliEntryPath)) { return; } yield* ensurePinnedRuntimeInstalled({ baseDir: input.baseDir, version: input.cliVersion, fs, path, runner, }).pipe( Effect.mapError((error) => error.step.startsWith("installing") ? new BootServiceCommandError({ step: error.step, exitCode: error.exitCode, stdoutLength: error.stdoutLength, stderrLength: error.stderrLength, cause: error.cause, }) : new BootServiceInstallError({ cause: error }), ), Effect.tapError((error) => DateTime.now.pipe( Effect.flatMap((now) => fs.writeFileString(logPath, `${DateTime.formatIso(now)} ${error.message}\n`, { flag: "a", }), ), Effect.ignore, ), ), ); }); // Where the unit will point: derivable without touching the network, so // status can compare units purely; install materializes it first. const plannedEntryPath = isEphemeralCacheEntry(host.cliEntryPath) ? runtimePaths.entryPath : host.cliEntryPath; const plan: BootServicePlan = { nodePath: host.execPath, t3EntryPath: plannedEntryPath, baseDir: input.baseDir, logPath, unitPath, }; const install: BootService["Service"]["install"] = Effect.gen(function* () { yield* requireSystemdLinux; yield* fs .makeDirectory(input.logsDir, { recursive: true }) .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause }))); yield* ensurePinnedRuntime; const previousUnit = yield* fs.exists(unitPath).pipe( Effect.flatMap((exists) => exists ? fs.readFileString(unitPath).pipe(Effect.map(Option.some)) : Effect.succeed(Option.none()), ), Effect.mapError((cause) => new BootServiceInstallError({ cause })), ); yield* fs.makeDirectory(unitDir, { recursive: true }).pipe( Effect.andThen(fs.writeFileString(unitPath, renderBootServiceUnit(plan))), Effect.mapError((cause) => new BootServiceInstallError({ cause })), ); // If any activation step fails, remove the unit again: a leftover file // would make service status report it as installed even though it was // never enabled or lingered. yield* Effect.gen(function* () { yield* runStep("reloading systemd user units", "systemctl", ["--user", "daemon-reload"]); yield* runStep("enabling the service", "systemctl", [ "--user", "enable", BOOT_SERVICE_UNIT_FILE, ]); // restart rather than enable --now: --now does not replace an already // running process, so repairing a stale unit would leave the old // server running until reboot. restart also starts a stopped service. yield* runStep("starting the service", "systemctl", [ "--user", "restart", BOOT_SERVICE_UNIT_FILE, ]); // Linger keeps the user manager (and this service) running without an // open session — the whole point on a box reached over SSH. No // username argument: loginctl defaults to the calling user, which is // always right, while $USER can be stale (su without -l) or unset. yield* runStep("enabling lingering for this user", "loginctl", ["enable-linger"]); }).pipe(Effect.tapError(() => rollbackFailedInstall(previousUnit))); return plan; }).pipe(Effect.withSpan("cloud.boot_service.install")); // If activation fails partway (e.g. enable succeeds but restart/linger // fails), leave nothing behind: disable removes the enable symlink, remove // deletes the file, daemon-reload clears the stale definition — otherwise a // dangling wants/ symlink logs "Failed to load unit" at every boot and the // next lifecycle command misreports the state. const rollbackFailedInstall = Effect.fn("cloud.boot_service.rollback_failed_install")(function* ( previousUnit: Option.Option, ) { if (Option.isSome(previousUnit)) { yield* fs.writeFileString(unitPath, previousUnit.value).pipe(Effect.ignore); } else { yield* runStep("cleaning up the service", "systemctl", [ "--user", "disable", "--now", BOOT_SERVICE_UNIT_FILE, ]).pipe(Effect.ignore); yield* fs.remove(unitPath).pipe(Effect.ignore); } yield* runStep("reloading systemd user units", "systemctl", ["--user", "daemon-reload"]).pipe( Effect.ignore, ); if (Option.isSome(previousUnit)) { yield* runStep("restoring the previous service", "systemctl", [ "--user", "restart", BOOT_SERVICE_UNIT_FILE, ]).pipe(Effect.ignore); } }); const uninstall: BootService["Service"]["uninstall"] = Effect.gen(function* () { yield* requireSystemdLinux; const exists = yield* fs .exists(unitPath) .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause }))); if (!exists) { 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 }; } const unitExists = yield* fs.exists(unitPath); if (!unitExists) { return { supported: true, installed: false, current: false, unitPath, logPath }; } const unit = yield* fs.readFileString(unitPath); // A unit is current only if it matches what install would write now (an // older CLI wrote a different runtime/node path) AND the entry point it // references still exists (a pinned runtime under ~/.t3 can be deleted to // reclaim space). Either mismatch makes connect offer a repair. const entryExists = yield* fs.exists(plannedEntryPath); const current = unit === renderBootServiceUnit(plan) && entryExists; return { supported: true, installed: true, current, 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));