@codyswann/lisa 4.26.4 → 4.26.5
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/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +10 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +10 -3
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +3 -3
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/rails/copy-contents/scripts/lisa-mutation.sh +8 -0
- package/rails/copy-overwrite/lefthook.yml +7 -2
- package/rails/copy-overwrite/scripts/lisa-scratch-run.sh +1030 -0
- package/scripts/two-channel-couplings.json +17 -4
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
3
|
+
# Do not edit directly — durable changes belong upstream in Lisa.
|
|
4
|
+
# -----------------------------------------------------------------------------
|
|
5
|
+
# Rails scratch supervisor — run-scoped scratch ownership with an outside
|
|
6
|
+
# cleanup authority that survives the tested process.
|
|
7
|
+
# -----------------------------------------------------------------------------
|
|
8
|
+
# WHY THIS IS SHELL AND NOT NODE
|
|
9
|
+
#
|
|
10
|
+
# A Rails repository is not required to have Node, npm, Bun, Yarn, a populated
|
|
11
|
+
# `node_modules`, or a network at test runtime. The Node-managed scratch
|
|
12
|
+
# supervisor cannot be reused here for exactly that reason. This file assumes
|
|
13
|
+
# only what the routes it wraps already assume: a POSIX shell, coreutils/BSD
|
|
14
|
+
# userland (`ps`, `mkdir`, `rm`, `date`, `od`), and Ruby/Bundler for the payload
|
|
15
|
+
# itself. It never fetches anything.
|
|
16
|
+
#
|
|
17
|
+
# USAGE
|
|
18
|
+
#
|
|
19
|
+
# sh scripts/lisa-scratch-run.sh --suite <label> -- <command> [args...]
|
|
20
|
+
#
|
|
21
|
+
# WHAT IT GUARANTEES
|
|
22
|
+
#
|
|
23
|
+
# 1. Authority precedes allocation. Before the payload can allocate one byte
|
|
24
|
+
# of scratch, a separate cleanup authority process — outside the payload's
|
|
25
|
+
# process group, and outside this shell's — has read and acknowledged the
|
|
26
|
+
# exact run token, the canonical run root, the run root's filesystem
|
|
27
|
+
# identity (device + inode), the suite label, the payload's process-group
|
|
28
|
+
# identity, and the payload's process-birth identity. The payload is held
|
|
29
|
+
# at a gate until that acknowledgement lands. An arming or acknowledgement
|
|
30
|
+
# failure REFUSES: the payload never executes.
|
|
31
|
+
# 2. Cleanup survives the payload. Pass, fail, timeout, SIGTERM, SIGINT,
|
|
32
|
+
# SIGHUP, payload SIGKILL, and foreground-supervisor SIGKILL all end with
|
|
33
|
+
# the owned process group boundedly drained and the owned run root absent,
|
|
34
|
+
# done by this invocation's own already-armed authority. No successor run
|
|
35
|
+
# is started and no global sweep is performed.
|
|
36
|
+
# 3. Only what this invocation owns is touched. The authority signals exactly
|
|
37
|
+
# one process group — the one it acknowledged, re-verified by birth
|
|
38
|
+
# identity so PID reuse cannot broaden it — and deletes exactly one root,
|
|
39
|
+
# re-verified by token and by device+inode so a swap cannot broaden it. An
|
|
40
|
+
# external database service is never signalled, stopped, reset, or deleted.
|
|
41
|
+
# 4. Ambiguity fails closed. Traversal, symlinked roots, inode swaps, owner
|
|
42
|
+
# token changes, malformed or oversized markers, PID reuse, birth
|
|
43
|
+
# mismatch, and foreign quarantine names all exit nonzero with a reason.
|
|
44
|
+
# 5. A fixture leak fails the suite. Unregistered direct children left under
|
|
45
|
+
# the payload's temp root are reported with the suite label, the leaked
|
|
46
|
+
# count, and the sorted leaked basenames, and make the route exit nonzero.
|
|
47
|
+
# A red payload verdict is never overwritten by the leak verdict, and a
|
|
48
|
+
# green payload never masks a leaking child.
|
|
49
|
+
#
|
|
50
|
+
# ENVIRONMENT
|
|
51
|
+
#
|
|
52
|
+
# LISA_SCRATCH_BASE Temp base. Default: $TMPDIR, else /tmp.
|
|
53
|
+
# LISA_SCRATCH_REGISTERED_PREFIXES
|
|
54
|
+
# Comma-separated basename prefixes that are
|
|
55
|
+
# batch-cleaned instead of reported as leaks.
|
|
56
|
+
# Default: "lisa-".
|
|
57
|
+
# LISA_SCRATCH_LEAK_GATE enforce (default) | report | off.
|
|
58
|
+
# LISA_SCRATCH_ARM_TIMEOUT_MS Arming handshake budget, in ms. Default 15000.
|
|
59
|
+
# LISA_SCRATCH_GATE_TIMEOUT_MS How long the held payload waits for the gate
|
|
60
|
+
# to open before refusing. Default 300000.
|
|
61
|
+
# LISA_SCRATCH_AUTHORITY_REFUSE Conformance control. When 1, the cleanup
|
|
62
|
+
# authority declines to acknowledge. The run
|
|
63
|
+
# must then REFUSE before the payload executes.
|
|
64
|
+
# It can only turn a run into a refusal; it can
|
|
65
|
+
# never let one through unsupervised.
|
|
66
|
+
# LISA_SCRATCH_DRAIN_MS SIGTERM→SIGKILL drain budget. Default 5000.
|
|
67
|
+
# LISA_SCRATCH_TRACE Append lifecycle events to this file. The
|
|
68
|
+
# file order IS the ordering proof.
|
|
69
|
+
# LISA_SCRATCH_INHERIT_STDIN 1 to give the payload this shell's stdin.
|
|
70
|
+
# Default: /dev/null (avoids SIGTTIN on a
|
|
71
|
+
# background process group).
|
|
72
|
+
# -----------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
set -eu
|
|
75
|
+
|
|
76
|
+
LISA_SCRATCH_NAMESPACE="lisa-rails-scratch"
|
|
77
|
+
LISA_SCRATCH_MARKER_MAX_BYTES=4096
|
|
78
|
+
LISA_SCRATCH_MARKER_MAX_LINES=64
|
|
79
|
+
LISA_SCRATCH_VERSION=1
|
|
80
|
+
|
|
81
|
+
# Exit codes. Distinct so a caller can tell refusal from payload failure.
|
|
82
|
+
EX_USAGE=64
|
|
83
|
+
EX_LEAK=65
|
|
84
|
+
EX_ARM=70
|
|
85
|
+
EX_AMBIGUOUS=78
|
|
86
|
+
|
|
87
|
+
lisa_die() {
|
|
88
|
+
_code="$1"
|
|
89
|
+
shift
|
|
90
|
+
printf '%s\n' "lisa-scratch-run: $*" >&2
|
|
91
|
+
exit "$_code"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
lisa_trace() {
|
|
95
|
+
[ -n "${LISA_SCRATCH_TRACE:-}" ] || return 0
|
|
96
|
+
printf '%s %s %s\n' "$(date +%s 2>/dev/null || echo 0)" "$LISA_SCRATCH_ROLE" "$*" \
|
|
97
|
+
>>"$LISA_SCRATCH_TRACE" 2>/dev/null || true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# --- portability probes ------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
# Stat device and inode of a path, portably.
|
|
103
|
+
# $1 path -> "<dev> <ino>" on stdout, nonzero when it cannot be determined.
|
|
104
|
+
lisa_dev_ino() {
|
|
105
|
+
_p="$1"
|
|
106
|
+
if _v="$(stat -f '%d %i' "$_p" 2>/dev/null)" && [ -n "$_v" ]; then
|
|
107
|
+
printf '%s\n' "$_v"
|
|
108
|
+
return 0
|
|
109
|
+
fi
|
|
110
|
+
if _v="$(stat -c '%d %i' "$_p" 2>/dev/null)" && [ -n "$_v" ]; then
|
|
111
|
+
printf '%s\n' "$_v"
|
|
112
|
+
return 0
|
|
113
|
+
fi
|
|
114
|
+
# Last-resort fallback: inode from `ls -di`, device stood in for by the
|
|
115
|
+
# mount point reported by `df -P`. Both are POSIX.
|
|
116
|
+
_ino="$(ls -di "$_p" 2>/dev/null | awk '{print $1}')" || return 1
|
|
117
|
+
[ -n "$_ino" ] || return 1
|
|
118
|
+
_dev="$(df -P "$_p" 2>/dev/null | awk 'NR==2 {print $NF}')" || return 1
|
|
119
|
+
[ -n "$_dev" ] || return 1
|
|
120
|
+
printf '%s %s\n' "$_dev" "$_ino"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Process-birth identity for a pid: something that changes when the PID is
|
|
124
|
+
# reused. `lstart` where available, `etime`+`ppid` otherwise.
|
|
125
|
+
# $1 pid -> birth string on stdout, nonzero when the process is gone.
|
|
126
|
+
lisa_birth() {
|
|
127
|
+
_pid="$1"
|
|
128
|
+
if _v="$(ps -o lstart= -p "$_pid" 2>/dev/null)" && [ -n "$_v" ]; then
|
|
129
|
+
printf '%s\n' "$(printf '%s' "$_v" | tr -s ' ' ' ' | sed 's/^ *//;s/ *$//')"
|
|
130
|
+
return 0
|
|
131
|
+
fi
|
|
132
|
+
if _v="$(ps -o ppid=,etime= -p "$_pid" 2>/dev/null)" && [ -n "$_v" ]; then
|
|
133
|
+
printf '%s\n' "$(printf '%s' "$_v" | tr -s ' ' ' ' | sed 's/^ *//;s/ *$//')"
|
|
134
|
+
return 0
|
|
135
|
+
fi
|
|
136
|
+
return 1
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
lisa_pgid_of() {
|
|
140
|
+
ps -o pgid= -p "$1" 2>/dev/null | tr -d ' \t' || return 1
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Whether this process leads its own process group.
|
|
144
|
+
#
|
|
145
|
+
# Only ever called from a REAL process, never from a `&` subshell: POSIX `$$`
|
|
146
|
+
# is the parent shell's pid inside a subshell, so a subshell asking this
|
|
147
|
+
# question would be told about its parent. That is why the launcher and the
|
|
148
|
+
# authority are spawned as separate `sh` processes rather than as functions in
|
|
149
|
+
# the background.
|
|
150
|
+
lisa_leads_own_group() {
|
|
151
|
+
_self_pgid="$(lisa_pgid_of "$$" || true)"
|
|
152
|
+
[ -n "$_self_pgid" ] && [ "$_self_pgid" = "$$" ]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Put this process in a process group of its own, without a controlling
|
|
156
|
+
# terminal, and WITHOUT changing its pid.
|
|
157
|
+
#
|
|
158
|
+
# Two mechanisms, because neither is portable alone:
|
|
159
|
+
#
|
|
160
|
+
# `set -m` in the PARENT. Works on Darwin, where /bin/sh is bash. It does
|
|
161
|
+
# not work under dash, whose job control initialisation opens /dev/tty — so
|
|
162
|
+
# on a Linux CI runner, which has no controlling terminal, every background
|
|
163
|
+
# job stays in the parent's group. Measured: the whole Rails supervisor
|
|
164
|
+
# suite refused with "payload did not get an isolated process group" on
|
|
165
|
+
# hosted Linux while passing on Darwin.
|
|
166
|
+
#
|
|
167
|
+
# `setsid`. Always present on Linux, absent from stock Darwin. It forks
|
|
168
|
+
# ONLY when the caller already leads a group, and the caller here provably
|
|
169
|
+
# does not — that is the condition we are in. So it execs in place, the pid
|
|
170
|
+
# is preserved, and the parent's `$!` and `wait` stay valid, which a forking
|
|
171
|
+
# setsid would silently break by returning its own status instead.
|
|
172
|
+
#
|
|
173
|
+
# $1 re-entry guard variable name, $2.. the argv to re-exec.
|
|
174
|
+
lisa_isolate_group() {
|
|
175
|
+
lisa_leads_own_group && return 0
|
|
176
|
+
_guard="$1"
|
|
177
|
+
shift
|
|
178
|
+
eval "_seen=\${$_guard:-0}"
|
|
179
|
+
if [ "$_seen" != "1" ] && command -v setsid >/dev/null 2>&1; then
|
|
180
|
+
export "$_guard=1"
|
|
181
|
+
exec setsid "$@"
|
|
182
|
+
fi
|
|
183
|
+
return 1
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
lisa_alive() {
|
|
187
|
+
kill -0 "$1" 2>/dev/null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Short sleep, degrading to a whole second where fractional sleep is absent.
|
|
191
|
+
#
|
|
192
|
+
# It publishes how long it actually slept, in `LISA_SCRATCH_NAP_MS`, and every
|
|
193
|
+
# wait loop advances its counter by THAT rather than by an assumed 50 ms.
|
|
194
|
+
# POSIX does not require `sleep` to accept a fractional operand — it is a
|
|
195
|
+
# widespread extension, not a guarantee — so on a shell where it is missing a
|
|
196
|
+
# loop that assumed 50 ms would run twenty times its stated budget: the 15 s
|
|
197
|
+
# arming handshake would wait 300 s, and the 5 s drain 100 s, before escalating
|
|
198
|
+
# to SIGKILL. A budget that is only a budget on some machines is not one.
|
|
199
|
+
LISA_SCRATCH_NAP_MS=50
|
|
200
|
+
lisa_nap() {
|
|
201
|
+
if [ "${LISA_SCRATCH_FRACTIONAL_SLEEP:-unknown}" = "unknown" ]; then
|
|
202
|
+
if sleep 0.05 2>/dev/null; then
|
|
203
|
+
LISA_SCRATCH_FRACTIONAL_SLEEP=yes
|
|
204
|
+
LISA_SCRATCH_NAP_MS=50
|
|
205
|
+
else
|
|
206
|
+
LISA_SCRATCH_FRACTIONAL_SLEEP=no
|
|
207
|
+
LISA_SCRATCH_NAP_MS=1000
|
|
208
|
+
fi
|
|
209
|
+
fi
|
|
210
|
+
if [ "$LISA_SCRATCH_FRACTIONAL_SLEEP" = yes ]; then
|
|
211
|
+
sleep 0.05
|
|
212
|
+
else
|
|
213
|
+
sleep 1
|
|
214
|
+
fi
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# --- marker (arming record) --------------------------------------------------
|
|
218
|
+
|
|
219
|
+
# Read one key out of a marker file, fail-closed on malformed or oversized
|
|
220
|
+
# input. $1 marker path, $2 key.
|
|
221
|
+
lisa_marker_get() {
|
|
222
|
+
_file="$1"
|
|
223
|
+
_key="$2"
|
|
224
|
+
[ -f "$_file" ] || return 1
|
|
225
|
+
if [ -L "$_file" ]; then return 1; fi
|
|
226
|
+
_bytes="$(wc -c <"$_file" 2>/dev/null | tr -d ' ')" || return 1
|
|
227
|
+
[ -n "$_bytes" ] || return 1
|
|
228
|
+
[ "$_bytes" -le "$LISA_SCRATCH_MARKER_MAX_BYTES" ] || return 1
|
|
229
|
+
_lines="$(wc -l <"$_file" 2>/dev/null | tr -d ' ')" || return 1
|
|
230
|
+
[ "$_lines" -le "$LISA_SCRATCH_MARKER_MAX_LINES" ] || return 1
|
|
231
|
+
_hits="$(grep -c "^${_key}=" "$_file" 2>/dev/null || true)"
|
|
232
|
+
[ "$_hits" = "1" ] || return 1
|
|
233
|
+
sed -n "s/^${_key}=//p" "$_file"
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# --- path safety -------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
# Canonicalize a directory that must already exist, without readlink -f (absent
|
|
239
|
+
# on stock macOS). Refuses a symlinked leaf outright.
|
|
240
|
+
lisa_canonical_dir() {
|
|
241
|
+
_p="$1"
|
|
242
|
+
[ -d "$_p" ] || return 1
|
|
243
|
+
[ -L "$_p" ] && return 1
|
|
244
|
+
(cd -P -- "$_p" 2>/dev/null && pwd -P) || return 1
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# A run root must be an absolute, traversal-free path directly beneath the
|
|
248
|
+
# namespace directory of the declared base.
|
|
249
|
+
lisa_root_is_well_formed() {
|
|
250
|
+
_root="$1"
|
|
251
|
+
_base="$2"
|
|
252
|
+
case "$_root" in
|
|
253
|
+
/*) : ;;
|
|
254
|
+
*) return 1 ;;
|
|
255
|
+
esac
|
|
256
|
+
case "$_root" in
|
|
257
|
+
*..*) return 1 ;;
|
|
258
|
+
esac
|
|
259
|
+
case "$_root" in
|
|
260
|
+
"${_base}/${LISA_SCRATCH_NAMESPACE}/"*) : ;;
|
|
261
|
+
*) return 1 ;;
|
|
262
|
+
esac
|
|
263
|
+
_leaf="${_root#"${_base}/${LISA_SCRATCH_NAMESPACE}/"}"
|
|
264
|
+
case "$_leaf" in
|
|
265
|
+
*/*) return 1 ;;
|
|
266
|
+
"") return 1 ;;
|
|
267
|
+
esac
|
|
268
|
+
return 0
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
lisa_token_is_well_formed() {
|
|
272
|
+
case "$1" in
|
|
273
|
+
"" | *[!0-9a-f]*) return 1 ;;
|
|
274
|
+
esac
|
|
275
|
+
[ "${#1}" -eq 64 ]
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
lisa_suite_is_well_formed() {
|
|
279
|
+
case "$1" in
|
|
280
|
+
"" | *[!A-Za-z0-9._-]*) return 1 ;;
|
|
281
|
+
esac
|
|
282
|
+
[ "${#1}" -le 64 ]
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# =============================================================================
|
|
286
|
+
# AUTHORITY MODE — the outside cleanup authority.
|
|
287
|
+
#
|
|
288
|
+
# Started by the supervisor BEFORE the payload's gate is opened, in its own
|
|
289
|
+
# process group, so that killing either the payload group or the supervisor
|
|
290
|
+
# group leaves it running. It is the same file and the same invocation: no
|
|
291
|
+
# successor Lisa run is ever started to do cleanup.
|
|
292
|
+
# =============================================================================
|
|
293
|
+
|
|
294
|
+
lisa_authority_main() {
|
|
295
|
+
LISA_SCRATCH_ROLE=authority
|
|
296
|
+
_root="$1"
|
|
297
|
+
|
|
298
|
+
# The authority's whole job is to outlive a kill aimed at the payload group
|
|
299
|
+
# or at the supervisor, so it must not share either. Best effort rather than
|
|
300
|
+
# fatal: it is the safety net, and a net that refuses to exist is worse than
|
|
301
|
+
# one that is merely in the wrong group. The launcher applies the same test
|
|
302
|
+
# and DOES refuse, so a platform that cannot isolate never gets this far.
|
|
303
|
+
lisa_isolate_group LISA_SCRATCH_AUTHORITY_REENTRY \
|
|
304
|
+
"$LISA_SCRATCH_SH" "$LISA_SCRATCH_SELF" --authority "$_root" || true
|
|
305
|
+
|
|
306
|
+
# Every field is re-derived from the marker and independently verified. A
|
|
307
|
+
# marker this cannot fully validate is never acted on.
|
|
308
|
+
_base="${LISA_SCRATCH_BASE:-${TMPDIR:-/tmp}}"
|
|
309
|
+
_base="${_base%/}"
|
|
310
|
+
_base="$(lisa_canonical_dir "$_base")" ||
|
|
311
|
+
lisa_die "$EX_AMBIGUOUS" "authority: temp base is not a canonical directory"
|
|
312
|
+
|
|
313
|
+
lisa_root_is_well_formed "$_root" "$_base" ||
|
|
314
|
+
lisa_die "$EX_AMBIGUOUS" "authority: refusing malformed run root: $_root"
|
|
315
|
+
if [ -L "$_root" ]; then
|
|
316
|
+
lisa_die "$EX_AMBIGUOUS" "authority: refusing symlinked run root: $_root"
|
|
317
|
+
fi
|
|
318
|
+
_canon="$(lisa_canonical_dir "$_root")" ||
|
|
319
|
+
lisa_die "$EX_AMBIGUOUS" "authority: run root is not a canonical directory: $_root"
|
|
320
|
+
[ "$_canon" = "$_root" ] ||
|
|
321
|
+
lisa_die "$EX_AMBIGUOUS" "authority: run root resolves elsewhere: $_root -> $_canon"
|
|
322
|
+
|
|
323
|
+
_marker="$_root/.lisa-scratch-arm"
|
|
324
|
+
_token="$(lisa_marker_get "$_marker" token)" ||
|
|
325
|
+
lisa_die "$EX_AMBIGUOUS" "authority: unreadable or malformed arming marker"
|
|
326
|
+
lisa_token_is_well_formed "$_token" ||
|
|
327
|
+
lisa_die "$EX_AMBIGUOUS" "authority: malformed run token"
|
|
328
|
+
_version="$(lisa_marker_get "$_marker" version)" ||
|
|
329
|
+
lisa_die "$EX_AMBIGUOUS" "authority: arming marker has no version"
|
|
330
|
+
[ "$_version" = "$LISA_SCRATCH_VERSION" ] ||
|
|
331
|
+
lisa_die "$EX_AMBIGUOUS" "authority: unsupported arming marker version: $_version"
|
|
332
|
+
_mroot="$(lisa_marker_get "$_marker" root)" ||
|
|
333
|
+
lisa_die "$EX_AMBIGUOUS" "authority: arming marker has no root"
|
|
334
|
+
[ "$_mroot" = "$_root" ] ||
|
|
335
|
+
lisa_die "$EX_AMBIGUOUS" "authority: arming marker root disagrees: $_mroot"
|
|
336
|
+
_suite="$(lisa_marker_get "$_marker" suite)" ||
|
|
337
|
+
lisa_die "$EX_AMBIGUOUS" "authority: arming marker has no suite"
|
|
338
|
+
lisa_suite_is_well_formed "$_suite" ||
|
|
339
|
+
lisa_die "$EX_AMBIGUOUS" "authority: malformed suite label"
|
|
340
|
+
_dev_ino="$(lisa_marker_get "$_marker" devino)" ||
|
|
341
|
+
lisa_die "$EX_AMBIGUOUS" "authority: arming marker has no filesystem identity"
|
|
342
|
+
_live_dev_ino="$(lisa_dev_ino "$_root")" ||
|
|
343
|
+
lisa_die "$EX_AMBIGUOUS" "authority: cannot read run root filesystem identity"
|
|
344
|
+
[ "$_dev_ino" = "$_live_dev_ino" ] ||
|
|
345
|
+
lisa_die "$EX_AMBIGUOUS" "authority: run root filesystem identity changed"
|
|
346
|
+
_pgid="$(lisa_marker_get "$_marker" pgid)" ||
|
|
347
|
+
lisa_die "$EX_AMBIGUOUS" "authority: arming marker has no process group"
|
|
348
|
+
case "$_pgid" in
|
|
349
|
+
"" | 0 | 1 | *[!0-9]*)
|
|
350
|
+
lisa_die "$EX_AMBIGUOUS" "authority: refusing process group '$_pgid'"
|
|
351
|
+
;;
|
|
352
|
+
esac
|
|
353
|
+
_birth="$(lisa_marker_get "$_marker" birth)" ||
|
|
354
|
+
lisa_die "$EX_AMBIGUOUS" "authority: arming marker has no process-birth identity"
|
|
355
|
+
[ -n "$_birth" ] ||
|
|
356
|
+
lisa_die "$EX_AMBIGUOUS" "authority: empty process-birth identity"
|
|
357
|
+
_sup_pid="$(lisa_marker_get "$_marker" suppid)" ||
|
|
358
|
+
lisa_die "$EX_AMBIGUOUS" "authority: arming marker has no supervisor pid"
|
|
359
|
+
case "$_sup_pid" in
|
|
360
|
+
"" | *[!0-9]*)
|
|
361
|
+
lisa_die "$EX_AMBIGUOUS" "authority: refusing supervisor pid '$_sup_pid'"
|
|
362
|
+
;;
|
|
363
|
+
esac
|
|
364
|
+
_sup_birth="$(lisa_marker_get "$_marker" supbirth)" ||
|
|
365
|
+
lisa_die "$EX_AMBIGUOUS" "authority: arming marker has no supervisor birth"
|
|
366
|
+
|
|
367
|
+
# The payload group must still be the one that was armed. Verifying birth
|
|
368
|
+
# here — not only later — is what makes PID reuse unable to broaden the kill.
|
|
369
|
+
_live_birth="$(lisa_birth "$_pgid" || true)"
|
|
370
|
+
if [ -n "$_live_birth" ] && [ "$_live_birth" != "$_birth" ]; then
|
|
371
|
+
lisa_die "$EX_AMBIGUOUS" "authority: payload process-birth identity mismatch"
|
|
372
|
+
fi
|
|
373
|
+
|
|
374
|
+
if [ "${LISA_SCRATCH_AUTHORITY_REFUSE:-0}" = "1" ]; then
|
|
375
|
+
lisa_trace "ack-declined"
|
|
376
|
+
lisa_die "$EX_ARM" "authority: declining to acknowledge (LISA_SCRATCH_AUTHORITY_REFUSE=1)"
|
|
377
|
+
fi
|
|
378
|
+
|
|
379
|
+
# Acknowledge. Only now may the supervisor open the payload gate.
|
|
380
|
+
printf 'version=%s\ntoken=%s\nroot=%s\ndevino=%s\nsuite=%s\npgid=%s\nbirth=%s\nauthority=%s\n' \
|
|
381
|
+
"$LISA_SCRATCH_VERSION" "$_token" "$_root" "$_dev_ino" "$_suite" "$_pgid" \
|
|
382
|
+
"$_birth" "$$" >"$_root/.lisa-scratch-ack.tmp"
|
|
383
|
+
mv -f "$_root/.lisa-scratch-ack.tmp" "$_root/.lisa-scratch-ack"
|
|
384
|
+
lisa_trace "ack token=$_token pgid=$_pgid suite=$_suite"
|
|
385
|
+
|
|
386
|
+
# Wait for the run to end, by any exit path. The root vanishing means the
|
|
387
|
+
# supervisor completed its own cleanup first; the supervisor's process
|
|
388
|
+
# disappearing means it was killed and cleanup falls to this process.
|
|
389
|
+
while :; do
|
|
390
|
+
[ -d "$_root" ] || break
|
|
391
|
+
if [ -f "$_root/.lisa-scratch-done" ]; then
|
|
392
|
+
break
|
|
393
|
+
fi
|
|
394
|
+
if ! lisa_alive "$_sup_pid"; then
|
|
395
|
+
break
|
|
396
|
+
fi
|
|
397
|
+
_now_birth="$(lisa_birth "$_sup_pid" || true)"
|
|
398
|
+
if [ -n "$_now_birth" ] && [ "$_now_birth" != "$_sup_birth" ]; then
|
|
399
|
+
break
|
|
400
|
+
fi
|
|
401
|
+
lisa_nap
|
|
402
|
+
done
|
|
403
|
+
|
|
404
|
+
lisa_trace "reap-begin"
|
|
405
|
+
lisa_drain_group "$_pgid" "$_birth"
|
|
406
|
+
lisa_remove_root "$_root" "$_token" "$_dev_ino" "$_base"
|
|
407
|
+
if [ -d "$_root" ]; then
|
|
408
|
+
lisa_trace "reap-end root-absent=no"
|
|
409
|
+
else
|
|
410
|
+
lisa_trace "reap-end root-absent=yes"
|
|
411
|
+
fi
|
|
412
|
+
exit 0
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
# =============================================================================
|
|
416
|
+
# Bounded process-group drain.
|
|
417
|
+
#
|
|
418
|
+
# Signals exactly the acknowledged process group, and only while its birth
|
|
419
|
+
# identity still matches. Never signals pid 1, never signals this process's own
|
|
420
|
+
# group, and therefore never reaches a database service running outside it.
|
|
421
|
+
# =============================================================================
|
|
422
|
+
|
|
423
|
+
lisa_drain_group() {
|
|
424
|
+
_pgid="$1"
|
|
425
|
+
_birth="$2"
|
|
426
|
+
|
|
427
|
+
case "$_pgid" in
|
|
428
|
+
"" | 0 | 1 | *[!0-9]*) return 0 ;;
|
|
429
|
+
esac
|
|
430
|
+
|
|
431
|
+
_own_pgid="$(lisa_pgid_of "$$" || true)"
|
|
432
|
+
if [ -n "$_own_pgid" ] && [ "$_own_pgid" = "$_pgid" ]; then
|
|
433
|
+
# Refusing to signal our own group is what keeps a misconfigured run from
|
|
434
|
+
# taking down the shell, the runner, or a sibling service.
|
|
435
|
+
printf '%s\n' "lisa-scratch-run: refusing to drain our own process group" >&2
|
|
436
|
+
return 0
|
|
437
|
+
fi
|
|
438
|
+
|
|
439
|
+
_live_birth="$(lisa_birth "$_pgid" || true)"
|
|
440
|
+
if [ -n "$_live_birth" ] && [ "$_live_birth" != "$_birth" ]; then
|
|
441
|
+
# A live process wearing our group's pid, born at a different moment: PID
|
|
442
|
+
# reuse. Draining it would signal someone else's work.
|
|
443
|
+
printf '%s\n' "lisa-scratch-run: process-birth identity mismatch; not draining $_pgid" >&2
|
|
444
|
+
return 0
|
|
445
|
+
fi
|
|
446
|
+
if [ -z "$_live_birth" ]; then
|
|
447
|
+
# The group LEADER is gone — which is precisely what happens when the
|
|
448
|
+
# payload is SIGKILLed — but a process group outlives its leader, so the
|
|
449
|
+
# survivors are the payload's own children and they are still ours.
|
|
450
|
+
#
|
|
451
|
+
# Returning here instead was a real leak: the orphaned children kept
|
|
452
|
+
# running, and because they still held the run's inherited stdout and
|
|
453
|
+
# stderr, the managed invocation could not report its outcome until they
|
|
454
|
+
# finished on their own. Measured as a 60-second hang on a run that had
|
|
455
|
+
# already been killed.
|
|
456
|
+
#
|
|
457
|
+
# Nothing else can be wearing this group id: a group id is its leader's
|
|
458
|
+
# pid, and no process holds that pid at all.
|
|
459
|
+
if ! kill -0 -"$_pgid" 2>/dev/null; then
|
|
460
|
+
return 0
|
|
461
|
+
fi
|
|
462
|
+
fi
|
|
463
|
+
|
|
464
|
+
kill -TERM -"$_pgid" 2>/dev/null || true
|
|
465
|
+
|
|
466
|
+
_budget="${LISA_SCRATCH_DRAIN_MS:-5000}"
|
|
467
|
+
case "$_budget" in
|
|
468
|
+
"" | *[!0-9]*) _budget=5000 ;;
|
|
469
|
+
esac
|
|
470
|
+
_waited=0
|
|
471
|
+
while [ "$_waited" -lt "$_budget" ]; do
|
|
472
|
+
kill -0 -"$_pgid" 2>/dev/null || return 0
|
|
473
|
+
lisa_nap
|
|
474
|
+
_waited=$((_waited + LISA_SCRATCH_NAP_MS))
|
|
475
|
+
done
|
|
476
|
+
|
|
477
|
+
kill -0 -"$_pgid" 2>/dev/null || return 0
|
|
478
|
+
kill -KILL -"$_pgid" 2>/dev/null || true
|
|
479
|
+
return 0
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# =============================================================================
|
|
483
|
+
# Token-and-identity-bound root removal.
|
|
484
|
+
#
|
|
485
|
+
# Re-verifies token and device+inode immediately before the rename, renames the
|
|
486
|
+
# root into a quarantine name derived from THIS run's token, and deletes only
|
|
487
|
+
# that name. A quarantine directory whose suffix is not our token is left
|
|
488
|
+
# alone, so a foreign quarantine cannot be swept.
|
|
489
|
+
# =============================================================================
|
|
490
|
+
|
|
491
|
+
lisa_remove_root() {
|
|
492
|
+
_root="$1"
|
|
493
|
+
_token="$2"
|
|
494
|
+
_dev_ino="$3"
|
|
495
|
+
_base="$4"
|
|
496
|
+
|
|
497
|
+
[ -d "$_root" ] || return 0
|
|
498
|
+
if [ -L "$_root" ]; then
|
|
499
|
+
printf '%s\n' "lisa-scratch-run: run root became a symlink; refusing to delete" >&2
|
|
500
|
+
return 1
|
|
501
|
+
fi
|
|
502
|
+
lisa_root_is_well_formed "$_root" "$_base" || {
|
|
503
|
+
printf '%s\n' "lisa-scratch-run: run root no longer well-formed; refusing to delete" >&2
|
|
504
|
+
return 1
|
|
505
|
+
}
|
|
506
|
+
_now="$(lisa_dev_ino "$_root")" || return 1
|
|
507
|
+
if [ "$_now" != "$_dev_ino" ]; then
|
|
508
|
+
printf '%s\n' "lisa-scratch-run: run root filesystem identity changed; refusing to delete" >&2
|
|
509
|
+
return 1
|
|
510
|
+
fi
|
|
511
|
+
_marker_token="$(lisa_marker_get "$_root/.lisa-scratch-arm" token || true)"
|
|
512
|
+
if [ -n "$_marker_token" ] && [ "$_marker_token" != "$_token" ]; then
|
|
513
|
+
printf '%s\n' "lisa-scratch-run: run root owner token changed; refusing to delete" >&2
|
|
514
|
+
return 1
|
|
515
|
+
fi
|
|
516
|
+
|
|
517
|
+
_quarantine="${_base}/${LISA_SCRATCH_NAMESPACE}/.quarantine.${_token}"
|
|
518
|
+
case "$_quarantine" in
|
|
519
|
+
"${_base}/${LISA_SCRATCH_NAMESPACE}/.quarantine.${_token}") : ;;
|
|
520
|
+
*) return 1 ;;
|
|
521
|
+
esac
|
|
522
|
+
if mv -f "$_root" "$_quarantine" 2>/dev/null; then
|
|
523
|
+
rm -rf "$_quarantine" 2>/dev/null || true
|
|
524
|
+
else
|
|
525
|
+
# Another participant of this same invocation may have taken it already.
|
|
526
|
+
[ -d "$_root" ] || return 0
|
|
527
|
+
rm -rf "$_root" 2>/dev/null || true
|
|
528
|
+
fi
|
|
529
|
+
if [ -d "$_root" ]; then return 1; fi
|
|
530
|
+
return 0
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
# =============================================================================
|
|
534
|
+
# GATED PAYLOAD LAUNCHER.
|
|
535
|
+
#
|
|
536
|
+
# Runs as the leader of its own process group, so the group identity exists —
|
|
537
|
+
# and can therefore be acknowledged — before the payload runs. It refuses to
|
|
538
|
+
# exec the payload unless the authority's acknowledgement for this exact token
|
|
539
|
+
# is on disk. That refusal is the enforcement of authority-before-allocation:
|
|
540
|
+
# even if the supervisor were wrong, the payload would not start.
|
|
541
|
+
# =============================================================================
|
|
542
|
+
|
|
543
|
+
lisa_launcher_main() {
|
|
544
|
+
LISA_SCRATCH_ROLE=launcher
|
|
545
|
+
_root="$1"
|
|
546
|
+
_token="$2"
|
|
547
|
+
shift 2
|
|
548
|
+
|
|
549
|
+
if ! lisa_isolate_group LISA_SCRATCH_LAUNCH_REENTRY \
|
|
550
|
+
"$LISA_SCRATCH_SH" "$LISA_SCRATCH_SELF" --launch "$_root" "$_token" "$@"; then
|
|
551
|
+
lisa_die "$EX_ARM" "cannot isolate the payload process group; refusing"
|
|
552
|
+
fi
|
|
553
|
+
|
|
554
|
+
# Report the group identity the authority has to acknowledge. This happens
|
|
555
|
+
# before the gate is even waited on, and long before any scratch exists, so
|
|
556
|
+
# the identity being acknowledged is a fact rather than a prediction.
|
|
557
|
+
_own_birth="$(lisa_birth "$$" || true)"
|
|
558
|
+
[ -n "$_own_birth" ] ||
|
|
559
|
+
lisa_die "$EX_ARM" "cannot read the payload process-birth identity"
|
|
560
|
+
printf 'version=%s\npgid=%s\nbirth=%s\n' \
|
|
561
|
+
"$LISA_SCRATCH_VERSION" "$$" "$_own_birth" \
|
|
562
|
+
>"$_root/.lisa-scratch-pg.tmp" ||
|
|
563
|
+
lisa_die "$EX_ARM" "cannot report the payload process group"
|
|
564
|
+
mv -f "$_root/.lisa-scratch-pg.tmp" "$_root/.lisa-scratch-pg"
|
|
565
|
+
lisa_trace "group-reported pgid=$$"
|
|
566
|
+
|
|
567
|
+
# Deliberately NOT the arming budget: shrinking the arming budget must
|
|
568
|
+
# produce a refusal that is observable as 'the payload never ran', which it
|
|
569
|
+
# cannot be if the held payload has already given up on its own.
|
|
570
|
+
_deadline="${LISA_SCRATCH_GATE_TIMEOUT_MS:-300000}"
|
|
571
|
+
case "$_deadline" in
|
|
572
|
+
"" | *[!0-9]*) _deadline=300000 ;;
|
|
573
|
+
esac
|
|
574
|
+
_waited=0
|
|
575
|
+
while [ ! -f "$_root/.lisa-scratch-go" ]; do
|
|
576
|
+
if [ "$_waited" -ge "$_deadline" ]; then
|
|
577
|
+
lisa_die "$EX_ARM" "payload gate never opened; refusing to run the payload"
|
|
578
|
+
fi
|
|
579
|
+
lisa_nap
|
|
580
|
+
_waited=$((_waited + LISA_SCRATCH_NAP_MS))
|
|
581
|
+
done
|
|
582
|
+
|
|
583
|
+
_ack_token="$(lisa_marker_get "$_root/.lisa-scratch-ack" token || true)"
|
|
584
|
+
[ "$_ack_token" = "$_token" ] ||
|
|
585
|
+
lisa_die "$EX_ARM" "no valid cleanup-authority acknowledgement; refusing to run the payload"
|
|
586
|
+
|
|
587
|
+
[ -d "$_root/tmp" ] ||
|
|
588
|
+
lisa_die "$EX_ARM" "payload scratch root missing; refusing to run the payload"
|
|
589
|
+
|
|
590
|
+
TMPDIR="$_root/tmp"
|
|
591
|
+
TMP="$TMPDIR"
|
|
592
|
+
TEMP="$TMPDIR"
|
|
593
|
+
export TMPDIR TMP TEMP
|
|
594
|
+
lisa_trace "payload-exec"
|
|
595
|
+
if [ "${LISA_SCRATCH_INHERIT_STDIN:-0}" = "1" ]; then
|
|
596
|
+
exec "$@"
|
|
597
|
+
else
|
|
598
|
+
exec "$@" </dev/null
|
|
599
|
+
fi
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
# =============================================================================
|
|
603
|
+
# Leak scan.
|
|
604
|
+
# =============================================================================
|
|
605
|
+
|
|
606
|
+
# Emits the leak report on stderr. Returns 0 clean, 1 leaked.
|
|
607
|
+
lisa_leak_scan() {
|
|
608
|
+
_tmp="$1"
|
|
609
|
+
_suite="$2"
|
|
610
|
+
|
|
611
|
+
[ -d "$_tmp" ] || return 0
|
|
612
|
+
|
|
613
|
+
_registered="${LISA_SCRATCH_REGISTERED_PREFIXES:-lisa-}"
|
|
614
|
+
|
|
615
|
+
_leaked=""
|
|
616
|
+
_count=0
|
|
617
|
+
for _entry in "$_tmp"/* "$_tmp"/.[!.]*; do
|
|
618
|
+
[ -e "$_entry" ] || continue
|
|
619
|
+
_name="${_entry##*/}"
|
|
620
|
+
_is_registered=no
|
|
621
|
+
_rest="$_registered"
|
|
622
|
+
while [ -n "$_rest" ]; do
|
|
623
|
+
case "$_rest" in
|
|
624
|
+
*,*)
|
|
625
|
+
_prefix="${_rest%%,*}"
|
|
626
|
+
_rest="${_rest#*,}"
|
|
627
|
+
;;
|
|
628
|
+
*)
|
|
629
|
+
_prefix="$_rest"
|
|
630
|
+
_rest=""
|
|
631
|
+
;;
|
|
632
|
+
esac
|
|
633
|
+
[ -n "$_prefix" ] || continue
|
|
634
|
+
case "$_name" in
|
|
635
|
+
"$_prefix"*) _is_registered=yes ;;
|
|
636
|
+
esac
|
|
637
|
+
done
|
|
638
|
+
if [ "$_is_registered" = yes ]; then
|
|
639
|
+
# Registered prefixes are batch-cleaned, never reported.
|
|
640
|
+
rm -rf "$_entry" 2>/dev/null || true
|
|
641
|
+
continue
|
|
642
|
+
fi
|
|
643
|
+
_count=$((_count + 1))
|
|
644
|
+
_leaked="$_leaked$_name
|
|
645
|
+
"
|
|
646
|
+
done
|
|
647
|
+
|
|
648
|
+
if [ "$_count" -eq 0 ]; then return 0; fi
|
|
649
|
+
|
|
650
|
+
_sorted="$(printf '%s' "$_leaked" | LC_ALL=C sort | tr '\n' ' ' | sed 's/ *$//')"
|
|
651
|
+
printf '%s\n' "lisa-scratch-run: scratch leak in suite '$_suite': $_count unregistered direct child(ren): $_sorted" >&2
|
|
652
|
+
printf '%s\n' "lisa-scratch-run: register benign prefixes via LISA_SCRATCH_REGISTERED_PREFIXES (comma-separated) or fix the fixture that leaked." >&2
|
|
653
|
+
return 1
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
# =============================================================================
|
|
657
|
+
# SUPERVISOR (default mode).
|
|
658
|
+
# =============================================================================
|
|
659
|
+
|
|
660
|
+
# Terminal path for a signal that arrives while the run is still arming, i.e.
|
|
661
|
+
# before the payload has been allowed to allocate anything. Nothing is owned
|
|
662
|
+
# yet beyond the run root and, at most, a payload held at the gate.
|
|
663
|
+
lisa_abort_arming() {
|
|
664
|
+
_sig="$1"
|
|
665
|
+
trap - TERM INT HUP
|
|
666
|
+
lisa_trace "arm-aborted signal=$_sig"
|
|
667
|
+
[ -n "${LISA_SCRATCH_PAYLOAD_PID:-}" ] && kill -TERM "$LISA_SCRATCH_PAYLOAD_PID" 2>/dev/null
|
|
668
|
+
[ -n "${LISA_SCRATCH_AUTHORITY_PID:-}" ] && kill -TERM "$LISA_SCRATCH_AUTHORITY_PID" 2>/dev/null
|
|
669
|
+
if [ -n "${LISA_SCRATCH_ROOT:-}" ] &&
|
|
670
|
+
lisa_root_is_well_formed "$LISA_SCRATCH_ROOT" "${LISA_SCRATCH_BASE_CANONICAL:-}"; then
|
|
671
|
+
rm -rf "$LISA_SCRATCH_ROOT" 2>/dev/null || true
|
|
672
|
+
fi
|
|
673
|
+
kill -"$_sig" $$
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
lisa_supervisor_main() {
|
|
677
|
+
LISA_SCRATCH_ROLE=supervisor
|
|
678
|
+
|
|
679
|
+
_suite=""
|
|
680
|
+
while [ "$#" -gt 0 ]; do
|
|
681
|
+
case "$1" in
|
|
682
|
+
--suite)
|
|
683
|
+
[ "$#" -ge 2 ] || lisa_die "$EX_USAGE" "--suite requires a value"
|
|
684
|
+
_suite="$2"
|
|
685
|
+
shift 2
|
|
686
|
+
;;
|
|
687
|
+
--suite=*)
|
|
688
|
+
_suite="${1#--suite=}"
|
|
689
|
+
shift
|
|
690
|
+
;;
|
|
691
|
+
--)
|
|
692
|
+
shift
|
|
693
|
+
break
|
|
694
|
+
;;
|
|
695
|
+
*)
|
|
696
|
+
lisa_die "$EX_USAGE" "unexpected argument '$1'; usage: --suite <label> -- <command>"
|
|
697
|
+
;;
|
|
698
|
+
esac
|
|
699
|
+
done
|
|
700
|
+
|
|
701
|
+
lisa_suite_is_well_formed "$_suite" ||
|
|
702
|
+
lisa_die "$EX_USAGE" "--suite must be 1-64 chars of [A-Za-z0-9._-]"
|
|
703
|
+
[ "$#" -ge 1 ] ||
|
|
704
|
+
lisa_die "$EX_USAGE" "no payload command after --"
|
|
705
|
+
|
|
706
|
+
# A supervised payload may not be another supervisor: exactly one boundary
|
|
707
|
+
# per route, no recursive alias.
|
|
708
|
+
case "${LISA_SCRATCH_TOKEN:-}" in
|
|
709
|
+
?*) lisa_die "$EX_USAGE" "refusing to nest inside an existing supervised run (suite ${LISA_SCRATCH_SUITE:-unknown})" ;;
|
|
710
|
+
esac
|
|
711
|
+
case "$1" in
|
|
712
|
+
*lisa-scratch-run.sh) lisa_die "$EX_USAGE" "refusing to supervise the supervisor" ;;
|
|
713
|
+
esac
|
|
714
|
+
|
|
715
|
+
# --- terminal paths, armed before anything can exist to leak ------------
|
|
716
|
+
# Installed as the very first thing the run does. A handler installed one
|
|
717
|
+
# statement after the directory it protects is a handler with a window, and
|
|
718
|
+
# the window is exactly where an interrupted run leaves its root behind
|
|
719
|
+
# forever. Installing it here means there is no ordering to get wrong: the
|
|
720
|
+
# handler is older than every resource it can be asked to release, and it
|
|
721
|
+
# simply does nothing while there is nothing to release.
|
|
722
|
+
LISA_SCRATCH_ROOT=""
|
|
723
|
+
LISA_SCRATCH_BASE_CANONICAL=""
|
|
724
|
+
LISA_SCRATCH_PAYLOAD_PID=""
|
|
725
|
+
LISA_SCRATCH_AUTHORITY_PID=""
|
|
726
|
+
for _sig in TERM INT HUP; do
|
|
727
|
+
# shellcheck disable=SC2064
|
|
728
|
+
trap "lisa_abort_arming $_sig" "$_sig"
|
|
729
|
+
done
|
|
730
|
+
|
|
731
|
+
# --- base + token -------------------------------------------------------
|
|
732
|
+
_base="${LISA_SCRATCH_BASE:-${TMPDIR:-/tmp}}"
|
|
733
|
+
_base="${_base%/}"
|
|
734
|
+
_base="$(lisa_canonical_dir "$_base")" ||
|
|
735
|
+
lisa_die "$EX_ARM" "temp base is not a canonical directory: ${LISA_SCRATCH_BASE:-${TMPDIR:-/tmp}}"
|
|
736
|
+
|
|
737
|
+
_token="$(od -An -tx1 -N32 /dev/urandom 2>/dev/null | tr -d ' \n')" || _token=""
|
|
738
|
+
lisa_token_is_well_formed "$_token" ||
|
|
739
|
+
lisa_die "$EX_ARM" "cannot obtain a 256-bit run token from /dev/urandom"
|
|
740
|
+
|
|
741
|
+
mkdir -p "$_base/$LISA_SCRATCH_NAMESPACE" ||
|
|
742
|
+
lisa_die "$EX_ARM" "cannot create scratch namespace under $_base"
|
|
743
|
+
chmod 700 "$_base/$LISA_SCRATCH_NAMESPACE" 2>/dev/null || true
|
|
744
|
+
|
|
745
|
+
_root="$_base/$LISA_SCRATCH_NAMESPACE/$_suite.$_token"
|
|
746
|
+
lisa_root_is_well_formed "$_root" "$_base" ||
|
|
747
|
+
lisa_die "$EX_ARM" "computed run root is not well-formed: $_root"
|
|
748
|
+
|
|
749
|
+
# The already-installed handler becomes able to act the moment these are set,
|
|
750
|
+
# which is one statement before the directory they name can exist.
|
|
751
|
+
LISA_SCRATCH_ROOT="$_root"
|
|
752
|
+
LISA_SCRATCH_TOKEN="$_token"
|
|
753
|
+
LISA_SCRATCH_SUITE="$_suite"
|
|
754
|
+
LISA_SCRATCH_BASE_CANONICAL="$_base"
|
|
755
|
+
export LISA_SCRATCH_ROOT LISA_SCRATCH_TOKEN LISA_SCRATCH_SUITE
|
|
756
|
+
|
|
757
|
+
# Exclusive create: mkdir of an existing leaf fails, so two runs can never
|
|
758
|
+
# share a root even if a token somehow repeated.
|
|
759
|
+
mkdir "$_root" || lisa_die "$EX_ARM" "cannot exclusively create run root"
|
|
760
|
+
chmod 700 "$_root" 2>/dev/null || true
|
|
761
|
+
_canon="$(lisa_canonical_dir "$_root")" ||
|
|
762
|
+
lisa_die "$EX_ARM" "run root is not a canonical directory"
|
|
763
|
+
[ "$_canon" = "$_root" ] ||
|
|
764
|
+
lisa_die "$EX_ARM" "run root resolves elsewhere: $_root -> $_canon"
|
|
765
|
+
_dev_ino="$(lisa_dev_ino "$_root")" ||
|
|
766
|
+
lisa_die "$EX_ARM" "cannot read run root filesystem identity"
|
|
767
|
+
|
|
768
|
+
lisa_trace "arm-begin suite=$_suite root=$_root"
|
|
769
|
+
|
|
770
|
+
# --- process group exists before it is acknowledged ---------------------
|
|
771
|
+
# The payload's group identity has to be knowable BEFORE the payload runs, so
|
|
772
|
+
# the launcher is started held at a gate, isolates itself into its own
|
|
773
|
+
# process group, and reports that group back. It cannot allocate scratch
|
|
774
|
+
# until the gate opens.
|
|
775
|
+
#
|
|
776
|
+
# `set -m` is a best-effort assist for the Darwin path, where it is what
|
|
777
|
+
# gives the background child its own group; the launcher verifies the result
|
|
778
|
+
# itself and reaches for setsid when it did not happen, so a shell without
|
|
779
|
+
# working job control is a fallback rather than a failure.
|
|
780
|
+
set -m 2>/dev/null || true
|
|
781
|
+
|
|
782
|
+
# Turning job control on resets the shell's SIGINT disposition — a terminal
|
|
783
|
+
# normally delivers SIGINT to the foreground job's group, so the shell steps
|
|
784
|
+
# out of the way. That silently drops the handler installed above, and a
|
|
785
|
+
# Ctrl-C landing in the handshake window would be discarded rather than
|
|
786
|
+
# honoured: measured as a run that continued to completion 15 seconds after
|
|
787
|
+
# being told to stop. Re-arming here is what closes it.
|
|
788
|
+
for _sig in TERM INT HUP; do
|
|
789
|
+
# shellcheck disable=SC2064
|
|
790
|
+
trap "lisa_abort_arming $_sig" "$_sig"
|
|
791
|
+
done
|
|
792
|
+
|
|
793
|
+
# A real process, not a `&` subshell: a subshell cannot learn its own pid
|
|
794
|
+
# portably, and "am I my own group leader?" is the question the whole
|
|
795
|
+
# handshake rests on.
|
|
796
|
+
"$LISA_SCRATCH_SH" "$LISA_SCRATCH_SELF" --launch "$_root" "$_token" "$@" &
|
|
797
|
+
_payload_pid="$!"
|
|
798
|
+
LISA_SCRATCH_PAYLOAD_PID="$_payload_pid"
|
|
799
|
+
|
|
800
|
+
_pg_deadline="${LISA_SCRATCH_ARM_TIMEOUT_MS:-15000}"
|
|
801
|
+
case "$_pg_deadline" in
|
|
802
|
+
"" | *[!0-9]*) _pg_deadline=15000 ;;
|
|
803
|
+
esac
|
|
804
|
+
_waited=0
|
|
805
|
+
_pgid=""
|
|
806
|
+
while [ "$_waited" -lt "$_pg_deadline" ]; do
|
|
807
|
+
_pgid="$(lisa_marker_get "$_root/.lisa-scratch-pg" pgid || true)"
|
|
808
|
+
[ -n "$_pgid" ] && break
|
|
809
|
+
lisa_alive "$_payload_pid" || break
|
|
810
|
+
lisa_nap
|
|
811
|
+
_waited=$((_waited + LISA_SCRATCH_NAP_MS))
|
|
812
|
+
done
|
|
813
|
+
_birth="$(lisa_marker_get "$_root/.lisa-scratch-pg" birth || true)"
|
|
814
|
+
|
|
815
|
+
_own_pgid="$(lisa_pgid_of "$$" || true)"
|
|
816
|
+
lisa_abort_launch() {
|
|
817
|
+
kill -TERM "$_payload_pid" 2>/dev/null || true
|
|
818
|
+
rm -rf "$_root" 2>/dev/null || true
|
|
819
|
+
lisa_die "$EX_ARM" "$1"
|
|
820
|
+
}
|
|
821
|
+
case "$_pgid" in
|
|
822
|
+
"" | 0 | 1 | *[!0-9]*)
|
|
823
|
+
lisa_abort_launch "payload did not report an isolated process group; refusing"
|
|
824
|
+
;;
|
|
825
|
+
esac
|
|
826
|
+
# The reported group must be the payload's own pid. Anything else means the
|
|
827
|
+
# launcher was replaced by a forking wrapper, and `wait` would then report
|
|
828
|
+
# the wrapper's status instead of the payload's.
|
|
829
|
+
[ "$_pgid" = "$_payload_pid" ] ||
|
|
830
|
+
lisa_abort_launch "payload process group $_pgid is not the payload pid $_payload_pid; refusing"
|
|
831
|
+
[ "$(lisa_pgid_of "$_pgid" || true)" = "$_pgid" ] ||
|
|
832
|
+
lisa_abort_launch "payload does not lead the group it reported; refusing"
|
|
833
|
+
if [ -n "$_own_pgid" ] && [ "$_pgid" = "$_own_pgid" ]; then
|
|
834
|
+
lisa_abort_launch "payload did not get an isolated process group; refusing"
|
|
835
|
+
fi
|
|
836
|
+
[ -n "$_birth" ] ||
|
|
837
|
+
lisa_abort_launch "cannot read the payload process-birth identity"
|
|
838
|
+
_sup_birth="$(lisa_birth "$$" || true)"
|
|
839
|
+
|
|
840
|
+
# --- write the arming record -------------------------------------------
|
|
841
|
+
{
|
|
842
|
+
printf 'version=%s\n' "$LISA_SCRATCH_VERSION"
|
|
843
|
+
printf 'token=%s\n' "$_token"
|
|
844
|
+
printf 'root=%s\n' "$_root"
|
|
845
|
+
printf 'devino=%s\n' "$_dev_ino"
|
|
846
|
+
printf 'suite=%s\n' "$_suite"
|
|
847
|
+
printf 'pgid=%s\n' "$_pgid"
|
|
848
|
+
printf 'birth=%s\n' "$_birth"
|
|
849
|
+
printf 'suppid=%s\n' "$$"
|
|
850
|
+
printf 'supbirth=%s\n' "$_sup_birth"
|
|
851
|
+
} >"$_root/.lisa-scratch-arm.tmp" || {
|
|
852
|
+
kill -TERM "$_payload_pid" 2>/dev/null || true
|
|
853
|
+
rm -rf "$_root" 2>/dev/null || true
|
|
854
|
+
lisa_die "$EX_ARM" "cannot write the arming record"
|
|
855
|
+
}
|
|
856
|
+
mv -f "$_root/.lisa-scratch-arm.tmp" "$_root/.lisa-scratch-arm"
|
|
857
|
+
lisa_trace "arm-written pgid=$_pgid"
|
|
858
|
+
|
|
859
|
+
# --- start the outside authority and wait for its acknowledgement -------
|
|
860
|
+
# Forked from THIS invocation, before the payload gate opens, into its own
|
|
861
|
+
# process group. It therefore survives both a payload-group kill and a
|
|
862
|
+
# supervisor kill, and no successor Lisa run is ever needed to clean up.
|
|
863
|
+
"$LISA_SCRATCH_SH" "$LISA_SCRATCH_SELF" --authority "$_root" &
|
|
864
|
+
_authority_pid="$!"
|
|
865
|
+
LISA_SCRATCH_AUTHORITY_PID="$_authority_pid"
|
|
866
|
+
lisa_trace "authority-started pid=$_authority_pid"
|
|
867
|
+
|
|
868
|
+
# Both process groups are assigned at fork time, so job control has done
|
|
869
|
+
# its job. Turning it back off keeps the shell's asynchronous 'Terminated'
|
|
870
|
+
# notices out of the payload's own stderr.
|
|
871
|
+
set +m 2>/dev/null || true
|
|
872
|
+
|
|
873
|
+
# Same reason as after `set -m`: the disposition change cuts both ways.
|
|
874
|
+
for _sig in TERM INT HUP; do
|
|
875
|
+
# shellcheck disable=SC2064
|
|
876
|
+
trap "lisa_abort_arming $_sig" "$_sig"
|
|
877
|
+
done
|
|
878
|
+
|
|
879
|
+
_deadline="${LISA_SCRATCH_ARM_TIMEOUT_MS:-15000}"
|
|
880
|
+
case "$_deadline" in
|
|
881
|
+
"" | *[!0-9]*) _deadline=15000 ;;
|
|
882
|
+
esac
|
|
883
|
+
_waited=0
|
|
884
|
+
_acked=no
|
|
885
|
+
while :; do
|
|
886
|
+
_ack_token="$(lisa_marker_get "$_root/.lisa-scratch-ack" token || true)"
|
|
887
|
+
if [ "$_ack_token" = "$_token" ]; then
|
|
888
|
+
_acked=yes
|
|
889
|
+
break
|
|
890
|
+
fi
|
|
891
|
+
if ! lisa_alive "$_authority_pid"; then
|
|
892
|
+
break
|
|
893
|
+
fi
|
|
894
|
+
if [ "$_waited" -ge "$_deadline" ]; then
|
|
895
|
+
break
|
|
896
|
+
fi
|
|
897
|
+
lisa_nap
|
|
898
|
+
_waited=$((_waited + LISA_SCRATCH_NAP_MS))
|
|
899
|
+
done
|
|
900
|
+
|
|
901
|
+
if [ "$_acked" != yes ]; then
|
|
902
|
+
# REFUSE. The payload has not run and never will on this invocation.
|
|
903
|
+
kill -TERM "$_payload_pid" 2>/dev/null || true
|
|
904
|
+
kill -TERM "$_authority_pid" 2>/dev/null || true
|
|
905
|
+
rm -rf "$_root" 2>/dev/null || true
|
|
906
|
+
lisa_trace "arm-refused"
|
|
907
|
+
lisa_die "$EX_ARM" "cleanup authority did not acknowledge the run; refusing to execute the payload"
|
|
908
|
+
fi
|
|
909
|
+
lisa_trace "arm-acked"
|
|
910
|
+
|
|
911
|
+
# --- terminal paths, installed BEFORE the gate opens --------------------
|
|
912
|
+
# A signal arriving in the window between allocation and trap installation
|
|
913
|
+
# would otherwise leave the root behind on the foreground path.
|
|
914
|
+
LISA_SCRATCH_SIGNAL=""
|
|
915
|
+
for _sig in TERM:143 INT:130 HUP:129; do
|
|
916
|
+
_signame="${_sig%%:*}"
|
|
917
|
+
_sigstatus="${_sig##*:}"
|
|
918
|
+
# shellcheck disable=SC2064
|
|
919
|
+
trap "LISA_SCRATCH_SIGNAL=$_signame; lisa_supervisor_finish $_payload_pid $_authority_pid \"\$LISA_SCRATCH_ROOT\" \"\$LISA_SCRATCH_TOKEN\" \"$_dev_ino\" \"$_base\" \"$_pgid\" \"$_birth\" $_sigstatus" "$_signame"
|
|
920
|
+
done
|
|
921
|
+
|
|
922
|
+
# --- allocation may begin -----------------------------------------------
|
|
923
|
+
mkdir "$_root/tmp" || {
|
|
924
|
+
kill -TERM "$_payload_pid" 2>/dev/null || true
|
|
925
|
+
lisa_die "$EX_ARM" "cannot create the payload scratch root"
|
|
926
|
+
}
|
|
927
|
+
chmod 700 "$_root/tmp" 2>/dev/null || true
|
|
928
|
+
: >"$_root/.lisa-scratch-go"
|
|
929
|
+
lisa_trace "gate-open"
|
|
930
|
+
|
|
931
|
+
_status=0
|
|
932
|
+
wait "$_payload_pid" || _status="$?"
|
|
933
|
+
lisa_trace "payload-exit status=$_status"
|
|
934
|
+
trap - TERM INT HUP
|
|
935
|
+
lisa_supervisor_finish "$_payload_pid" "$_authority_pid" "$_root" "$_token" \
|
|
936
|
+
"$_dev_ino" "$_base" "$_pgid" "$_birth" "$_status"
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
# Drain, scan, delete, verify, then exit preserving the payload's verdict.
|
|
940
|
+
lisa_supervisor_finish() {
|
|
941
|
+
_payload_pid="$1"
|
|
942
|
+
_authority_pid="$2"
|
|
943
|
+
_root="$3"
|
|
944
|
+
_token="$4"
|
|
945
|
+
_dev_ino="$5"
|
|
946
|
+
_base="$6"
|
|
947
|
+
_pgid="$7"
|
|
948
|
+
_birth="$8"
|
|
949
|
+
_status="$9"
|
|
950
|
+
|
|
951
|
+
lisa_trace "finish-begin status=$_status"
|
|
952
|
+
|
|
953
|
+
# Drain first: a survivor in the owned group could still be creating
|
|
954
|
+
# scratch, which would make the leak verdict a race rather than a fact.
|
|
955
|
+
lisa_drain_group "$_pgid" "$_birth"
|
|
956
|
+
|
|
957
|
+
_leak=0
|
|
958
|
+
if [ "${LISA_SCRATCH_LEAK_GATE:-enforce}" != "off" ]; then
|
|
959
|
+
lisa_leak_scan "$_root/tmp" "$LISA_SCRATCH_SUITE" || _leak=1
|
|
960
|
+
fi
|
|
961
|
+
|
|
962
|
+
: >"$_root/.lisa-scratch-done" 2>/dev/null || true
|
|
963
|
+
lisa_remove_root "$_root" "$_token" "$_dev_ino" "$_base" || true
|
|
964
|
+
|
|
965
|
+
if [ -d "$_root" ]; then
|
|
966
|
+
# The authority still owns this root. Do not kill it — it is the safety
|
|
967
|
+
# net, and it is already armed.
|
|
968
|
+
printf '%s\n' "lisa-scratch-run: run root still present after cleanup: $_root" >&2
|
|
969
|
+
lisa_trace "finish-root-present"
|
|
970
|
+
if [ "$_status" -eq 0 ]; then _status="$EX_AMBIGUOUS"; fi
|
|
971
|
+
else
|
|
972
|
+
lisa_trace "finish-root-absent"
|
|
973
|
+
# The owned group is drained and the owned root is gone, so the authority
|
|
974
|
+
# has nothing left to own. Retire it and reap it here rather than leaving
|
|
975
|
+
# an orphan behind.
|
|
976
|
+
#
|
|
977
|
+
# Polling `kill -0` instead would not work: a background child that has
|
|
978
|
+
# already exited is a zombie until it is waited for, and signal 0 succeeds
|
|
979
|
+
# against a zombie. That reads as "still working" for the whole budget, and
|
|
980
|
+
# cost a flat two seconds on every single supervised run.
|
|
981
|
+
kill -TERM "$_authority_pid" 2>/dev/null || true
|
|
982
|
+
wait "$_authority_pid" 2>/dev/null || true
|
|
983
|
+
fi
|
|
984
|
+
|
|
985
|
+
if [ "$_leak" -eq 1 ] && [ "${LISA_SCRATCH_LEAK_GATE:-enforce}" = "enforce" ]; then
|
|
986
|
+
# A red payload keeps its own verdict; a green payload does not get to
|
|
987
|
+
# mask a leaking child.
|
|
988
|
+
if [ "$_status" -eq 0 ]; then _status="$EX_LEAK"; fi
|
|
989
|
+
fi
|
|
990
|
+
|
|
991
|
+
if [ -n "${LISA_SCRATCH_SIGNAL:-}" ]; then
|
|
992
|
+
# Re-raise the original terminal signal so the caller sees what really
|
|
993
|
+
# happened, after cleanup has completed.
|
|
994
|
+
trap - "$LISA_SCRATCH_SIGNAL"
|
|
995
|
+
kill -"$LISA_SCRATCH_SIGNAL" $$
|
|
996
|
+
fi
|
|
997
|
+
|
|
998
|
+
exit "$_status"
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
# =============================================================================
|
|
1002
|
+
# Entry point.
|
|
1003
|
+
# =============================================================================
|
|
1004
|
+
|
|
1005
|
+
LISA_SCRATCH_ROLE=supervisor
|
|
1006
|
+
|
|
1007
|
+
case "${0}" in
|
|
1008
|
+
/*) LISA_SCRATCH_SELF="$0" ;;
|
|
1009
|
+
*) LISA_SCRATCH_SELF="$(cd -P -- "$(dirname -- "$0")" && pwd -P)/$(basename -- "$0")" ;;
|
|
1010
|
+
esac
|
|
1011
|
+
# The file is POSIX shell throughout, so re-entering it under /bin/sh is exact
|
|
1012
|
+
# regardless of which interpreter the route used to start it.
|
|
1013
|
+
LISA_SCRATCH_SH="/bin/sh"
|
|
1014
|
+
export LISA_SCRATCH_SELF LISA_SCRATCH_SH
|
|
1015
|
+
|
|
1016
|
+
if [ "${1:-}" = "--authority" ]; then
|
|
1017
|
+
[ "$#" -eq 2 ] || lisa_die "$EX_USAGE" "--authority takes exactly one run root"
|
|
1018
|
+
lisa_authority_main "$2"
|
|
1019
|
+
fi
|
|
1020
|
+
|
|
1021
|
+
# Internal: the held payload launcher. Not a user-facing entry point — the
|
|
1022
|
+
# supervisor re-enters here, and so does setsid when it has to build the
|
|
1023
|
+
# process group the handshake acknowledges.
|
|
1024
|
+
if [ "${1:-}" = "--launch" ]; then
|
|
1025
|
+
[ "$#" -ge 4 ] || lisa_die "$EX_USAGE" "--launch takes a run root, a token and a command"
|
|
1026
|
+
shift
|
|
1027
|
+
lisa_launcher_main "$@"
|
|
1028
|
+
fi
|
|
1029
|
+
|
|
1030
|
+
lisa_supervisor_main "$@"
|