Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
427
extensions/acpx/src/process-reaper.ts
Normal file
427
extensions/acpx/src/process-reaper.ts
Normal file
@@ -0,0 +1,427 @@
|
||||
/**
|
||||
* ACPX process ownership checks and cleanup. The reaper only terminates
|
||||
* OpenClaw-owned wrapper trees after validating paths, packages, and lease ids.
|
||||
*/
|
||||
import { execFile } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { splitCommandParts } from "./command-line.js";
|
||||
import { resolveAcpxPluginRoot } from "./config.js";
|
||||
import { OPENCLAW_ACPX_LEASE_ID_ARG, OPENCLAW_GATEWAY_INSTANCE_ID_ARG } from "./process-lease.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const requireFromHere = createRequire(import.meta.url);
|
||||
const GENERATED_WRAPPER_BASENAMES = new Set([
|
||||
"codex-acp-wrapper.mjs",
|
||||
"claude-agent-acp-wrapper.mjs",
|
||||
]);
|
||||
const OPENCLAW_PLUGIN_DEPS_MARKER = "/plugin-runtime-deps/";
|
||||
const OWNED_ACP_PACKAGE_NAMES = [
|
||||
"@zed-industries/codex-acp",
|
||||
"@zed-industries/codex-acp-darwin-arm64",
|
||||
"@zed-industries/codex-acp-darwin-x64",
|
||||
"@zed-industries/codex-acp-linux-arm64",
|
||||
"@zed-industries/codex-acp-linux-x64",
|
||||
"@zed-industries/codex-acp-win32-arm64",
|
||||
"@zed-industries/codex-acp-win32-x64",
|
||||
"@agentclientprotocol/claude-agent-acp",
|
||||
"acpx",
|
||||
];
|
||||
const ACP_PACKAGE_MARKERS = [
|
||||
...OWNED_ACP_PACKAGE_NAMES.map((packageName) => `/node_modules/${packageName}/`),
|
||||
"/acpx/dist/",
|
||||
];
|
||||
|
||||
/** Minimal process-table row used by ACPX cleanup. */
|
||||
export type AcpxProcessInfo = {
|
||||
pid: number;
|
||||
ppid: number;
|
||||
command: string;
|
||||
};
|
||||
|
||||
/** Injectable process-listing and termination hooks for tests. */
|
||||
export type AcpxProcessCleanupDeps = {
|
||||
listProcesses?: () => Promise<AcpxProcessInfo[]>;
|
||||
killProcess?: (pid: number, signal: NodeJS.Signals) => void;
|
||||
sleep?: (ms: number) => Promise<void>;
|
||||
};
|
||||
|
||||
/** Result from cleaning up a single ACPX process tree. */
|
||||
export type AcpxProcessCleanupResult = {
|
||||
inspectedPids: number[];
|
||||
terminatedPids: number[];
|
||||
skippedReason?: "missing-root" | "not-openclaw-owned" | "unverified-root";
|
||||
};
|
||||
|
||||
/** Result from startup orphan reaping. */
|
||||
export type AcpxStartupReapResult = {
|
||||
inspectedPids: number[];
|
||||
terminatedPids: number[];
|
||||
skippedReason?: "unsupported-platform" | "process-list-unavailable";
|
||||
};
|
||||
|
||||
function normalizePathLike(value: string): string {
|
||||
return value.replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
function resolvePackageRoot(packageName: string): string | undefined {
|
||||
try {
|
||||
return normalizePathLike(path.dirname(requireFromHere.resolve(`${packageName}/package.json`)));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveOpenClawInstallRoot(pluginRoot: string): string {
|
||||
if (
|
||||
path.basename(pluginRoot) === "acpx" &&
|
||||
path.basename(path.dirname(pluginRoot)) === "extensions"
|
||||
) {
|
||||
const parent = path.dirname(path.dirname(pluginRoot));
|
||||
return path.basename(parent) === "dist" ? path.dirname(parent) : parent;
|
||||
}
|
||||
return path.resolve(pluginRoot, "..");
|
||||
}
|
||||
|
||||
function resolveOwnedAcpPackageRootCandidates(packageName: string): string[] {
|
||||
const pluginRoot = resolveAcpxPluginRoot(import.meta.url);
|
||||
const openClawRoot = resolveOpenClawInstallRoot(pluginRoot);
|
||||
return [
|
||||
resolvePackageRoot(packageName),
|
||||
path.join(pluginRoot, "node_modules", packageName),
|
||||
path.join(openClawRoot, "node_modules", packageName),
|
||||
].flatMap((root) => (root ? [normalizePathLike(root)] : []));
|
||||
}
|
||||
|
||||
const OWNED_ACP_PACKAGE_ROOTS = Array.from(
|
||||
new Set(OWNED_ACP_PACKAGE_NAMES.flatMap(resolveOwnedAcpPackageRootCandidates)),
|
||||
);
|
||||
|
||||
function commandBelongsToResolvedAcpPackage(command: string): boolean {
|
||||
return OWNED_ACP_PACKAGE_ROOTS.some((root) => command.includes(`${root}/`));
|
||||
}
|
||||
|
||||
function commandMentionsGeneratedWrapper(command: string): boolean {
|
||||
return Array.from(GENERATED_WRAPPER_BASENAMES).some((basename) => command.includes(basename));
|
||||
}
|
||||
|
||||
function commandWrapperBelongsToRoot(command: string, wrapperRoot: string | undefined): boolean {
|
||||
if (!wrapperRoot) {
|
||||
return true;
|
||||
}
|
||||
const normalizedCommand = normalizePathLike(command);
|
||||
const normalizedRoot = normalizePathLike(wrapperRoot).replace(/\/+$/, "");
|
||||
return Array.from(GENERATED_WRAPPER_BASENAMES).some((basename) =>
|
||||
normalizedCommand.includes(`${normalizedRoot}/${basename}`),
|
||||
);
|
||||
}
|
||||
|
||||
/** Check whether a command references an OpenClaw-generated ACPX wrapper path. */
|
||||
export function isOpenClawLeaseAwareAcpxProcessCommand(params: {
|
||||
command: string | undefined;
|
||||
wrapperRoot?: string;
|
||||
}): boolean {
|
||||
const command = params.command?.trim();
|
||||
if (!command) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizePathLike(command);
|
||||
return (
|
||||
commandMentionsGeneratedWrapper(normalized) &&
|
||||
commandWrapperBelongsToRoot(normalized, params.wrapperRoot)
|
||||
);
|
||||
}
|
||||
|
||||
function commandsReferToSameRootCommand(liveCommand: string, storedCommand: string | undefined) {
|
||||
if (!storedCommand?.trim()) {
|
||||
return true;
|
||||
}
|
||||
return normalizePathLike(liveCommand).trim() === normalizePathLike(storedCommand).trim();
|
||||
}
|
||||
|
||||
function commandOptionEquals(
|
||||
parts: string[],
|
||||
option: string,
|
||||
expected: string | undefined,
|
||||
): boolean {
|
||||
if (!expected) {
|
||||
return true;
|
||||
}
|
||||
const index = parts.indexOf(option);
|
||||
return index >= 0 && parts[index + 1] === expected;
|
||||
}
|
||||
|
||||
function liveCommandMatchesLeaseIdentity(params: {
|
||||
command: string | undefined;
|
||||
expectedLeaseId?: string;
|
||||
expectedGatewayInstanceId?: string;
|
||||
}): boolean {
|
||||
if (!params.expectedLeaseId && !params.expectedGatewayInstanceId) {
|
||||
return true;
|
||||
}
|
||||
const parts = splitCommandParts(params.command ?? "");
|
||||
return (
|
||||
commandOptionEquals(parts, OPENCLAW_ACPX_LEASE_ID_ARG, params.expectedLeaseId) &&
|
||||
commandOptionEquals(parts, OPENCLAW_GATEWAY_INSTANCE_ID_ARG, params.expectedGatewayInstanceId)
|
||||
);
|
||||
}
|
||||
|
||||
/** Check whether a command is owned by OpenClaw ACPX runtime packages or wrappers. */
|
||||
export function isOpenClawOwnedAcpxProcessCommand(params: {
|
||||
command: string | undefined;
|
||||
wrapperRoot?: string;
|
||||
}): boolean {
|
||||
const command = params.command?.trim();
|
||||
if (!command) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizePathLike(command);
|
||||
if (
|
||||
isOpenClawLeaseAwareAcpxProcessCommand({
|
||||
command: normalized,
|
||||
wrapperRoot: params.wrapperRoot,
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (commandBelongsToResolvedAcpPackage(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (!normalized.includes(OPENCLAW_PLUGIN_DEPS_MARKER)) {
|
||||
return false;
|
||||
}
|
||||
return ACP_PACKAGE_MARKERS.some((marker) => normalized.includes(marker));
|
||||
}
|
||||
|
||||
function parseProcessList(stdout: string): AcpxProcessInfo[] {
|
||||
const processes: AcpxProcessInfo[] = [];
|
||||
for (const line of stdout.split(/\r?\n/)) {
|
||||
const match = /^\s*(?<pid>\d+)\s+(?<ppid>\d+)\s+(?<command>.+?)\s*$/.exec(line);
|
||||
if (!match?.groups) {
|
||||
continue;
|
||||
}
|
||||
processes.push({
|
||||
pid: Number.parseInt(match.groups.pid, 10),
|
||||
ppid: Number.parseInt(match.groups.ppid, 10),
|
||||
command: match.groups.command,
|
||||
});
|
||||
}
|
||||
return processes;
|
||||
}
|
||||
|
||||
/** List host processes in the compact shape needed by ACPX cleanup. */
|
||||
export async function listPlatformProcesses(): Promise<AcpxProcessInfo[]> {
|
||||
if (process.platform === "win32") {
|
||||
return [];
|
||||
}
|
||||
const { stdout } = await execFileAsync("ps", ["-axo", "pid=,ppid=,command="], {
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
});
|
||||
return parseProcessList(stdout);
|
||||
}
|
||||
|
||||
function collectProcessTree(processes: AcpxProcessInfo[], rootPid: number): AcpxProcessInfo[] {
|
||||
const childrenByParent = new Map<number, AcpxProcessInfo[]>();
|
||||
for (const processInfo of processes) {
|
||||
const children = childrenByParent.get(processInfo.ppid) ?? [];
|
||||
children.push(processInfo);
|
||||
childrenByParent.set(processInfo.ppid, children);
|
||||
}
|
||||
|
||||
const byPid = new Map(processes.map((processInfo) => [processInfo.pid, processInfo]));
|
||||
const root = byPid.get(rootPid);
|
||||
const collected: AcpxProcessInfo[] = [];
|
||||
if (root) {
|
||||
collected.push(root);
|
||||
}
|
||||
|
||||
const queue = [...(childrenByParent.get(rootPid) ?? [])];
|
||||
while (queue.length > 0) {
|
||||
const next = queue.shift();
|
||||
if (!next || collected.some((processInfo) => processInfo.pid === next.pid)) {
|
||||
continue;
|
||||
}
|
||||
collected.push(next);
|
||||
queue.push(...(childrenByParent.get(next.pid) ?? []));
|
||||
}
|
||||
|
||||
return collected;
|
||||
}
|
||||
|
||||
function uniquePids(processes: AcpxProcessInfo[]): number[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
processes
|
||||
.map((processInfo) => processInfo.pid)
|
||||
.filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function isProcessAlive(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function terminatePids(
|
||||
pids: number[],
|
||||
deps: AcpxProcessCleanupDeps | undefined,
|
||||
): Promise<number[]> {
|
||||
const killProcess = deps?.killProcess ?? ((pid, signal) => process.kill(pid, signal));
|
||||
const sleep =
|
||||
deps?.sleep ??
|
||||
((ms) =>
|
||||
new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
}));
|
||||
const terminated: number[] = [];
|
||||
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
killProcess(pid, "SIGTERM");
|
||||
terminated.push(pid);
|
||||
} catch {
|
||||
// The process may already be gone.
|
||||
}
|
||||
}
|
||||
if (terminated.length === 0) {
|
||||
return terminated;
|
||||
}
|
||||
await sleep(750);
|
||||
for (const pid of terminated) {
|
||||
if (deps?.killProcess || isProcessAlive(pid)) {
|
||||
try {
|
||||
killProcess(pid, "SIGKILL");
|
||||
} catch {
|
||||
// Best-effort cleanup only.
|
||||
}
|
||||
}
|
||||
}
|
||||
return terminated;
|
||||
}
|
||||
|
||||
/** Terminate one validated OpenClaw-owned ACPX wrapper process tree. */
|
||||
export async function cleanupOpenClawOwnedAcpxProcessTree(params: {
|
||||
rootPid?: number;
|
||||
rootCommand?: string;
|
||||
expectedLeaseId?: string;
|
||||
expectedGatewayInstanceId?: string;
|
||||
wrapperRoot?: string;
|
||||
deps?: AcpxProcessCleanupDeps;
|
||||
}): Promise<AcpxProcessCleanupResult> {
|
||||
const rootPid = params.rootPid;
|
||||
if (!rootPid || rootPid <= 0 || rootPid === process.pid) {
|
||||
return { inspectedPids: [], terminatedPids: [], skippedReason: "missing-root" };
|
||||
}
|
||||
|
||||
let processes: AcpxProcessInfo[];
|
||||
try {
|
||||
processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
|
||||
} catch {
|
||||
processes = [];
|
||||
}
|
||||
|
||||
const listedTree = collectProcessTree(processes, rootPid);
|
||||
// Session-store PIDs are stale data. If the live process table cannot prove
|
||||
// that this PID still belongs to an OpenClaw-owned wrapper, fail closed to
|
||||
// avoid killing an unrelated process after PID reuse.
|
||||
if (listedTree.length === 0) {
|
||||
return { inspectedPids: [], terminatedPids: [], skippedReason: "unverified-root" };
|
||||
}
|
||||
const rootCommand = listedTree[0]?.command ?? params.rootCommand;
|
||||
const liveCommandWasGeneratedWrapper = commandMentionsGeneratedWrapper(
|
||||
normalizePathLike(rootCommand ?? ""),
|
||||
);
|
||||
const storedCommandWasGeneratedWrapper = commandMentionsGeneratedWrapper(
|
||||
normalizePathLike(params.rootCommand ?? ""),
|
||||
);
|
||||
if (!liveCommandWasGeneratedWrapper && storedCommandWasGeneratedWrapper) {
|
||||
return {
|
||||
inspectedPids: listedTree.map((processInfo) => processInfo.pid),
|
||||
terminatedPids: [],
|
||||
skippedReason: "not-openclaw-owned",
|
||||
};
|
||||
}
|
||||
if (
|
||||
!liveCommandWasGeneratedWrapper &&
|
||||
!commandsReferToSameRootCommand(rootCommand ?? "", params.rootCommand)
|
||||
) {
|
||||
return {
|
||||
inspectedPids: listedTree.map((processInfo) => processInfo.pid),
|
||||
terminatedPids: [],
|
||||
skippedReason: "not-openclaw-owned",
|
||||
};
|
||||
}
|
||||
if (
|
||||
!isOpenClawOwnedAcpxProcessCommand({
|
||||
command: rootCommand,
|
||||
wrapperRoot: params.wrapperRoot,
|
||||
})
|
||||
) {
|
||||
return {
|
||||
inspectedPids: listedTree.map((processInfo) => processInfo.pid),
|
||||
terminatedPids: [],
|
||||
skippedReason: "not-openclaw-owned",
|
||||
};
|
||||
}
|
||||
if (
|
||||
!liveCommandMatchesLeaseIdentity({
|
||||
command: rootCommand,
|
||||
expectedLeaseId: params.expectedLeaseId,
|
||||
expectedGatewayInstanceId: params.expectedGatewayInstanceId,
|
||||
})
|
||||
) {
|
||||
return {
|
||||
inspectedPids: listedTree.map((processInfo) => processInfo.pid),
|
||||
terminatedPids: [],
|
||||
skippedReason: "not-openclaw-owned",
|
||||
};
|
||||
}
|
||||
|
||||
const pids = uniquePids(listedTree.toReversed());
|
||||
return {
|
||||
inspectedPids: uniquePids(listedTree),
|
||||
terminatedPids: await terminatePids(pids, params.deps),
|
||||
};
|
||||
}
|
||||
|
||||
/** Reap orphaned OpenClaw-owned ACPX wrapper trees during runtime startup. */
|
||||
export async function reapStaleOpenClawOwnedAcpxOrphans(params: {
|
||||
wrapperRoot: string;
|
||||
deps?: AcpxProcessCleanupDeps;
|
||||
}): Promise<AcpxStartupReapResult> {
|
||||
if (process.platform === "win32") {
|
||||
return { inspectedPids: [], terminatedPids: [], skippedReason: "unsupported-platform" };
|
||||
}
|
||||
|
||||
let processes: AcpxProcessInfo[];
|
||||
try {
|
||||
processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
|
||||
} catch {
|
||||
return { inspectedPids: [], terminatedPids: [], skippedReason: "process-list-unavailable" };
|
||||
}
|
||||
|
||||
const orphans = processes.filter(
|
||||
(processInfo) =>
|
||||
processInfo.ppid === 1 &&
|
||||
isOpenClawOwnedAcpxProcessCommand({
|
||||
command: processInfo.command,
|
||||
wrapperRoot: params.wrapperRoot,
|
||||
}),
|
||||
);
|
||||
// Startup reaping starts from currently visible orphan roots and then expands
|
||||
// each tree, so adapter grandchildren do not survive as fresh orphans after
|
||||
// the wrapper root exits.
|
||||
const orphanTrees = orphans.map((orphan) => collectProcessTree(processes, orphan.pid));
|
||||
const inspectedPids = uniquePids(orphanTrees.flat());
|
||||
const pids = uniquePids(orphanTrees.flatMap((tree) => tree.toReversed()));
|
||||
return {
|
||||
inspectedPids,
|
||||
terminatedPids: await terminatePids(pids, params.deps),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user