authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-01 22:42:39-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-01 22:42:39-07:00
log64bf0161919ec0e41470af3304928d9fc8711bf4
tree98caf7f24875e42ea5cfa05f85d07878de1c2a36
parent5192f777fe54c2a2a359f6c25ecf5fbde46d49b0
signature Signed by PGP key B5690EEEBB952194

fix(server): stop npx service updates from silently leaving the old server running (#5217)

Co-authored-by: Claude <noreply@anthropic.com>

2 files changed, 22 insertions(+), 15 deletions(-)

apps/server/src/cloud/bootService.test.ts+17-2
......@@ -33,6 +33,7 @@ it("keeps systemd pinned to the stable launcher rather than a versioned server",
3333
3434const makeHarness = Effect.fn("test.make_boot_service_harness")(function* (
3535 platform: NodeJS.Platform = "linux",
36 usePinnedLauncher = false,
3637) {
3738 const fs = yield* FileSystem.FileSystem;
3839 const path = yield* Path.Path;
......@@ -44,6 +45,10 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* (
4445 const runtime = pinnedRuntimePaths(path, baseDir, "1.2.3");
4546 yield* fs.makeDirectory(path.dirname(runtime.entryPath), { recursive: true });
4647 yield* fs.writeFileString(runtime.entryPath, "export {};\n");
48 yield* fs.writeFileString(
49 path.join(path.dirname(runtime.entryPath), "service-launcher.mjs"),
50 "export const source = 'pinned runtime';\n",
51 );
4752 yield* fs.writeFileString(runtime.sentinelPath, "1.2.3\n");
4853
4954 const commands: string[] = [];
......@@ -69,8 +74,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* (
6974 cliVersion: "1.2.3",
7075 host: {
7176 execPath: "/usr/bin/node",
72 cliEntryPath: path.join(home, "bin.mjs"),
73 launcherSourcePath: sourceLauncher,
77 ...(usePinnedLauncher ? {} : { launcherSourcePath: sourceLauncher }),
7478 },
7579 }).pipe(
7680 Effect.provideService(ProcessRunner.ProcessRunner, runner),
......@@ -109,6 +113,17 @@ it.layer(NodeServices.layer)("boot service install", (it) => {
109113 }),
110114 );
111115
116 it.effect("copies the launcher from the prepared pinned runtime", () =>
117 Effect.gen(function* () {
118 const { service, fs } = yield* makeHarness("linux", true);
119 const plan = yield* service.install;
120
121 expect(yield* fs.readFileString(plan.launcherPath)).toBe(
122 "export const source = 'pinned runtime';\n",
123 );
124 }),
125 );
126
112127 it.effect("restarts an installed service when repair fails", () =>
113128 Effect.gen(function* () {
114129 const { service, commands, control } = yield* makeHarness();
apps/server/src/cloud/bootService.ts+5-13
......@@ -1,8 +1,4 @@
1import {
2 HostProcessArguments,
3 HostProcessExecutablePath,
4 HostProcessPlatform,
5} from "@t3tools/shared/hostProcess";
1import { HostProcessExecutablePath, HostProcessPlatform } from "@t3tools/shared/hostProcess";
62import * as Config from "effect/Config";
73import * as Context from "effect/Context";
84import * as DateTime from "effect/DateTime";
......@@ -138,7 +134,6 @@ export class BootService extends Context.Service<
138134
139135export interface BootServiceHost {
140136 readonly execPath: string;
141 readonly cliEntryPath: string;
142137 readonly launcherSourcePath?: string;
143138}
144139
......@@ -148,26 +143,23 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: {
148143 readonly cliVersion: string;
149144 readonly host?: BootServiceHost;
150145}) {
151 const hostArguments = yield* HostProcessArguments;
152146 const hostExecPath = yield* HostProcessExecutablePath;
153147 const platform = yield* HostProcessPlatform;
154148 const homeDir = yield* Config.string("HOME").pipe(Config.withDefault(""));
155149 const fs = yield* FileSystem.FileSystem;
156150 const path = yield* Path.Path;
157151 const runner = yield* ProcessRunner.ProcessRunner;
158 const host = input.host ?? {
159 execPath: hostExecPath,
160 cliEntryPath: hostArguments[1] ?? "",
161 };
152 const host = input.host ?? { execPath: hostExecPath };
162153
163154 const unitDir = path.join(homeDir, ".config", "systemd", "user");
164155 const unitPath = path.join(unitDir, BOOT_SERVICE_UNIT_FILE);
165156 const logPath = path.join(input.logsDir, "boot-service.log");
166157 const launcherPath = path.join(input.baseDir, "runtime", SERVICE_LAUNCHER_FILE);
167158 const statePath = path.join(input.baseDir, "runtime", SERVICE_STATE_FILE);
168 const launcherSourcePath =
169 host.launcherSourcePath ?? path.join(path.dirname(host.cliEntryPath), SERVICE_LAUNCHER_FILE);
170159 const runtimePaths = pinnedRuntimePaths(path, input.baseDir, input.cliVersion);
160 const launcherSourcePath =
161 host.launcherSourcePath ??
162 path.join(path.dirname(runtimePaths.entryPath), SERVICE_LAUNCHER_FILE);
171163 const writeDurably = (filePath: string, contents: string) =>
172164 Effect.scoped(
173165 Effect.gen(function* () {