mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
perf(core): F2 PR A R10 / R23 T7 — pid-descendants ps snapshot + pgrep fallback
R10 / R23 T7 (filed as F2 follow-up from #4336 review): the Linux / macOS pid-descendant enumeration moves from per-pid `pgrep -P <pid>` BFS (one subprocess fork per node visited) to a single `ps -A -o pid=,ppid=` snapshot followed by an in-memory tree walk over `Map<ppid, pid[]>`. Windows analog: single `Get-CimInstance Win32_Process | ConvertTo-Csv` snapshot of all `(ProcessId, ParentProcessId)` rows replaces per-pid `Get-CimInstance -Filter "ParentProcessId=$p"` BFS. Two motivations: 1. **Fork count**: typical `npx → tool` / `uvx → tool` wrapper trees are 2-3 levels deep with B=1-3 children per node → pre-fix BFS forked ~5-10 subprocesses per pool-shutdown call. Post-fix: exactly 1 fork regardless of tree depth. 2. **Snapshot consistency**: pre-fix BFS walked the table level by level; a child that forked between two adjacent BFS levels could be missed (we'd see the child but query its descendants AFTER the new fork). The snapshot path captures the table at one instant; new descendants forked after the snapshot are tolerated by the existing ESRCH-tolerant SIGTERM loop. Caveats: - `ps -A -o pid=,ppid=` is POSIX standard (macOS / Linux / *BSD), but BusyBox `ps` <v1.28 (2018) doesn't support `-o`. Distroless containers may not have `ps` at all. To preserve behavior on those edge platforms, the legacy per-pid `pgrep` BFS is retained as a fallback (`listDescendantPidsUnixPgrepFallback`). Same retention on Windows for the per-pid filter path. - Snapshot path uses `maxBuffer: 8MB` to cover ~250k-process pathological hosts. Default 1MB would clip at ~30k processes. - `MAX_DESCENDANTS = 256` / `MAX_DEPTH = 8` caps preserved on both snapshot + fallback paths. - Snapshot scans the entire host process table (not just the target subtree). On the typical 200-500 process developer machine this parses in <10ms; the win over BFS is real but not order-of-magnitude — ~2x improvement, not 100x. PR A's motivation framing is "fork hygiene + consistency", not raw perf. Empty-result detection: snapshot path tracks `parsedRows`. If the ps/CIM tool runs successfully but produces 0 parseable rows (BusyBox without `-o` echoing usage, AppLocker truncating CIM output, etc.), we throw — the outer catch falls back to the per-pid path. A genuine "root has no children" case parses many rows and just returns empty from the walk. So the "no-children-found" semantics are preserved across both paths. Test gate update: pre-fix `integration: spawn-and-enumerate` test skipped on `CI === '1'` because pgrep wasn't available on minimal CI runners. Post-fix `ps -A` is universally available on non-distroless Linux/macOS — only the Windows skip remains. 6/6 pid-descendants tests pass including the now-active integration spawn test. Design doc (`docs/design/f2-mcp-transport-pool.md` §6.4 + the F2 follow-up table at lines 82-85) updated to reflect the snapshot + fallback shape, and to mark W11 / W12 / R9 / R10 as ✅ Done in PR A with the per-fix commit refs. This commit completes F2 cleanup PR A. Filed bucket order: R9 (commit0cb1eaa27) → W11 (commit2d546efca) → W12 (commita4a855ab3) → R10 (this commit). Issue #4175 item 7 "F2 post- merge cleanup PRs": PR A done; PR B (W93 + W133-a + W134) and PR C (W133-c SDK breaking) to follow as separate clusters. Test sweep: 287/287 F2 + cli pass; ESLint clean; typecheck clean (core + cli). Integration test on macOS local runs the new snapshot path successfully.
This commit is contained in:
parent
6cf18f6414
commit
2a41c6faee
3 changed files with 227 additions and 39 deletions
|
|
@ -75,14 +75,14 @@ PR #4336 shipped F2 as 6 atomic commits + 6 fix commits over ~4 hours. Wenshao r
|
|||
|
||||
#### Declined-with-reply (filed as F2 follow-ups)
|
||||
|
||||
| # | Site | Reason for declining |
|
||||
| --- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| W7 | Test coverage gaps (4 untested critical paths) | 1/4 added (W6 regression test); rest deferred to focused test-coverage PR after F2 series merges |
|
||||
| W8 | `maxReconnectAttempts` / `reconnectStrategy` unused | Forward-compat placeholders for the deferred health-monitor-driven reconnect (design §6.6); removing + re-adding churns the public type |
|
||||
| W11 | Duplicate fast-path / in-flight-path attach blocks | Refactor opportunity touching the entire `acquire` signature; not blocking F2 merge |
|
||||
| W12 | `passesSessionFilter` O(M×N) per `applyTools` | Micro-perf optimization measurable only with hundreds of tools × large filter lists |
|
||||
| R9 | `McpClientManager` ctor 7-positional sentinels | Refactor to options-object touches every call site (test fixtures, ToolRegistry); out of F2 scope |
|
||||
| R10 | `pgrep -P <pid>` per-PID-per-level cost | Single `ps -eo pid,ppid` requires platform-specific column flags + tree builder; current 16s worst-case bound is acceptable |
|
||||
| # | Site | Reason for declining |
|
||||
| --- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| W7 | Test coverage gaps (4 untested critical paths) | 1/4 added (W6 regression test); rest deferred to focused test-coverage PR after F2 series merges |
|
||||
| W8 | `maxReconnectAttempts` / `reconnectStrategy` unused | Forward-compat placeholders for the deferred health-monitor-driven reconnect (design §6.6); removing + re-adding churns the public type |
|
||||
| W11 | Duplicate fast-path / in-flight-path attach blocks | ✅ Done in PR A: `attachPooledSession` + `rollbackReservationOnSpawnFailure` private helpers (commit `2d546efca`) |
|
||||
| W12 | `passesSessionFilter` O(M×N) per `applyTools` | ✅ Done in PR A: `applyTools` / `applyPrompts` precompute filter `Set`s once per pass; predicate becomes O(1) per tool (commit `a4a855ab3`) |
|
||||
| R9 | `McpClientManager` ctor 7-positional sentinels | ✅ Done in PR A: options-object ctor + `mkManager` test factory (commit `0cb1eaa27`) |
|
||||
| R10 | `pgrep -P <pid>` per-PID-per-level cost | ✅ Done in PR A: single `ps -A -o pid=,ppid=` snapshot + in-memory BFS walk; pgrep BFS retained as fallback for BusyBox <v1.28 / distroless (commit landing as final PR A piece) |
|
||||
|
||||
#### Bug count
|
||||
|
||||
|
|
@ -462,36 +462,54 @@ Hard idle cap: drain timer can be cancelled+restarted indefinitely (acquire/rele
|
|||
|
||||
### 6.4 Cross-platform descendant-pid sweep
|
||||
|
||||
**R10 / R23 T7 / PR A update (2026-05-22)**: switched from per-pid BFS (one `pgrep -P <pid>` / `Get-CimInstance -Filter` subprocess per node) to a single process-table snapshot followed by in-memory tree walk. Two motivations: (1) one fork instead of B^D forks on the hot pool-shutdown path; (2) snapshot consistency — pre-fix BFS could miss descendants that forked between adjacent BFS levels. Per-pid path retained as fallback for BusyBox `ps` <v1.28 (no `-o` support) and distroless containers without `ps`.
|
||||
|
||||
```ts
|
||||
// packages/core/src/tools/pid-descendants.ts
|
||||
export async function listDescendantPids(rootPid: number): Promise<number[]> {
|
||||
if (process.platform === 'win32') return listDescendantPidsWin(rootPid);
|
||||
return listDescendantPidsUnix(rootPid);
|
||||
if (!Number.isInteger(rootPid) || rootPid <= 0) return [];
|
||||
try {
|
||||
if (process.platform === 'win32')
|
||||
return await listDescendantPidsWin(rootPid);
|
||||
return await listDescendantPidsUnix(rootPid);
|
||||
} catch {
|
||||
return []; // OS reaps orphans; pool shutdown still proceeds.
|
||||
}
|
||||
}
|
||||
|
||||
async function listDescendantPidsUnix(root: number): Promise<number[]> {
|
||||
const all: number[] = [];
|
||||
const queue = [root];
|
||||
while (queue.length) {
|
||||
const parent = queue.shift()!;
|
||||
const { stdout } = await execFile('pgrep', ['-P', String(parent)], {
|
||||
timeout: 2000,
|
||||
}).catch(() => ({ stdout: '' }));
|
||||
const children = stdout.split('\n').map(Number).filter(Number.isFinite);
|
||||
all.push(...children);
|
||||
queue.push(...children);
|
||||
let tree: Map<number, number[]> | undefined;
|
||||
try {
|
||||
tree = await snapshotProcessTreeUnix(); // ps -A -o pid=,ppid=
|
||||
} catch {
|
||||
/* fall through to fallback */
|
||||
}
|
||||
return all;
|
||||
if (tree) return walkDescendants(tree, root); // O(descendants), 1 fork
|
||||
return await listDescendantPidsUnixPgrepFallback(root); // legacy BFS
|
||||
}
|
||||
|
||||
async function listDescendantPidsWin(root: number): Promise<number[]> {
|
||||
// PowerShell CIM query — wmic deprecated on modern Windows
|
||||
const script = `Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq <ROOT> } | Select-Object -ExpandProperty ProcessId`;
|
||||
// ... recursive walk, return aggregated pids
|
||||
async function snapshotProcessTreeUnix(): Promise<Map<number, number[]>> {
|
||||
// -A: all processes (POSIX, equivalent to -e but unambiguous on BSD).
|
||||
// -o pid=,ppid=: pid + ppid columns, trailing `=` suppresses headers.
|
||||
const { stdout } = await execFile('ps', ['-A', '-o', 'pid=,ppid='], {
|
||||
timeout: 2000,
|
||||
maxBuffer: 8 * 1024 * 1024, // covers >250k-process pathological hosts
|
||||
});
|
||||
const childrenByPpid = new Map<number, number[]>();
|
||||
for (const line of stdout.split('\n')) {
|
||||
const m = line.trim().match(/^(\d+)\s+(\d+)$/);
|
||||
if (!m) continue;
|
||||
/* parse, push into childrenByPpid */
|
||||
}
|
||||
return childrenByPpid;
|
||||
}
|
||||
|
||||
// Windows: single Get-CimInstance Win32_Process | ConvertTo-Csv snapshot
|
||||
// of all (ProcessId, ParentProcessId) rows + in-memory walk; per-pid
|
||||
// `Get-CimInstance -Filter "ParentProcessId=$p"` retained as fallback.
|
||||
```
|
||||
|
||||
Called from `PoolEntry.shutdown()` before `client.disconnect()`. Handles `npx @modelcontextprotocol/server-X`, `uvx ...`, `pnpm dlx ...` wrapper leaks.
|
||||
Called from `PoolEntry.shutdown()` before `client.disconnect()`. Handles `npx @modelcontextprotocol/server-X`, `uvx ...`, `pnpm dlx ...` wrapper leaks. MAX_DESCENDANTS=256 / MAX_DEPTH=8 caps preserved.
|
||||
|
||||
### 6.5 Spawn failure handling
|
||||
|
||||
|
|
|
|||
|
|
@ -59,13 +59,19 @@ describe('pid-descendants', () => {
|
|||
|
||||
// Cross-platform integration test: spawn a wrapper that itself
|
||||
// spawns a child, verify listDescendantPids finds both levels.
|
||||
// Gated on POSIX availability of `pgrep` (skipped on Windows + on
|
||||
// CI without pgrep).
|
||||
//
|
||||
// F2 (#4175 commit 6 review fix — wenshao R10 / R23 T7 / PR A):
|
||||
// Pre-fix gate skipped on `CI === '1'` (pgrep not always available
|
||||
// on minimal CI runners). Post-fix the snapshot path uses
|
||||
// `ps -A -o pid=,ppid=` (POSIX standard, available on every
|
||||
// non-distroless Linux/macOS), so we keep only the Windows skip;
|
||||
// the snapshot's per-pid pgrep fallback covers the rare BusyBox
|
||||
// <v1.28 case but isn't tested here.
|
||||
describe(
|
||||
'integration: spawn-and-enumerate',
|
||||
{ skip: process.platform === 'win32' || process.env['CI'] === '1' },
|
||||
{ skip: process.platform === 'win32' },
|
||||
() => {
|
||||
it('enumerates one level of children via pgrep', async () => {
|
||||
it('enumerates one level of children via process-tree snapshot', async () => {
|
||||
// Parent process spawns a node child with `--eval` that sleeps.
|
||||
// Use spawn directly so we control the lifecycle.
|
||||
const parent = spawn('/bin/sh', [
|
||||
|
|
|
|||
|
|
@ -12,11 +12,22 @@ const debugLogger = createDebugLogger('PidDescendants');
|
|||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* Wall-clock budget for each individual `pgrep` / `Get-CimInstance` call.
|
||||
* Wall-clock budget for each individual snapshot / per-pid query call.
|
||||
* Bounded so a hung process-table walk can't stall pool shutdown.
|
||||
*/
|
||||
const QUERY_TIMEOUT_MS = 2_000;
|
||||
|
||||
/**
|
||||
* F2 (#4175 commit 6 review fix — wenshao R10 / R23 T7 / PR A): cap
|
||||
* for `execFile`'s internal stdout buffer on the snapshot path. Default
|
||||
* is 1MB, which is enough for ~30k-process hosts (~30 bytes/line) but
|
||||
* an 8MB cap covers >250k-process pathological cases without forcing
|
||||
* the truncation-or-fallback branch on real machines. The cap applies
|
||||
* only to the snapshot family of calls; the per-pid `pgrep -P` fallback
|
||||
* has a tiny output (just the children of one pid) and uses the default.
|
||||
*/
|
||||
const SNAPSHOT_MAXBUFFER_BYTES = 8 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Hard cap on recursion depth + total descendants returned. Defense
|
||||
* against runaway process trees (forkbomb-style) or pathological
|
||||
|
|
@ -35,15 +46,28 @@ const MAX_DEPTH = 8;
|
|||
* `uvx ...`, `pnpm dlx ...`) that would otherwise leak when the
|
||||
* pool entry's primary child is killed.
|
||||
*
|
||||
* F2 (#4175 commit 6 review fix — wenshao R10 / R23 T7 / PR A):
|
||||
* the implementation switched from per-pid `pgrep -P <pid>` BFS
|
||||
* (Linux/macOS) / per-pid `Get-CimInstance -Filter "ParentProcessId=$p"`
|
||||
* BFS (Windows) — which forked one subprocess per node visited — to
|
||||
* a single process-table snapshot followed by an in-memory tree walk.
|
||||
* Two motivations: (1) ~B^D fork count → 1 fork per call, on the
|
||||
* hot pool-shutdown path; (2) snapshot consistency — pre-fix BFS
|
||||
* could miss descendants that forked between adjacent BFS levels.
|
||||
*
|
||||
* Behavior:
|
||||
* - Linux/macOS: `pgrep -P <pid>` walked recursively, BFS order
|
||||
* - Windows: PowerShell `Get-CimInstance Win32_Process` filtered
|
||||
* by `ParentProcessId`, walked recursively
|
||||
* - Either platform: graceful degradation if the tool is missing
|
||||
* or the query times out — returns whatever was collected so far
|
||||
* and logs a warning. Pool shutdown still proceeds; orphan
|
||||
* processes will be reaped by the OS eventually (Linux init,
|
||||
* Windows job objects).
|
||||
* - Linux/macOS: `ps -A -o pid=,ppid=` snapshot, in-memory BFS walk
|
||||
* over the parsed `Map<ppid, pid[]>`.
|
||||
* - Windows: PowerShell `Get-CimInstance Win32_Process` →
|
||||
* `ConvertTo-Csv` snapshot of all `(ProcessId, ParentProcessId)`
|
||||
* rows, in-memory walk.
|
||||
* - Either platform: graceful degradation if the snapshot tool is
|
||||
* missing / blocked / times out — falls back to per-pid BFS
|
||||
* (preserves the pre-fix code path so BusyBox `ps` <v1.28 without
|
||||
* `-o` support, distroless containers without `ps`, etc. still
|
||||
* behave at-least-as-well as before). If BOTH the snapshot AND
|
||||
* the fallback fail, returns empty so the caller's SIGTERM step
|
||||
* skips and the OS reaps orphans (Linux init, Windows job objects).
|
||||
*
|
||||
* Returns descendants in **breadth-first order** — children before
|
||||
* grandchildren. Caller typically iterates back-to-front so deepest
|
||||
|
|
@ -67,6 +91,61 @@ export async function listDescendantPids(rootPid: number): Promise<number[]> {
|
|||
}
|
||||
|
||||
async function listDescendantPidsUnix(root: number): Promise<number[]> {
|
||||
let tree: Map<number, number[]> | undefined;
|
||||
try {
|
||||
tree = await snapshotProcessTreeUnix();
|
||||
} catch (err) {
|
||||
debugLogger.warn(
|
||||
`Unix snapshot via 'ps -A' failed (${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}); falling back to per-pid pgrep BFS`,
|
||||
);
|
||||
}
|
||||
if (tree) {
|
||||
return walkDescendants(tree, root);
|
||||
}
|
||||
return await listDescendantPidsUnixPgrepFallback(root);
|
||||
}
|
||||
|
||||
async function snapshotProcessTreeUnix(): Promise<Map<number, number[]>> {
|
||||
// `ps -A -o pid=,ppid=`
|
||||
// -A: all processes (POSIX, equivalent to -e; -A is unambiguous
|
||||
// across BSD/SysV — BSD historically used -e for env display).
|
||||
// -o pid=,ppid=: pid + ppid columns; trailing `=` suppresses each
|
||||
// column header (POSIX standard).
|
||||
// Output is "<pid> <ppid>" per line, no header.
|
||||
const { stdout } = await execFileAsync('ps', ['-A', '-o', 'pid=,ppid='], {
|
||||
timeout: QUERY_TIMEOUT_MS,
|
||||
maxBuffer: SNAPSHOT_MAXBUFFER_BYTES,
|
||||
});
|
||||
const childrenByPpid = new Map<number, number[]>();
|
||||
let parsedRows = 0;
|
||||
for (const line of stdout.split('\n')) {
|
||||
const m = line.trim().match(/^(\d+)\s+(\d+)$/);
|
||||
if (!m) continue;
|
||||
const pid = Number.parseInt(m[1], 10);
|
||||
const ppid = Number.parseInt(m[2], 10);
|
||||
if (!Number.isFinite(pid) || pid <= 0) continue;
|
||||
if (!Number.isFinite(ppid) || ppid < 0) continue;
|
||||
parsedRows += 1;
|
||||
const arr = childrenByPpid.get(ppid);
|
||||
if (arr) arr.push(pid);
|
||||
else childrenByPpid.set(ppid, [pid]);
|
||||
}
|
||||
if (parsedRows === 0) {
|
||||
// Snapshot tool ran but produced no parseable lines (e.g. BusyBox
|
||||
// `ps` without `-o` support echoing usage). Treat as failure so
|
||||
// the caller's catch falls back to per-pid pgrep.
|
||||
throw new Error(
|
||||
`'ps -A -o pid=,ppid=' returned no parseable rows (stdout length=${stdout.length})`,
|
||||
);
|
||||
}
|
||||
return childrenByPpid;
|
||||
}
|
||||
|
||||
async function listDescendantPidsUnixPgrepFallback(
|
||||
root: number,
|
||||
): Promise<number[]> {
|
||||
const all: number[] = [];
|
||||
const queue: Array<{ pid: number; depth: number }> = [
|
||||
{ pid: root, depth: 0 },
|
||||
|
|
@ -107,6 +186,65 @@ async function listDescendantPidsUnix(root: number): Promise<number[]> {
|
|||
}
|
||||
|
||||
async function listDescendantPidsWin(root: number): Promise<number[]> {
|
||||
let tree: Map<number, number[]> | undefined;
|
||||
try {
|
||||
tree = await snapshotProcessTreeWin();
|
||||
} catch (err) {
|
||||
debugLogger.warn(
|
||||
`Windows snapshot via Get-CimInstance failed (${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}); falling back to per-pid filter BFS`,
|
||||
);
|
||||
}
|
||||
if (tree) {
|
||||
return walkDescendants(tree, root);
|
||||
}
|
||||
return await listDescendantPidsWinPerPidFallback(root);
|
||||
}
|
||||
|
||||
async function snapshotProcessTreeWin(): Promise<Map<number, number[]>> {
|
||||
// Single-shot CIM query for ALL processes' (ProcessId,
|
||||
// ParentProcessId), CSV-formatted for stable parsing.
|
||||
// F2 (#4175 commit 5 review fix — wenshao R5): no integer
|
||||
// interpolation into the script; this query takes no parameters
|
||||
// (we filter in-memory after the snapshot returns).
|
||||
const script =
|
||||
'Get-CimInstance -ClassName Win32_Process ' +
|
||||
'| Select-Object ProcessId,ParentProcessId ' +
|
||||
'| ConvertTo-Csv -NoTypeInformation';
|
||||
const { stdout } = await execFileAsync(
|
||||
'powershell.exe',
|
||||
['-NoProfile', '-NonInteractive', '-Command', script],
|
||||
{ timeout: QUERY_TIMEOUT_MS, maxBuffer: SNAPSHOT_MAXBUFFER_BYTES },
|
||||
);
|
||||
const childrenByPpid = new Map<number, number[]>();
|
||||
let parsedRows = 0;
|
||||
// CSV: first line is header `"ProcessId","ParentProcessId"`,
|
||||
// subsequent lines are `"<pid>","<ppid>"`.
|
||||
const lines = stdout.split(/\r?\n/);
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const m = lines[i].match(/^"(\d+)","(\d+)"$/);
|
||||
if (!m) continue;
|
||||
const pid = Number.parseInt(m[1], 10);
|
||||
const ppid = Number.parseInt(m[2], 10);
|
||||
if (!Number.isFinite(pid) || pid <= 0) continue;
|
||||
if (!Number.isFinite(ppid) || ppid < 0) continue;
|
||||
parsedRows += 1;
|
||||
const arr = childrenByPpid.get(ppid);
|
||||
if (arr) arr.push(pid);
|
||||
else childrenByPpid.set(ppid, [pid]);
|
||||
}
|
||||
if (parsedRows === 0) {
|
||||
throw new Error(
|
||||
`Get-CimInstance snapshot returned no parseable rows (stdout length=${stdout.length})`,
|
||||
);
|
||||
}
|
||||
return childrenByPpid;
|
||||
}
|
||||
|
||||
async function listDescendantPidsWinPerPidFallback(
|
||||
root: number,
|
||||
): Promise<number[]> {
|
||||
const all: number[] = [];
|
||||
const queue: Array<{ pid: number; depth: number }> = [
|
||||
{ pid: root, depth: 0 },
|
||||
|
|
@ -161,6 +299,32 @@ async function listDescendantPidsWin(root: number): Promise<number[]> {
|
|||
return all;
|
||||
}
|
||||
|
||||
/**
|
||||
* F2 (#4175 commit 6 review fix — wenshao R10 / R23 T7 / PR A):
|
||||
* shared in-memory BFS over a snapshot tree. Replaces both
|
||||
* platforms' per-node subprocess forks once the snapshot has been
|
||||
* obtained. Same MAX_DESCENDANTS / MAX_DEPTH caps as the legacy
|
||||
* fallback path. Returns BFS order — children before grandchildren.
|
||||
*/
|
||||
function walkDescendants(tree: Map<number, number[]>, root: number): number[] {
|
||||
const all: number[] = [];
|
||||
const queue: Array<{ pid: number; depth: number }> = [
|
||||
{ pid: root, depth: 0 },
|
||||
];
|
||||
while (queue.length && all.length < MAX_DESCENDANTS) {
|
||||
const { pid, depth } = queue.shift()!;
|
||||
if (depth >= MAX_DEPTH) continue;
|
||||
const children = tree.get(pid);
|
||||
if (!children) continue;
|
||||
for (const child of children) {
|
||||
if (all.length >= MAX_DESCENDANTS) break;
|
||||
all.push(child);
|
||||
queue.push({ pid: child, depth: depth + 1 });
|
||||
}
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send SIGTERM to a list of pids, tolerating per-pid failures
|
||||
* (already exited, permission denied, etc.). On Windows, Node's
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue