From 6e2059e77e648f09b5dcac90da83900a708e0e31 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 17:16:12 +0000 Subject: [PATCH 1/4] test(server): launcher protocol preflight gives Windows hosts Windows steps Fails on main: the blocked-update reason always names curl | sh, which -a and ~/.local/bin/t3 service install, none of which apply on Windows (#34435). AI-assisted; verified with the tests and tools listed above. --- .../server/src/cloud/servicePreflight.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/server/src/cloud/servicePreflight.test.ts b/apps/server/src/cloud/servicePreflight.test.ts index efc37266be8b0..91487d68141af 100644 --- a/apps/server/src/cloud/servicePreflight.test.ts +++ b/apps/server/src/cloud/servicePreflight.test.ts @@ -33,6 +33,27 @@ it.each([1])("blocks legacy launcher protocol %i with exact recovery steps", (la expect(reason).toContain("which -a t3"); }); +// regression: #34435 Windows hosts were told to run `curl | sh` and `~/.local/bin/t3 service install` +it("gives Windows hosts recovery steps that run on Windows", () => { + const platform = Object.getOwnPropertyDescriptor(process, "platform")!; + Object.defineProperty(process, "platform", { ...platform, value: "win32" }); + try { + const result = runServicePreflight({ + databasePath: "C:\\missing\\state.sqlite", + launcherProtocol: 3, + version: "9.8.7", + }); + const reason = result.status === "blocked" ? result.reason : ""; + expect(reason).toMatch(new RegExp(`\\b${SERVICE_LAUNCHER_PROTOCOL}\\b`)); + expect(reason).toContain("9.8.7"); + expect(reason).not.toContain("| sh"); + expect(reason).not.toContain("which -a"); + expect(reason).not.toContain("~/.local/bin/t3"); + } finally { + Object.defineProperty(process, "platform", platform); + } +}); + it.layer(NodeServices.layer)("service preflight database", (it) => { it.effect("blocks a database that a newer build migrated", () => Effect.gen(function* () { From 9a197b23506c9a86b941fa7916a36deedb21c2cc Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 19:25:44 +0000 Subject: [PATCH 2/4] fix(server): give Windows hosts Windows steps when an old service launcher blocks an update A remote update blocked by an old launcher protocol always told the host to run `curl | sh`, `which -a t3` and `~/.local/bin/t3 service install`, none of which work on Windows, where `t3 service install` does not exist. The preflight now takes the host platform from HostProcess.Platform and, on Windows, names the PowerShell installer pinned to the target version, `where.exe`, the `t3.cmd` path to start the launcher from, and that remote updates never move a Windows launcher, so the swap repeats on each launcher protocol bump. Fixes pingdotgg/t3code#34435 AI-assisted; verified with the tests and tools listed above. --- apps/server/src/cli/servicePreflight.ts | 9 ++++- .../server/src/cloud/servicePreflight.test.ts | 37 ++++++++++--------- apps/server/src/cloud/servicePreflight.ts | 16 ++++++-- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/apps/server/src/cli/servicePreflight.ts b/apps/server/src/cli/servicePreflight.ts index eefb2a35d37cb..9f01677141051 100644 --- a/apps/server/src/cli/servicePreflight.ts +++ b/apps/server/src/cli/servicePreflight.ts @@ -1,3 +1,4 @@ +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Console from "effect/Console"; import * as Effect from "effect/Effect"; import { Command, Flag } from "effect/cli"; @@ -10,8 +11,12 @@ export const servicePreflightCommand = Command.make("__service-preflight", { }).pipe( Command.unlisted, Command.withHandler(({ databasePath, launcherProtocol }) => - Console.log(JSON.stringify(runServicePreflight({ databasePath, launcherProtocol }))).pipe( - Effect.asVoid, + HostProcess.Platform.pipe( + Effect.flatMap((platform) => + Console.log( + JSON.stringify(runServicePreflight({ databasePath, launcherProtocol, platform })), + ), + ), ), ), ); diff --git a/apps/server/src/cloud/servicePreflight.test.ts b/apps/server/src/cloud/servicePreflight.test.ts index 91487d68141af..54ea28c70d148 100644 --- a/apps/server/src/cloud/servicePreflight.test.ts +++ b/apps/server/src/cloud/servicePreflight.test.ts @@ -20,6 +20,7 @@ it.each([1])("blocks legacy launcher protocol %i with exact recovery steps", (la const result = runServicePreflight({ databasePath: "/missing/state.sqlite", launcherProtocol, + platform: "linux", version: "9.8.7", }); @@ -35,23 +36,18 @@ it.each([1])("blocks legacy launcher protocol %i with exact recovery steps", (la // regression: #34435 Windows hosts were told to run `curl | sh` and `~/.local/bin/t3 service install` it("gives Windows hosts recovery steps that run on Windows", () => { - const platform = Object.getOwnPropertyDescriptor(process, "platform")!; - Object.defineProperty(process, "platform", { ...platform, value: "win32" }); - try { - const result = runServicePreflight({ - databasePath: "C:\\missing\\state.sqlite", - launcherProtocol: 3, - version: "9.8.7", - }); - const reason = result.status === "blocked" ? result.reason : ""; - expect(reason).toMatch(new RegExp(`\\b${SERVICE_LAUNCHER_PROTOCOL}\\b`)); - expect(reason).toContain("9.8.7"); - expect(reason).not.toContain("| sh"); - expect(reason).not.toContain("which -a"); - expect(reason).not.toContain("~/.local/bin/t3"); - } finally { - Object.defineProperty(process, "platform", platform); - } + const result = runServicePreflight({ + databasePath: "C:\\missing\\state.sqlite", + launcherProtocol: 3, + platform: "win32", + version: "9.8.7", + }); + const reason = result.status === "blocked" ? result.reason : ""; + expect(reason).toMatch(new RegExp(`\\b${SERVICE_LAUNCHER_PROTOCOL}\\b`)); + expect(reason).toContain("9.8.7"); + expect(reason).not.toContain("| sh"); + expect(reason).not.toContain("which -a"); + expect(reason).not.toContain("~/.local/bin/t3"); }); it.layer(NodeServices.layer)("service preflight database", (it) => { @@ -68,7 +64,11 @@ it.layer(NodeServices.layer)("service preflight database", (it) => { const insert = db.prepare("INSERT INTO effect_sql_migrations VALUES (?, ?)"); for (const [id, name] of migrationManifest) insert.run(id, name); const preflight = () => - runServicePreflight({ databasePath, launcherProtocol: SERVICE_LAUNCHER_PROTOCOL }); + runServicePreflight({ + databasePath, + launcherProtocol: SERVICE_LAUNCHER_PROTOCOL, + platform: "linux", + }); expect(preflight().status).toBe("ready"); expect(databaseMigrationHead(databasePath)).toBe(BUILD_MIGRATION_HEAD); @@ -90,6 +90,7 @@ it.layer(NodeServices.layer)("service preflight database", (it) => { runServicePreflight({ databasePath: unreadable, launcherProtocol: SERVICE_LAUNCHER_PROTOCOL, + platform: "linux", }).status, ).toBe("blocked"); // A missing database is a fresh install. diff --git a/apps/server/src/cloud/servicePreflight.ts b/apps/server/src/cloud/servicePreflight.ts index 9479f3f4a8194..fadcc0bb19a7f 100644 --- a/apps/server/src/cloud/servicePreflight.ts +++ b/apps/server/src/cloud/servicePreflight.ts @@ -17,6 +17,7 @@ export type ServicePreflightResult = export function runServicePreflight(input: { readonly databasePath: string; readonly launcherProtocol: number; + readonly platform: NodeJS.Platform; readonly version?: string; }): ServicePreflightResult { const version = input.version ?? packageJson.version; @@ -28,9 +29,18 @@ export function runServicePreflight(input: { // reinstall itself and report success, so name the exact install and the path check. reason: [ `The installed service launcher speaks protocol ${input.launcherProtocol}, but t3@${version} requires protocol ${SERVICE_LAUNCHER_PROTOCOL}.`, - `On the server machine, install this exact CLI: \`curl -fsSL https://t3.codes/install.sh | T3CODE_VERSION=${version} sh\`.`, - `Check that \`t3 --version\` prints ${version} and that \`which -a t3\` lists no older t3 (such as an npm or nvm install) first, then run \`~/.local/bin/t3 service install\` (the installer's default path).`, - "Updating the desktop app does not update this CLI, and `t3 service update` only repairs the service with whichever CLI runs it.", + ...(input.platform === "win32" + ? [ + `On the server machine, install this exact CLI in PowerShell: \`$env:T3CODE_VERSION="${version}"; irm https://t3.codes/install.ps1 | iex\`.`, + `Check that \`t3 --version\` prints ${version} and that \`where.exe t3\` lists no older t3 first.`, + `Windows has no \`t3 service install\`, so change whatever runs \`t3 __service-launcher\` to start it from \`%USERPROFILE%\\.local\\bin\\t3.cmd\` (the installer's default path) and restart it. Remote updates do not move a Windows launcher, so repeat this whenever a release changes the launcher protocol.`, + "Updating the desktop app does not update this CLI.", + ] + : [ + `On the server machine, install this exact CLI: \`curl -fsSL https://t3.codes/install.sh | T3CODE_VERSION=${version} sh\`.`, + `Check that \`t3 --version\` prints ${version} and that \`which -a t3\` lists no older t3 (such as an npm or nvm install) first, then run \`~/.local/bin/t3 service install\` (the installer's default path).`, + "Updating the desktop app does not update this CLI, and `t3 service update` only repairs the service with whichever CLI runs it.", + ]), ].join(" "), }; } From b45926d401915ce4d689d6e4ece331a9f4996529 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 19:53:04 +0000 Subject: [PATCH 3/4] fix(server): service launcher repoints the Windows t3.cmd after a remote update On Linux and macOS a committed remote update repoints the systemd unit or launchd plist, so the launcher restarts on the new runtime. Windows has no unit, so a Windows launcher stayed on the version that first started it and every launcher protocol bump blocked remote updates again. With no service unit, the launcher now moves recorded installer t3.cmd shims (exactly as the install script writes them, into this home's runtime) forward to the committed version, never backwards. The Windows preflight text no longer says to repeat the manual swap on each protocol bump. Fixes pingdotgg/t3code#34435 AI-assisted; verified with the tests and tools listed above. --- apps/server/src/cloud/servicePreflight.ts | 2 +- apps/server/src/serviceLauncher.test.ts | 26 ++++++++++++-- apps/server/src/serviceLauncher.ts | 41 ++++++++++++++++++++++- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/apps/server/src/cloud/servicePreflight.ts b/apps/server/src/cloud/servicePreflight.ts index fadcc0bb19a7f..515b26008d472 100644 --- a/apps/server/src/cloud/servicePreflight.ts +++ b/apps/server/src/cloud/servicePreflight.ts @@ -33,7 +33,7 @@ export function runServicePreflight(input: { ? [ `On the server machine, install this exact CLI in PowerShell: \`$env:T3CODE_VERSION="${version}"; irm https://t3.codes/install.ps1 | iex\`.`, `Check that \`t3 --version\` prints ${version} and that \`where.exe t3\` lists no older t3 first.`, - `Windows has no \`t3 service install\`, so change whatever runs \`t3 __service-launcher\` to start it from \`%USERPROFILE%\\.local\\bin\\t3.cmd\` (the installer's default path) and restart it. Remote updates do not move a Windows launcher, so repeat this whenever a release changes the launcher protocol.`, + `Windows has no \`t3 service install\`, so change whatever runs \`t3 __service-launcher\` to start it from \`%USERPROFILE%\\.local\\bin\\t3.cmd\` (the installer's default path) and restart it. Later remote updates repoint that t3.cmd, so a launcher started through it moves forward on its next restart.`, "Updating the desktop app does not update this CLI.", ] : [ diff --git a/apps/server/src/serviceLauncher.test.ts b/apps/server/src/serviceLauncher.test.ts index 3304211d9e7c6..70522ed98e2e1 100644 --- a/apps/server/src/serviceLauncher.test.ts +++ b/apps/server/src/serviceLauncher.test.ts @@ -395,6 +395,7 @@ it.layer(NodeServices.layer)("service state persistence", (it) => { render = renderBootServiceUnit, missingUnit = false, legacyInstall = false, + windowsShim = false, ) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; @@ -407,6 +408,16 @@ it.layer(NodeServices.layer)("service state persistence", (it) => { yield* fs.writeFileString(databasePath, "before trial"); const unit = yield* fakeServiceUnit(fs, path, root, "1.0.0", render); if (missingUnit) yield* fs.remove(unit.path); + const shimPath = path.join(root, "bin", "t3.cmd"); + if (windowsShim) { + yield* fs.makeDirectory(path.dirname(shimPath), { recursive: true }); + const target = path.join(root, "runtime", "versions", "1.0.0", "t3.exe"); + yield* fs.writeFileString(shimPath, `@echo off\r\n"${target}" %*`); + yield* fs.makeDirectory(path.join(root, "runtime"), { recursive: true }); + yield* fs.writeFileString(path.join(root, "runtime", "launchers"), `${shimPath}\r\n`); + } + // Windows has no service unit; the launcher runs through the installer's shim. + const launcherOptions = windowsShim ? {} : { serviceUnit: unit }; const childSource = ` import assert from "node:assert/strict"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; @@ -462,7 +473,7 @@ if (context.update?.status === "pending") { const launcher = new Launcher( root, yield* Effect.promise(() => readServiceState(statePath)), - { serviceUnit: unit }, + launcherOptions, ); const failure = yield* Effect.promise(() => launcher.run().then( @@ -480,13 +491,13 @@ if (context.update?.status === "pending") { const fresh = new Launcher( root, yield* Effect.promise(() => readServiceState(statePath)), - { serviceUnit: unit }, + launcherOptions, ); const running = fresh.run(); yield* Effect.promise(() => fresh.stop("SIGTERM")); yield* Effect.promise(() => running); }); - return { fs, root, unit, recover }; + return { fs, root, unit, shimPath, recover }; }); it.effect.each([ @@ -516,6 +527,15 @@ if (context.update?.status === "pending") { }), ); + it.effect("repoints the installer's t3.cmd on commit when there is no service unit", () => + Effect.gen(function* () { + const { fs, root, shimPath } = yield* commitUpdate(renderBootServiceUnit, false, false, true); + const path = yield* Path.Path; + const target = path.join(root, "runtime", "versions", "1.1.0", "t3.exe"); + assert.equal(yield* fs.readFileString(shimPath), `@echo off\r\n"${target}" %*`); + }), + ); + it.effect( "keeps a durable commit through unit failure and retries a failed reload on restart", () => diff --git a/apps/server/src/serviceLauncher.ts b/apps/server/src/serviceLauncher.ts index e6d3431f39b3b..6990ca48b3410 100644 --- a/apps/server/src/serviceLauncher.ts +++ b/apps/server/src/serviceLauncher.ts @@ -139,6 +139,14 @@ function repointServiceUnit(contents: string, version: string): string { return contents; } +// A `t3.cmd` exactly as the install script or `t3 update` writes it, running a version in `versionsDir`. +const generatedShimVersion = (contents: string, versionsDir: string) => { + const target = /^@echo off\r?\n"([^"]+)" %\*\s*$/.exec(contents)?.[1]; + if (target === undefined) return undefined; + const relative = NodePath.relative(versionsDir, target); + return /^([^\\/]+)[\\/]t3\.exe$/.exec(relative)?.[1]; +}; + const DB_FILE_SUFFIXES = ["", "-wal", "-shm"] as const; const RESTORE_MARKER = ".restore-pending"; @@ -972,7 +980,7 @@ export class Launcher { // Selection is durable before this best-effort unit reconciliation. async #repointServiceUnit(version: string): Promise { const unit = this.#serviceUnit; - if (unit === undefined) return; + if (unit === undefined) return this.#repointShims(version); try { const release = await acquireServiceSelectionLock(unit.path); try { @@ -1008,6 +1016,37 @@ export class Launcher { } } + /** + * Windows has no unit to repoint, so a launcher started through the installer's `t3.cmd` + * restarts on whatever the shim runs. Move recorded shims forward with the commit. + */ + async #repointShims(version: string): Promise { + const runtimeDir = NodePath.join(this.#baseDir, "runtime"); + const versionsDir = NodePath.join(runtimeDir, "versions"); + const recorded = await NodeFSP.readFile(NodePath.join(runtimeDir, "launchers"), "utf8").catch( + () => "", + ); + for (const shim of recorded.split(/\r?\n/)) { + if (!shim.toLowerCase().endsWith(".cmd")) continue; + try { + const contents = await NodeFSP.readFile(shim, "utf8"); + const current = generatedShimVersion(contents, versionsDir); + if ( + current === undefined || + (isExactServiceVersion(current) && compareExactServiceVersions(current, version) >= 0) + ) { + continue; + } + const target = NodePath.join(versionsDir, version, "t3.exe"); + await replaceFileDurably(shim, `@echo off\r\n"${target}" %*`); + } catch (error) { + process.stderr.write( + `[service-launcher] Could not point ${shim} at t3@${version}: ${error instanceof Error ? error.message : String(error)}\n`, + ); + } + } + } + async #handlePreparedTimeout(child: ManagedChild): Promise { const pending = this.#state.update; if (this.#child !== child || child.role !== "trial" || pending?.status !== "pending") { From 6ea69c96d2e20581ecc996bc4bfd8dee949d1335 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 20:03:13 +0000 Subject: [PATCH 4/4] fix(server): Windows launcher recovery selects the installed runtime Following the Windows steps left service-state.json selecting the old server: install.ps1 installs the runtime and rewrites t3.cmd but does not touch the service selection, so the new launcher started the old server with a context it rejects. The steps now stop the launcher and write a service state that selects the installed version with no update record before starting it again. This also drops the launcher's t3.cmd repoint: local shim writers take no lock it could share, so a concurrent install or `t3 update` could be downgraded. The message says plainly that the manual swap repeats on each launcher protocol change until Windows has a service installer. Fixes pingdotgg/t3code#34435 AI-assisted; verified with the tests and tools listed above. --- .../server/src/cloud/servicePreflight.test.ts | 3 ++ apps/server/src/cloud/servicePreflight.ts | 5 ++- apps/server/src/serviceLauncher.test.ts | 26 ++---------- apps/server/src/serviceLauncher.ts | 41 +------------------ 4 files changed, 11 insertions(+), 64 deletions(-) diff --git a/apps/server/src/cloud/servicePreflight.test.ts b/apps/server/src/cloud/servicePreflight.test.ts index 54ea28c70d148..c66b2070202a4 100644 --- a/apps/server/src/cloud/servicePreflight.test.ts +++ b/apps/server/src/cloud/servicePreflight.test.ts @@ -48,6 +48,9 @@ it("gives Windows hosts recovery steps that run on Windows", () => { expect(reason).not.toContain("| sh"); expect(reason).not.toContain("which -a"); expect(reason).not.toContain("~/.local/bin/t3"); + expect(reason).toContain( + `service-state.json" -Value '{"protocol":${SERVICE_LAUNCHER_PROTOCOL},"activeVersion":"9.8.7"}'`, + ); }); it.layer(NodeServices.layer)("service preflight database", (it) => { diff --git a/apps/server/src/cloud/servicePreflight.ts b/apps/server/src/cloud/servicePreflight.ts index 515b26008d472..2e808440e8f5a 100644 --- a/apps/server/src/cloud/servicePreflight.ts +++ b/apps/server/src/cloud/servicePreflight.ts @@ -33,7 +33,10 @@ export function runServicePreflight(input: { ? [ `On the server machine, install this exact CLI in PowerShell: \`$env:T3CODE_VERSION="${version}"; irm https://t3.codes/install.ps1 | iex\`.`, `Check that \`t3 --version\` prints ${version} and that \`where.exe t3\` lists no older t3 first.`, - `Windows has no \`t3 service install\`, so change whatever runs \`t3 __service-launcher\` to start it from \`%USERPROFILE%\\.local\\bin\\t3.cmd\` (the installer's default path) and restart it. Later remote updates repoint that t3.cmd, so a launcher started through it moves forward on its next restart.`, + // Without `t3 service install`, service-state.json still selects the old server, which + // rejects the new launcher's context; select the installed runtime by hand. + `Windows has no \`t3 service install\`, so do its steps by hand: stop whatever runs \`t3 __service-launcher\`, then select this runtime and clear the stale update record: \`Set-Content -LiteralPath "$HOME\\.t3\\runtime\\service-state.json" -Value '{"protocol":${SERVICE_LAUNCHER_PROTOCOL},"activeVersion":"${version}"}'\` (use your T3CODE_HOME in place of \`$HOME\\.t3\` if you set one).`, + `Start the launcher again from \`$HOME\\.local\\bin\\t3.cmd\` (the installer's default path). Nothing moves a Windows launcher forward after a remote update, so repeat these steps whenever a release changes the launcher protocol.`, "Updating the desktop app does not update this CLI.", ] : [ diff --git a/apps/server/src/serviceLauncher.test.ts b/apps/server/src/serviceLauncher.test.ts index 70522ed98e2e1..3304211d9e7c6 100644 --- a/apps/server/src/serviceLauncher.test.ts +++ b/apps/server/src/serviceLauncher.test.ts @@ -395,7 +395,6 @@ it.layer(NodeServices.layer)("service state persistence", (it) => { render = renderBootServiceUnit, missingUnit = false, legacyInstall = false, - windowsShim = false, ) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; @@ -408,16 +407,6 @@ it.layer(NodeServices.layer)("service state persistence", (it) => { yield* fs.writeFileString(databasePath, "before trial"); const unit = yield* fakeServiceUnit(fs, path, root, "1.0.0", render); if (missingUnit) yield* fs.remove(unit.path); - const shimPath = path.join(root, "bin", "t3.cmd"); - if (windowsShim) { - yield* fs.makeDirectory(path.dirname(shimPath), { recursive: true }); - const target = path.join(root, "runtime", "versions", "1.0.0", "t3.exe"); - yield* fs.writeFileString(shimPath, `@echo off\r\n"${target}" %*`); - yield* fs.makeDirectory(path.join(root, "runtime"), { recursive: true }); - yield* fs.writeFileString(path.join(root, "runtime", "launchers"), `${shimPath}\r\n`); - } - // Windows has no service unit; the launcher runs through the installer's shim. - const launcherOptions = windowsShim ? {} : { serviceUnit: unit }; const childSource = ` import assert from "node:assert/strict"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; @@ -473,7 +462,7 @@ if (context.update?.status === "pending") { const launcher = new Launcher( root, yield* Effect.promise(() => readServiceState(statePath)), - launcherOptions, + { serviceUnit: unit }, ); const failure = yield* Effect.promise(() => launcher.run().then( @@ -491,13 +480,13 @@ if (context.update?.status === "pending") { const fresh = new Launcher( root, yield* Effect.promise(() => readServiceState(statePath)), - launcherOptions, + { serviceUnit: unit }, ); const running = fresh.run(); yield* Effect.promise(() => fresh.stop("SIGTERM")); yield* Effect.promise(() => running); }); - return { fs, root, unit, shimPath, recover }; + return { fs, root, unit, recover }; }); it.effect.each([ @@ -527,15 +516,6 @@ if (context.update?.status === "pending") { }), ); - it.effect("repoints the installer's t3.cmd on commit when there is no service unit", () => - Effect.gen(function* () { - const { fs, root, shimPath } = yield* commitUpdate(renderBootServiceUnit, false, false, true); - const path = yield* Path.Path; - const target = path.join(root, "runtime", "versions", "1.1.0", "t3.exe"); - assert.equal(yield* fs.readFileString(shimPath), `@echo off\r\n"${target}" %*`); - }), - ); - it.effect( "keeps a durable commit through unit failure and retries a failed reload on restart", () => diff --git a/apps/server/src/serviceLauncher.ts b/apps/server/src/serviceLauncher.ts index 6990ca48b3410..e6d3431f39b3b 100644 --- a/apps/server/src/serviceLauncher.ts +++ b/apps/server/src/serviceLauncher.ts @@ -139,14 +139,6 @@ function repointServiceUnit(contents: string, version: string): string { return contents; } -// A `t3.cmd` exactly as the install script or `t3 update` writes it, running a version in `versionsDir`. -const generatedShimVersion = (contents: string, versionsDir: string) => { - const target = /^@echo off\r?\n"([^"]+)" %\*\s*$/.exec(contents)?.[1]; - if (target === undefined) return undefined; - const relative = NodePath.relative(versionsDir, target); - return /^([^\\/]+)[\\/]t3\.exe$/.exec(relative)?.[1]; -}; - const DB_FILE_SUFFIXES = ["", "-wal", "-shm"] as const; const RESTORE_MARKER = ".restore-pending"; @@ -980,7 +972,7 @@ export class Launcher { // Selection is durable before this best-effort unit reconciliation. async #repointServiceUnit(version: string): Promise { const unit = this.#serviceUnit; - if (unit === undefined) return this.#repointShims(version); + if (unit === undefined) return; try { const release = await acquireServiceSelectionLock(unit.path); try { @@ -1016,37 +1008,6 @@ export class Launcher { } } - /** - * Windows has no unit to repoint, so a launcher started through the installer's `t3.cmd` - * restarts on whatever the shim runs. Move recorded shims forward with the commit. - */ - async #repointShims(version: string): Promise { - const runtimeDir = NodePath.join(this.#baseDir, "runtime"); - const versionsDir = NodePath.join(runtimeDir, "versions"); - const recorded = await NodeFSP.readFile(NodePath.join(runtimeDir, "launchers"), "utf8").catch( - () => "", - ); - for (const shim of recorded.split(/\r?\n/)) { - if (!shim.toLowerCase().endsWith(".cmd")) continue; - try { - const contents = await NodeFSP.readFile(shim, "utf8"); - const current = generatedShimVersion(contents, versionsDir); - if ( - current === undefined || - (isExactServiceVersion(current) && compareExactServiceVersions(current, version) >= 0) - ) { - continue; - } - const target = NodePath.join(versionsDir, version, "t3.exe"); - await replaceFileDurably(shim, `@echo off\r\n"${target}" %*`); - } catch (error) { - process.stderr.write( - `[service-launcher] Could not point ${shim} at t3@${version}: ${error instanceof Error ? error.message : String(error)}\n`, - ); - } - } - } - async #handlePreparedTimeout(child: ManagedChild): Promise { const pending = this.#state.update; if (this.#child !== child || child.role !== "trial" || pending?.status !== "pending") {