mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
Raise eligible Linux child processes own oom_score_adj from a child-side /bin/sh exec shim so cgroup memory pressure prefers transient workers over the long-lived gateway. Cover supervisor children, PTY shells, MCP stdio servers, and OpenClaw-launched browser processes through the shared process runtime seam. Harden the wrapper for distroless images, shell startup env, per-child and process-level opt-outs, dash-compatible exec, and leading-dash command names. Document Linux verification and OOM behavior. Fixes #70404. Co-authored-by: Neerav Makwana <261249544+neeravmakwana@users.noreply.github.com>
260 lines
7.5 KiB
TypeScript
260 lines
7.5 KiB
TypeScript
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
expectRealExitWinsOverSigkillFallback,
|
|
expectWaitStaysPendingUntilSigkillFallback,
|
|
} from "./test-support.js";
|
|
|
|
const { spawnMock, ptyKillMock, killProcessTreeMock } = vi.hoisted(() => ({
|
|
spawnMock: vi.fn(),
|
|
ptyKillMock: vi.fn(),
|
|
killProcessTreeMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@lydell/node-pty", () => ({
|
|
spawn: (...args: unknown[]) => spawnMock(...args),
|
|
}));
|
|
|
|
vi.mock("../../kill-tree.js", () => ({
|
|
killProcessTree: (...args: unknown[]) => killProcessTreeMock(...args),
|
|
}));
|
|
|
|
function createStubPty(pid = 1234) {
|
|
let exitListener: ((event: { exitCode: number; signal?: number }) => void) | null = null;
|
|
return {
|
|
pid,
|
|
write: vi.fn(),
|
|
onData: vi.fn(() => ({ dispose: vi.fn() })),
|
|
onExit: vi.fn((listener: (event: { exitCode: number; signal?: number }) => void) => {
|
|
exitListener = listener;
|
|
return { dispose: vi.fn() };
|
|
}),
|
|
kill: (signal?: string) => ptyKillMock(signal),
|
|
emitExit: (event: { exitCode: number; signal?: number }) => {
|
|
exitListener?.(event);
|
|
},
|
|
};
|
|
}
|
|
|
|
function expectSpawnEnv() {
|
|
const spawnOptions = spawnMock.mock.calls[0]?.[2] as { env?: Record<string, string> };
|
|
return spawnOptions?.env;
|
|
}
|
|
|
|
function expectSpawnCommand() {
|
|
return spawnMock.mock.calls[0]?.[0] as string | undefined;
|
|
}
|
|
|
|
function expectSpawnArgs() {
|
|
return spawnMock.mock.calls[0]?.[1] as string[] | undefined;
|
|
}
|
|
|
|
describe("createPtyAdapter", () => {
|
|
let createPtyAdapter: typeof import("./pty.js").createPtyAdapter;
|
|
|
|
beforeAll(async () => {
|
|
({ createPtyAdapter } = await import("./pty.js"));
|
|
});
|
|
|
|
beforeEach(() => {
|
|
spawnMock.mockClear();
|
|
ptyKillMock.mockClear();
|
|
killProcessTreeMock.mockClear();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("forwards explicit signals to node-pty kill on non-Windows", async () => {
|
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
|
|
try {
|
|
spawnMock.mockReturnValue(createStubPty());
|
|
|
|
const adapter = await createPtyAdapter({
|
|
shell: "bash",
|
|
args: ["-lc", "sleep 10"],
|
|
});
|
|
|
|
adapter.kill("SIGTERM");
|
|
expect(ptyKillMock).toHaveBeenCalledWith("SIGTERM");
|
|
expect(killProcessTreeMock).not.toHaveBeenCalled();
|
|
} finally {
|
|
if (originalPlatform) {
|
|
Object.defineProperty(process, "platform", originalPlatform);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("uses process-tree kill for SIGKILL by default", async () => {
|
|
spawnMock.mockReturnValue(createStubPty());
|
|
|
|
const adapter = await createPtyAdapter({
|
|
shell: "bash",
|
|
args: ["-lc", "sleep 10"],
|
|
});
|
|
|
|
adapter.kill();
|
|
expect(killProcessTreeMock).toHaveBeenCalledWith(1234);
|
|
expect(ptyKillMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("wait does not settle immediately on SIGKILL", async () => {
|
|
vi.useFakeTimers();
|
|
spawnMock.mockReturnValue(createStubPty());
|
|
|
|
const adapter = await createPtyAdapter({
|
|
shell: "bash",
|
|
args: ["-lc", "sleep 10"],
|
|
});
|
|
|
|
await expectWaitStaysPendingUntilSigkillFallback(adapter.wait(), () => {
|
|
adapter.kill();
|
|
});
|
|
});
|
|
|
|
it("prefers real PTY exit over SIGKILL fallback settle", async () => {
|
|
vi.useFakeTimers();
|
|
const stub = createStubPty();
|
|
spawnMock.mockReturnValue(stub);
|
|
|
|
const adapter = await createPtyAdapter({
|
|
shell: "bash",
|
|
args: ["-lc", "sleep 10"],
|
|
});
|
|
|
|
await expectRealExitWinsOverSigkillFallback({
|
|
waitPromise: adapter.wait(),
|
|
triggerKill: () => {
|
|
adapter.kill();
|
|
},
|
|
emitExit: () => {
|
|
stub.emitExit({ exitCode: 0, signal: 9 });
|
|
},
|
|
expected: { code: 0, signal: 9 },
|
|
});
|
|
});
|
|
|
|
it("resolves wait when exit fires before wait is called", async () => {
|
|
const stub = createStubPty();
|
|
spawnMock.mockReturnValue(stub);
|
|
|
|
const adapter = await createPtyAdapter({
|
|
shell: "bash",
|
|
args: ["-lc", "exit 3"],
|
|
});
|
|
|
|
expect(stub.onExit).toHaveBeenCalledTimes(1);
|
|
stub.emitExit({ exitCode: 3, signal: 0 });
|
|
await expect(adapter.wait()).resolves.toEqual({ code: 3, signal: null });
|
|
});
|
|
|
|
it("keeps inherited env when no override env is provided on non-Linux", async () => {
|
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
|
|
try {
|
|
const stub = createStubPty();
|
|
spawnMock.mockReturnValue(stub);
|
|
|
|
await createPtyAdapter({
|
|
shell: "bash",
|
|
args: ["-lc", "env"],
|
|
});
|
|
|
|
expect(expectSpawnCommand()).toBe("bash");
|
|
expect(expectSpawnArgs()).toEqual(["-lc", "env"]);
|
|
expect(expectSpawnEnv()).toBeUndefined();
|
|
} finally {
|
|
if (originalPlatform) {
|
|
Object.defineProperty(process, "platform", originalPlatform);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("wraps Linux PTY spawns so shell children inherit higher OOM score", async () => {
|
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
|
|
try {
|
|
const stub = createStubPty();
|
|
spawnMock.mockReturnValue(stub);
|
|
|
|
await createPtyAdapter({
|
|
shell: "bash",
|
|
args: ["-lc", "env"],
|
|
env: { PATH: "/usr/bin", BASH_ENV: "/tmp/bashenv" },
|
|
});
|
|
} finally {
|
|
if (originalPlatform) {
|
|
Object.defineProperty(process, "platform", originalPlatform);
|
|
}
|
|
}
|
|
|
|
expect(expectSpawnCommand()).toBe("/bin/sh");
|
|
expect(expectSpawnArgs()).toEqual([
|
|
"-c",
|
|
'echo 1000 > /proc/self/oom_score_adj 2>/dev/null; exec "$0" "$@"',
|
|
"bash",
|
|
"-lc",
|
|
"env",
|
|
]);
|
|
expect(expectSpawnEnv()).toEqual({ PATH: "/usr/bin" });
|
|
});
|
|
|
|
it("passes explicit env overrides as strings", async () => {
|
|
const stub = createStubPty();
|
|
spawnMock.mockReturnValue(stub);
|
|
|
|
await createPtyAdapter({
|
|
shell: "bash",
|
|
args: ["-lc", "env"],
|
|
env: { FOO: "bar", COUNT: "12", DROP_ME: undefined },
|
|
});
|
|
|
|
expect(expectSpawnEnv()).toEqual({ FOO: "bar", COUNT: "12" });
|
|
});
|
|
|
|
it("does not pass a signal to node-pty on Windows", async () => {
|
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
|
try {
|
|
spawnMock.mockReturnValue(createStubPty());
|
|
|
|
const adapter = await createPtyAdapter({
|
|
shell: "powershell.exe",
|
|
args: ["-NoLogo"],
|
|
});
|
|
|
|
adapter.kill("SIGTERM");
|
|
expect(ptyKillMock).toHaveBeenCalledWith(undefined);
|
|
expect(killProcessTreeMock).not.toHaveBeenCalled();
|
|
} finally {
|
|
if (originalPlatform) {
|
|
Object.defineProperty(process, "platform", originalPlatform);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("uses process-tree kill for SIGKILL on Windows", async () => {
|
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
|
try {
|
|
spawnMock.mockReturnValue(createStubPty(4567));
|
|
|
|
const adapter = await createPtyAdapter({
|
|
shell: "powershell.exe",
|
|
args: ["-NoLogo"],
|
|
});
|
|
|
|
adapter.kill("SIGKILL");
|
|
expect(killProcessTreeMock).toHaveBeenCalledWith(4567);
|
|
expect(ptyKillMock).not.toHaveBeenCalled();
|
|
} finally {
|
|
if (originalPlatform) {
|
|
Object.defineProperty(process, "platform", originalPlatform);
|
|
}
|
|
}
|
|
});
|
|
});
|