From 2d76bb735b6bb3a2e681772abeb5b3a4d318ea89 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 3 Nov 2026 03:00:13 +0000 Subject: [PATCH 1/9] test(server): reproduce #29881 service install without a user session Without a logind session, `service install` refuses with user-manager-unavailable instead of enabling lingering to start the user manager, and its systemd user commands get no XDG_RUNTIME_DIR to find the user bus. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/bootService.test.ts | 54 +++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/apps/server/src/cloud/bootService.test.ts b/apps/server/src/cloud/bootService.test.ts index 4979f8d4f3418..a82978506340d 100644 --- a/apps/server/src/cloud/bootService.test.ts +++ b/apps/server/src/cloud/bootService.test.ts @@ -116,12 +116,15 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( const control: { failCommand: string | undefined; stateAfterStop?: string; - linger: string; + /** `undefined` makes `loginctl show-user` fail, as when logind does not know the user. */ + linger: string | undefined; + managerReachable: boolean; enabled: boolean; active: boolean; } = { failCommand: undefined, linger: "yes", + managerReachable: true, enabled: true, active: true, }; @@ -134,8 +137,10 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( invocations.push(input); timeouts.set(command, input.timeout); const failed = command === control.failCommand; - if (!failed && command === "loginctl enable-linger --no-ask-password 501") + if (!failed && command === "loginctl enable-linger --no-ask-password 501") { control.linger = "yes"; + control.managerReachable = true; + } if (!failed && command === "systemctl --user enable t3code.service") control.enabled = true; if (!failed && command === "systemctl --user restart t3code.service") control.active = true; // A timed-out stop may still stop the service. @@ -163,7 +168,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( : input.args[0] === "--version" ? `t3 v${/versions\/([^/]+)\//.exec(input.command)?.[1] ?? "1.2.3"}\n` : input.command === "loginctl" && input.args[0] === "show-user" - ? `${control.linger}\n` + ? `${control.linger ?? ""}\n` : input.args[1] === "is-enabled" ? control.enabled ? "enabled\n" @@ -172,6 +177,10 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( stderr: "", code: ChildProcessSpawner.ExitCode( failed || + (command === "systemctl --user show-environment" && !control.managerReachable) || + (input.command === "loginctl" && + input.args[0] === "show-user" && + control.linger === undefined) || (input.args[1] === "is-active" && !control.active) || (input.args[1] === "is-enabled" && !control.enabled) ? 1 @@ -322,6 +331,45 @@ it.layer(NodeServices.layer)("boot service install", (it) => { }), ); + // regression: #29881 + it.effect.each([ + { logind: "has lingering disabled", linger: "no" }, + { logind: "does not know the user", linger: undefined }, + ])( + "enables lingering to bring up an unreachable user manager when logind $logind", + ({ linger }) => + Effect.gen(function* () { + const { service, commands, control } = yield* makeHarness(); + control.linger = linger; + control.managerReachable = false; + + yield* service.install(); + + expect(commands).toContain("loginctl enable-linger --no-ask-password 501"); + expect((yield* service.status).current).toBe(true); + }), + ); + + // regression: #29881 + it.effect("points systemd user commands at the user's runtime directory when none is set", () => + Effect.gen(function* () { + const { service, invocations, makeService } = yield* makeHarness(); + yield* service.install(); + const inside = yield* makeService(undefined, "1.2.4", undefined, true); + yield* inside.install({ allowDowngrade: true }); + + const systemdCalls = invocations.filter(({ command }) => + ["systemctl", "busctl", "systemd-run", "loginctl"].includes(command), + ); + expect(new Set(systemdCalls.map(({ command }) => command)).size).toBe(4); + expect( + systemdCalls + .filter(({ env }) => env?.XDG_RUNTIME_DIR !== "/run/user/501") + .map(({ command, args }) => `${command} ${args.join(" ")}`), + ).toEqual([]); + }), + ); + it.effect.each([ { command: "systemctl --user show-environment", problem: "user-manager-unavailable" }, { command: "loginctl show-user 501 --property=Linger --value", problem: "linger-unavailable" }, From 12929031df20e9fa68474f6aeaadb6d9adef9b29 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 3 Nov 2026 03:08:16 +0000 Subject: [PATCH 2/9] fix(server): t3 service install brings up the user manager on hosts without a login session Without a logind session (no user@.service, lingering off, XDG_RUNTIME_DIR unset), `t3 service install` failed with user-manager-unavailable before it ever tried to enable lingering, and its systemd user commands could not find the user bus. On Linux, systemctl/busctl/systemd-run/loginctl now get XDG_RUNTIME_DIR=/run/user/ when the installer has none. When lingering is not on (disabled, or unknown to logind), install tries the unprivileged enable-linger first, then re-checks with a short bounded retry while the user manager comes up. Fixes pingdotgg/t3code#29881 Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/bootService.ts | 44 ++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts index 49e748e004d13..ba1b3eb33e84c 100644 --- a/apps/server/src/cloud/bootService.ts +++ b/apps/server/src/cloud/bootService.ts @@ -9,6 +9,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; +import * as Schedule from "effect/Schedule"; import { HttpClient } from "effect/http"; import * as Schema from "effect/Schema"; @@ -392,8 +393,7 @@ const decodeSystemdUnitPaths = Schema.decodeUnknownEffect( // The manager may not share the installer's environment; JSON preserves spaces in its paths. const readSystemdUnitDirectory = Effect.fn("cloud.boot_service.read_systemd_unit_directory")( - function* () { - const runner = yield* ProcessRunner.ProcessRunner; + function* (runner: ProcessRunner.ProcessRunner["Service"]) { const path = yield* Path.Path; const result = yield* runner.run({ command: "busctl", @@ -581,12 +581,26 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { const homeDir = yield* Config.String("HOME").pipe(Config.withDefault("")); const xdgConfigHome = yield* Config.String("XDG_CONFIG_HOME").pipe(Config.withDefault("")); const installerPath = yield* Config.String("PATH").pipe(Config.withDefault("")); + const xdgRuntimeDir = yield* Config.String("XDG_RUNTIME_DIR").pipe(Config.withDefault("")); const runningInsideService = (yield* Config.String(BOOT_SERVICE_UNIT_ENV).pipe(Config.withDefault(""))) === BOOT_SERVICE_UNIT_FILE; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const runner = yield* ProcessRunner.ProcessRunner; + const processRunner = yield* ProcessRunner.ProcessRunner; + // Without a login session nothing sets this, so `--user` commands cannot find the user bus. + // The directory only appears once lingering starts the manager, so it need not exist yet. + const userManagerEnv = + platform === "linux" && xdgRuntimeDir === "" && uid !== undefined + ? { XDG_RUNTIME_DIR: `/run/user/${uid}` } + : undefined; + const runner: ProcessRunner.ProcessRunner["Service"] = + userManagerEnv === undefined + ? processRunner + : { + run: (request) => + processRunner.run({ ...request, env: { ...userManagerEnv, ...request.env } }), + }; const host = input.host ?? { execPath: hostExecPath }; const runtimePaths = pinnedRuntimePaths(path, input.baseDir, input.cliVersion, platform); const versionsDir = pinnedRuntimeVersionsDir(path, input.baseDir); @@ -617,7 +631,7 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { ), ).join(":"); - const systemdUnitDir = platform === "linux" ? yield* readSystemdUnitDirectory() : undefined; + const systemdUnitDir = platform === "linux" ? yield* readSystemdUnitDirectory(runner) : undefined; const detectedManager = selectBootServiceManager({ platform, homeDir, @@ -752,19 +766,31 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { const requireSystemdPrerequisites = Effect.gen(function* () { const problems = yield* readSystemdProblems(false); - const unavailable = problems.find((problem) => problem !== "linger-disabled"); - if (unavailable) return yield* new BootServicePrerequisiteError({ problem: unavailable }); - if (!problems.includes("linger-disabled")) return; + // Logind reports an unknown user, one without a session yet, as linger-unavailable. + const notLingering = problems.find( + (problem) => problem === "linger-disabled" || problem === "linger-unavailable", + ); + if (problems[0] === undefined) return; + if (notLingering === undefined) { + return yield* new BootServicePrerequisiteError({ problem: problems[0] }); + } yield* runStep("enabling lingering for this user", "loginctl", [ "enable-linger", "--no-ask-password", ...(uid === undefined ? [] : [String(uid)]), ]).pipe( Effect.mapError( - (cause) => new BootServicePrerequisiteError({ problem: "linger-disabled", cause }), + (cause) => new BootServicePrerequisiteError({ problem: notLingering, cause }), ), ); - const remaining = yield* readSystemdProblems(false); + // Lingering starts the user manager asynchronously. + const remaining = yield* readSystemdProblems(false).pipe( + Effect.repeat({ + schedule: Schedule.spaced("500 millis"), + times: 10, + while: (current) => current.includes("user-manager-unavailable"), + }), + ); if (remaining[0]) return yield* new BootServicePrerequisiteError({ problem: remaining[0] }); }); From a877fcb75ef566a4cb7e5fe754096fcba7a89ddb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 3 Nov 2026 03:10:43 +0000 Subject: [PATCH 3/9] fix(server): a refused enable-linger asks for an administrator Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/bootService.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts index ba1b3eb33e84c..f1a24fe80587a 100644 --- a/apps/server/src/cloud/bootService.ts +++ b/apps/server/src/cloud/bootService.ts @@ -766,12 +766,9 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { const requireSystemdPrerequisites = Effect.gen(function* () { const problems = yield* readSystemdProblems(false); - // Logind reports an unknown user, one without a session yet, as linger-unavailable. - const notLingering = problems.find( - (problem) => problem === "linger-disabled" || problem === "linger-unavailable", - ); if (problems[0] === undefined) return; - if (notLingering === undefined) { + // Logind reports an unknown user, one without a session yet, as linger-unavailable. + if (!problems.includes("linger-disabled") && !problems.includes("linger-unavailable")) { return yield* new BootServicePrerequisiteError({ problem: problems[0] }); } yield* runStep("enabling lingering for this user", "loginctl", [ @@ -780,7 +777,7 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { ...(uid === undefined ? [] : [String(uid)]), ]).pipe( Effect.mapError( - (cause) => new BootServicePrerequisiteError({ problem: notLingering, cause }), + (cause) => new BootServicePrerequisiteError({ problem: "linger-disabled", cause }), ), ); // Lingering starts the user manager asynchronously. From 923d89e69b4fecc0693f5b444f43029174931fdb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 3 Nov 2026 03:17:28 +0000 Subject: [PATCH 4/9] fix(server): enable lingering for an unknown user only when its manager is down Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/bootService.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts index f1a24fe80587a..6bf95a3554b9f 100644 --- a/apps/server/src/cloud/bootService.ts +++ b/apps/server/src/cloud/bootService.ts @@ -767,8 +767,10 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { const requireSystemdPrerequisites = Effect.gen(function* () { const problems = yield* readSystemdProblems(false); if (problems[0] === undefined) return; - // Logind reports an unknown user, one without a session yet, as linger-unavailable. - if (!problems.includes("linger-disabled") && !problems.includes("linger-unavailable")) { + // Logind reports a user without a session, whose manager is not running yet, as linger-unavailable. + const withoutSession = + problems.includes("linger-unavailable") && problems.includes("user-manager-unavailable"); + if (!problems.includes("linger-disabled") && !withoutSession) { return yield* new BootServicePrerequisiteError({ problem: problems[0] }); } yield* runStep("enabling lingering for this user", "loginctl", [ From c90f865d638c2c0fb552b422a52f905ba55d917b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 3 Nov 2026 03:28:02 +0000 Subject: [PATCH 5/9] fix(server): install the unit where the user manager lingering starts looks for it Without a login session the user manager is unreachable when the boot service is constructed, so its unit directory fell back to the installer shell's XDG_CONFIG_HOME. Once enable-linger brings the manager up it may use another config root, and `systemctl --user enable` then cannot find the unit. Install now re-reads the manager's unit directory after the prerequisites bring it up and uses that for the unit path, plan, writes and status. Also covers the bounded wait for a manager that starts late or never starts. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/bootService.test.ts | 86 +++++++++++++++++++++-- apps/server/src/cloud/bootService.ts | 61 +++++++++------- 2 files changed, 118 insertions(+), 29 deletions(-) diff --git a/apps/server/src/cloud/bootService.test.ts b/apps/server/src/cloud/bootService.test.ts index a82978506340d..6969594a8adc4 100644 --- a/apps/server/src/cloud/bootService.test.ts +++ b/apps/server/src/cloud/bootService.test.ts @@ -1,6 +1,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { expect, it } from "@effect/vitest"; import * as HostProcess from "@t3tools/shared/HostProcess"; +import * as Clock from "effect/Clock"; import * as ConfigProvider from "effect/ConfigProvider"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; @@ -119,12 +120,14 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( /** `undefined` makes `loginctl show-user` fail, as when logind does not know the user. */ linger: string | undefined; managerReachable: boolean; + lingerStartsManager: boolean; enabled: boolean; active: boolean; } = { failCommand: undefined, linger: "yes", managerReachable: true, + lingerStartsManager: true, enabled: true, active: true, }; @@ -139,10 +142,14 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( const failed = command === control.failCommand; if (!failed && command === "loginctl enable-linger --no-ask-password 501") { control.linger = "yes"; - control.managerReachable = true; + control.managerReachable ||= control.lingerStartsManager; } if (!failed && command === "systemctl --user enable t3code.service") control.enabled = true; if (!failed && command === "systemctl --user restart t3code.service") control.active = true; + // The manager loads units only from its own unit directory. + const unitOutsideManager = + command === "systemctl --user enable t3code.service" && + !(yield* fs.exists(path.join(managerUnitDir, "t3code.service"))); // A timed-out stop may still stop the service. if (command === "systemctl --user stop t3code.service") control.active = false; if ( @@ -150,7 +157,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( (command === "systemctl --user stop t3code.service" || command.startsWith("launchctl bootout --wait ")) ) { - yield* fs.writeFileString(statePath, control.stateAfterStop).pipe(Effect.orDie); + yield* fs.writeFileString(statePath, control.stateAfterStop); } return { stdout: @@ -177,7 +184,9 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( stderr: "", code: ChildProcessSpawner.ExitCode( failed || - (command === "systemctl --user show-environment" && !control.managerReachable) || + unitOutsideManager || + ((command === "systemctl --user show-environment" || input.command === "busctl") && + !control.managerReachable) || (input.command === "loginctl" && input.args[0] === "show-user" && control.linger === undefined) || @@ -192,7 +201,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( stdoutInvalidUtf8: false, stderrInvalidUtf8: false, }; - }), + }, Effect.orDie), }); const makeService = ( environmentPath: string | undefined = installerPath, @@ -350,6 +359,75 @@ it.layer(NodeServices.layer)("boot service install", (it) => { }), ); + // regression: #29881 + it.effect("installs into the unit directory of the user manager that lingering starts", () => + Effect.gen(function* () { + const path = yield* Path.Path; + const fs = yield* FileSystem.FileSystem; + const shellConfigHome = yield* fs.makeTempDirectoryScoped({ prefix: "t3-xdg-config-" }); + const { makeService, home, control } = yield* makeHarness( + "linux", + undefined, + shellConfigHome, + "", + ); + control.linger = "no"; + control.managerReachable = false; + const service = yield* makeService(); + + const plan = yield* service.install(); + + const unitPath = path.join(home, ".config", "systemd", "user", "t3code.service"); + expect(plan.unitPath).toBe(unitPath); + expect(yield* service.status).toMatchObject({ current: true, unitPath }); + }), + ); + + // regression: #29881 + it.effect.each([ + { manager: "starts after three probes", upAfterPauses: 3, pauses: 3 }, + { manager: "never starts", upAfterPauses: undefined, pauses: 10 }, + ])( + "waits a bounded time for the user manager lingering starts when it $manager", + ({ upAfterPauses, pauses }) => + Effect.gen(function* () { + const { service, fs, commands, control } = yield* makeHarness(); + const { unitPath } = yield* service.status; + control.linger = "no"; + control.managerReachable = false; + control.lingerStartsManager = false; + const clock = yield* Clock.Clock; + const waited: number[] = []; + + const result = yield* service.install().pipe( + Effect.provideService(Clock.Clock, { + ...clock, + sleep: (duration) => + Effect.sync(() => { + if (waited.push(Duration.toMillis(duration)) === upAfterPauses) { + control.managerReachable = true; + } + }), + }), + Effect.result, + ); + + expect(waited).toEqual(Array.from({ length: pauses }, () => 500)); + expect( + commands.filter((command) => command === "systemctl --user show-environment"), + ).toHaveLength(pauses + 2); + if (upAfterPauses === undefined) { + expect(result).toMatchObject({ + failure: { _tag: "BootServicePrerequisiteError", problem: "user-manager-unavailable" }, + }); + expect(yield* fs.exists(unitPath)).toBe(false); + } else { + expect(result._tag).toBe("Success"); + expect((yield* service.status).current).toBe(true); + } + }), + ); + // regression: #29881 it.effect("points systemd user commands at the user's runtime directory when none is set", () => Effect.gen(function* () { diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts index 6bf95a3554b9f..a39347879ac39 100644 --- a/apps/server/src/cloud/bootService.ts +++ b/apps/server/src/cloud/bootService.ts @@ -393,8 +393,7 @@ const decodeSystemdUnitPaths = Schema.decodeUnknownEffect( // The manager may not share the installer's environment; JSON preserves spaces in its paths. const readSystemdUnitDirectory = Effect.fn("cloud.boot_service.read_systemd_unit_directory")( - function* (runner: ProcessRunner.ProcessRunner["Service"]) { - const path = yield* Path.Path; + function* (runner: ProcessRunner.ProcessRunner["Service"], path: Path.Path) { const result = yield* runner.run({ command: "busctl", args: [ @@ -631,18 +630,29 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { ), ).join(":"); - const systemdUnitDir = platform === "linux" ? yield* readSystemdUnitDirectory(runner) : undefined; - const detectedManager = selectBootServiceManager({ - platform, - homeDir, - xdgConfigHome, - systemdUnitDir, - uid, - path, - environmentPath, - }); - const unitPath = detectedManager?.unitPath ?? ""; const logPath = path.join(input.logsDir, BOOT_SERVICE_LOG_FILE); + const resolveTarget = (systemdUnitDir: string | undefined) => { + const manager = selectBootServiceManager({ + platform, + homeDir, + xdgConfigHome, + systemdUnitDir, + uid, + path, + environmentPath, + }); + const unitPath = manager?.unitPath ?? ""; + const plan: BootServicePlan = { + program: [runtimePaths.entryPath, "__service-launcher"], + baseDir: input.baseDir, + logPath, + unitPath, + }; + return { manager, unitPath, plan }; + }; + let systemdUnitDir = + platform === "linux" ? yield* readSystemdUnitDirectory(runner, path) : undefined; + let { manager: detectedManager, unitPath, plan } = resolveTarget(systemdUnitDir); const statePath = path.join(input.baseDir, "runtime", SERVICE_STATE_FILE); const restartPendingPath = path.join(input.baseDir, "runtime", SERVICE_RESTART_PENDING_FILE); const writeDurably = (filePath: string, contents: string) => @@ -664,13 +674,6 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { ); }), ).pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause }))); - const plan: BootServicePlan = { - program: [runtimePaths.entryPath, "__service-launcher"], - baseDir: input.baseDir, - logPath, - unitPath, - }; - const requireManager = Effect.suspend(() => detectedManager === undefined ? new BootServiceUnsupportedError({ platform }) @@ -797,13 +800,20 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { readonly allowDowngrade?: boolean; readonly start?: boolean; }) { - const manager = yield* requireManager; + let manager = yield* requireManager; yield* fs .makeDirectory(input.logsDir, { recursive: true }) .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause }))); if (manager.kind === "systemd") { yield* requireSystemdPrerequisites.pipe(Effect.tapError(logFailure)); + // Without a session the manager was unreachable at construction, and the one lingering + // started may use another config root than this shell's XDG_CONFIG_HOME. + if (systemdUnitDir === undefined) { + systemdUnitDir = yield* readSystemdUnitDirectory(runner, path); + ({ manager: detectedManager, unitPath, plan } = resolveTarget(systemdUnitDir)); + manager = yield* requireManager; + } } yield* ensurePinnedRuntimeInstalled({ @@ -1048,7 +1058,8 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { }).pipe(Effect.withSpan("cloud.boot_service.uninstall")); const status: BootService["Service"]["status"] = Effect.gen(function* () { - if (detectedManager === undefined) { + const manager = detectedManager; + if (manager === undefined) { return { supported: false, installed: false, current: false, unitPath, logPath }; } if (!(yield* fs.exists(unitPath))) { @@ -1066,11 +1077,11 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { : undefined; const installedBaseDir = bootServiceBaseDirOf(unit); const normalizeUnit = (contents: string) => - detectedManager.kind === "launchd" + manager.kind === "launchd" ? contents.replace(/(PATH<\/key>\n\s*)[^<]*(<\/string>)/, "$1$2") : contents; const problems: BootServiceProblem[] = - detectedManager.kind === "systemd" ? [...(yield* readSystemdProblems(true))] : []; + manager.kind === "systemd" ? [...(yield* readSystemdProblems(true))] : []; if (yield* fs.exists(restartPendingPath)) problems.push("restart-pending"); return { supported: true, @@ -1080,7 +1091,7 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { problems, current: problems.length === 0 && - normalizeUnit(unit) === normalizeUnit(detectedManager.render(plan)) && + normalizeUnit(unit) === normalizeUnit(manager.render(plan)) && runtimeEntryExists && Option.isSome(runtimeSentinel) && runtimeSentinel.value.trim() === input.cliVersion && From 87c209127d037a0ccfd941b2235a047c28591ac8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 3 Nov 2026 03:37:40 +0000 Subject: [PATCH 6/9] fix(server): a host without systemd keeps its user-manager advice Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/bootService.test.ts | 16 ++++++++++++++++ apps/server/src/cloud/bootService.ts | 15 +++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/apps/server/src/cloud/bootService.test.ts b/apps/server/src/cloud/bootService.test.ts index 6969594a8adc4..7fd2a51381e15 100644 --- a/apps/server/src/cloud/bootService.test.ts +++ b/apps/server/src/cloud/bootService.test.ts @@ -359,6 +359,22 @@ it.layer(NodeServices.layer)("boot service install", (it) => { }), ); + // regression: #29881 + it.effect("reports the unreachable user manager when lingering cannot start it", () => + Effect.gen(function* () { + const { service, fs, statePath, control } = yield* makeHarness(); + control.linger = undefined; + control.managerReachable = false; + control.failCommand = "loginctl enable-linger --no-ask-password 501"; + + expect(yield* service.install().pipe(Effect.flip)).toMatchObject({ + _tag: "BootServicePrerequisiteError", + problem: "user-manager-unavailable", + }); + expect(yield* fs.exists(statePath)).toBe(false); + }), + ); + // regression: #29881 it.effect("installs into the unit directory of the user manager that lingering starts", () => Effect.gen(function* () { diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts index a39347879ac39..d8381afe31a14 100644 --- a/apps/server/src/cloud/bootService.ts +++ b/apps/server/src/cloud/bootService.ts @@ -769,12 +769,14 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { const requireSystemdPrerequisites = Effect.gen(function* () { const problems = yield* readSystemdProblems(false); - if (problems[0] === undefined) return; + const firstProblem = problems[0]; + if (firstProblem === undefined) return; // Logind reports a user without a session, whose manager is not running yet, as linger-unavailable. const withoutSession = problems.includes("linger-unavailable") && problems.includes("user-manager-unavailable"); - if (!problems.includes("linger-disabled") && !withoutSession) { - return yield* new BootServicePrerequisiteError({ problem: problems[0] }); + const lingerDisabled = problems.includes("linger-disabled"); + if (!lingerDisabled && !withoutSession) { + return yield* new BootServicePrerequisiteError({ problem: firstProblem }); } yield* runStep("enabling lingering for this user", "loginctl", [ "enable-linger", @@ -782,7 +784,12 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { ...(uid === undefined ? [] : [String(uid)]), ]).pipe( Effect.mapError( - (cause) => new BootServicePrerequisiteError({ problem: "linger-disabled", cause }), + // Without a session this also fails on hosts that do not run systemd at all. + (cause) => + new BootServicePrerequisiteError({ + problem: lingerDisabled ? "linger-disabled" : firstProblem, + cause, + }), ), ); // Lingering starts the user manager asynchronously. From 53b7b439e699a1ee6d51246eb44f1a0821518620 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 3 Nov 2026 03:49:31 +0000 Subject: [PATCH 7/9] fix(server): sudo t3 service install still refuses to set up root Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/bootService.test.ts | 21 +++++++++++++++++++-- apps/server/src/cloud/bootService.ts | 6 +++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/apps/server/src/cloud/bootService.test.ts b/apps/server/src/cloud/bootService.test.ts index 7fd2a51381e15..4d3c9dc0d4ba8 100644 --- a/apps/server/src/cloud/bootService.test.ts +++ b/apps/server/src/cloud/bootService.test.ts @@ -140,7 +140,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( invocations.push(input); timeouts.set(command, input.timeout); const failed = command === control.failCommand; - if (!failed && command === "loginctl enable-linger --no-ask-password 501") { + if (!failed && command.startsWith("loginctl enable-linger --no-ask-password ")) { control.linger = "yes"; control.managerReachable ||= control.lingerStartsManager; } @@ -209,6 +209,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( serviceBaseDir = baseDir, insideService = false, hostExecPath = "/usr/bin/t3", + underSudo = false, ) => Effect.gen(function* () { const paths = pinnedRuntimePaths(path, serviceBaseDir, cliVersion, platform); @@ -226,7 +227,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( Effect.provide( Layer.mergeAll( Layer.succeed(HostProcess.Platform, platform), - Layer.succeed(HostProcess.UserId, 501), + Layer.succeed(HostProcess.UserId, underSudo ? 0 : 501), Layer.succeed(HostProcess.ExecutablePath, hostExecPath), Layer.succeed( HttpClient.HttpClient, @@ -241,6 +242,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( ? {} : { PATH: environmentPath }), ...(insideService ? { T3_BOOT_SERVICE_UNIT: "t3code.service" } : {}), + ...(underSudo ? { SUDO_USER: "julius" } : {}), }, }), ), @@ -375,6 +377,21 @@ it.layer(NodeServices.layer)("boot service install", (it) => { }), ); + it.effect("does not enable lingering for root under sudo", () => + Effect.gen(function* () { + const { makeService, commands, control } = yield* makeHarness(); + control.linger = undefined; + control.managerReachable = false; + const service = yield* makeService(undefined, "1.2.3", undefined, false, undefined, true); + + expect(yield* service.install().pipe(Effect.flip)).toMatchObject({ + _tag: "BootServicePrerequisiteError", + problem: "user-manager-unavailable", + }); + expect(commands.filter((command) => command.includes("enable-linger"))).toEqual([]); + }), + ); + // regression: #29881 it.effect("installs into the unit directory of the user manager that lingering starts", () => Effect.gen(function* () { diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts index d8381afe31a14..46ad54419d923 100644 --- a/apps/server/src/cloud/bootService.ts +++ b/apps/server/src/cloud/bootService.ts @@ -581,6 +581,7 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { const xdgConfigHome = yield* Config.String("XDG_CONFIG_HOME").pipe(Config.withDefault("")); const installerPath = yield* Config.String("PATH").pipe(Config.withDefault("")); const xdgRuntimeDir = yield* Config.String("XDG_RUNTIME_DIR").pipe(Config.withDefault("")); + const sudoUser = yield* Config.String("SUDO_USER").pipe(Config.withDefault("")); const runningInsideService = (yield* Config.String(BOOT_SERVICE_UNIT_ENV).pipe(Config.withDefault(""))) === BOOT_SERVICE_UNIT_FILE; @@ -772,8 +773,11 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { const firstProblem = problems[0]; if (firstProblem === undefined) return; // Logind reports a user without a session, whose manager is not running yet, as linger-unavailable. + // Root under sudo has no session either, but must not get a separate root service. const withoutSession = - problems.includes("linger-unavailable") && problems.includes("user-manager-unavailable"); + problems.includes("linger-unavailable") && + problems.includes("user-manager-unavailable") && + !(uid === 0 && sudoUser !== ""); const lingerDisabled = problems.includes("linger-disabled"); if (!lingerDisabled && !withoutSession) { return yield* new BootServicePrerequisiteError({ problem: firstProblem }); From a66d091cc1980833cd41b95cc0d6a2b070b7779a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 3 Nov 2026 04:07:37 +0000 Subject: [PATCH 8/9] fix(server): root under sudo does not reach its own user manager Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/bootService.test.ts | 20 +++++++++++++++++++- apps/server/src/cloud/bootService.ts | 9 +++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/server/src/cloud/bootService.test.ts b/apps/server/src/cloud/bootService.test.ts index 4d3c9dc0d4ba8..e75288557336f 100644 --- a/apps/server/src/cloud/bootService.test.ts +++ b/apps/server/src/cloud/bootService.test.ts @@ -120,6 +120,8 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( /** `undefined` makes `loginctl show-user` fail, as when logind does not know the user. */ linger: string | undefined; managerReachable: boolean; + /** The user bus is found only through `XDG_RUNTIME_DIR`, as without a login session. */ + busNeedsRuntimeDir: boolean; lingerStartsManager: boolean; enabled: boolean; active: boolean; @@ -127,6 +129,7 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( failCommand: undefined, linger: "yes", managerReachable: true, + busNeedsRuntimeDir: false, lingerStartsManager: true, enabled: true, active: true, @@ -186,7 +189,8 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( failed || unitOutsideManager || ((command === "systemctl --user show-environment" || input.command === "busctl") && - !control.managerReachable) || + (!control.managerReachable || + (control.busNeedsRuntimeDir && input.env?.XDG_RUNTIME_DIR === undefined))) || (input.command === "loginctl" && input.args[0] === "show-user" && control.linger === undefined) || @@ -377,6 +381,20 @@ it.layer(NodeServices.layer)("boot service install", (it) => { }), ); + it.effect("does not reach a running root user manager under sudo", () => + Effect.gen(function* () { + const { makeService, commands, control } = yield* makeHarness(); + control.busNeedsRuntimeDir = true; + const service = yield* makeService(undefined, "1.2.3", undefined, false, undefined, true); + + expect(yield* service.install().pipe(Effect.flip)).toMatchObject({ + _tag: "BootServicePrerequisiteError", + problem: "user-manager-unavailable", + }); + expect(commands).not.toContain("systemctl --user enable t3code.service"); + }), + ); + it.effect("does not enable lingering for root under sudo", () => Effect.gen(function* () { const { makeService, commands, control } = yield* makeHarness(); diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts index 46ad54419d923..0e8eb4356c05f 100644 --- a/apps/server/src/cloud/bootService.ts +++ b/apps/server/src/cloud/bootService.ts @@ -581,7 +581,9 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { const xdgConfigHome = yield* Config.String("XDG_CONFIG_HOME").pipe(Config.withDefault("")); const installerPath = yield* Config.String("PATH").pipe(Config.withDefault("")); const xdgRuntimeDir = yield* Config.String("XDG_RUNTIME_DIR").pipe(Config.withDefault("")); - const sudoUser = yield* Config.String("SUDO_USER").pipe(Config.withDefault("")); + // Root under sudo must not reach or start a separate root service. + const rootUnderSudo = + uid === 0 && (yield* Config.String("SUDO_USER").pipe(Config.withDefault(""))) !== ""; const runningInsideService = (yield* Config.String(BOOT_SERVICE_UNIT_ENV).pipe(Config.withDefault(""))) === BOOT_SERVICE_UNIT_FILE; @@ -591,7 +593,7 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { // Without a login session nothing sets this, so `--user` commands cannot find the user bus. // The directory only appears once lingering starts the manager, so it need not exist yet. const userManagerEnv = - platform === "linux" && xdgRuntimeDir === "" && uid !== undefined + platform === "linux" && xdgRuntimeDir === "" && uid !== undefined && !rootUnderSudo ? { XDG_RUNTIME_DIR: `/run/user/${uid}` } : undefined; const runner: ProcessRunner.ProcessRunner["Service"] = @@ -773,11 +775,10 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { const firstProblem = problems[0]; if (firstProblem === undefined) return; // Logind reports a user without a session, whose manager is not running yet, as linger-unavailable. - // Root under sudo has no session either, but must not get a separate root service. const withoutSession = problems.includes("linger-unavailable") && problems.includes("user-manager-unavailable") && - !(uid === 0 && sudoUser !== ""); + !rootUnderSudo; const lingerDisabled = problems.includes("linger-disabled"); if (!lingerDisabled && !withoutSession) { return yield* new BootServicePrerequisiteError({ problem: firstProblem }); From a3150d7072cfbd3ad36d6dd43ca1c8272a184ce7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 3 Nov 2026 04:13:32 +0000 Subject: [PATCH 9/9] fix(server): sudo t3 service install never enables lingering for root Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/bootService.test.ts | 7 +++++-- apps/server/src/cloud/bootService.ts | 6 ++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/server/src/cloud/bootService.test.ts b/apps/server/src/cloud/bootService.test.ts index e75288557336f..aaac33c7e9380 100644 --- a/apps/server/src/cloud/bootService.test.ts +++ b/apps/server/src/cloud/bootService.test.ts @@ -395,10 +395,13 @@ it.layer(NodeServices.layer)("boot service install", (it) => { }), ); - it.effect("does not enable lingering for root under sudo", () => + it.effect.each([ + { logind: "does not know root", linger: undefined }, + { logind: "has root lingering disabled", linger: "no" }, + ])("does not enable lingering for root under sudo when logind $logind", ({ linger }) => Effect.gen(function* () { const { makeService, commands, control } = yield* makeHarness(); - control.linger = undefined; + control.linger = linger; control.managerReachable = false; const service = yield* makeService(undefined, "1.2.3", undefined, false, undefined, true); diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts index 0e8eb4356c05f..1c92c14f2a2dc 100644 --- a/apps/server/src/cloud/bootService.ts +++ b/apps/server/src/cloud/bootService.ts @@ -776,11 +776,9 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { if (firstProblem === undefined) return; // Logind reports a user without a session, whose manager is not running yet, as linger-unavailable. const withoutSession = - problems.includes("linger-unavailable") && - problems.includes("user-manager-unavailable") && - !rootUnderSudo; + problems.includes("linger-unavailable") && problems.includes("user-manager-unavailable"); const lingerDisabled = problems.includes("linger-disabled"); - if (!lingerDisabled && !withoutSession) { + if (rootUnderSudo || (!lingerDisabled && !withoutSession)) { return yield* new BootServicePrerequisiteError({ problem: firstProblem }); } yield* runStep("enabling lingering for this user", "loginctl", [