diff --git a/apps/server/src/cli/service.ts b/apps/server/src/cli/service.ts index 62edb31d14b62..884cd28c9df87 100644 --- a/apps/server/src/cli/service.ts +++ b/apps/server/src/cli/service.ts @@ -314,6 +314,14 @@ const serviceProtectCommand = Command.make("protect", { ); } + yield* Console.log( + [ + "Keeps T3 Code running when agents fill this machine's memory:", + " - reserves memory for the T3 Code server, so the kernel takes memory from agents first", + " - makes the out-of-memory killer pick agents before the server", + "It adds systemd drop-in files and changes nothing else. Undo with `t3 service protect --undo`.", + ].join("\n"), + ); const names = remote.map((unit) => unit.replace(/\.service$/, "")).join(" and "); const protectRemote = remote.length > 0 && @@ -328,7 +336,7 @@ const serviceProtectCommand = Command.make("protect", { }); yield* Console.log( [ - "As root, through sudo:", + "Drop-ins it writes, as root through sudo:", ...formatDropIns(plan.system), "As you:", ...formatDropIns([plan.managerDefaults, ...plan.user]), diff --git a/apps/server/src/cloud/serviceProtection.test.ts b/apps/server/src/cloud/serviceProtection.test.ts index b2e4a1506cb31..eef78cd3f82e7 100644 --- a/apps/server/src/cloud/serviceProtection.test.ts +++ b/apps/server/src/cloud/serviceProtection.test.ts @@ -11,6 +11,7 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; +import type * as ChildProcess from "effect/process/ChildProcess"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; import * as ProcessRunner from "../processRunner.ts"; @@ -122,13 +123,13 @@ it.each([ expect(ServiceProtection.loadedRemoteAccessUnits(shown)).toEqual(expected); }); -const fakeSudo = (exitCode: number, onRun: () => void) => +const fakeSudo = (exitCode: number, onRun: (command: ChildProcess.StandardCommand) => void) => ChildProcessSpawner.make((command) => Effect.gen(function* () { if (command._tag !== "StandardCommand" || command.command !== "sudo") { return yield* Effect.die("Unexpected command"); } - onRun(); + onRun(command); return ChildProcessSpawner.makeHandle({ pid: ChildProcessSpawner.ProcessId(123), stdout: Stream.empty, @@ -180,11 +181,13 @@ it.effect("lowers no score before the manager default is in place, and undoes in writesManagerDropIn = false, ) => { let atSudo: ReturnType | undefined; + let sudoOptions: ChildProcess.CommandOptions | undefined; return effect.pipe( Effect.provideService( ChildProcessSpawner.ChildProcessSpawner, - fakeSudo(exitCode, () => { + fakeSudo(exitCode, (command) => { atSudo = present(); + sudoOptions = command.options; if (writesManagerDropIn) { NodeFS.mkdirSync(NodePath.dirname(managerDropIn), { recursive: true }); NodeFS.writeFileSync(managerDropIn, "[Service]\nOOMScoreAdjust=-900\n"); @@ -192,7 +195,7 @@ it.effect("lowers no score before the manager default is in place, and undoes in }), ), Effect.exit, - Effect.map((exit) => ({ exit, atSudo })), + Effect.map((exit) => ({ exit, atSudo, sudoOptions })), ); }; const refused = yield* withSudo(ServiceProtection.applyProtection(plan), 1); @@ -209,6 +212,8 @@ it.effect("lowers no score before the manager default is in place, and undoes in const applied = yield* withSudo(ServiceProtection.applyProtection(plan), 0); expect(applied.atSudo).toEqual({ managerDefaults: true, user: 0 }); + // sudo needs the controlling terminal to ask for a password. + expect(applied.sudoOptions).toMatchObject({ detached: false, stdin: "inherit" }); expect(present()).toEqual({ managerDefaults: true, user: plan.user.length }); const removed = yield* withSudo(ServiceProtection.removeProtection(plan), 0); diff --git a/apps/server/src/cloud/serviceProtection.ts b/apps/server/src/cloud/serviceProtection.ts index b23cd36e3a545..780aee83b4c48 100644 --- a/apps/server/src/cloud/serviceProtection.ts +++ b/apps/server/src/cloud/serviceProtection.ts @@ -240,7 +240,10 @@ const runPrivileged = Effect.fn("ServiceProtection.runPrivileged")(function* (sc const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const exitCode = yield* spawner .exitCode( + // Effect detaches children on POSIX by default; sudo then has no controlling + // terminal and cannot ask for a password. ChildProcess.make("sudo", ["/bin/sh", "-c", script], { + detached: false, stdin: "inherit", stdout: "inherit", stderr: "inherit",