From 747860aa93b335555231deb0e5d66bb2492167bb Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Sat, 1 Aug 2026 22:33:10 -0700 Subject: [PATCH] fix(server): repair the background service when run through npx The installer resolved service-launcher.mjs from process.argv[1], which is the .bin/t3 symlink under npx. That directory has no launcher, so the update failed before stopping systemd and the old server kept running. Source the launcher from the pinned runtime the installer just prepared. Co-Authored-By: Claude --- apps/server/src/cloud/bootService.test.ts | 19 +++++++++++++++++-- apps/server/src/cloud/bootService.ts | 18 +++++------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/apps/server/src/cloud/bootService.test.ts b/apps/server/src/cloud/bootService.test.ts index b0ff61bba4b..9af69eb1792 100644 --- a/apps/server/src/cloud/bootService.test.ts +++ b/apps/server/src/cloud/bootService.test.ts @@ -33,6 +33,7 @@ it("keeps systemd pinned to the stable launcher rather than a versioned server", const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( platform: NodeJS.Platform = "linux", + usePinnedLauncher = false, ) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -44,6 +45,10 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( const runtime = pinnedRuntimePaths(path, baseDir, "1.2.3"); yield* fs.makeDirectory(path.dirname(runtime.entryPath), { recursive: true }); yield* fs.writeFileString(runtime.entryPath, "export {};\n"); + yield* fs.writeFileString( + path.join(path.dirname(runtime.entryPath), "service-launcher.mjs"), + "export const source = 'pinned runtime';\n", + ); yield* fs.writeFileString(runtime.sentinelPath, "1.2.3\n"); const commands: string[] = []; @@ -69,8 +74,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( cliVersion: "1.2.3", host: { execPath: "/usr/bin/node", - cliEntryPath: path.join(home, "bin.mjs"), - launcherSourcePath: sourceLauncher, + ...(usePinnedLauncher ? {} : { launcherSourcePath: sourceLauncher }), }, }).pipe( Effect.provideService(ProcessRunner.ProcessRunner, runner), @@ -109,6 +113,17 @@ it.layer(NodeServices.layer)("boot service install", (it) => { }), ); + it.effect("copies the launcher from the prepared pinned runtime", () => + Effect.gen(function* () { + const { service, fs } = yield* makeHarness("linux", true); + const plan = yield* service.install; + + expect(yield* fs.readFileString(plan.launcherPath)).toBe( + "export const source = 'pinned runtime';\n", + ); + }), + ); + it.effect("restarts an installed service when repair fails", () => Effect.gen(function* () { const { service, commands, control } = yield* makeHarness(); diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts index 874b6b712a0..9a8481b11b5 100644 --- a/apps/server/src/cloud/bootService.ts +++ b/apps/server/src/cloud/bootService.ts @@ -1,8 +1,4 @@ -import { - HostProcessArguments, - HostProcessExecutablePath, - HostProcessPlatform, -} from "@t3tools/shared/hostProcess"; +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"; @@ -138,7 +134,6 @@ export class BootService extends Context.Service< export interface BootServiceHost { readonly execPath: string; - readonly cliEntryPath: string; readonly launcherSourcePath?: string; } @@ -148,26 +143,23 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { readonly cliVersion: string; readonly host?: BootServiceHost; }) { - const hostArguments = yield* HostProcessArguments; 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, - cliEntryPath: hostArguments[1] ?? "", - }; + 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 launcherSourcePath = - host.launcherSourcePath ?? path.join(path.dirname(host.cliEntryPath), SERVICE_LAUNCHER_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* () {