Skip to content

Commit 6ea69c9

Browse files
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 #34435 AI-assisted; verified with the tests and tools listed above.
1 parent b45926d commit 6ea69c9

4 files changed

Lines changed: 11 additions & 64 deletions

File tree

‎apps/server/src/cloud/servicePreflight.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ it("gives Windows hosts recovery steps that run on Windows", () => {
4848
expect(reason).not.toContain("| sh");
4949
expect(reason).not.toContain("which -a");
5050
expect(reason).not.toContain("~/.local/bin/t3");
51+
expect(reason).toContain(
52+
`service-state.json" -Value '{"protocol":${SERVICE_LAUNCHER_PROTOCOL},"activeVersion":"9.8.7"}'`,
53+
);
5154
});
5255

5356
it.layer(NodeServices.layer)("service preflight database", (it) => {

‎apps/server/src/cloud/servicePreflight.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ export function runServicePreflight(input: {
3333
? [
3434
`On the server machine, install this exact CLI in PowerShell: \`$env:T3CODE_VERSION="${version}"; irm https://t3.codes/install.ps1 | iex\`.`,
3535
`Check that \`t3 --version\` prints ${version} and that \`where.exe t3\` lists no older t3 first.`,
36-
`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.`,
36+
// Without `t3 service install`, service-state.json still selects the old server, which
37+
// rejects the new launcher's context; select the installed runtime by hand.
38+
`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).`,
39+
`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.`,
3740
"Updating the desktop app does not update this CLI.",
3841
]
3942
: [

‎apps/server/src/serviceLauncher.test.ts‎

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,6 @@ it.layer(NodeServices.layer)("service state persistence", (it) => {
395395
render = renderBootServiceUnit,
396396
missingUnit = false,
397397
legacyInstall = false,
398-
windowsShim = false,
399398
) =>
400399
Effect.gen(function* () {
401400
const fs = yield* FileSystem.FileSystem;
@@ -408,16 +407,6 @@ it.layer(NodeServices.layer)("service state persistence", (it) => {
408407
yield* fs.writeFileString(databasePath, "before trial");
409408
const unit = yield* fakeServiceUnit(fs, path, root, "1.0.0", render);
410409
if (missingUnit) yield* fs.remove(unit.path);
411-
const shimPath = path.join(root, "bin", "t3.cmd");
412-
if (windowsShim) {
413-
yield* fs.makeDirectory(path.dirname(shimPath), { recursive: true });
414-
const target = path.join(root, "runtime", "versions", "1.0.0", "t3.exe");
415-
yield* fs.writeFileString(shimPath, `@echo off\r\n"${target}" %*`);
416-
yield* fs.makeDirectory(path.join(root, "runtime"), { recursive: true });
417-
yield* fs.writeFileString(path.join(root, "runtime", "launchers"), `${shimPath}\r\n`);
418-
}
419-
// Windows has no service unit; the launcher runs through the installer's shim.
420-
const launcherOptions = windowsShim ? {} : { serviceUnit: unit };
421410
const childSource = `
422411
import assert from "node:assert/strict";
423412
import { existsSync, readFileSync, writeFileSync } from "node:fs";
@@ -473,7 +462,7 @@ if (context.update?.status === "pending") {
473462
const launcher = new Launcher(
474463
root,
475464
yield* Effect.promise(() => readServiceState(statePath)),
476-
launcherOptions,
465+
{ serviceUnit: unit },
477466
);
478467
const failure = yield* Effect.promise(() =>
479468
launcher.run().then(
@@ -491,13 +480,13 @@ if (context.update?.status === "pending") {
491480
const fresh = new Launcher(
492481
root,
493482
yield* Effect.promise(() => readServiceState(statePath)),
494-
launcherOptions,
483+
{ serviceUnit: unit },
495484
);
496485
const running = fresh.run();
497486
yield* Effect.promise(() => fresh.stop("SIGTERM"));
498487
yield* Effect.promise(() => running);
499488
});
500-
return { fs, root, unit, shimPath, recover };
489+
return { fs, root, unit, recover };
501490
});
502491

503492
it.effect.each([
@@ -527,15 +516,6 @@ if (context.update?.status === "pending") {
527516
}),
528517
);
529518

530-
it.effect("repoints the installer's t3.cmd on commit when there is no service unit", () =>
531-
Effect.gen(function* () {
532-
const { fs, root, shimPath } = yield* commitUpdate(renderBootServiceUnit, false, false, true);
533-
const path = yield* Path.Path;
534-
const target = path.join(root, "runtime", "versions", "1.1.0", "t3.exe");
535-
assert.equal(yield* fs.readFileString(shimPath), `@echo off\r\n"${target}" %*`);
536-
}),
537-
);
538-
539519
it.effect(
540520
"keeps a durable commit through unit failure and retries a failed reload on restart",
541521
() =>

‎apps/server/src/serviceLauncher.ts‎

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,6 @@ function repointServiceUnit(contents: string, version: string): string {
139139
return contents;
140140
}
141141

142-
// A `t3.cmd` exactly as the install script or `t3 update` writes it, running a version in `versionsDir`.
143-
const generatedShimVersion = (contents: string, versionsDir: string) => {
144-
const target = /^@echo off\r?\n"([^"]+)" %\*\s*$/.exec(contents)?.[1];
145-
if (target === undefined) return undefined;
146-
const relative = NodePath.relative(versionsDir, target);
147-
return /^([^\\/]+)[\\/]t3\.exe$/.exec(relative)?.[1];
148-
};
149-
150142
const DB_FILE_SUFFIXES = ["", "-wal", "-shm"] as const;
151143
const RESTORE_MARKER = ".restore-pending";
152144

@@ -980,7 +972,7 @@ export class Launcher {
980972
// Selection is durable before this best-effort unit reconciliation.
981973
async #repointServiceUnit(version: string): Promise<void> {
982974
const unit = this.#serviceUnit;
983-
if (unit === undefined) return this.#repointShims(version);
975+
if (unit === undefined) return;
984976
try {
985977
const release = await acquireServiceSelectionLock(unit.path);
986978
try {
@@ -1016,37 +1008,6 @@ export class Launcher {
10161008
}
10171009
}
10181010

1019-
/**
1020-
* Windows has no unit to repoint, so a launcher started through the installer's `t3.cmd`
1021-
* restarts on whatever the shim runs. Move recorded shims forward with the commit.
1022-
*/
1023-
async #repointShims(version: string): Promise<void> {
1024-
const runtimeDir = NodePath.join(this.#baseDir, "runtime");
1025-
const versionsDir = NodePath.join(runtimeDir, "versions");
1026-
const recorded = await NodeFSP.readFile(NodePath.join(runtimeDir, "launchers"), "utf8").catch(
1027-
() => "",
1028-
);
1029-
for (const shim of recorded.split(/\r?\n/)) {
1030-
if (!shim.toLowerCase().endsWith(".cmd")) continue;
1031-
try {
1032-
const contents = await NodeFSP.readFile(shim, "utf8");
1033-
const current = generatedShimVersion(contents, versionsDir);
1034-
if (
1035-
current === undefined ||
1036-
(isExactServiceVersion(current) && compareExactServiceVersions(current, version) >= 0)
1037-
) {
1038-
continue;
1039-
}
1040-
const target = NodePath.join(versionsDir, version, "t3.exe");
1041-
await replaceFileDurably(shim, `@echo off\r\n"${target}" %*`);
1042-
} catch (error) {
1043-
process.stderr.write(
1044-
`[service-launcher] Could not point ${shim} at t3@${version}: ${error instanceof Error ? error.message : String(error)}\n`,
1045-
);
1046-
}
1047-
}
1048-
}
1049-
10501011
async #handlePreparedTimeout(child: ManagedChild): Promise<void> {
10511012
const pending = this.#state.update;
10521013
if (this.#child !== child || child.role !== "trial" || pending?.status !== "pending") {

0 commit comments

Comments
 (0)