@brutalsystems/muster 0.7.8 → 0.7.9
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/README.md +31 -0
- package/dist/identity/processes.js +68 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,6 +180,37 @@ OpenCode sessions otherwise use the same tmux/pty, viewer, list, stop, and human
|
|
|
180
180
|
output behavior described above; OpenCode tasks use the same detached output
|
|
181
181
|
and exit-status lifecycle as other tasks.
|
|
182
182
|
|
|
183
|
+
## Process ownership
|
|
184
|
+
|
|
185
|
+
Muster identifies a launched process by its **pid paired with its start time**,
|
|
186
|
+
and revalidates that pair whenever the registry is touched — including after
|
|
187
|
+
Muster itself restarts.
|
|
188
|
+
|
|
189
|
+
This is not a compromise chosen for convenience. On macOS it is the strongest
|
|
190
|
+
check the public platform offers:
|
|
191
|
+
|
|
192
|
+
- There is no cgroup. Process groups and sessions are the nearest approximation
|
|
193
|
+
and are escapable by design, so they bound a tree rather than owning it.
|
|
194
|
+
- The kernel's unique process identifier is not exposed in the public SDK.
|
|
195
|
+
- The audit-token route requires a message from the target process, which a
|
|
196
|
+
supervisor observing arbitrary children never has.
|
|
197
|
+
|
|
198
|
+
Two consequences worth stating, because both look like redundant work to a
|
|
199
|
+
reader who does not know why they are there:
|
|
200
|
+
|
|
201
|
+
- **Identity is revalidated at the moment of use, not at discovery.** Every
|
|
202
|
+
signal, every status answer and every registry pass re-checks the pair. A pid
|
|
203
|
+
discovered a moment ago may already belong to something else.
|
|
204
|
+
- **Revalidation after a Muster restart is deliberate.** Comparable supervisors
|
|
205
|
+
trust what they recorded before they died. Muster does not, which is why a
|
|
206
|
+
session that ended while Muster was not running is reported as ended rather
|
|
207
|
+
than as running.
|
|
208
|
+
|
|
209
|
+
Process-tree membership is derived from the parent chain, which has a known
|
|
210
|
+
limit: a child reparented after its own parent exits leaves the chain and cannot
|
|
211
|
+
be recovered from it. Tracking that needs a membership signal the chain does not
|
|
212
|
+
carry.
|
|
213
|
+
|
|
183
214
|
## Configuration and permissions
|
|
184
215
|
|
|
185
216
|
Muster reads `~/.muster/config.toml` once at startup and **never writes it**.
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process identity on macOS.
|
|
3
|
+
*
|
|
4
|
+
* A launched process is identified by `(pid, start time)`. That pair is the
|
|
5
|
+
* ceiling of what this platform offers a supervisor, not a shortcut taken to
|
|
6
|
+
* avoid something better: there is no cgroup to own a tree, the kernel's unique
|
|
7
|
+
* process identifier is absent from the public SDK, and `audit_token` requires
|
|
8
|
+
* a message from the target that arbitrary children never send.
|
|
9
|
+
*
|
|
10
|
+
* Two rules follow, and both look like wasted work if the reason is not stated:
|
|
11
|
+
*
|
|
12
|
+
* 1. Revalidate at the moment of use, never at discovery. The gap between
|
|
13
|
+
* learning a pid and acting on it is where reuse happens, so `isSame()` is
|
|
14
|
+
* called immediately before each signal rather than once per operation.
|
|
15
|
+
* 2. Revalidation survives our own restart. The registry re-checks generations
|
|
16
|
+
* on every transaction, so a process that died while Muster was not running
|
|
17
|
+
* is reported as dead rather than trusted from what was recorded.
|
|
18
|
+
*
|
|
19
|
+
* Membership is derived from the parent chain and inherits its one real limit:
|
|
20
|
+
* a reparented child is unreachable from the root. See issue #24.
|
|
21
|
+
*/
|
|
1
22
|
import { execFile } from "node:child_process";
|
|
2
23
|
import { promisify } from "node:util";
|
|
3
24
|
const exec = promisify(execFile);
|
|
@@ -35,25 +56,59 @@ export async function descendants(root, deadline = Date.now() + 1000) {
|
|
|
35
56
|
}
|
|
36
57
|
return [...found];
|
|
37
58
|
}
|
|
59
|
+
async function liveTree(root) {
|
|
60
|
+
return (await Promise.all((await descendants(root)).map(processRef))).filter((r) => !!r);
|
|
61
|
+
}
|
|
62
|
+
function signal(ref, name) {
|
|
63
|
+
try {
|
|
64
|
+
process.kill(ref.pid, name);
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A tree enumerated once, before the wait between SIGTERM and SIGKILL, cannot
|
|
70
|
+
* contain anything the tree forked during that wait — and a process asked to
|
|
71
|
+
* terminate is exactly the kind that spawns a last child. So the membership is
|
|
72
|
+
* re-read after the wait and unioned with what was already known, and only then
|
|
73
|
+
* is the kill pass run. Each entry is still revalidated immediately before
|
|
74
|
+
* every signal, because a pid that exits between discovery and the signal may
|
|
75
|
+
* already belong to something else.
|
|
76
|
+
*
|
|
77
|
+
* Reparented descendants are a separate gap: once an intermediate process dies,
|
|
78
|
+
* its children leave the parent chain entirely and no re-read will find them.
|
|
79
|
+
* That needs a membership signal the chain does not provide (see issue #24).
|
|
80
|
+
*/
|
|
38
81
|
export async function stopTree(ref) {
|
|
39
82
|
if (!(await isSame(ref)))
|
|
40
83
|
return;
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
84
|
+
const known = new Map();
|
|
85
|
+
const remember = (refs) => {
|
|
86
|
+
const added = [];
|
|
87
|
+
for (const r of refs) {
|
|
88
|
+
const key = `${r.pid}:${r.start}`;
|
|
89
|
+
if (!known.has(key)) {
|
|
90
|
+
known.set(key, r);
|
|
91
|
+
added.push(r);
|
|
46
92
|
}
|
|
47
|
-
catch { }
|
|
48
93
|
}
|
|
94
|
+
return added;
|
|
95
|
+
};
|
|
96
|
+
const deepestFirst = () => [...known.values()].reverse();
|
|
97
|
+
remember(await liveTree(ref.pid));
|
|
98
|
+
for (const r of deepestFirst())
|
|
99
|
+
if (await isSame(r))
|
|
100
|
+
signal(r, "SIGTERM");
|
|
49
101
|
await delay(150);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
102
|
+
const late = (await isSame(ref)) ? remember(await liveTree(ref.pid)) : [];
|
|
103
|
+
if (late.length) {
|
|
104
|
+
for (const r of late.reverse())
|
|
105
|
+
if (await isSame(r))
|
|
106
|
+
signal(r, "SIGTERM");
|
|
107
|
+
await delay(50);
|
|
108
|
+
}
|
|
109
|
+
for (const r of deepestFirst())
|
|
110
|
+
if (await isSame(r))
|
|
111
|
+
signal(r, "SIGKILL");
|
|
57
112
|
}
|
|
58
113
|
// Called only by a terminal/task supervisor that is the leader of its own group.
|
|
59
114
|
// Keeping the supervisor alive until this point prevents process-group ID reuse.
|