diff --git a/apps/server/src/cloud/bootService.test.ts b/apps/server/src/cloud/bootService.test.ts index d3a8af2320c4f..d33215a92dcc8 100644 --- a/apps/server/src/cloud/bootService.test.ts +++ b/apps/server/src/cloud/bootService.test.ts @@ -151,6 +151,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( cliVersion = "1.2.3", serviceBaseDir = baseDir, insideService = false, + hostExecPath = "/usr/bin/t3", ) => Effect.gen(function* () { const paths = pinnedRuntimePaths(path, serviceBaseDir, cliVersion, platform); @@ -161,7 +162,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( baseDir: serviceBaseDir, logsDir: path.join(serviceBaseDir, "userdata", "logs"), cliVersion, - host: { execPath: "/usr/bin/t3" }, + host: { execPath: hostExecPath }, }); }).pipe( Effect.provideService(ProcessRunner.ProcessRunner, runner), @@ -169,7 +170,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( Layer.mergeAll( Layer.succeed(HostProcess.Platform, platform), Layer.succeed(HostProcess.UserId, 501), - Layer.succeed(HostProcess.ExecutablePath, "/usr/bin/t3"), + Layer.succeed(HostProcess.ExecutablePath, hostExecPath), Layer.succeed( HttpClient.HttpClient, HttpClient.make(() => Effect.die("no release download expected")), @@ -817,6 +818,29 @@ it.layer(NodeServices.layer)("boot service install", (it) => { }), ); + // regression: #17532 + it.effect("puts the runtime an update installs on the launch agent PATH", () => + Effect.gen(function* () { + const { makeService, fs, baseDir, runtime } = yield* makeHarness("darwin"); + const path = yield* Path.Path; + const previous = runtime.versionDir; + const installed = pinnedRuntimePaths(path, baseDir, "1.2.4", "darwin").versionDir; + // The 1.2.3 CLI, or the 1.2.3 service's own PATH, installs 1.2.4. + const updated = yield* makeService( + `/usr/bin:${previous}:/bin`, + "1.2.4", + undefined, + false, + runtime.entryPath, + ); + const plan = yield* updated.install(); + + expect(yield* fs.readFileString(plan.unitPath)).toContain( + ` PATH\n /usr/bin:${installed}:/bin:/opt/homebrew/bin:/usr/local/bin:/usr/sbin:/sbin`, + ); + }), + ); + it.effect("drops PATH directories that cannot be represented in a launch agent plist", () => Effect.gen(function* () { const { service, fs } = yield* makeHarness( diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts index c1be1fc2c5ef4..eb28613a46aed 100644 --- a/apps/server/src/cloud/bootService.ts +++ b/apps/server/src/cloud/bootService.ts @@ -17,8 +17,10 @@ import { CLI_RELEASE_BASE_URL_ENV } from "@t3tools/shared/cliRelease"; import * as ProcessRunner from "../processRunner.ts"; import { ensurePinnedRuntimeInstalled, + launcherOwnsVersionsDir, pinnedRuntimeCommand, pinnedRuntimePaths, + pinnedRuntimeVersionsDir, PinnedRuntimeInstallError, } from "./pinnedRuntime.ts"; import { @@ -565,6 +567,8 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { const path = yield* Path.Path; const runner = yield* ProcessRunner.ProcessRunner; const host = input.host ?? { execPath: hostExecPath }; + const runtimePaths = pinnedRuntimePaths(path, input.baseDir, input.cliVersion, platform); + const versionsDir = pinnedRuntimeVersionsDir(path, input.baseDir); const xmlSafeInstallerDirectories = installerPath.split(":").filter( (directory) => directory.length > 0 && @@ -573,17 +577,23 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { return code >= 0x20 || code === 0x09 || code === 0x0a || code === 0x0d; }), ); + // During an update, the running CLI and a PATH inherited from the service name the runtime + // being replaced. The service and its agent shells should find the runtime it starts. const environmentPath = Array.from( - new Set([ - ...xmlSafeInstallerDirectories, - path.dirname(host.execPath), - "/opt/homebrew/bin", - "/usr/local/bin", - "/usr/bin", - "/bin", - "/usr/sbin", - "/sbin", - ]), + new Set( + [ + ...xmlSafeInstallerDirectories, + path.dirname(host.execPath), + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "/bin", + "/usr/sbin", + "/sbin", + ].map((directory) => + launcherOwnsVersionsDir(path, versionsDir, directory) ? runtimePaths.versionDir : directory, + ), + ), ).join(":"); const detectedManager = selectBootServiceManager({ @@ -597,7 +607,6 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { const logPath = path.join(input.logsDir, BOOT_SERVICE_LOG_FILE); const statePath = path.join(input.baseDir, "runtime", SERVICE_STATE_FILE); const restartPendingPath = path.join(input.baseDir, "runtime", SERVICE_RESTART_PENDING_FILE); - const runtimePaths = pinnedRuntimePaths(path, input.baseDir, input.cliVersion, platform); const writeDurably = (filePath: string, contents: string) => Effect.scoped( Effect.gen(function* () {