| author | |
| committer | |
| log | f328db30da063a7bce9a9d038fc583d3ff83673a |
| tree | a2f1e765088e14daba3919672174b3b67864ab73 |
| parent | 9130d932f4ddb8abab595b7d12553b46a11c7b22 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>5 files changed, 25 insertions(+), 92 deletions(-)
apps/server/src/cloud/pinnedRuntime.test.ts-48| ... | @@ -1,4 +1,3 @@ | ... | @@ -1,4 +1,3 @@ |
| 1 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | ||
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 4 | import * as Deferred from "effect/Deferred"; | 3 | import * as Deferred from "effect/Deferred"; |
| ... | @@ -95,19 +94,6 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => { | ... | @@ -95,19 +94,6 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => { |
| 95 | assert.deepEqual(commands, ["tar"]); | 94 | assert.deepEqual(commands, ["tar"]); |
| 96 | assert.equal(yield* fs.readFileString(paths.sentinelPath), `${version}\n`); | 95 | assert.equal(yield* fs.readFileString(paths.sentinelPath), `${version}\n`); |
| 97 | assert.isFalse(yield* fs.exists(path.join(paths.versionDir, "t3-runtime-archive"))); | 96 | assert.isFalse(yield* fs.exists(path.join(paths.versionDir, "t3-runtime-archive"))); |
| 98 | if ((yield* HostProcessPlatform) !== "win32") { | ||
| 99 | // The old launcher must still be able to start this archive after the | ||
| 100 | // first npm-to-executable update, including from the final directory. | ||
| 101 | yield* fs.writeFileString(paths.entryPath, '#!/bin/sh\nprintf "%s\\n" "$@"\n'); | ||
| 102 | yield* fs.chmod(paths.entryPath, 0o755); | ||
| 103 | const runner = yield* ProcessRunner.make(); | ||
| 104 | const legacyStart = yield* runner.run({ | ||
| 105 | command: process.execPath, | ||
| 106 | args: [path.join(paths.versionDir, "node_modules/t3/dist/bin.mjs"), "serve"], | ||
| 107 | }); | ||
| 108 | assert.equal(Number(legacyStart.code), 0, legacyStart.stderr); | ||
| 109 | assert.equal(legacyStart.stdout.trim(), "serve"); | ||
| 110 | } | ||
| 111 | }), | 97 | }), |
| 112 | ); | 98 | ); |
| 113 | 99 | ||
| ... | @@ -223,40 +209,6 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => { | ... | @@ -223,40 +209,6 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => { |
| 223 | }), | 209 | }), |
| 224 | ); | 210 | ); |
| 225 | 211 | ||
| 226 | it.effect("backfills a cached archive without downloading or replacing it", () => | ||
| 227 | Effect.gen(function* () { | ||
| 228 | const fs = yield* FileSystem.FileSystem; | ||
| 229 | const path = yield* Path.Path; | ||
| 230 | const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pinned-legacy-cache-" }); | ||
| 231 | const cached = pinnedRuntimePaths(path, baseDir, version, "linux"); | ||
| 232 | const legacyEntry = path.join(cached.versionDir, "node_modules/t3/dist/bin.mjs"); | ||
| 233 | yield* fs.makeDirectory(cached.versionDir, { recursive: true }); | ||
| 234 | yield* fs.writeFileString(cached.entryPath, "cached executable\n"); | ||
| 235 | yield* fs.writeFileString(cached.sentinelPath, `${version}\n`); | ||
| 236 | const requests: string[] = []; | ||
| 237 | const commands: string[] = []; | ||
| 238 | yield* ensurePinnedRuntimeInstalled({ | ||
| 239 | baseDir, | ||
| 240 | version, | ||
| 241 | fs, | ||
| 242 | path, | ||
| 243 | platform: "linux", | ||
| 244 | arch: "x64", | ||
| 245 | httpClient: releaseHttpClient(yield* validChecksums, requests), | ||
| 246 | runner: extractingRunner(fs, path, commands), | ||
| 247 | validate: () => | ||
| 248 | fs.exists(legacyEntry).pipe( | ||
| 249 | Effect.flatMap((exists) => (exists ? Effect.void : Effect.die("missing legacy entry"))), | ||
| 250 | Effect.orDie, | ||
| 251 | ), | ||
| 252 | }); | ||
| 253 | assert.deepEqual(requests, []); | ||
| 254 | assert.deepEqual(commands, []); | ||
| 255 | assert.equal(yield* fs.readFileString(cached.entryPath), "cached executable\n"); | ||
| 256 | assert.equal(yield* fs.readFileString(cached.sentinelPath), `${version}\n`); | ||
| 257 | }), | ||
| 258 | ); | ||
| 259 | |||
| 260 | it.effect("preserves a completed runtime when validation fails", () => | 212 | it.effect("preserves a completed runtime when validation fails", () => |
| 261 | Effect.gen(function* () { | 213 | Effect.gen(function* () { |
| 262 | const fs = yield* FileSystem.FileSystem; | 214 | const fs = yield* FileSystem.FileSystem; |
apps/server/src/cloud/pinnedRuntime.ts+1-26| ... | @@ -8,7 +8,6 @@ import * as Option from "effect/Option"; | ... | @@ -8,7 +8,6 @@ import * as Option from "effect/Option"; |
| 8 | import * as Semaphore from "effect/Semaphore"; | 8 | import * as Semaphore from "effect/Semaphore"; |
| 9 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; | 9 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; |
| 10 | 10 | ||
| 11 | import { legacyCliLauncherScript } from "@t3tools/shared/legacyCliLauncher"; | ||
| 12 | import { | 11 | import { |
| 13 | CLI_RELEASE_CHECKSUMS_FILE, | 12 | CLI_RELEASE_CHECKSUMS_FILE, |
| 14 | cliArchiveFileName, | 13 | cliArchiveFileName, |
| ... | @@ -230,24 +229,6 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( | ... | @@ -230,24 +229,6 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( |
| 230 | input: PinnedRuntimeInstallInput, | 229 | input: PinnedRuntimeInstallInput, |
| 231 | ) { | 230 | ) { |
| 232 | const { fs } = input; | 231 | const { fs } = input; |
| 233 | // Old service launchers still use the npm entry point, including when an | ||
| 234 | // archive was cached before this compatibility wrapper existed. | ||
| 235 | const ensureLegacyEntry = Effect.fn("cloud.pinned_runtime.ensure_legacy_entry")( | ||
| 236 | function* (versionDir: string) { | ||
| 237 | const legacyDir = input.path.join(versionDir, "node_modules", "t3", "dist"); | ||
| 238 | const entryPath = input.path.join(legacyDir, "bin.mjs"); | ||
| 239 | if (yield* fs.exists(entryPath)) return; | ||
| 240 | yield* fs.makeDirectory(legacyDir, { recursive: true }); | ||
| 241 | yield* fs.writeFileString(entryPath, legacyCliLauncherScript("archive")); | ||
| 242 | }, | ||
| 243 | Effect.mapError( | ||
| 244 | (cause) => | ||
| 245 | new PinnedRuntimeInstallError({ | ||
| 246 | step: "writing the legacy service entry point", | ||
| 247 | cause, | ||
| 248 | }), | ||
| 249 | ), | ||
| 250 | ); | ||
| 251 | const paths = pinnedRuntimePaths(input.path, input.baseDir, input.version, input.platform); | 232 | const paths = pinnedRuntimePaths(input.path, input.baseDir, input.version, input.platform); |
| 252 | const [versionDirExists, entryExists, sentinel] = yield* Effect.all([ | 233 | const [versionDirExists, entryExists, sentinel] = yield* Effect.all([ |
| 253 | fs.exists(paths.versionDir), | 234 | fs.exists(paths.versionDir), |
| ... | @@ -261,7 +242,6 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( | ... | @@ -261,7 +242,6 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( |
| 261 | const alreadyPinned = | 242 | const alreadyPinned = |
| 262 | entryExists && Option.isSome(sentinel) && sentinel.value.trim() === input.version; | 243 | entryExists && Option.isSome(sentinel) && sentinel.value.trim() === input.version; |
| 263 | if (alreadyPinned) { | 244 | if (alreadyPinned) { |
| 264 | yield* ensureLegacyEntry(paths.versionDir); | ||
| 265 | yield* input.validate(paths); | 245 | yield* input.validate(paths); |
| 266 | return paths; | 246 | return paths; |
| 267 | } | 247 | } |
| ... | @@ -310,8 +290,6 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( | ... | @@ -310,8 +290,6 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( |
| 310 | return yield* Effect.gen(function* () { | 290 | return yield* Effect.gen(function* () { |
| 311 | yield* installFromArchive(input, stagingDir); | 291 | yield* installFromArchive(input, stagingDir); |
| 312 | 292 | ||
| 313 | yield* ensureLegacyEntry(stagingDir); | ||
| 314 | |||
| 315 | yield* input.validate(stagingPaths); | 293 | yield* input.validate(stagingPaths); |
| 316 | yield* fs | 294 | yield* fs |
| 317 | .writeFileString(stagingPaths.sentinelPath, `${input.version}\n`) | 295 | .writeFileString(stagingPaths.sentinelPath, `${input.version}\n`) |
| ... | @@ -350,10 +328,7 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( | ... | @@ -350,10 +328,7 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( |
| 350 | ), | 328 | ), |
| 351 | ), | 329 | ), |
| 352 | ); | 330 | ); |
| 353 | if (!published) { | 331 | if (!published) yield* input.validate(paths); |
| 354 | yield* ensureLegacyEntry(paths.versionDir); | ||
| 355 | yield* input.validate(paths); | ||
| 356 | } | ||
| 357 | return paths; | 332 | return paths; |
| 358 | }).pipe( | 333 | }).pipe( |
| 359 | Effect.ensuring(fs.remove(stagingDir, { recursive: true, force: true }).pipe(Effect.ignore)), | 334 | Effect.ensuring(fs.remove(stagingDir, { recursive: true, force: true }).pipe(Effect.ignore)), |
packages/shared/src/legacyCliLauncher.test.ts+8-8| ... | @@ -14,22 +14,22 @@ const hostPlatform = NodeOS.platform(); | ... | @@ -14,22 +14,22 @@ const hostPlatform = NodeOS.platform(); |
| 14 | const hostArch = NodeOS.arch(); | 14 | const hostArch = NodeOS.arch(); |
| 15 | 15 | ||
| 16 | // The fixture executable uses a POSIX shebang. The wrapper itself also runs on Windows. | 16 | // The fixture executable uses a POSIX shebang. The wrapper itself also runs on Windows. |
| 17 | it.skipIf(hostPlatform === "win32").each(["npm", "archive"] as const)( | 17 | it.skipIf(hostPlatform === "win32")( |
| 18 | "keeps %s service IPC, arguments, and termination connected", | 18 | "keeps service IPC, arguments, and termination connected", |
| 19 | async (distribution) => { | 19 | async () => { |
| 20 | const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-legacy-launcher-")); | 20 | const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-legacy-launcher-")); |
| 21 | const entry = NodePath.join(root, "node_modules/t3/dist/bin.mjs"); | 21 | const entry = NodePath.join(root, "node_modules/t3/dist/bin.mjs"); |
| 22 | const executable = | 22 | const executable = NodePath.join( |
| 23 | distribution === "archive" | 23 | root, |
| 24 | ? NodePath.join(root, "t3") | 24 | `node_modules/@t3code/t3-${hostPlatform}-${hostArch}/t3`, |
| 25 | : NodePath.join(root, `node_modules/@t3code/t3-${hostPlatform}-${hostArch}/t3`); | 25 | ); |
| 26 | await NodeFSP.mkdir(NodePath.dirname(entry), { recursive: true }); | 26 | await NodeFSP.mkdir(NodePath.dirname(entry), { recursive: true }); |
| 27 | await NodeFSP.mkdir(NodePath.dirname(executable), { recursive: true }); | 27 | await NodeFSP.mkdir(NodePath.dirname(executable), { recursive: true }); |
| 28 | await NodeFSP.writeFile( | 28 | await NodeFSP.writeFile( |
| 29 | NodePath.join(NodePath.dirname(executable), "package.json"), | 29 | NodePath.join(NodePath.dirname(executable), "package.json"), |
| 30 | '{"type":"commonjs"}', | 30 | '{"type":"commonjs"}', |
| 31 | ); | 31 | ); |
| 32 | await NodeFSP.writeFile(entry, legacyCliLauncherScript(distribution)); | 32 | await NodeFSP.writeFile(entry, legacyCliLauncherScript()); |
| 33 | await NodeFSP.writeFile( | 33 | await NodeFSP.writeFile( |
| 34 | executable, | 34 | executable, |
| 35 | `#!${process.execPath} | 35 | `#!${process.execPath} |
packages/shared/src/legacyCliLauncher.ts+15-9| ... | @@ -1,17 +1,23 @@ | ... | @@ -1,17 +1,23 @@ |
| 1 | /** Node entry point for service launchers installed before executable releases. */ | 1 | /** |
| 2 | export function legacyCliLauncherScript(distribution: "npm" | "archive"): string { | 2 | * `dist/bin.mjs` of the `t3` npm package: the entry point boot-service |
| 3 | const executable = | 3 | * launchers installed before 0.0.41 run with Node to start a new version they |
| 4 | distribution === "npm" | 4 | * just npm-installed. It forwards everything (arguments, stdio, the IPC |
| 5 | ? 'join(dirname(require.resolve("@t3code/t3-" + process.platform + "-" + process.arch + "/package.json")), executableName)' | 5 | * channel the launcher talks over, signals, exit status) to the platform |
| 6 | : 'resolve(dirname(fileURLToPath(import.meta.url)), "../../..", executableName)'; | 6 | * executable in the sibling `@t3code/t3-<platform>-<arch>` package. |
| 7 | * | ||
| 8 | * The first server started this way rewrites the service unit to run the | ||
| 9 | * executable directly, so nothing depends on this file after one update. | ||
| 10 | * Remove it once no supported release predates the executable (after the | ||
| 11 | * first stable release that ships it). | ||
| 12 | */ | ||
| 13 | export function legacyCliLauncherScript(): string { | ||
| 7 | return `import { spawn } from "node:child_process"; | 14 | return `import { spawn } from "node:child_process"; |
| 8 | import { constants } from "node:os"; | 15 | import { constants } from "node:os"; |
| 9 | import { dirname, join, resolve } from "node:path"; | 16 | import { dirname, join } from "node:path"; |
| 10 | import { fileURLToPath } from "node:url"; | ||
| 11 | import { createRequire } from "node:module"; | 17 | import { createRequire } from "node:module"; |
| 12 | const require = createRequire(import.meta.url); | 18 | const require = createRequire(import.meta.url); |
| 13 | const executableName = process.platform === "win32" ? "t3.exe" : "t3"; | 19 | const executableName = process.platform === "win32" ? "t3.exe" : "t3"; |
| 14 | const executable = ${executable}; | 20 | const executable = join(dirname(require.resolve("@t3code/t3-" + process.platform + "-" + process.arch + "/package.json")), executableName); |
| 15 | const ipc = process.send !== undefined; | 21 | const ipc = process.send !== undefined; |
| 16 | const child = spawn(executable, process.argv.slice(2), { | 22 | const child = spawn(executable, process.argv.slice(2), { |
| 17 | stdio: ipc ? ["inherit", "inherit", "inherit", "ipc"] : "inherit", | 23 | stdio: ipc ? ["inherit", "inherit", "inherit", "ipc"] : "inherit", |
scripts/build-npm-platform-packages.ts+1-1| ... | @@ -389,7 +389,7 @@ const stageLauncherPackage = Effect.fn("stageLauncherPackage")(function* (input: | ... | @@ -389,7 +389,7 @@ const stageLauncherPackage = Effect.fn("stageLauncherPackage")(function* (input: |
| 389 | // Older service updaters and launchers run this exact path with Node. | 389 | // Older service updaters and launchers run this exact path with Node. |
| 390 | // Keep it in the package so they can preflight and start the new executable. | 390 | // Keep it in the package so they can preflight and start the new executable. |
| 391 | yield* fs.makeDirectory(path.join(stageDir, "dist")); | 391 | yield* fs.makeDirectory(path.join(stageDir, "dist")); |
| 392 | yield* fs.writeFileString(path.join(stageDir, "dist/bin.mjs"), legacyCliLauncherScript("npm")); | 392 | yield* fs.writeFileString(path.join(stageDir, "dist/bin.mjs"), legacyCliLauncherScript()); |
| 393 | const readme = yield* path.fromFileUrl(new URL("../apps/server/README.md", import.meta.url)); | 393 | const readme = yield* path.fromFileUrl(new URL("../apps/server/README.md", import.meta.url)); |
| 394 | if (yield* fs.exists(readme)) { | 394 | if (yield* fs.exists(readme)) { |
| 395 | yield* fs.copyFile(readme, path.join(stageDir, "README.md")); | 395 | yield* fs.copyFile(readme, path.join(stageDir, "README.md")); |