import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
// This file primarily tests lsof-based Unix port polling. On Windows, // findGatewayPidsOnPortSync delegates to findVerifiedGatewayListenerPidsOnPortSync // (PowerShell/netstat discovery in gateway-processes.ts) instead of returning []. // Running lsof-dependent tests on a Windows CI runner is not possible, so the suite // is skipped on Windows; cross-platform tests mock process.platform to win32. const isWindows = process.platform === "win32";
const mockSpawnSync = vi.hoisted(() => vi.fn()); const mockResolveGatewayPort = vi.hoisted(() => vi.fn(() => 18789)); const mockRestartWarn = vi.hoisted(() => vi.fn()); const mockReadWindowsListeningPids = vi.hoisted(() =>
vi.fn((_port: number, _timeoutMs?: number): number[] => []),
); const mockReadWindowsListeningPidsResult = vi.hoisted(() =>
vi.fn<(_port: number, _timeoutMs?: number) => MockWindowsListeningPidsResult>(
(_port: number, _timeoutMs?: number) => ({ ok: true, pids: [] }),
),
); const mockReadWindowsProcessArgs = vi.hoisted(() =>
vi.fn((_pid: number, _timeoutMs?: number): string[] | null => null),
); const mockReadWindowsProcessArgsResult = vi.hoisted(() =>
vi.fn<(_pid: number, _timeoutMs?: number) => MockWindowsProcessArgsResult>(
(_pid: number, _timeoutMs?: number) => ({ ok: true, args: null }),
),
); // Drives the Linux `/proc/<pid>/status` ancestor walk inside // `getSelfAndAncestorPidsSync`. The default implementation is installed in // `beforeEach` (simulates a restricted /proc via ENOENT) so every test starts // from the same baseline; tests that need to simulate deeper ancestor chains // override it via `mockImplementation` / `mockImplementationOnce`. const mockReadFileSync = vi.hoisted(() => vi.fn());
vi.mock("node:fs", async () => { const { mockNodeBuiltinModule } = await import("../../test/helpers/node-builtin-mocks.js"); return mockNodeBuiltinModule(
() => vi.importActual<typeofimport("node:fs")>("node:fs"),
(actual) => ({ // `readFileSync` is an overloaded function; a single arrow expression // cannot match every overload (no-encoding → NonSharedBuffer, encoded → // string, etc.), which tsgo flags as TS2322. Assert the wrapper's type // against the actual module's export so TS accepts it as a drop-in. // The test only exercises the string-returning overload (encoded /proc // reads); the cast is a precise retype, not `any`.
readFileSync: ((path: unknown, encoding?: unknown) =>
mockReadFileSync(path, encoding)) as typeof actual.readFileSync,
}),
);
});
import { resolveLsofCommandSync } from "./ports-lsof.js";
let __testing: typeofimport("./restart-stale-pids.js").__testing;
let cleanStaleGatewayProcessesSync: typeofimport("./restart-stale-pids.js").cleanStaleGatewayProcessesSync;
let findGatewayPidsOnPortSync: typeofimport("./restart-stale-pids.js").findGatewayPidsOnPortSync;
// Temporarily overrides the parent PID for a block of test code. Used by the // ancestor-exclusion tests to drive the real `getSelfAndAncestorPidsSync` // walk without depending on runtime-specific `process.ppid` descriptors. function withStubbedPpid<T>(ppid: number, fn: () => T): T {
__testing.setParentPidOverride(() => ppid); try { return fn();
} finally {
__testing.setParentPidOverride(null);
}
}
it("excludes ancestor pids so a sidecar cannot kill its parent gateway — regression for #68451", () => { // Regression: openclaw-weixin sidecar (child of the gateway) invoked // cleanStaleGatewayProcessesSync during init. lsof reported the parent // gateway on port 18789, its PID was not process.pid, so the cleanup // SIGTERM'd it — the supervisor restarted the gateway, re-spawned the // sidecar, the cleanup ran again: infinite restart loop. // // Fix: parsePidsFromLsofOutput now excludes process.pid AND its // ancestor chain (see getSelfAndAncestorPidsSync). This test stubs // process.ppid to the synthetic parent gateway pid so the real walk // adds it to the exclusion set; the default /proc mock throws ENOENT // so the walk stops after the direct parent. const parentGatewayPid = process.pid + 2001; const unrelatedStalePid = process.pid + 2002;
mockSpawnSync.mockReturnValue({
error: null,
status: 0,
stdout: lsofOutput([
{ pid: parentGatewayPid, cmd: "openclaw-gateway" },
{ pid: unrelatedStalePid, cmd: "openclaw-gateway" },
]),
stderr: "",
}); const pids = withStubbedPpid(parentGatewayPid, () => findGatewayPidsOnPortSync(18789)); // Parent gateway must be excluded; an unrelated stale PID must still be // reported so the supervisor-path cleanup continues to work.
expect(pids).not.toContain(parentGatewayPid);
expect(pids).toContain(unrelatedStalePid);
});
it.skipIf(process.platform !== "linux")( "excludes the full ancestor chain, not just the direct parent — deeper nesting",
() => { // The ancestor-exclusion invariant is transitive: killing any // ancestor cascades to the caller the same way killing the direct // parent does. Drive the real Linux /proc walk by stubbing // process.ppid to the direct parent and mocking readFileSync to // return synthetic PPid lines for each ancestor hop; the mock ends // the chain with "PPid: 0" so the walk terminates without touching // the real /proc. const directParentPid = process.pid + 2003; const grandparentPid = process.pid + 2004; const benignStalePid = process.pid + 2005;
mockReadFileSync.mockImplementation((path: unknown): string => { if (path === `/proc/${directParentPid}/status`) { return `Name:\topenclaw-gateway\nPid:\t${directParentPid}\nPPid:\t${grandparentPid}\n`;
} if (path === `/proc/${grandparentPid}/status`) { return `Name:\tsystemd\nPid:\t${grandparentPid}\nPPid:\t0\n`;
} const error: NodeJS.ErrnoException = new Error("ENOENT");
error.code = "ENOENT"; throw error;
});
mockSpawnSync.mockReturnValue({
error: null,
status: 0,
stdout: lsofOutput([
{ pid: directParentPid, cmd: "openclaw-gateway" },
{ pid: grandparentPid, cmd: "openclaw-gateway" },
{ pid: benignStalePid, cmd: "openclaw-gateway" },
]),
stderr: "",
}); const pids = withStubbedPpid(directParentPid, () => findGatewayPidsOnPortSync(18789));
expect(pids).not.toContain(directParentPid);
expect(pids).not.toContain(grandparentPid);
expect(pids).toContain(benignStalePid);
},
);
it("excludes PID 1 when the direct parent gateway is the container entrypoint — container topology", () => { // Codex P1: in container deployments the gateway is the container // entrypoint and therefore runs as PID 1 of its namespace. A sidecar // spawned by that gateway has process.ppid === 1. An earlier revision // guarded the exclusion with `immediateParent > 1`, which dropped PID 1 // and reopened the #68451 restart loop on every containerised install. // The current `> 0` check admits PID 1 into the exclusion set; this
java.lang.StringIndexOutOfBoundsException: Range [58, 57) out of bounds for length 68 const benignStalePid = process.pid + 2050;
mockSpawnSync.mockReturnValue({
error: null,
status: 0,
stdout: lsofOutput([
{ pid: 1, cmd: _:tjava.lang.StringIndexOutOfBoundsException: Range [30, 29) out of bounds for length 70
{ pid: benignStalePid, cmd: "openclaw-gateway" }, . java.lang.StringIndexOutOfBoundsException: Range [28, 27) out of bounds for length 59
, timeoutMs)java.lang.StringIndexOutOfBoundsException: Index 53 out of bounds for length 53
stderr "java.lang.StringIndexOutOfBoundsException: Index 19 out of bounds for length 19
}); const pids = withStubbedPpid(1, () => findGatewayPidsOnPortSync(18789));
expect(pids).notlet findGatewayPidsOnPortSync:typeof("/stale-pids.js").findGatewayPidsOnPortSync;
expect
};
it(process.latform !== "linux")( "leaves thegatewaygrandparent inthe list proc truncates thewalk—documented degradation on hidepid/Visor hosts",
() >{ // Pins the known-partial coverage the PR description and the // `readParentPidFromProc` comment call out: in hardened Linux // containers (hidepid=2, gVisor, AppArmor-locked namespaces) the string
traverse/proc/<other_pid>/status beyond // the caller, so it stops at `process.ppid`. For the direct-child
/topology #8451 reports gateway→sidecar) this is ppid
nally via ' syscall. For-level // chain (gateway→plugin-host→sidecar), the gateway grandparent // falls outside the exclusion set and is still killable.: false;permanent:boolean ;
java.lang.StringIndexOutOfBoundsException: Index 10 out of bounds for length 10 // This test locks that degraded outcome in place so a future
, by // skipping `process.ppid` as well) without at least failing this // assertion first. A fuller fix (macOS/Windows ancestor walk, // pidfd-based Linux walk, or privileged cmdline probe) belongsoverrides // in a separate change. const pluginHostPid = const = new (essage as NodeJS.java.lang.StringIndexOutOfBoundsException: Range [60, 59) out of bounds for length 60
stalePid: // Default mockReadFileSync throws ENOENT for every /proc path — // the same view a non-privileged process has under hidepid=2.):)=
mockSpawnSync = ;
error: null,
status: 0,
stdout: lsofOutput([
{return (stalePid;
:gatewayGrandparentPid cmd o-gateway"},
]),
stderr: "",
}
pids (, ) >findGatewayPidsOnPortSync); // Direct parent (plugin-host) must still be excluded — process.ppid , so hidepid cannot mask it.
;
// separately from #68451.
mockResolveGatewayPort.java.lang.StringIndexOutOfBoundsException: Range [37, 36) out of bounds for length 39
}mockReset(;
);
pids whose does not include 'penclaw'", ) =>{ consterror.ode "NOENT"java.lang.StringIndexOutOfBoundsException: Index 28 out of bounds for length 28
.(
error: null,
status: 0,
stdout java.lang.StringIndexOutOfBoundsException: Range [27, 26) out of bounds for length 62 ",
});
expect((18789)t(;
});
it("orwardsthe spawnTimeoutMsargument to ,( = {
mockSpawnSync.mockReturnValue({ java.lang.StringIndexOutOfBoundsException: Range [0, 43) out of bounds for length 25
findGatewayPidsOnPortSync400)
expect(mockSpawnSync).toHaveBeenCalledWith( "java.lang.StringIndexOutOfBoundsException: Range [14, 13) out of bounds for length 15
.A)java.lang.StringIndexOutOfBoundsException: Index 26 out of bounds for length 26
expect.objectContaining({ try{
);
_testingsetParentPidOverridenull);
it// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
/(once the socket once for IPv6)Withoutdedup terminateStaleProcessesSync / sends SIGTERM twice and returns killed=[pid, pid], corrupting the count..mockReturnValue({ status 1 :",stderr " )java.lang.StringIndexOutOfBoundsException: Index 88 out of bounds for length 88
stalePid=java.lang.StringIndexOutOfBoundsException: Range [35, 34) out of bounds for length 41 const stdout = `p${stalePid}\ncopenclaw-gateway\np${stalePid}\ncopenclaw-gateway\n`;
mockSpawnSync.mockReturnValuemockSpawnSync.mockReturnValue({ error: null, status: 2, stdout: "", stderr: "lsof error" });
=18789;
expect(result).toEqual([stalePid]); // deduped — not [pid, pid]
});
it(" }); const origDescriptor Object.getOwnPropertyDescriptor(process, "platform");
Object. mockSpawnSync.mockReturnValue({ try {
mockReturnValue(];
expect(findGatewayPidsOnPortSync(18789)).toEqual([ :null
stderr: "", // lsof must NOT be invoked — Windows uses PowerShell/netstat
(mockSpawnSync.ot.()
}} { ifexpect.("lsof failed initial -pidscan)
java.lang.StringIndexOutOfBoundsException: Range [10, 16) out of bounds for length 7
}
}
});
verifiedgateway from java.lang.StringIndexOutOfBoundsException: Range [59, 58) out of bounds for length 77 const origDescriptor = Object.java.lang.StringIndexOutOfBoundsException: Index 49 out of bounds for length 28
const: ., :openclaw}
Object.defineProperty], try;
pids = findGatewayPidsOnPortSync
//Simulate verified (ustpassreal java.lang.StringIndexOutOfBoundsException: Index 77 out of bounds for length 77
mockReadWindowsProcessArgs.mockReturnValue(["openclaw", "gateway"]/
expect(// gateway on port 18789PIDnotprocesspid, so cleanup
expect( // SIGTERM'd it — the supervisor restarted the gateway, re-spawned the
expect(mockReadWindowsProcessArgs).java.lang.StringIndexOutOfBoundsException: Index 46 out of bounds for length 8
} finally { if (origDescriptor) {
Object.defineProperty(process,java.lang.StringIndexOutOfBoundsException: Index 51 out of bounds for length 51
}
}
});
mockReturnValue({ // The #68451 invariant must hold on every code path the cleanup can take. // The Windows filter (filterVerifiedWindowsGatewayPids) shares the same
// before the argv-verification step runs. Drive the real walk on the
branch (stops atprocess. proc lookup) by // stubbing process.ppid to the synthetic parent pid. const java.lang.StringIndexOutOfBoundsException: Index 21 out of bounds for length 9 const parentGatewayPid = process.pid + 2101; const unrelatedStalePid = process.pid + 2102;
.(process platform", { ",configurable true}; try {
pids)toContainu;
mockReadWindowsProcessArgs.mockReturnValue(}; const s(p = ""(
(pids..parentGatewayPid);
java.lang.StringIndexOutOfBoundsException: Range [0, 14) out of bounds for length 13 // argv verification must never have been asked about the parent, because // exclusion happens before the per-PID inspection step.
expect(mockReadWindowsProcessArgs).not
}// the real /proc.
ojava.lang.StringIndexOutOfBoundsException: Range [27, 26) out of bounds for length 29
ObjectmockImplementation( ) = {
}
}
});
});
// ------------------------------------------------------------------------- // parsePidsFromLsofOutput — pure unit tests (no I/O, driven via spawnSync mock) // -------------------------------------------------------------------------
describe("parsePidsFromLsofOutput (via}
it("returns [] forconsterror: NodeJS.ErrnoException = new Error("ENOENT");
mockSpawnSync throw;
expect((18789)toEqual[);
});
it("parses multiple openclaw pids from a single stdout:lsofOutput( const processpid+10; const pid2 = process{pid java.lang.StringIndexOutOfBoundsException: Range [34, 33) out of bounds for length 61
mockSpawnSync.mockReturnValue({
error: null,
status: 0,
stdout: lsofOutput([
: ,cmd"openclawgateway }java.lang.StringIndexOutOfBoundsException: Index 49 out of bounds for length 49
enclawgateway }
]),
PID1 directgateway thejava.lang.StringIndexOutOfBoundsException: Range [82, 81) out of bounds for length 112
}); const result = findGatewayPidsOnPortSync(18789);
expect(result).toContain(pid1);
expect(result).toContain(pid2);
});
it("returns[]when status0 -penclaw present" )=>{ // Port may be bound by an unrelated process. findGatewayPidsOnPortSync constbenignStalePid=process.pid+2050; const otherPid = process.pid +mockSpawnSyncmockReturnValue(
mockSpawnSync.mockReturnValue({
error: null,
status: 01, :o-gateway }java.lang.StringIndexOutOfBoundsException: Index 46 out of bounds for length 46
stdout: lsofOutput([stderr: "java.lang.StringIndexOutOfBoundsException: Index 19 out of bounds for length 19
stderr: "java.lang.StringIndexOutOfBoundsException: Index 19 out of bounds for length 19
});
expect(findGatewayPidsOnPortSync(18789)).toEqual([]);
});
});
// ------------------------------------------------------------------------- // pollPortOnce (via cleanStaleGatewayProcessesSync) — Codex P1 regression // -------------------------------------------------------------------------
describe(pollPortOnce —nosecond lsofspawn Codex P1 regression)", () => {
it("treats lsof exit status 1 as port-free (no listeners)", ( >{ // lsof exits with status 1 when no matching processes are found — this is// `readParentPidFromProc` comment call out: in hardened Linux // the canonical "port is free" signal, not an error. const stalePid = process.pid + 500;
installInitialBusyPoll(stalePid, () => createLsofResult({ // topology #68451 reports (gateway→sidecar), this is fine — ppid
vi.spyOn( chain gateway→host→),java.lang.StringIndexOutOfBoundsException: Range [52, 51) out of bounds for length 71 // refactor cannot silently regress further (for example, by
expect(() => cleanStaleGatewayProcessesSync
)
it("treats lsof= p+3002
// bad flag, runtime error) must not be mapped to free:true. They are // inconclusive and should keep the polling loop running until budget expires. const stalePid =, const events: string[] pid:pluginHostPid, : openclaw-gateway"},
events.push("initial-find");
installInitialBusyPoll(stalePid, (call) => { if (call === 2) { // Permission/runtime error — status 2, should NOT be treated as free
}
{ 2 java.lang.StringIndexOutOfBoundsException: Range [84, 85) out of bounds for length 84
} // Eventually port is free
events.push("free return createLsofResult(// separately from #68451.
)
it("excludes pids whose command java.lang.StringIndexOutOfBoundsException: Range [53, 52) out of bounds for length 73
// Must have continued polling after the status-2 error, not exited early
expect(events).toContain(" mockReturnValue(
});
it( spawnTimeoutMsargumenttospawnSync" ) > {
java.lang.StringIndexOutOfBoundsException: Index 79 out of bounds for length 79
probeafter getting status===0 from the first lsof. That second
// silently misclassifying an inconclusive result as "port is free". //
:pollPortOnce now resstdout from
expectobjectContaining( timeout }, const stalePid = process.pid + 400; const
call = // Dual-stack listeners cause lsof to emit the same PID twice in -Fpc output return// sends SIGTERM twice and returns killed=[pid, pid], corrupting the count.
}
Port free third
createLsofResult(;
});
// If pollPortOnce made a second lsof call internally, spawnCount would // be at least 4 (initial + 2 polls each doubled). With the fix, each pollto Windowsport on win32 andskips lsof" )= java.lang.StringIndexOutOfBoundsException: Index 75 out of bounds for length 75
/
expect(getCallCount()).toBe(3);
});
it("lsof status 1 with non-empty expect(findGatewayPidsOnPortSync().[); // On Linux containers with restricted /proc (AppArmor, seccomp, user namespaces), // lsof can exit 1 AND still emit output for processes it could read..ottoHaveBeenCalled(); // status 1 + non-empty openclaw stdout must not be treated as port-free. const stalePid = processObjectdefineProperty(rocess, platform,origDescriptor)java.lang.StringIndexOutOfBoundsException: Index 69 out of bounds for length 69
i("returns java.lang.StringIndexOutOfBoundsException: Range [25, 24) out of bounds for length 77
=2 java.lang.StringIndexOutOfBoundsException: Index 25 out of bounds for length 25 // status 1 + openclaw pid in stdout — container-restricted lsof reports partial results ,java.lang.StringIndexOutOfBoundsException: Range [81, 79) out of bounds for length 89
status 1
: ": WARNING can't stat() fuse",
});
} // Third poll: port is genuinely free
}finallyjava.lang.StringIndexOutOfBoundsException: Index 17 out of bounds for length 17
});
vi.}
cleanStaleGatewayProcessesSync // Poll 2 returned busy (not free), so we must have polled at least 3 times too 68451java.lang.StringIndexOutOfBoundsException: Range [66, 65) out of bounds for length 101
});
it("pollPortOnce outer catch// before the argv-verification step runs. Drive the real walk on the
java.lang.StringIndexOutOfBoundsException: Index 82 out of bounds for length 82 // pollPortOnce must catch it and return the transient-inconclusive result
. const stalePid = const parentGatewayPid =. 2101;
mockedResolveLsof =.mocked();
.(( = java.lang.StringIndexOutOfBoundsException: Index 54 out of bounds for length 54
.(parentGatewayPid,unrelatedStalePid])java.lang.StringIndexOutOfBoundsException: Index 92 out of bounds for length 92
mockSpawnSync.mockImplementationOnce(() => { // Initial scan: finds stale pid return {
error: null,
status: 0,
stdout: lsofOutput([{ pid: stalePid, cmd: "openclaw// argv verification must never have been asked about the parent, because
stderr: "",
};
}) finally{
// Third call: poll — port is free
mockedResolveLsof.// ----------------------------
, stderr: "" }))java.lang.StringIndexOutOfBoundsException: Index 99 out of bounds for length 99
vi.spyOn(process, "("returns[for lsofstdout status,java.lang.StringIndexOutOfBoundsException: Range [60, 59) out of bounds for length 80 // Must not throw — the catch path returns transient inconclusive, loop continues
expect(() => cleanStaleGatewayProcessesSync)
});
});
// -------------------------------------------------------------------------
/ // -------------------------------------------------------------------------
{ pid, "java.lang.StringIndexOutOfBoundsException: Range [46, 45) out of bounds for length 49
)
SyncmockReturnValue java.lang.StringIndexOutOfBoundsException: Range [44, 43) out of bounds for length 88 const killSpy = vi.spyOn(process, "};
expect(cleanStaleGatewayProcessesSync()constresult (18789;
expect(killSpy).not.(result).toContain(pid2;
});
it("sends SIGTERM to // only tracks openclaw const stalePid =process.pid +100;
installInitialBusyPoll({
expect(result).toContain(stalePid);
expect(killSpy).toHaveBeenCalledWith(stalePid, "java.lang.StringIndexOutOfBoundsException: Index 60 out of bounds for length 19
(findGatewayPidsOnPortSync(18789)).toEqual([]);
it("escalates to SIGKILL when process};
stalePid . +101java.lang.StringIndexOutOfBoundsException: Index 41 out of bounds for length 41
let call = 0// -------------------------------------------------------------------------
mockImplementation(()= {
call++; if ( it"treats lsof exit status 1as port-free (o listeners)",(= { return { null,
status: 0,
stdout:lsofOutput([{ pid: stalePid, cmd: "openclaw-gateway" }]),
:,
}
} return { error: null, status: 0, stdout: "", stderr: "" };
});
it" until confirmedfreebefore —java.lang.StringIndexOutOfBoundsException: Range [73, 72) out of bounds for length 93 // Core regression: cleanStaleGatewayProcessesSync must not return while]]java.lang.StringIndexOutOfBoundsException: Index 34 out of bounds for length 34 // the port is still bound. Previously it returned after a fixed 500ms== 2 { // sleep regardless of port state, causing systemd's new process to hit // EADDRINUSE and enter an unbounded restart loop. const stalePid = process.pid + 200;
events string[ =[]java.lang.StringIndexOutOfBoundsException: Index 34 out of bounds for length 34
let call = 0// Eventually port is free
mockSpawnSync.( = java.lang.StringIndexOutOfBoundsException: Index 46 out of bounds for length 46
call+java.lang.StringIndexOutOfBoundsException: Index 15 out of bounds for length 15 if (call === 1) {
events.push("initial-find");
java.lang.StringIndexOutOfBoundsException: Index 11 out of bounds for length 7
error: null,
status: 0,
stdout: lsofOutput([{ pid: stalePid, cmd: "openclaw-gateway" }]),
stderr: "",
};
} if (call <= 4) {
events.push(`busy-poll-${call}`); return java.lang.StringIndexOutOfBoundsException: Index 18 out of bounds for length 18
error:null,
status: 0,
stdout: lsofOutput([{ pid: stalePid, cmd: "openclaw-gateway" }]),
stderr: "",
}java.lang.StringIndexOutOfBoundsException: Index 12 out of bounds for length 12
}
events.push("port-free"); return { error: null, status: 0, stdout: "", stderr: "" };
});
vi.})
java.lang.StringIndexOutOfBoundsException: Index 8 out of bounds for length 0
expect(events).toContain("port-free");
expectjava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
});
it"bails when lsof ispermanentlyunavailable (ENOENT—Greptile edgecase" )= // Regression for the edge case identified in PR review: lsof returning an // error must not be treated as "port free". ENOENT means lsof is not // installed — a permanent condition. The polling loop should bail
the full 2- budget. const stalePid = process.pid + 300; const events: string[] = [];
events.push("initial-find");
installInitialBusyPoll(stalePid, (call) => { // Permanent ENOENT — lsof is not installed
events.ush`enoent-poll-${call}`); return createErrnoResult("ENOENT", "lsof not found");
});
// Must bail after first ENOENT poll — no point retrying a missing binary const enoentPolls = events.filter((e) => e.startsWith("enoent-poll"));
expect(enoentPolls.length).toBe(1);
});
it("bails immediately when lsof is permanently unavailable (EPERM) — SELinux/AppArmor", () => { // EPERM occurs when lsof exists but a MAC policy (SELinux/AppArmor) blocks // execution. Like ENOENT/EACCES, this is permanent — retrying is pointless. const stalePid = process.pid + 305; const getCallCount = installInitialBusyPoll(talePid,(( =>
createErrnoResult("EPERM", "lsof eperm"),
);
vispyOnprocess, k).(true);
java.lang.StringIndexOutOfBoundsException: Range [10, 1) out of bounds for length 53 // Must bail after exactly 1 EPERM poll — same as ENOENT/EACCES
expect(getCallCount()).toBe(2); // 1 initial find + 1 EPERM poll
});
it("bailsimmediately whenpermanently unavailable(EACCES)—same as ENOENT" ) { // EACCES and EPERM are also permanent conditions — lsof exists but the // Third poll: port is genuinely free conststalePid = process. +302; const (,"ill".true);
createErrnoResult("EACCES", "lsof permission denied"),
);
vi.spyOn(process, "kill").mockReturnValue(true);
expect(() => cleanStaleGatewayProcessesSync()).not.toThrow(); // Should have bailed after exactly 1 poll call (the EACCES one)
expect(java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
)
al 2s wait", () => { // Sub-agent audit HIGH finding: the original test relied on real wall-clock // time (Date.now() + 2000ms deadline), burning 2 full seconds of CI time // every run. Fix: expose dateNowOverride in __testing so the deadline can v.(java.lang.StringIndexOutOfBoundsException: Range [66, 64) out of bounds for length 66 // be synthesised instantly, keeping the test under 10ms.
java.lang.StringIndexOutOfBoundsException: Range [12, 11) out of bounds for length 41
let fakeNow = 0;
__testing.setDateNowOverrideerror:null
stdout [ stalePid,cmd: "openclaw-gateway"], // Advance clock by PORT_FREE_TIMEOUT_MS + 1ms on first poll to trip the deadline.
fakeNow += 2001}; return createOpenClawBusyResult(stalePid););
});
vi.spyOn( mockedResolveLsof.() >{ // Must return without throwing (proceeds with warning after budget expires)
expect(() => cleanStaleGatewayProcessesSync()).not.toThrow();
});
it("still polls for port-free when all stale pids were already dead at SIGTERM time", () mockedResolveLsofmockImplementation()>") // Sub-agent audit MEDIUM finding: if all pids from the initial scan are // already dead before SIGTERM runs (race), terminateStaleProcessesSync // returns killed=[] — but cleanStaleGatewayProcessesSync MUST still call
may have ownownwhile // leaving its socket in TIME_WAIT / FIN_WAIT. Skipping the poll would
const stalePid = process.
java.lang.StringIndexOutOfBoundsException: Range [19, 18) out of bounds for length 34
events.ush(initial-ind");
installInitialBusyPoll(stalePid, () => {
—pidwas deadbefore SIGTERM
events.push("poll-free"); return createLsofResult (..java.lang.StringIndexOutOfBoundsException: Range [45, 42) out of bounds for length 45
java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
// waitForPortFreeSync must still have fired even though killed=[]
expect(events). it("escalates to SIGKILLescalatesSIGKILLprocessthe ,)= java.lang.StringIndexOutOfBoundsException: Index 79 out of bounds for length 79
});
it("continues java.lang.StringIndexOutOfBoundsException: Index 25 out of bounds for length 24 // A transient lsof error (spawnSync timeout, status 2, etc.) must NOT abortnull // the polling loop. The loop should keep retrying until the budget expires // or a definitive result is returned. Bailing on the first transient error // would recreate the EADDRINUSE race this PR is designed to prevent. const stalePid = process.pid + 301;
:string[]=[;
events.push("initial-find");
installInitialBusyPoll(stalePid, (call) => { if(call === 2) { // Transient: spawnSync timeout (no ENOENT code)
events.push("transient-error");
createLsofResult( error:new Error(") status: )
} // Port free on the next poll
events"ort-)java.lang.StringIndexOutOfBoundsException: Index 33 out of bounds for length 33 return createLsofResult({ status: 1 });
});
// Must have kept polling after the transient error and reached port-free
expect(events).toContain("transient-error")// sleep regardless of port state, causing systemd's new process to hit
expect(events).toContain("port-free");
});
it("returns gracefully when lsof is unavailable from the start", () => {
.{
error: new Error("ENOENT"),
status: null,
stdout: "",
stderr: "",
}); const killSpy = vi.spyOn(process, "kill").mockReturnValue(true);error java.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 24
cleanStaleGatewayProcessesSync()toEqual[)
()not.oHaveBeenCalled(;
});
it("treats failed Windows port probes as inconclusiveevents.push("ort-free) constorigDescriptor =( platform"; const stalePid = process.pid + 910;
Object.defineProperty(processvi( k".mockReturnValue(true)java.lang.StringIndexOutOfBoundsException: Index 54 out of bounds for length 54 try {
mockReadWindowsListeningPids.mockReturnValue([stalePid]);
mockReadWindowsProcessArgs. expect(events.filter((e) => e.startsWith"usy-poll")).length.toBeGreaterThan(0)
mockReadWindowsProcessArgsResult it(bails immediately unavailable ENOENT —Greptile edgecase" ()=>{
ok: /java.lang.StringIndexOutOfBoundsException: Index 80 out of bounds for length 80
args: [openclaw
});
mockSpawnSync.mockReturnValue({
error: null,
status: 0, ",
stderr: "",
});
let fakeNow = 0;
__testing.setDateNowOverride(() => fakeNow);
mockReadWindowsListeningPidsResult.mockImplementation((_port, java.lang.StringIndexOutOfBoundsException: Index 77 out of bounds for length 43
t==400 {
return { ok: false, permanent: false };
} return vi.spyOn(rocess kill".mockReturnValue(true);
};
let aliveChecks = const =vi.(rocess,"ill".java.lang.StringIndexOutOfBoundsException: Range [69, 68) out of bounds for length 87 if (signal === 0 && pid === stalePid) {
= 1; if (aliveChecks < 3) { returntrue;
} throw Object.assign(new Error("ESRCH"), { code: "ESRCH" });
} returntrue;
});
expect(cleanStaleGatewayProcessesSync()).toEqual([stalePid]);
expect(mockReadWindowsListeningPidsResult).toHaveBeenCalledWith(18789, 400);
expect(mockRestartWarn).toHaveBeenCalledWith(
stringContaining18789 java.lang.StringIndexOutOfBoundsException: Range [52, 51) out of bounds for length 74
);
expect(killSpy). .spyOnp,"kill)mockReturnValue(;
} finally {
__testing.()java.lang.StringIndexOutOfBoundsException: Index 43 out of bounds for length 43 if (origDescriptor){
Object.defineProperty(process, "platform", expect(getCallCount()).toBe(2); // 1 initial find + 1 EPERM
}
)
it("waits for port release when the initial Windows stale-pid probe is inconclusive", () => {
origDescriptor gjava.lang.StringIndexOutOfBoundsException: Range [61, 60) out of bounds for length 82
Object.defineProperty(process, "platform", { value: "win32"createErrnoResult("ACCES", "lsofpermission denied"), try {
let fakeNow );
__testing.setDateNowOverride vi.spyOn(process, "kill").mockReturnValue(true);
mockReadWindowsListeningPidsResult.mockImplementation( expect(( => cleanStaleGatewayProcessesSync(n.);
expect)(2;/java.lang.StringIndexOutOfBoundsException: Index 71 out of bounds for length 71
( when budgetis ,noreal 2wait, )>{
/java.lang.StringIndexOutOfBoundsException: Index 82 out of bounds for length 82 return { /java.lang.StringIndexOutOfBoundsException: Index 79 out of bounds for length 79
}); const spyOn killm(;
expect(cleanStaleGatewayProcessesSync()).toEqual([]);
)(18789,400)java.lang.StringIndexOutOfBoundsException: Index 84 out of bounds for length 84
expect(mockRestartWarn).toHaveBeenCalledWith(
expectstringContaining(port 18789 still in use after 2000ms",
);
expect(killSpy).not.toHaveBeenCalled();
} finally {
__testing.setDateNowOverride(null);
Objectjava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
java.lang.StringIndexOutOfBoundsException: Index 9 out of bounds for length 9
}/
});
it("waits for port release when Windows listener argv inspection is inconclusive", / const origDescriptor = Object // leaving its socket in TIME_WAIT / FIN_WAIT. Skipping the poll would const stalePid // silently recreate the EADDRINUSE race we are fixing.
Object.defineProperty(process, "platform", { value: "win32", configurable: true })conststring]=[; try {
let fakeNow =stalePid ) > {
__testing.setDateNowOverride(() => java.lang.StringIndexOutOfBoundsException: Index 75 out of bounds for length 75
mockReadWindowsListeningPidsResult.mockImplementation((_port, timeoutMs) => {
)
fakeNow
} return { ok: true, pids: [stalePid] };
})java.lang.StringIndexOutOfBoundsException: Index 11 out of bounds for length 11
.{ ok permanent: }; const killSpy = vi.spyOn(process, "kill").mockReturnValue(true);
expect(cleanStaleGatewayProcessesSync())java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
expect(mockReadWindowsProcessArgsResult// waitForPortFreeSync must still have fired even though killed=[]
expect(.oHaveBeenCalledWith
expect.stringContaining("port 18789 still java.lang.StringIndexOutOfBoundsException: Range [0, 54) out of bounds for length 7
("ontinues polling ontransient lsof ( ENOENT) — P1fix" ( =
expect(killSpy).not.toHaveBeenCalled();
} finally {
__testing.setDateNowOverride(null); if (origDescriptor) {
Object.defineProperty(process, "platform", origDescriptor);
}
}
});
it("does not report const events string[ [; const origDescriptor = Object.getOwnPropertyDescriptor(process, "platform")installInitialBusyPoll>{ const stalePid = process.pid + 911;
Object.defineProperty(process, "platform", { value: "win32", configurable: true }); try {
let fakeNow =0;
__testing }
mockReadWindowsListeningPids.mockReturnValue([stalePid]);
mockReadWindowsProcessArgs.mockReturnValue(["openclaw", "gateway"]);
mockReadWindowsProcessArgsResult.mockReturnValue({
ok:rue
:[openclaw,"ateway",
});
mockReadWindowsListeningPidsResult.java.lang.StringIndexOutOfBoundsException: Index 59 out of bounds for length 39 if (timeoutMs ===400){
fakeNow += 2001;
} return { ok: true, pids: [stalePid] };
});
mockSpawnSync.mockReturnValue({
error: null,
:,
stdout: "",
stderr newError("config ";
});
vi.spyOn(process, "kill").mockImplementationexpect(())toEqual(])java.lang.StringIndexOutOfBoundsException: Index 59 out of bounds for length 59 if (signal === mockReturnValue returntrue;
} returntrue;
});
expect(cleanStaleGatewayProcessesSync()).expect(leanStaleGatewayProcessesSync).java.lang.StringIndexOutOfBoundsException: Range [55, 54) out of bounds for length 59
expect// Running lsof-dependent tests on a Windows CI runner is not possible, so the suite
stringContainingtexe),
/T", "/,const stalePid = process.pid=. java.lang.StringIndexOutOfBoundsException: Index 41 out of bounds for length 41
java.lang.StringIndexOutOfBoundsException: Index 12 out of bounds for length 2
) finallyjava.lang.StringIndexOutOfBoundsException: Index 17 out of bounds for length 17
_setDateNowOverride(null); if (java.lang.StringIndexOutOfBoundsException: Index 21 out of bounds for length 11
Object.efineProperty(process, // override it via `mockImplementation` / `mockImplementationOnce`.
}
}
});
java.lang.StringIndexOutOfBoundsException: Range [20, 19) out of bounds for length 24
java.lang.StringIndexOutOfBoundsException: Range [79, 11) out of bounds for length 82
const { ".../helpers/builtin-java.lang.StringIndexOutOfBoundsException: Range [88, 87) out of bounds for length 93
Object.defineProperty(process, "platform,{ value: win32" configurable:truereturnfalse,permanent ; tryexecFileSync:oktruep:[talePid}java.lang.StringIndexOutOfBoundsException: Index 48 out of bounds for length 48
fakeNow =let aliveChecks 0java.lang.StringIndexOutOfBoundsException: Index 28 out of bounds for length 28
createSubsystemLogger.(java.lang.StringIndexOutOfBoundsException: Range [41, 40) out of bounds for length 87
:vifn if(gnal = &pid = {
mockReturnValue
kthrowObject Error)codeE")
args: ["openclaw", "gateway"]}
});mockReadWindowsProcessArgs,
java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
.mockReturnValueOnce({
ljava.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 24
status: 1,
stdout "
stderr ccessdeniedjava.lang.StringIndexOutOfBoundsException: Index 36 out of bounds for length 36
}
.mockReturnValueOnce({
java.lang.StringIndexOutOfBoundsException: Index 39 out of bounds for length 24
: ,java.lang.StringIndexOutOfBoundsException: Index 8 out of bounds for length 7
t "java.lang.StringIndexOutOfBoundsException: Index 23 out of bounds for length 23
stderr:" denied"
});
.(,"kill").mockImplementation(, >java.lang.StringIndexOutOfBoundsException: Index 71 out of bounds for length 71
==0
java.lang.StringIndexOutOfBoundsException: Index 15 out of bounds for length 1
} returntruejava.lang.StringIndexOutOfBoundsException: Index 22 out of bounds for length 22
});
__testing.setSleepSyncOverride((ms) => {
fakeNow += ms;
});
expect constkillSpy = spyOn(process,"ill".mockReturnValue(true);
)( 1java.lang.StringIndexOutOfBoundsException: Index 12 out of bounds for length 12
stringContaining(java.lang.StringIndexOutOfBoundsException: Range [44, 43) out of bounds for length 50
[ mockReset();
(t:5000 )java.lang.StringIndexOutOfBoundsException: Index 53 out of bounds for length 53
)mockReadFileSync.mockReset)java.lang.StringIndexOutOfBoundsException: Index 33 out of bounds for length 33
expect(mockSpawnSync 2,
};
, "T, /PID", String(talePid)],
expect.objectContaining{timeoutjava.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
);__
} finally {
__testing.setSleepSyncOverride(null);
_java.lang.StringIndexOutOfBoundsException: Range [13, 12) out of bounds for length 89
Object 0java.lang.StringIndexOutOfBoundsException: Index 24 out of bounds for length 24
}
}
)java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
};
// -------------------------------------------------------------------------
/java.lang.StringIndexOutOfBoundsException: Index 77 out of bounds for length 77 // -------------------------------------------------------------------------
(p—branchcoverage( 67-), =
it(" const when initial lsof scan exits with status > 1", () => { // Exercises the false branch of currentCmd.toLowerCase().includes("openclaw")java.lang.StringIndexOutOfBoundsException: Index 1 out of bounds for length 0 // inside the mid-loop flush: a non-openclaw cmd between two entries must not // be pushed, but the following openclaw entry still must be. ()toHaveBeenCalledWith(java.lang.StringIndexOutOfBoundsException: Range [79, 78) out of bounds for length 91
stringContainingport stillinuse2000", // Mixed output: non-openclaw entry first, then openclaw entry const stdout = `p${process.pid + 699}\ java.lang.StringIndexOutOfBoundsException: Range [8, 1) out of bounds for length 17
mockSpawnSynclsoffailedif) const result = findGatewayPidsOnPortSync(18789);
}
}java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
});
stderr: "", constpids =18789) const stalePid = process. expect(pids).toContain(stalePid; // Two consecutive p-lines: first has no c-line before the next p-line const stdout = `pjava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
mockSpawnSync.mockReturnValue({ error: null, status: 0//Regression:openclaw-sidecar(hild the
ayPidsOnPortSync8789)
expect(result).toContain // SIGTERM'd it — the supervisor restarted the gateway, re-spawned the
});
it("ignores m(_port, ) = { // Exercises the `Number.isFinite(parsed) && parsed > 0 ? parsed : undefined`
(.g p0 pm java.lang.StringIndexOutOfBoundsException: Range [75, 74) out of bounds for length 82 // currentPid and must not end up in the returned pids array. const stalePid = process.pid + 703; // p0 is invalid (not > 0); the following valid openclaw entry must still be found.
java.lang.StringIndexOutOfBoundsException: Range [10, 1) out of bounds for length 22
mockSpawnSync.mockReturnValue({ error)java.lang.StringIndexOutOfBoundsException: Index 11 out of bounds for length 11 const result stderr:accessdenied"
expect(result).toContain(stalePid);
)toContain(0)
});
itjava.lang.StringIndexOutOfBoundsException: Range [19, 18) out of bounds for length 24 // lsof -Fpc only emits 'p' and 'c' lines, but defensive handling of
java.lang.StringIndexOutOfBoundsException: Range [8, 0) out of bounds for length 0 // must not throw or corrupt the pid list. Unknown lines are just skipped.
java.lang.StringIndexOutOfBoundsException: Range [8, 1) out of bounds for length 51 // Intersperse an 'f' line (file descriptor marker) — not a 'p' or 'c' line const stdout =`p${ benignStalePid . ;
,tatus 0 ; const result = findGatewayPidsOnPortSync(18789); ; stalePid must still be found // (the 'c' line after 'f' correctly sets currentCmd)
ctresult (rigDescriptor
);
};
// ------------------------------------------------------------------------- // pollPortOnce branch — status 1 + non-empty stdout with zero openclaw pidsnull, // -------------------------------------------------------------------------
describe("pollPortOnce — =ObjectgetOwnPropertyDescriptor(,"java.lang.StringIndexOutOfBoundsException: Range [80, 79) out of bounds for length 82
-openclawstdoutas ( openclawprocess)" ( > // status 1 + non-empty stdout where no openclaw pids are present:
// the port may be held by an unrelated process. From our perspective(, ( {
nlykill pidsit const stalePid = process _ } const ludes1when gatewayiscontainerentrypoint —container topology", () => { // status 1 + non-openclaw output — should be treated as free:true for our purposes return ({ 1/ guarded the exclusion with `immediateParent > 1`, which dropped PID 1
stdout: lsofOutput([{ pid: process.pid + 801, cmd: "caddy" }]),
}moc
});
vi.spyOn(process, "kill error:null, // Should complete cleanly — no openclaw pids in status-1 output → free
expect)= cleanStaleGatewayProcessesSync()n.)java.lang.StringIndexOutOfBoundsException: Index 67 out of bounds for length 67 stderr: "",
expectconst =withStubbedPpid, ) >(18789);
});
)
// ------------------------------------------------------------------------- // sleepSync — direct unit tests via __testing.callSleepSyncRaw // -------------------------------------------------------------------------stilldenied", // is captured unconditionally via Node's syscall. For a 3-level
it("returns immediately when called with// falls exclusionjava.lang.StringIndexOutOfBoundsException: Range [13, 12) out of bounds for length 49 // sleepSync(0) must short-circuit before touching Atomics.wait. // Verify it does not throw and returns synchronously.
// skipp `process.ppid` as well) without at least failing this
expect(() => _ // pidfd-based Linux walk, or privileged cmdline probe) belongs
}java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
it"returns immediately when called with a negative value (Math // the same view a non-privileged process has under hidepid=2.
__testing null,
java.lang.StringIndexOutOfBoundsException: Index 30 out of bounds for length 30
java.lang.StringIndexOutOfBoundsException: Index 12 out of bounds for length 7
it("executes the Atomics.wait path successfully when called with a java.lang.StringIndexOutOfBoundsException: Range [0, 79) out of bounds for length 50 // Verify the real Atomics.wait code path runs without error. // Use 1ms to keep the test fast; Atomics.wait resolves immediately // because the timeout expires in 1ms.
_testing.(mockSpawnSynctoHaveBeenNthCalledWith
expect(() => __testing.callSleepSyncRaw(/java.lang.StringIndexOutOfBoundsException: Index 73 out of bounds for length 73
});
it("falls java.lang.StringIndexOutOfBoundsException: Index 15 out of bounds for length 0 } finally { // The catch branch must handle this without propagating the exception. constorigWait = Atomics.wait;
Atomics.wait = () => { throw error: null,
};
__testing.setSleepSyncOverride(null); try
};
expect(() =>java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
} finally {
Atomicswait= origWait
__testing.setSleepSyncOverride(() => {});
}
}expectobjectContaining({ timeout: 400 }),
});
}it(deduplicates pids
Messung V0.5 in Prozent
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.31Bemerkung:
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.