@hayasaka7/haya-pet 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/README.md +28 -12
- package/apps/cli/src/haya-pet.js +110 -21
- package/apps/cli/test/haya-pet.test.mjs +111 -7
- package/apps/companion/src/main/index.js +40 -1
- package/apps/companion/test/position-store.test.mjs +1 -1
- package/docs/architecture.md +33 -10
- package/docs/cross-os-qa.md +72 -0
- package/docs/known-issues.md +92 -9
- package/docs/troubleshooting.md +3 -1
- package/package.json +1 -1
- package/packages/adapters/src/codex-hooks.js +152 -0
- package/packages/adapters/src/codex-transcript.js +73 -0
- package/packages/adapters/test/codex-hooks.test.mjs +120 -0
- package/packages/adapters/test/codex-transcript.test.mjs +97 -0
- package/packages/app-state/src/state.js +10 -5
- package/packages/cli-core/src/codex-hook-injection.js +49 -0
- package/packages/cli-core/src/codex-transcript-watcher.js +160 -0
- package/packages/cli-core/test/codex-hook-injection.test.mjs +45 -0
- package/packages/cli-core/test/codex-transcript-watcher.test.mjs +108 -0
- package/packages/daemon-core/src/approval-process-watcher.js +169 -0
- package/packages/daemon-core/test/approval-process-watcher.test.mjs +295 -0
- package/packages/platform-core/src/process-snapshot.js +88 -0
- package/packages/platform-core/test/process-snapshot.test.mjs +105 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "../../../test/harness.mjs";
|
|
3
|
+
import {
|
|
4
|
+
watchForApprovedProcess,
|
|
5
|
+
createApprovalWatchCoordinator
|
|
6
|
+
} from "../src/approval-process-watcher.js";
|
|
7
|
+
|
|
8
|
+
// --- watchForApprovedProcess -------------------------------------------------
|
|
9
|
+
// The watcher's contract is event-based, never time-based: it may only report
|
|
10
|
+
// "approved" because a real NEW process appeared under the client and stayed
|
|
11
|
+
// alive across two consecutive polls. A missed detection is always safe (the
|
|
12
|
+
// pet just keeps showing "waiting"); a false detection would hide a pending
|
|
13
|
+
// approval warning, so the persistence filter errs toward not firing.
|
|
14
|
+
|
|
15
|
+
function makeWatcher(snapshots, overrides = {}) {
|
|
16
|
+
// `snapshots` is a queue of process tables the fake lister returns per call.
|
|
17
|
+
const calls = { cleared: 0 };
|
|
18
|
+
const watcher = watchForApprovedProcess({
|
|
19
|
+
rootPid: 100,
|
|
20
|
+
listProcesses: async () => {
|
|
21
|
+
if (snapshots.length === 0) {
|
|
22
|
+
throw new Error("test ran out of snapshots");
|
|
23
|
+
}
|
|
24
|
+
const next = snapshots.shift();
|
|
25
|
+
if (next instanceof Error) {
|
|
26
|
+
throw next;
|
|
27
|
+
}
|
|
28
|
+
return next;
|
|
29
|
+
},
|
|
30
|
+
immediate: false,
|
|
31
|
+
setInterval: () => ({ unref() {} }),
|
|
32
|
+
clearInterval: () => {
|
|
33
|
+
calls.cleared += 1;
|
|
34
|
+
},
|
|
35
|
+
...overrides
|
|
36
|
+
});
|
|
37
|
+
return { watcher, calls };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ROOT = { pid: 100, ppid: 1 };
|
|
41
|
+
const BASE_CHILD = { pid: 110, ppid: 100 };
|
|
42
|
+
|
|
43
|
+
test("approval watcher ignores descendants that existed at baseline", async () => {
|
|
44
|
+
const approved = [];
|
|
45
|
+
const { watcher } = makeWatcher(
|
|
46
|
+
[
|
|
47
|
+
[ROOT, BASE_CHILD],
|
|
48
|
+
[ROOT, BASE_CHILD],
|
|
49
|
+
[ROOT, BASE_CHILD]
|
|
50
|
+
],
|
|
51
|
+
{ onApproved: (event) => approved.push(event) }
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
await watcher._tick(); // baseline
|
|
55
|
+
await watcher._tick();
|
|
56
|
+
await watcher._tick();
|
|
57
|
+
|
|
58
|
+
assert.deepEqual(approved, []);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("approval watcher fires once when a new descendant persists across two polls", async () => {
|
|
62
|
+
const approved = [];
|
|
63
|
+
const newChild = { pid: 200, ppid: 100 };
|
|
64
|
+
const { watcher, calls } = makeWatcher(
|
|
65
|
+
[
|
|
66
|
+
[ROOT, BASE_CHILD],
|
|
67
|
+
[ROOT, BASE_CHILD, newChild],
|
|
68
|
+
[ROOT, BASE_CHILD, newChild],
|
|
69
|
+
[ROOT, BASE_CHILD, newChild, { pid: 300, ppid: 100 }]
|
|
70
|
+
],
|
|
71
|
+
{ onApproved: (event) => approved.push(event) }
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
await watcher._tick(); // baseline
|
|
75
|
+
await watcher._tick(); // candidate seen once — not yet
|
|
76
|
+
assert.deepEqual(approved, []);
|
|
77
|
+
await watcher._tick(); // candidate persisted — fire
|
|
78
|
+
assert.deepEqual(approved, [{ pid: 200 }]);
|
|
79
|
+
|
|
80
|
+
// Fires once and stops itself; later ticks are no-ops.
|
|
81
|
+
await watcher._tick();
|
|
82
|
+
assert.equal(approved.length, 1);
|
|
83
|
+
assert.equal(calls.cleared, 1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("approval watcher ignores a short-lived blip (hook spawn)", async () => {
|
|
87
|
+
const approved = [];
|
|
88
|
+
const blip = { pid: 200, ppid: 100 };
|
|
89
|
+
const { watcher } = makeWatcher(
|
|
90
|
+
[
|
|
91
|
+
[ROOT, BASE_CHILD],
|
|
92
|
+
[ROOT, BASE_CHILD, blip],
|
|
93
|
+
[ROOT, BASE_CHILD],
|
|
94
|
+
[ROOT, BASE_CHILD]
|
|
95
|
+
],
|
|
96
|
+
{ onApproved: (event) => approved.push(event) }
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await watcher._tick(); // baseline
|
|
100
|
+
await watcher._tick(); // blip appears
|
|
101
|
+
await watcher._tick(); // blip gone — must not fire
|
|
102
|
+
await watcher._tick();
|
|
103
|
+
|
|
104
|
+
assert.deepEqual(approved, []);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("approval watcher detects new grandchildren, not just direct children", async () => {
|
|
108
|
+
const approved = [];
|
|
109
|
+
// New process hangs off the pre-existing shell child (root -> shell -> command).
|
|
110
|
+
const grandchild = { pid: 200, ppid: 110 };
|
|
111
|
+
const { watcher } = makeWatcher(
|
|
112
|
+
[
|
|
113
|
+
[ROOT, BASE_CHILD],
|
|
114
|
+
[ROOT, BASE_CHILD, grandchild],
|
|
115
|
+
[ROOT, BASE_CHILD, grandchild]
|
|
116
|
+
],
|
|
117
|
+
{ onApproved: (event) => approved.push(event) }
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
await watcher._tick();
|
|
121
|
+
await watcher._tick();
|
|
122
|
+
await watcher._tick();
|
|
123
|
+
|
|
124
|
+
assert.deepEqual(approved, [{ pid: 200 }]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("approval watcher does not detect unrelated processes", async () => {
|
|
128
|
+
const approved = [];
|
|
129
|
+
const unrelated = { pid: 200, ppid: 999 };
|
|
130
|
+
const { watcher } = makeWatcher(
|
|
131
|
+
[
|
|
132
|
+
[ROOT, BASE_CHILD],
|
|
133
|
+
[ROOT, BASE_CHILD, unrelated],
|
|
134
|
+
[ROOT, BASE_CHILD, unrelated]
|
|
135
|
+
],
|
|
136
|
+
{ onApproved: (event) => approved.push(event) }
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await watcher._tick();
|
|
140
|
+
await watcher._tick();
|
|
141
|
+
await watcher._tick();
|
|
142
|
+
|
|
143
|
+
assert.deepEqual(approved, []);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("approval watcher survives lister errors and keeps working", async () => {
|
|
147
|
+
const approved = [];
|
|
148
|
+
const newChild = { pid: 200, ppid: 100 };
|
|
149
|
+
const { watcher } = makeWatcher(
|
|
150
|
+
[
|
|
151
|
+
[ROOT, BASE_CHILD],
|
|
152
|
+
new Error("snapshot failed"),
|
|
153
|
+
[ROOT, BASE_CHILD, newChild],
|
|
154
|
+
[ROOT, BASE_CHILD, newChild]
|
|
155
|
+
],
|
|
156
|
+
{ onApproved: (event) => approved.push(event) }
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
await watcher._tick(); // baseline
|
|
160
|
+
await watcher._tick(); // error — swallowed
|
|
161
|
+
await watcher._tick(); // candidate
|
|
162
|
+
await watcher._tick(); // persisted — fire
|
|
163
|
+
|
|
164
|
+
assert.deepEqual(approved, [{ pid: 200 }]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("approval watcher stop() prevents any further detection", async () => {
|
|
168
|
+
const approved = [];
|
|
169
|
+
const newChild = { pid: 200, ppid: 100 };
|
|
170
|
+
const { watcher, calls } = makeWatcher(
|
|
171
|
+
[
|
|
172
|
+
[ROOT, BASE_CHILD],
|
|
173
|
+
[ROOT, BASE_CHILD, newChild],
|
|
174
|
+
[ROOT, BASE_CHILD, newChild]
|
|
175
|
+
],
|
|
176
|
+
{ onApproved: (event) => approved.push(event) }
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await watcher._tick();
|
|
180
|
+
watcher.stop();
|
|
181
|
+
await watcher._tick();
|
|
182
|
+
await watcher._tick();
|
|
183
|
+
|
|
184
|
+
assert.deepEqual(approved, []);
|
|
185
|
+
assert.equal(calls.cleared, 1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// --- createApprovalWatchCoordinator -------------------------------------------
|
|
189
|
+
// Bridges session state changes to per-session watchers: watch only while a
|
|
190
|
+
// session with a known pid sits in waiting_approval; stop on any other state.
|
|
191
|
+
|
|
192
|
+
function makeCoordinator(overrides = {}) {
|
|
193
|
+
const events = { started: [], stopped: [], approved: [] };
|
|
194
|
+
const watchers = new Map();
|
|
195
|
+
const coordinator = createApprovalWatchCoordinator({
|
|
196
|
+
createWatcher: ({ rootPid, onApproved }) => {
|
|
197
|
+
events.started.push(rootPid);
|
|
198
|
+
const watcher = {
|
|
199
|
+
onApproved,
|
|
200
|
+
stop: () => events.stopped.push(rootPid)
|
|
201
|
+
};
|
|
202
|
+
watchers.set(rootPid, watcher);
|
|
203
|
+
return watcher;
|
|
204
|
+
},
|
|
205
|
+
onApproved: (sessionId, event) => events.approved.push({ sessionId, ...event }),
|
|
206
|
+
...overrides
|
|
207
|
+
});
|
|
208
|
+
return { coordinator, events, watchers };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function session(state, overrides = {}) {
|
|
212
|
+
return { sessionId: "sess_a", pid: 100, state, ...overrides };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
test("coordinator starts a watcher when a session enters waiting_approval", () => {
|
|
216
|
+
const { coordinator, events } = makeCoordinator();
|
|
217
|
+
|
|
218
|
+
coordinator.onSessionChanged(session("thinking"));
|
|
219
|
+
assert.deepEqual(events.started, []);
|
|
220
|
+
|
|
221
|
+
coordinator.onSessionChanged(session("waiting_approval"));
|
|
222
|
+
assert.deepEqual(events.started, [100]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("coordinator does not start a duplicate watcher for the same session", () => {
|
|
226
|
+
const { coordinator, events } = makeCoordinator();
|
|
227
|
+
|
|
228
|
+
coordinator.onSessionChanged(session("waiting_approval"));
|
|
229
|
+
coordinator.onSessionChanged(session("waiting_approval"));
|
|
230
|
+
|
|
231
|
+
assert.deepEqual(events.started, [100]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("coordinator stops the watcher when the session leaves waiting_approval", () => {
|
|
235
|
+
const { coordinator, events } = makeCoordinator();
|
|
236
|
+
|
|
237
|
+
coordinator.onSessionChanged(session("waiting_approval"));
|
|
238
|
+
coordinator.onSessionChanged(session("thinking"));
|
|
239
|
+
|
|
240
|
+
assert.deepEqual(events.stopped, [100]);
|
|
241
|
+
|
|
242
|
+
// Re-entering waiting_approval starts a fresh watcher.
|
|
243
|
+
coordinator.onSessionChanged(session("waiting_approval"));
|
|
244
|
+
assert.deepEqual(events.started, [100, 100]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("coordinator ignores sessions without a pid", () => {
|
|
248
|
+
const { coordinator, events } = makeCoordinator();
|
|
249
|
+
|
|
250
|
+
coordinator.onSessionChanged(session("waiting_approval", { pid: undefined }));
|
|
251
|
+
|
|
252
|
+
assert.deepEqual(events.started, []);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("coordinator ignores undefined session changes", () => {
|
|
256
|
+
const { coordinator, events } = makeCoordinator();
|
|
257
|
+
|
|
258
|
+
coordinator.onSessionChanged(undefined);
|
|
259
|
+
|
|
260
|
+
assert.deepEqual(events.started, []);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("coordinator forwards approval with the sessionId and stops tracking", () => {
|
|
264
|
+
const { coordinator, events, watchers } = makeCoordinator();
|
|
265
|
+
|
|
266
|
+
coordinator.onSessionChanged(session("waiting_approval"));
|
|
267
|
+
watchers.get(100).onApproved({ pid: 200 });
|
|
268
|
+
|
|
269
|
+
assert.deepEqual(events.approved, [{ sessionId: "sess_a", pid: 200 }]);
|
|
270
|
+
|
|
271
|
+
// The fired watcher is forgotten — a later waiting_approval starts a new one.
|
|
272
|
+
coordinator.onSessionChanged(session("waiting_approval"));
|
|
273
|
+
assert.deepEqual(events.started, [100, 100]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("coordinator tracks multiple sessions independently", () => {
|
|
277
|
+
const { coordinator, events } = makeCoordinator();
|
|
278
|
+
|
|
279
|
+
coordinator.onSessionChanged(session("waiting_approval"));
|
|
280
|
+
coordinator.onSessionChanged(session("waiting_approval", { sessionId: "sess_b", pid: 300 }));
|
|
281
|
+
coordinator.onSessionChanged(session("idle"));
|
|
282
|
+
|
|
283
|
+
assert.deepEqual(events.started, [100, 300]);
|
|
284
|
+
assert.deepEqual(events.stopped, [100]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("coordinator stopAll stops every active watcher", () => {
|
|
288
|
+
const { coordinator, events } = makeCoordinator();
|
|
289
|
+
|
|
290
|
+
coordinator.onSessionChanged(session("waiting_approval"));
|
|
291
|
+
coordinator.onSessionChanged(session("waiting_approval", { sessionId: "sess_b", pid: 300 }));
|
|
292
|
+
coordinator.stopAll();
|
|
293
|
+
|
|
294
|
+
assert.deepEqual(events.stopped.sort(), [100, 300]);
|
|
295
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Cross-OS process-table snapshots: every lister resolves to the full process
|
|
2
|
+
// list as [{ pid, ppid }], so consumers (the approval watcher) never branch on
|
|
3
|
+
// platform. Returns undefined on platforms we don't support — callers treat
|
|
4
|
+
// that as "feature unavailable", never as an error.
|
|
5
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { readdir as readdirFs, readFile as readFileFs } from "node:fs/promises";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFileCb);
|
|
10
|
+
|
|
11
|
+
// One pid+ppid pair per line, whitespace-separated — both `ps` and our
|
|
12
|
+
// PowerShell query are shaped to emit exactly this.
|
|
13
|
+
function parsePidTable(stdout) {
|
|
14
|
+
const table = [];
|
|
15
|
+
for (const line of String(stdout).split("\n")) {
|
|
16
|
+
const parts = line.trim().split(/\s+/);
|
|
17
|
+
if (parts.length !== 2) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const pid = Number.parseInt(parts[0], 10);
|
|
21
|
+
const ppid = Number.parseInt(parts[1], 10);
|
|
22
|
+
if (Number.isInteger(pid) && Number.isInteger(ppid)) {
|
|
23
|
+
table.push({ pid, ppid });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return table;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createProcessSnapshotLister(options = {}) {
|
|
30
|
+
const {
|
|
31
|
+
platform = process.platform,
|
|
32
|
+
execFile = execFileAsync,
|
|
33
|
+
readdir = readdirFs,
|
|
34
|
+
readFile = readFileFs
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
if (platform === "win32") {
|
|
38
|
+
// CIM query via PowerShell (wmic is removed on current Windows 11). Output
|
|
39
|
+
// is forced to bare "pid ppid" lines to keep parsing trivial.
|
|
40
|
+
const script =
|
|
41
|
+
"Get-CimInstance -ClassName Win32_Process | ForEach-Object { \"$($_.ProcessId) $($_.ParentProcessId)\" }";
|
|
42
|
+
return async () => {
|
|
43
|
+
const { stdout } = await execFile(
|
|
44
|
+
"powershell.exe",
|
|
45
|
+
["-NoProfile", "-NonInteractive", "-Command", script],
|
|
46
|
+
{ windowsHide: true }
|
|
47
|
+
);
|
|
48
|
+
return parsePidTable(stdout);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (platform === "darwin") {
|
|
53
|
+
return async () => {
|
|
54
|
+
const { stdout } = await execFile("ps", ["-axo", "pid=,ppid="]);
|
|
55
|
+
return parsePidTable(stdout);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (platform === "linux") {
|
|
60
|
+
// Read /proc directly — no subprocess at all. stat field 4 is ppid, but the
|
|
61
|
+
// comm field (2) is parenthesised and may contain spaces/parens, so parse
|
|
62
|
+
// from AFTER the last ')'.
|
|
63
|
+
return async () => {
|
|
64
|
+
const entries = await readdir("/proc");
|
|
65
|
+
const table = [];
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (!/^\d+$/.test(entry)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
let stat;
|
|
71
|
+
try {
|
|
72
|
+
stat = String(await readFile(`/proc/${entry}/stat`, "utf8"));
|
|
73
|
+
} catch {
|
|
74
|
+
continue; // process exited mid-scan
|
|
75
|
+
}
|
|
76
|
+
const afterComm = stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/);
|
|
77
|
+
const ppid = Number.parseInt(afterComm[1], 10);
|
|
78
|
+
const pid = Number.parseInt(entry, 10);
|
|
79
|
+
if (Number.isInteger(pid) && Number.isInteger(ppid)) {
|
|
80
|
+
table.push({ pid, ppid });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return table;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "../../../test/harness.mjs";
|
|
3
|
+
import { createProcessSnapshotLister } from "../src/process-snapshot.js";
|
|
4
|
+
|
|
5
|
+
// Each platform lister returns the same shape — the full process table as
|
|
6
|
+
// [{ pid, ppid }] — so the approval watcher core stays platform-agnostic.
|
|
7
|
+
|
|
8
|
+
test("windows lister parses the PowerShell pid/ppid table", async () => {
|
|
9
|
+
const invocations = [];
|
|
10
|
+
const lister = createProcessSnapshotLister({
|
|
11
|
+
platform: "win32",
|
|
12
|
+
execFile: async (file, args) => {
|
|
13
|
+
invocations.push({ file, args });
|
|
14
|
+
return { stdout: "4 0\r\n100 4\r\n2332 100\r\n\r\n" };
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const table = await lister();
|
|
19
|
+
|
|
20
|
+
assert.deepEqual(table, [
|
|
21
|
+
{ pid: 4, ppid: 0 },
|
|
22
|
+
{ pid: 100, ppid: 4 },
|
|
23
|
+
{ pid: 2332, ppid: 100 }
|
|
24
|
+
]);
|
|
25
|
+
assert.equal(invocations.length, 1);
|
|
26
|
+
assert.equal(invocations[0].file, "powershell.exe");
|
|
27
|
+
// Must never load the user's profile or prompt interactively.
|
|
28
|
+
assert.ok(invocations[0].args.includes("-NoProfile"));
|
|
29
|
+
assert.ok(invocations[0].args.includes("-NonInteractive"));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("darwin lister parses ps output with ragged spacing", async () => {
|
|
33
|
+
const lister = createProcessSnapshotLister({
|
|
34
|
+
platform: "darwin",
|
|
35
|
+
execFile: async (file, args) => {
|
|
36
|
+
assert.equal(file, "ps");
|
|
37
|
+
assert.deepEqual(args, ["-axo", "pid=,ppid="]);
|
|
38
|
+
return { stdout: " 1 0\n 455 1\n12034 455\n" };
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
assert.deepEqual(await lister(), [
|
|
43
|
+
{ pid: 1, ppid: 0 },
|
|
44
|
+
{ pid: 455, ppid: 1 },
|
|
45
|
+
{ pid: 12034, ppid: 455 }
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("linux lister reads /proc stat files, handling names with spaces and parens", async () => {
|
|
50
|
+
const files = {
|
|
51
|
+
"/proc/1/stat": "1 (systemd) S 0 1 1 0 -1 4194560",
|
|
52
|
+
"/proc/455/stat": "455 (my (weird) name) S 1 455 455 0 -1 0",
|
|
53
|
+
"/proc/9999/stat": "9999 (node) R 455 9999 9999 0 -1 0"
|
|
54
|
+
};
|
|
55
|
+
const lister = createProcessSnapshotLister({
|
|
56
|
+
platform: "linux",
|
|
57
|
+
readdir: async (dir) => {
|
|
58
|
+
assert.equal(dir, "/proc");
|
|
59
|
+
return ["1", "455", "9999", "self", "acpi", "cpuinfo"];
|
|
60
|
+
},
|
|
61
|
+
readFile: async (path) => {
|
|
62
|
+
if (!(path in files)) {
|
|
63
|
+
throw new Error(`unexpected read: ${path}`);
|
|
64
|
+
}
|
|
65
|
+
return files[path];
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
assert.deepEqual(await lister(), [
|
|
70
|
+
{ pid: 1, ppid: 0 },
|
|
71
|
+
{ pid: 455, ppid: 1 },
|
|
72
|
+
{ pid: 9999, ppid: 455 }
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("linux lister skips processes that vanish mid-scan", async () => {
|
|
77
|
+
const lister = createProcessSnapshotLister({
|
|
78
|
+
platform: "linux",
|
|
79
|
+
readdir: async () => ["1", "2"],
|
|
80
|
+
readFile: async (path) => {
|
|
81
|
+
if (path === "/proc/2/stat") {
|
|
82
|
+
throw new Error("ENOENT: gone");
|
|
83
|
+
}
|
|
84
|
+
return "1 (init) S 0 1 1 0 -1 0";
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.deepEqual(await lister(), [{ pid: 1, ppid: 0 }]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("listers skip malformed lines instead of failing", async () => {
|
|
92
|
+
const lister = createProcessSnapshotLister({
|
|
93
|
+
platform: "win32",
|
|
94
|
+
execFile: async () => ({ stdout: "4 0\nnot numbers\n77\n100 4\n" })
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
assert.deepEqual(await lister(), [
|
|
98
|
+
{ pid: 4, ppid: 0 },
|
|
99
|
+
{ pid: 100, ppid: 4 }
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("unsupported platforms yield no lister", () => {
|
|
104
|
+
assert.equal(createProcessSnapshotLister({ platform: "sunos" }), undefined);
|
|
105
|
+
});
|