diff --git a/apps/server/src/provider/acp/AcpSessionRuntime.processTree.test.ts b/apps/server/src/provider/acp/AcpSessionRuntime.processTree.test.ts index aba92ae2a7e7..9067ee93f096 100644 --- a/apps/server/src/provider/acp/AcpSessionRuntime.processTree.test.ts +++ b/apps/server/src/provider/acp/AcpSessionRuntime.processTree.test.ts @@ -4,7 +4,7 @@ import * as NodeFS from "node:fs"; import * as NodePath from "node:path"; import * as NodeURL from "node:url"; -import { describe, expect, it } from "@effect/vitest"; +import { describe, expect, it, vi } from "@effect/vitest"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as Clock from "effect/Clock"; import * as Effect from "effect/Effect"; @@ -242,6 +242,80 @@ describe("terminatePosixOwnedProcessTree", () => { }), ); + it("contains a packaged-runtime command without requiring cgroup delegation", () => { + if (HostProcessPlatform.defaultValue() !== "linux") return; + const scratchRoot = NodePath.join(process.cwd(), "tmp"); + NodeFS.mkdirSync(scratchRoot, { recursive: true }); + const scratch = NodeFS.mkdtempSync(NodePath.join(scratchRoot, "acp-cgroup-wrapper-fake-")); + const leasePath = NodePath.join(scratch, "lease with spaces"); + const outputPath = NodePath.join(scratch, "argv output"); + NodeFS.mkdirSync(leasePath); + NodeFS.writeFileSync(NodePath.join(leasePath, "cgroup.procs"), ""); + const relativePath = parseUnifiedCgroupPath(NodeFS.readFileSync("/proc/self/cgroup", "utf8")); + expect(relativePath).toBeDefined(); + const lease: AcpLinuxCgroupLease = { + contains: () => false, + exists: () => true, + path: leasePath, + relativePath: relativePath!, + kill: () => undefined, + populated: () => false, + remove: () => undefined, + }; + const packagedExecPath = vi.spyOn(process, "execPath", "get").mockReturnValue("/bin/sh"); + try { + const wrapped = wrapCommandForLinuxCgroup(lease, "/bin/sh", [ + "-c", + 'printf "%s\\n" "$0" "$1" "$2" "$3" "${ELECTRON_RUN_AS_NODE-}" "${T3_ACP_CGROUP_WRAPPER-}" > "$4"', + "packaged-target", + "space value", + "single'quote", + 'double"quote', + outputPath, + ]); + const result = NodeChildProcess.spawnSync(wrapped.command, wrapped.args, { + encoding: "utf8", + env: { + ...process.env, + ELECTRON_RUN_AS_NODE: "1", + T3_ACP_CGROUP_WRAPPER: "1", + }, + }); + expect(result.status, result.stderr).toBe(0); + expect(NodeFS.readFileSync(outputPath, "utf8")).toBe( + "packaged-target\nspace value\nsingle'quote\ndouble\"quote\n\n\n", + ); + expect(NodeFS.readFileSync(NodePath.join(leasePath, "cgroup.procs"), "utf8")).toMatch( + /^\d+\n$/, + ); + + const mismatch = wrapCommandForLinuxCgroup( + { ...lease, relativePath: "/not-the-current-cgroup" }, + "/bin/sh", + ["-c", 'printf executed > "$0"', outputPath], + ); + NodeFS.rmSync(outputPath); + const mismatchResult = NodeChildProcess.spawnSync(mismatch.command, mismatch.args, { + encoding: "utf8", + }); + expect(mismatchResult.status, mismatchResult.stderr).toBe(126); + expect(NodeFS.existsSync(outputPath)).toBe(false); + + const missingTarget = wrapCommandForLinuxCgroup(lease, "/nonexistent-t3-probe", []); + const missingTargetResult = NodeChildProcess.spawnSync( + missingTarget.command, + missingTarget.args, + { + encoding: "utf8", + }, + ); + expect(missingTargetResult.status, missingTargetResult.stderr).toBe(125); + } finally { + packagedExecPath.mockRestore(); + NodeFS.rmSync(scratch, { recursive: true, force: true }); + } + }); + it.live("kills a post-TERM detached double fork without touching an unrelated sentinel", () => Effect.gen(function* () { if ((yield* HostProcessPlatform) !== "linux") return; diff --git a/apps/server/src/provider/acp/AcpSessionRuntime.ts b/apps/server/src/provider/acp/AcpSessionRuntime.ts index c2fd17bd0351..8050de06e5ff 100644 --- a/apps/server/src/provider/acp/AcpSessionRuntime.ts +++ b/apps/server/src/provider/acp/AcpSessionRuntime.ts @@ -264,21 +264,22 @@ export function wrapCommandForLinuxCgroup( args: ReadonlyArray, ): { readonly command: string; readonly args: ReadonlyArray } { return { - command: process.execPath, + command: "/bin/sh", args: [ - "-e", + "-c", [ - 'const fs = require("node:fs");', - "try {", - ' fs.writeFileSync(process.argv[1] + "/cgroup.procs", String(process.pid) + "\\n");', - ' const actual = fs.readFileSync("/proc/self/cgroup", "utf8").split("\\n").find((line) => line.startsWith("0::"))?.slice(3);', - " if (actual !== process.argv[2]) process.exit(126);", - " const env = { ...process.env };", - " delete env.ELECTRON_RUN_AS_NODE;", - " delete env.T3_ACP_CGROUP_WRAPPER;", - " process.execve(process.argv[3], process.argv.slice(3), env);", - "} catch { process.exit(125); }", + "lease_path=$1; expected=$2; shift 2", + 'printf "%s\\n" "$$" > "$lease_path/cgroup.procs" || exit 125', + "actual=", + "while IFS= read -r line; do", + ' case "$line" in 0::*) [ -z "$actual" ] || exit 126; actual=${line#0::};; esac', + "done < /proc/self/cgroup || exit 125", + '[ "$actual" = "$expected" ] || exit 126', + "unset ELECTRON_RUN_AS_NODE T3_ACP_CGROUP_WRAPPER", + "trap 'exit 125' 0", + 'exec "$@"', ].join("\n"), + "t3-acp-cgroup-wrapper", lease.path, lease.relativePath, command,