Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@ npm install tinyps
## Usage

```ts
import { killTree, getProcessTree } from 'tinyps';
import { killTree, getProcessTree, getProcessAncestry } from 'tinyps';

// kill a process and everything it spawned
await killTree(child.pid);

// or inspect the tree first
const tree = await getProcessTree(child.pid);
console.log(tree.get(child.pid)); // [ 1234, 1235 ]

// trace a process's parent chain back to the root
const ancestry = await getProcessAncestry(child.pid);
console.log(ancestry.map((proc) => proc.pid)); // [ childPid, parentPid, ... ]
```

## API
Expand Down Expand Up @@ -49,6 +53,15 @@ process which has exited but has not yet been released by its parent.
- `pid` (`number`) - process to collect descendants of
- Returns `Promise<number[]>`

### `getProcessAncestry(pid?)`

- `pid` (`number`, optional) - process to trace ancestry from, defaults to `process.pid`
- Returns `Promise<ProcessInfo[]>`

Returns an ordered list of process info objects starting from the target process and traversing upward through its parents to the root process.

**Note:** On Windows, a parent may have exited and had its pid reassigned. In that case, the ancestry may contain unrelated processes.

### `listProcesses()`

- Returns `Promise<ProcessInfo[]>`
Expand Down
63 changes: 63 additions & 0 deletions src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
findProcessesByName,
findProcessesByPort,
getDescendants,
getProcessAncestry,
getProcessInfo,
getProcessTree,
isRunning,
Expand Down Expand Up @@ -310,6 +311,68 @@ describe('getDescendants', () => {
});
});

describe('getProcessAncestry', () => {
let parent: ChildProcess;
let childPid: number;
let grandchildPid: number;

beforeEach(async () => {
({ parent, childPid, grandchildPid } = await spawnTree());
});

afterEach(() => {
killTreeSync(parent, childPid, grandchildPid);
});

it('resolves the ancestry chain of the given process', async () => {
const ancestry = await getProcessAncestry(grandchildPid);
const pids = ancestry.map((proc) => proc.pid);

expect(pids.slice(0, 3)).toEqual([grandchildPid, childPid, parent.pid!]);
expect(ancestry[2]?.command).toContain('parent.js');
});

it('defaults to the current process pid', async () => {
const ancestry = await getProcessAncestry();

expect(ancestry[0]?.pid).toBe(process.pid);
});

it.skipIf(process.platform === 'win32')(
'ends with the root process',
async () => {
const ancestry = await getProcessAncestry();

expect(ancestry.at(-1)?.pid).toBe(1);
},
);

it.runIf(process.platform === 'win32')(
'ends with the system process or a missing parent',
async () => {
const ancestry = await getProcessAncestry();
const pids = (await listProcesses()).map((proc) => proc.pid);
const last = ancestry.at(-1)!;

expect(last.pid === 4 || !pids.includes(last.ppid)).toBe(true);
},
);

it('throws for a non-positive pid', async () => {
await expect(getProcessAncestry(0)).rejects.toThrow(TypeError);
await expect(getProcessAncestry(-1)).rejects.toThrow(TypeError);
});

it('resolves an empty list for an unknown pid', async () => {
killTreeSync(parent, childPid, grandchildPid);
await vi.waitFor(() => {
expect(isAlive(parent.pid!)).toBe(false);
});

expect(await getProcessAncestry(parent.pid!)).toEqual([]);
});
});

describe('listProcesses', () => {
let parent: ChildProcess;
let childPid: number;
Expand Down
31 changes: 31 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,34 @@ export async function getDescendants(pid: number): Promise<number[]> {
const tree = await getProcessTree(pid);
return Array.from(tree.keys()).filter((current) => current !== pid);
}

export async function getProcessAncestry(
pid: number = process.pid,
): Promise<ProcessInfo[]> {
if (pid <= 0) {
throw new TypeError(`Expected a positive pid, received: ${pid}`);
}

const processes = await listProcesses();
const processMap = new Map<number, ProcessInfo>(
processes.map((proc) => [proc.pid, proc]),
);

const ancestry: ProcessInfo[] = [];
let currentPid: number = pid;

// root processes have a ppid of 0, which ends the walk
while (currentPid > 0) {
const info = processMap.get(currentPid);

if (!info || ancestry.includes(info)) {
break;
}

ancestry.push(info);

currentPid = info.ppid;
}

return ancestry;
}
Loading