419 lines
9.6 KiB
TypeScript
419 lines
9.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
containsProcess,
|
|
getDescendants,
|
|
maxTreeDepth,
|
|
reconstructProcessTree,
|
|
type ProcessTreeEvent,
|
|
type ProcessEventType,
|
|
type ProcessNode,
|
|
type ProcessTreeReconstruction,
|
|
} from "../src/domain/eventProcessTree.js";
|
|
|
|
describe("event-backed process tree capture", () => {
|
|
it("reconstructs a simple process tree", () => {
|
|
const events: ProcessTreeEvent[] = [
|
|
{
|
|
sequence: 0,
|
|
at_ms: 0,
|
|
type: "spawn",
|
|
pid: 1,
|
|
ppid: 0,
|
|
process_name: "root",
|
|
executable: "/root",
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
{
|
|
sequence: 1,
|
|
at_ms: 10,
|
|
type: "spawn",
|
|
pid: 2,
|
|
ppid: 1,
|
|
process_name: "child",
|
|
executable: "/child",
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
{
|
|
sequence: 2,
|
|
at_ms: 20,
|
|
type: "exit",
|
|
pid: 2,
|
|
ppid: 1,
|
|
process_name: "child",
|
|
executable: null,
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: 0,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
{
|
|
sequence: 3,
|
|
at_ms: 30,
|
|
type: "exit",
|
|
pid: 1,
|
|
ppid: 0,
|
|
process_name: "root",
|
|
executable: null,
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: 0,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
];
|
|
const result = reconstructProcessTree(events, 1);
|
|
expect(result.processes).toHaveLength(2);
|
|
expect(result.events_consumed).toBe(4);
|
|
expect(result.events_unmatched).toBe(0);
|
|
expect(result.short_lived_descendants).toBe(2);
|
|
});
|
|
|
|
it("detects short-lived descendants", () => {
|
|
const events: ProcessTreeEvent[] = [
|
|
{
|
|
sequence: 0,
|
|
at_ms: 0,
|
|
type: "spawn",
|
|
pid: 1,
|
|
ppid: 0,
|
|
process_name: "root",
|
|
executable: "/root",
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
{
|
|
sequence: 1,
|
|
at_ms: 10,
|
|
type: "spawn",
|
|
pid: 2,
|
|
ppid: 1,
|
|
process_name: "short",
|
|
executable: "/short",
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
{
|
|
sequence: 2,
|
|
at_ms: 11,
|
|
type: "exit",
|
|
pid: 2,
|
|
ppid: 1,
|
|
process_name: "short",
|
|
executable: null,
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: 0,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
];
|
|
const result = reconstructProcessTree(events, 1);
|
|
expect(result.short_lived_descendants).toBe(1);
|
|
});
|
|
|
|
it("detects re-exec", () => {
|
|
const events: ProcessTreeEvent[] = [
|
|
{
|
|
sequence: 0,
|
|
at_ms: 0,
|
|
type: "spawn",
|
|
pid: 1,
|
|
ppid: 0,
|
|
process_name: "root",
|
|
executable: "/root",
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
{
|
|
sequence: 1,
|
|
at_ms: 10,
|
|
type: "re_exec",
|
|
pid: 1,
|
|
ppid: 0,
|
|
process_name: "root",
|
|
executable: "/newroot",
|
|
arguments: ["--new"],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
];
|
|
const result = reconstructProcessTree(events, 1);
|
|
expect(result.has_re_exec).toBe(true);
|
|
const root = result.processes.find((p) => p.pid === 1)!;
|
|
expect(root.is_re_exec).toBe(true);
|
|
expect(root.executable).toBe("/newroot");
|
|
});
|
|
|
|
it("detects reparenting", () => {
|
|
const events: ProcessTreeEvent[] = [
|
|
{
|
|
sequence: 0,
|
|
at_ms: 0,
|
|
type: "spawn",
|
|
pid: 1,
|
|
ppid: 0,
|
|
process_name: "root",
|
|
executable: "/root",
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
{
|
|
sequence: 1,
|
|
at_ms: 10,
|
|
type: "spawn",
|
|
pid: 2,
|
|
ppid: 1,
|
|
process_name: "child",
|
|
executable: "/child",
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
{
|
|
sequence: 2,
|
|
at_ms: 20,
|
|
type: "reparent",
|
|
pid: 2,
|
|
ppid: 0,
|
|
process_name: "child",
|
|
executable: null,
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: 1,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
];
|
|
const result = reconstructProcessTree(events, 1);
|
|
expect(result.has_reparenting).toBe(true);
|
|
const child = result.processes.find((p) => p.pid === 2)!;
|
|
expect(child.is_reparented).toBe(true);
|
|
expect(child.previous_ppid).toBe(1);
|
|
expect(child.ppid).toBe(0);
|
|
});
|
|
|
|
it("reports unmatched events for unknown exits", () => {
|
|
const events: ProcessTreeEvent[] = [
|
|
{
|
|
sequence: 0,
|
|
at_ms: 0,
|
|
type: "exit",
|
|
pid: 999,
|
|
ppid: null,
|
|
process_name: null,
|
|
executable: null,
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: 1,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
];
|
|
const result = reconstructProcessTree(events, 1);
|
|
expect(result.events_unmatched).toBe(1);
|
|
expect(result.processes).toHaveLength(0);
|
|
});
|
|
|
|
it("gets descendants", () => {
|
|
const events: ProcessTreeEvent[] = [
|
|
{
|
|
sequence: 0,
|
|
at_ms: 0,
|
|
type: "spawn",
|
|
pid: 1,
|
|
ppid: 0,
|
|
process_name: "root",
|
|
executable: "/root",
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
{
|
|
sequence: 1,
|
|
at_ms: 10,
|
|
type: "spawn",
|
|
pid: 2,
|
|
ppid: 1,
|
|
process_name: "child1",
|
|
executable: "/c1",
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
{
|
|
sequence: 2,
|
|
at_ms: 20,
|
|
type: "spawn",
|
|
pid: 3,
|
|
ppid: 2,
|
|
process_name: "grandchild",
|
|
executable: "/gc",
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
];
|
|
const result = reconstructProcessTree(events, 1);
|
|
const descendants = getDescendants(result.processes, 1);
|
|
expect(descendants).toHaveLength(2);
|
|
expect(descendants).toContain(2);
|
|
expect(descendants).toContain(3);
|
|
});
|
|
|
|
it("checks process containment", () => {
|
|
const events: ProcessTreeEvent[] = [
|
|
{
|
|
sequence: 0,
|
|
at_ms: 0,
|
|
type: "spawn",
|
|
pid: 1,
|
|
ppid: 0,
|
|
process_name: "root",
|
|
executable: "/root",
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
];
|
|
const result = reconstructProcessTree(events, 1);
|
|
expect(containsProcess(result.processes, 1)).toBe(true);
|
|
expect(containsProcess(result.processes, 999)).toBe(false);
|
|
});
|
|
|
|
it("computes max tree depth", () => {
|
|
const events: ProcessTreeEvent[] = [
|
|
{
|
|
sequence: 0,
|
|
at_ms: 0,
|
|
type: "spawn",
|
|
pid: 1,
|
|
ppid: 0,
|
|
process_name: "root",
|
|
executable: "/root",
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
{
|
|
sequence: 1,
|
|
at_ms: 10,
|
|
type: "spawn",
|
|
pid: 2,
|
|
ppid: 1,
|
|
process_name: "child",
|
|
executable: "/c",
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
{
|
|
sequence: 2,
|
|
at_ms: 20,
|
|
type: "spawn",
|
|
pid: 3,
|
|
ppid: 2,
|
|
process_name: "gc",
|
|
executable: "/gc",
|
|
arguments: [],
|
|
previous_pid: null,
|
|
previous_ppid: null,
|
|
exit_code: null,
|
|
signal: null,
|
|
tid: null,
|
|
},
|
|
];
|
|
const result = reconstructProcessTree(events, 1);
|
|
expect(maxTreeDepth(result.processes, 1)).toBe(2);
|
|
});
|
|
|
|
it("uses exported types", () => {
|
|
const node: ProcessNode = {
|
|
pid: 1,
|
|
ppid: null,
|
|
process_name: "test",
|
|
executable: "/test",
|
|
arguments: [],
|
|
children: [],
|
|
spawn_time_ms: 0,
|
|
exit_time_ms: null,
|
|
exit_code: null,
|
|
is_re_exec: false,
|
|
is_reparented: false,
|
|
previous_ppid: null,
|
|
};
|
|
expect(node.pid).toBe(1);
|
|
const recon: ProcessTreeReconstruction = {
|
|
root_pid: 1,
|
|
processes: [node],
|
|
events_consumed: 1,
|
|
events_unmatched: 0,
|
|
short_lived_descendants: 0,
|
|
has_re_exec: false,
|
|
has_reparenting: false,
|
|
};
|
|
expect(recon.root_pid).toBe(1);
|
|
});
|
|
|
|
it("uses ProcessEventType", () => {
|
|
const t: ProcessEventType = "spawn";
|
|
expect(t).toBe("spawn");
|
|
});
|
|
});
|