caproom 0.7.6 → 0.9.2
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 +84 -158
- package/bin/caproom-mcp +0 -0
- package/bin/caproom-rs +0 -0
- package/package.json +10 -13
- package/prebuilt/caproom-darwin-arm64 +0 -0
- package/prebuilt/caproom-darwin-x64 +0 -0
- package/prebuilt/caproom-linux-x64 +0 -0
- package/prebuilt/caproom-win32-x64.exe +0 -0
- package/bin/caproom +0 -1025
- package/bin/caproom-mcp.js +0 -267
- package/bin/caproom.js +0 -21
- package/bin/caproom.ps1 +0 -753
- package/scripts/postinstall.js +0 -7
package/bin/caproom
DELETED
|
@@ -1,1025 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# caproom — memory-cap any command (AI coding agents, builds, background jobs)
|
|
3
|
-
# on macOS/Linux. macOS has no working RLIMIT_AS/DATA/RSS or launchd RSS
|
|
4
|
-
# enforcement (verified empirically — both are no-ops on modern macOS), so
|
|
5
|
-
# this uses whichever real enforcement mechanism is available:
|
|
6
|
-
# 1. Host-native polling watchdog (process-tree RSS + SIGKILL) — the
|
|
7
|
-
# DEFAULT. Runs in your real environment: same PATH, auth, native
|
|
8
|
-
# binaries, tty. Small race window bounded by --interval.
|
|
9
|
-
# 2. Docker cgroup (--memory) — opt-in via --docker. Hard cap, zero race
|
|
10
|
-
# window, but runs the command inside a Linux container (native-module
|
|
11
|
-
# and toolchain drift; see README). Fails loudly if the daemon is down.
|
|
12
|
-
set -euo pipefail
|
|
13
|
-
|
|
14
|
-
usage() {
|
|
15
|
-
local stream=/dev/stderr
|
|
16
|
-
local code=1
|
|
17
|
-
if [[ "${1:-}" == "help" ]]; then stream=/dev/stdout; code=0; fi
|
|
18
|
-
cat >"$stream" << 'EOF'
|
|
19
|
-
usage: caproom [--limit <mb>] [--image <docker-image>] [--interval <sec>] -- <command> [args...]
|
|
20
|
-
caproom park <pid>
|
|
21
|
-
caproom wake <pid>
|
|
22
|
-
caproom status <pid>
|
|
23
|
-
caproom guard [--threshold <pct>] [--interval <sec>] <pid...>
|
|
24
|
-
caproom init <command> [--limit <mb>] [--grace <sec>]
|
|
25
|
-
|
|
26
|
-
--limit <mb> memory cap in MB (default: 4096)
|
|
27
|
-
--interval <sec> watchdog poll interval in seconds (default: 0.2)
|
|
28
|
-
--grace <sec> seconds to wait after SIGTERM before SIGKILL, watchdog
|
|
29
|
-
backend only (default: 5) — gives the process a chance
|
|
30
|
-
to flush/save state before a hard kill
|
|
31
|
-
--docker opt in to the Docker cgroup backend instead of the default
|
|
32
|
-
host-native watchdog (needs the daemon running)
|
|
33
|
-
--image <name> docker image used by the --docker backend
|
|
34
|
-
(default: node:22-slim)
|
|
35
|
-
--force-watchdog no-op; the host-native watchdog IS the default — kept so
|
|
36
|
-
existing scripts and 'init' snippets keep working
|
|
37
|
-
--no-intercept-tty bypass stdio interposition for TUI/pty apps (OSC 10/11,
|
|
38
|
-
DSR CPR, mouse DEC 1003) — exec directly and monitor via
|
|
39
|
-
detached 'caproom watch --auto-park' instead
|
|
40
|
-
--pty allocate a pty (forkpty) and forward bytes verbatim — full
|
|
41
|
-
terminal fidelity for TUI (requires python3 or script)
|
|
42
|
-
implies --no-intercept-tty + pty watchdog on the pty
|
|
43
|
-
tree; fallback to bypass if pty alloc fails
|
|
44
|
-
--no-pty disable pty allocation (use bypass or watchdog)
|
|
45
|
-
|
|
46
|
-
park / wake — freeze an idle process so the kernel CAN reclaim/compress its
|
|
47
|
-
memory without killing it. Honest semantics: SIGSTOP only makes the pages
|
|
48
|
-
eligible — the kernel reclaims them lazily, when real memory pressure hits.
|
|
49
|
-
Park a 2GB agent on a quiet machine and it may stay ~2GB resident for hours.
|
|
50
|
-
Park is insurance against OOM, not immediate RAM return; use it for processes
|
|
51
|
-
too expensive to restart. `caproom park <pid>` (SIGSTOP), `caproom wake
|
|
52
|
-
<pid>` (SIGCONT) brings it back instantly, same state, no restart needed.
|
|
53
|
-
Any agent can call these directly — they're just SIGSTOP/SIGCONT, no daemon,
|
|
54
|
-
no tracking file required.
|
|
55
|
-
|
|
56
|
-
guard — watch SYSTEM-WIDE free memory (not any single process) and auto-park
|
|
57
|
-
tracked pids (SIGSTOP) when free mem drops below --threshold percent, before
|
|
58
|
-
the kernel OOM-killer has to pick a victim. Use it when unrelated heavy
|
|
59
|
-
processes (e.g. a GPU inference job in one terminal, a TTS job in another)
|
|
60
|
-
share a box and neither is individually over any --limit cap. Foreground,
|
|
61
|
-
blocking; exits once all watched pids have exited. Does not auto-wake —
|
|
62
|
-
`caproom wake <pid>` when memory pressure clears:
|
|
63
|
-
|
|
64
|
-
caproom guard --threshold 10 --interval 5 -- 12345 12346
|
|
65
|
-
|
|
66
|
-
init <command> — print a shell snippet that auto-caps <command> on every
|
|
67
|
-
invocation, so a new terminal tab is capped with no extra typing. Append the
|
|
68
|
-
output to your shell rc (~/.zshrc, ~/.bashrc):
|
|
69
|
-
|
|
70
|
-
caproom init claude >> ~/.zshrc && source ~/.zshrc
|
|
71
|
-
|
|
72
|
-
env vars (override flags): CAPROOM_LIMIT_MB, CAPROOM_IMAGE, CAPROOM_INTERVAL, CAPROOM_GRACE, CAPROOM_BYPASS_TTY=1, CAPROOM_PTY=1
|
|
73
|
-
|
|
74
|
-
TUI note: 'caproom -- <TUI>' (opencode, vim, htop, ...) bypasses stdio when [ -t 0 ] && [ -t 1 ]
|
|
75
|
-
so terminal queries (OSC, mouse) stay on the pty. The cap is then advisory via a detached
|
|
76
|
-
'caproom watch --auto-park' monitor, or via '--pty' which allocates a real pty (forkpty)
|
|
77
|
-
and forwards bytes verbatim (python3 or script). Piped/batch 'caproom -- opencode run "task"'
|
|
78
|
-
stays fully capped. Use --no-intercept-tty / CAPROOM_BYPASS_TTY=1 to force bypass, --pty /
|
|
79
|
-
CAPROOM_PTY=1 to force pty.
|
|
80
|
-
|
|
81
|
-
examples:
|
|
82
|
-
caproom --limit 2048 -- npm run build
|
|
83
|
-
caproom --limit 512 -- claude --dangerously-skip-permissions -p "task"
|
|
84
|
-
caproom --limit 4096 --docker --image python:3.12-slim -- python train.py
|
|
85
|
-
caproom park 12345
|
|
86
|
-
caproom wake 12345
|
|
87
|
-
caproom top --json [--pid <pid>] [--park-min-mb <mb>]
|
|
88
|
-
caproom watch [--threshold-mb <mb>] [--auto-park] [--auto-wake-free-pct <pct>] [--json] <pid...>
|
|
89
|
-
caproom setup [--guard] [--threshold <pct>] [--uninstall]
|
|
90
|
-
caproom freemem
|
|
91
|
-
caproom init claude --limit 6144 --grace 10
|
|
92
|
-
EOF
|
|
93
|
-
exit "$code"
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
cmd_init() {
|
|
97
|
-
local target="${1:-}"
|
|
98
|
-
[[ -z "$target" ]] && { echo "usage: caproom init <command> [--limit <mb>] [--grace <sec>]" >&2; exit 1; }
|
|
99
|
-
shift
|
|
100
|
-
local limit=4096
|
|
101
|
-
local grace=5
|
|
102
|
-
while [[ $# -gt 0 ]]; do
|
|
103
|
-
case "$1" in
|
|
104
|
-
--limit) limit="$2"; shift 2 ;;
|
|
105
|
-
--grace) grace="$2"; shift 2 ;;
|
|
106
|
-
*) echo "caproom: unknown init flag $1" >&2; exit 1 ;;
|
|
107
|
-
esac
|
|
108
|
-
done
|
|
109
|
-
local fn="${target}_capped"
|
|
110
|
-
local is_tui=0
|
|
111
|
-
case "$target" in
|
|
112
|
-
opencode|claude|codex|vim|nvim|htop|less|fzf|nano|emacs) is_tui=1 ;;
|
|
113
|
-
esac
|
|
114
|
-
if [[ $is_tui -eq 1 ]]; then
|
|
115
|
-
echo "caproom: warning: '$target' is a TUI — interactive tty runs will bypass stdio and use a detached 'caproom watch --auto-park' monitor to preserve pty (OSC 10/11, DSR CPR, mouse DEC 1003). Batch 'caproom -- $target run' stays fully capped. Set CAPROOM_BYPASS_TTY=1 to always bypass, or CAPROOM_BYPASS_TTY=0 + re-init to force legacy pipe mode." >&2
|
|
116
|
-
cat << EOF
|
|
117
|
-
# caproom: auto-cap '$target' — added by 'caproom init $target'
|
|
118
|
-
# override per-shell: CAPROOM_LIMIT_MB=8192 $target ...
|
|
119
|
-
# TUI note: when [ -t 0 ] && [ -t 1 ], '$target' runs directly and a detached 'caproom watch --auto-park' monitors it
|
|
120
|
-
# (preserves OSC 10/11, DSR CPR, mouse DEC 1003). Batch/headless (piped) still goes through the watchdog.
|
|
121
|
-
# Set CAPROOM_BYPASS_TTY=1 to always bypass, 0 to force legacy mode.
|
|
122
|
-
$fn() {
|
|
123
|
-
if [ "\${CAPROOM_BYPASS_TTY:-0}" = "1" ]; then
|
|
124
|
-
command caproom --no-intercept-tty --limit "\${CAPROOM_LIMIT_MB:-$limit}" --grace "\${CAPROOM_GRACE:-$grace}" -- command $target "\$@"
|
|
125
|
-
return \$?
|
|
126
|
-
fi
|
|
127
|
-
if [ -t 0 ] && [ -t 1 ]; then
|
|
128
|
-
command $target "\$@" &
|
|
129
|
-
local _cap_pid=\$!
|
|
130
|
-
command caproom watch --threshold-mb "\${CAPROOM_LIMIT_MB:-$limit}" --interval "\${CAPROOM_INTERVAL:-0.2}" --auto-park --json "\$_cap_pid" >/dev/null 2>&1 &
|
|
131
|
-
wait "\$_cap_pid"
|
|
132
|
-
return \$?
|
|
133
|
-
fi
|
|
134
|
-
command caproom --limit "\${CAPROOM_LIMIT_MB:-$limit}" --force-watchdog --grace "\${CAPROOM_GRACE:-$grace}" -- command $target "\$@"
|
|
135
|
-
}
|
|
136
|
-
alias $target=$fn
|
|
137
|
-
EOF
|
|
138
|
-
else
|
|
139
|
-
cat << EOF
|
|
140
|
-
# caproom: auto-cap '$target' — added by 'caproom init $target'
|
|
141
|
-
# override per-shell: CAPROOM_LIMIT_MB=8192 $target ...
|
|
142
|
-
$fn() {
|
|
143
|
-
if [ "\${CAPROOM_BYPASS_TTY:-0}" = "1" ]; then
|
|
144
|
-
command caproom --no-intercept-tty --limit "\${CAPROOM_LIMIT_MB:-$limit}" --grace "\${CAPROOM_GRACE:-$grace}" -- command $target "\$@"
|
|
145
|
-
return \$?
|
|
146
|
-
fi
|
|
147
|
-
command caproom --limit "\${CAPROOM_LIMIT_MB:-$limit}" --force-watchdog --grace "\${CAPROOM_GRACE:-$grace}" -- command $target "\$@"
|
|
148
|
-
}
|
|
149
|
-
alias $target=$fn
|
|
150
|
-
EOF
|
|
151
|
-
fi
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
cmd_park() {
|
|
155
|
-
local pid="${1:-}"
|
|
156
|
-
[[ -z "$pid" ]] && { echo "usage: caproom park <pid>" >&2; exit 1; }
|
|
157
|
-
kill -0 "$pid" 2>/dev/null || { echo "caproom: no such pid $pid" >&2; exit 1; }
|
|
158
|
-
kill -STOP "$pid"
|
|
159
|
-
echo "caproom: pid $pid parked (SIGSTOP) — pages now eligible for kernel reclaim, but the kernel acts only under real memory pressure; on a quiet machine RSS may not drop. wake with: caproom wake $pid" >&2
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
cmd_wake() {
|
|
163
|
-
local pid="${1:-}"
|
|
164
|
-
[[ -z "$pid" ]] && { echo "usage: caproom wake <pid>" >&2; exit 1; }
|
|
165
|
-
kill -0 "$pid" 2>/dev/null || { echo "caproom: no such pid $pid" >&2; exit 1; }
|
|
166
|
-
kill -CONT "$pid"
|
|
167
|
-
echo "caproom: pid $pid woken (SIGCONT)" >&2
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
cmd_status() {
|
|
171
|
-
local pid="${1:-}"
|
|
172
|
-
[[ -z "$pid" ]] && { echo "usage: caproom status <pid>" >&2; exit 1; }
|
|
173
|
-
ps -o pid,stat,rss,etime,command -p "$pid" 2>/dev/null || { echo "caproom: no such pid $pid" >&2; exit 1; }
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
# ---- process-tree inventory (top / watch foundation) ----
|
|
177
|
-
|
|
178
|
-
# One ps pass filling the SNAP_* global arrays for the current user.
|
|
179
|
-
read_snapshot() {
|
|
180
|
-
SNAP_PID=(); SNAP_PPID=(); SNAP_RSS=(); SNAP_ST=(); SNAP_ET=(); SNAP_CMD=()
|
|
181
|
-
local myuid uid pid ppid rss st et cmd
|
|
182
|
-
myuid="$(id -u)"
|
|
183
|
-
while read -r uid pid ppid rss st et cmd; do
|
|
184
|
-
[[ "$uid" != "$myuid" ]] && continue
|
|
185
|
-
SNAP_PID+=("$pid"); SNAP_PPID+=("$ppid"); SNAP_RSS+=("${rss:-0}")
|
|
186
|
-
SNAP_ST+=("${st:-?}"); SNAP_ET+=("${et:-0}"); SNAP_CMD+=("${cmd:-}")
|
|
187
|
-
done < <(ps -eo uid=,pid=,ppid=,rss=,state=,etime=,command=)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
# Walk the subtree of $1 over the existing SNAP_* arrays, filling
|
|
191
|
-
# TREE_PIDS / TREE_RSS_KB. Does NOT re-read ps — cheap enough to call
|
|
192
|
-
# once per tree root from a single snapshot.
|
|
193
|
-
walk_tree() {
|
|
194
|
-
local root="$1" cur i j
|
|
195
|
-
local -a lpids=("${SNAP_PPID[@]}") lq=()
|
|
196
|
-
TREE_PIDS=(); TREE_RSS_KB=0
|
|
197
|
-
lq=("$root")
|
|
198
|
-
while [[ ${#lq[@]} -gt 0 ]]; do
|
|
199
|
-
cur="${lq[0]}"
|
|
200
|
-
if [[ ${#lq[@]} -gt 1 ]]; then lq=("${lq[@]:1}"); else lq=(); fi
|
|
201
|
-
for i in "${!SNAP_PID[@]}"; do
|
|
202
|
-
if [[ "${SNAP_PID[$i]}" == "$cur" ]]; then
|
|
203
|
-
TREE_PIDS+=("$cur")
|
|
204
|
-
TREE_RSS_KB=$(( TREE_RSS_KB + SNAP_RSS[$i] ))
|
|
205
|
-
for j in "${!lpids[@]}"; do
|
|
206
|
-
if [[ "${lpids[$j]}" == "$cur" ]]; then
|
|
207
|
-
lq+=("${SNAP_PID[$j]}")
|
|
208
|
-
lpids[$j]=""
|
|
209
|
-
fi
|
|
210
|
-
done
|
|
211
|
-
break
|
|
212
|
-
fi
|
|
213
|
-
done
|
|
214
|
-
done
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
json_escape() {
|
|
218
|
-
local s="$1"
|
|
219
|
-
s="${s//\\/\\\\}"
|
|
220
|
-
s="${s//\"/\\\"}"
|
|
221
|
-
s="${s//$'\n'/ }"
|
|
222
|
-
s="${s//$'\r'/ }"
|
|
223
|
-
s="${s//$'\t'/ }"
|
|
224
|
-
printf '%s' "$s"
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
cmd_top() {
|
|
228
|
-
local json=0 park_min_kb=$(( 512 * 1024 )) filter_pid=""
|
|
229
|
-
while [[ $# -gt 0 ]]; do
|
|
230
|
-
case "$1" in
|
|
231
|
-
--json) json=1; shift ;;
|
|
232
|
-
--park-min-mb) park_min_kb=$(( $2 * 1024 )); shift 2 ;;
|
|
233
|
-
--pid) filter_pid="$2"; shift 2 ;;
|
|
234
|
-
*) echo "caproom top: unknown option $1" >&2; exit 1 ;;
|
|
235
|
-
esac
|
|
236
|
-
done
|
|
237
|
-
|
|
238
|
-
read_snapshot
|
|
239
|
-
[[ ${#SNAP_PID[@]} -eq 0 ]] && { [[ $json -eq 1 ]] && printf '{"schema":1,"ts":%s,"limit_mb_default":%s,"processes":[]}\n' "$(date +%s)" "${CAPROOM_LIMIT_MB:-4096}"; return 0; }
|
|
240
|
-
|
|
241
|
-
# Tree roots: parents outside the visible set (or init-reparented).
|
|
242
|
-
local -a roots=()
|
|
243
|
-
local i j p found
|
|
244
|
-
if [[ -n "$filter_pid" ]]; then
|
|
245
|
-
found=""
|
|
246
|
-
for i in "${!SNAP_PID[@]}"; do
|
|
247
|
-
[[ "${SNAP_PID[$i]}" == "$filter_pid" ]] && { roots+=("$filter_pid"); found=1; break; }
|
|
248
|
-
done
|
|
249
|
-
if [[ -z "$found" ]]; then
|
|
250
|
-
echo "caproom: no such pid $filter_pid (or not owned by you)" >&2
|
|
251
|
-
exit 1
|
|
252
|
-
fi
|
|
253
|
-
else
|
|
254
|
-
for i in "${!SNAP_PID[@]}"; do
|
|
255
|
-
[[ "${SNAP_PID[$i]}" == "$$" ]] && continue # never report ourselves
|
|
256
|
-
p="${SNAP_PPID[$i]}"
|
|
257
|
-
if [[ "$p" == "1" ]]; then roots+=("${SNAP_PID[$i]}"); continue; fi
|
|
258
|
-
found=""
|
|
259
|
-
for j in "${!SNAP_PID[@]}"; do
|
|
260
|
-
if [[ "${SNAP_PID[$j]}" == "$p" ]]; then found=1; break; fi
|
|
261
|
-
done
|
|
262
|
-
[[ -z "$found" ]] && roots+=("${SNAP_PID[$i]}")
|
|
263
|
-
done
|
|
264
|
-
fi
|
|
265
|
-
|
|
266
|
-
# Walk each root once; keep results in parallel arrays, then sort by
|
|
267
|
-
# tree RSS descending via a sortable temp stream.
|
|
268
|
-
local -a r_pid=() r_trss=() r_tpids=() r_st=() r_et=() r_cmd=() order=()
|
|
269
|
-
for p in "${roots[@]}"; do
|
|
270
|
-
walk_tree "$p"
|
|
271
|
-
local tjoin=""
|
|
272
|
-
[[ ${#TREE_PIDS[@]} -gt 0 ]] && tjoin="$(printf '%s,' "${TREE_PIDS[@]}")" && tjoin="${tjoin%,}"
|
|
273
|
-
for i in "${!SNAP_PID[@]}"; do
|
|
274
|
-
if [[ "${SNAP_PID[$i]}" == "$p" ]]; then
|
|
275
|
-
r_pid+=("$p"); r_trss+=("$TREE_RSS_KB")
|
|
276
|
-
r_tpids+=("$tjoin")
|
|
277
|
-
r_st+=("${SNAP_ST[$i]}"); r_et+=("${SNAP_ET[$i]}")
|
|
278
|
-
r_cmd+=("${SNAP_CMD[$i]}")
|
|
279
|
-
order+=("$(printf '%010d %d\n' "$TREE_RSS_KB" $(( ${#r_pid[@]} - 1 )))")
|
|
280
|
-
break
|
|
281
|
-
fi
|
|
282
|
-
done
|
|
283
|
-
done
|
|
284
|
-
|
|
285
|
-
if [[ $json -eq 1 ]]; then
|
|
286
|
-
local ts out='[' first=1 idx state cand reason kb
|
|
287
|
-
ts="$(date +%s)"
|
|
288
|
-
local -a sorted
|
|
289
|
-
sorted=($(printf '%s\n' "${order[@]:-}" | sort -rn | awk '{print $2}'))
|
|
290
|
-
for idx in "${sorted[@]:-}"; do
|
|
291
|
-
[[ -z "$idx" ]] && continue
|
|
292
|
-
kb="${r_trss[$idx]}"
|
|
293
|
-
local st0="${r_st[$idx]:0:1}"
|
|
294
|
-
case "$st0" in
|
|
295
|
-
T) state="parked" ;;
|
|
296
|
-
Z) state="zombie" ;;
|
|
297
|
-
*) state="running" ;;
|
|
298
|
-
esac
|
|
299
|
-
cand=false; reason=""
|
|
300
|
-
if [[ "$state" == "running" && ( "$st0" == "S" || "$st0" == "I" ) ]] && [[ "$kb" -ge "$park_min_kb" ]]; then
|
|
301
|
-
cand=true
|
|
302
|
-
reason="root sleeping + tree_rss ${kb}KB >= ${park_min_kb}KB park threshold"
|
|
303
|
-
fi
|
|
304
|
-
[[ $first -eq 1 ]] || out+=','
|
|
305
|
-
first=0
|
|
306
|
-
out+="{\"pid\":${r_pid[$idx]},\"cmd\":\"$(json_escape "${r_cmd[$idx]}")\",\"tree_rss_kb\":${kb},\"tree_pids\":[${r_tpids[$idx]}],\"state\":\"$state\",\"park_candidate\":$cand,\"reason\":\"$(json_escape "$reason")\"}"
|
|
307
|
-
done
|
|
308
|
-
out+=']'
|
|
309
|
-
printf '{"schema":1,"ts":%s,"limit_mb_default":%s,"processes":%s}\n' "$ts" "${CAPROOM_LIMIT_MB:-4096}" "$out"
|
|
310
|
-
else
|
|
311
|
-
local idx mb
|
|
312
|
-
printf '%-8s %10s %-8s %-9s %s\n' PID TREE_MB STATE ETIME COMMAND
|
|
313
|
-
local -a sorted
|
|
314
|
-
sorted=($(printf '%s\n' "${order[@]:-}" | sort -rn | awk '{print $2}'))
|
|
315
|
-
for idx in "${sorted[@]:-}"; do
|
|
316
|
-
[[ -z "$idx" ]] && continue
|
|
317
|
-
mb=$(( r_trss[idx] / 1024 ))
|
|
318
|
-
case "${r_st[$idx]}" in T) printf '%-8s %10s %-8s %-9s %s\n' "${r_pid[$idx]}" "$mb" PARKED "${r_et[$idx]}" "${r_cmd[$idx]:0:60}" ;; *) printf '%-8s %10s %-8s %-9s %s\n' "${r_pid[$idx]}" "$mb" "-" "${r_et[$idx]}" "${r_cmd[$idx]:0:60}" ;; esac
|
|
319
|
-
done
|
|
320
|
-
fi
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
cmd_watch() {
|
|
324
|
-
# Daemon: watch explicit pids, emit events when their TREE crosses a
|
|
325
|
-
# RSS threshold. --auto-park freezes breaching trees (SIGSTOP every pid
|
|
326
|
-
# in the snapshot) — only for pids passed explicitly, since stopping a
|
|
327
|
-
# mid-write process risks corruption; naming the pid IS the opt-in.
|
|
328
|
-
# --auto-wake-free-pct N undoes its own parks when free memory recovers.
|
|
329
|
-
local threshold_kb=$(( 2048 * 1024 )) interval=5 json=0 auto=0 wake_pct=""
|
|
330
|
-
local -a pids=()
|
|
331
|
-
while [[ $# -gt 0 ]]; do
|
|
332
|
-
case "$1" in
|
|
333
|
-
--threshold-mb) threshold_kb=$(( $2 * 1024 )); shift 2 ;;
|
|
334
|
-
--interval) interval="$2"; shift 2 ;;
|
|
335
|
-
--auto-park) auto=1; shift ;;
|
|
336
|
-
--auto-wake-free-pct) wake_pct="$2"; shift 2 ;;
|
|
337
|
-
--json) json=1; shift ;;
|
|
338
|
-
*) pids+=("$1"); shift ;;
|
|
339
|
-
esac
|
|
340
|
-
done
|
|
341
|
-
[[ ${#pids[@]} -eq 0 ]] && { echo "usage: caproom watch [--threshold-mb <mb>] [--interval <sec>] [--auto-park] [--auto-wake-free-pct <pct>] [--json] <pid...>" >&2; exit 1; }
|
|
342
|
-
|
|
343
|
-
local -a parked_by_us=() breaching=()
|
|
344
|
-
local mode
|
|
345
|
-
mode="watch"
|
|
346
|
-
[[ $auto -eq 1 ]] && mode="auto-park"
|
|
347
|
-
if [[ $json -eq 1 ]]; then
|
|
348
|
-
printf '{"schema":1,"event":"started","ts":%s,"mode":"%s","threshold_kb":%s,"pids":[%s]}\n' "$(date +%s)" "$mode" "$threshold_kb" "$(printf '%s,' "${pids[@]}" | sed 's/,$//')"
|
|
349
|
-
else
|
|
350
|
-
echo "caproom: watching ${#pids[@]} pid(s), tree threshold $(( threshold_kb / 1024 ))MB, poll ${interval}s$([[ $auto -eq 1 ]] && echo ', AUTO-PARK ARMED')$([[ -n "$wake_pct" ]] && echo ", auto-wake at >=${wake_pct}% free")" >&2
|
|
351
|
-
fi
|
|
352
|
-
|
|
353
|
-
while :; do
|
|
354
|
-
local -a alive=()
|
|
355
|
-
local pid i st0
|
|
356
|
-
for pid in "${pids[@]}"; do
|
|
357
|
-
kill -0 "$pid" 2>/dev/null && alive+=("$pid")
|
|
358
|
-
done
|
|
359
|
-
if [[ ${#alive[@]} -eq 0 ]]; then
|
|
360
|
-
[[ $json -eq 1 ]] && printf '{"schema":1,"event":"all-exited","ts":%s}\n' "$(date +%s)"
|
|
361
|
-
echo "caproom: watch: all watched pids exited" >&2
|
|
362
|
-
exit 0
|
|
363
|
-
fi
|
|
364
|
-
pids=("${alive[@]}")
|
|
365
|
-
|
|
366
|
-
# Auto-wake first: restore what WE parked once pressure clears.
|
|
367
|
-
if [[ -n "$wake_pct" && ${#parked_by_us[@]} -gt 0 ]]; then
|
|
368
|
-
local pct
|
|
369
|
-
pct=$(mem_free_pct)
|
|
370
|
-
if [[ "$pct" -ge "$wake_pct" ]]; then
|
|
371
|
-
local -a woke=()
|
|
372
|
-
for pid in "${parked_by_us[@]}"; do
|
|
373
|
-
if kill -0 "$pid" 2>/dev/null && kill -CONT "$pid" 2>/dev/null; then
|
|
374
|
-
woke+=("$pid")
|
|
375
|
-
if [[ $json -eq 1 ]]; then
|
|
376
|
-
printf '{"schema":1,"event":"woke","ts":%s,"pid":%s,"free_pct":%s}\n' "$(date +%s)" "$pid" "$pct"
|
|
377
|
-
else
|
|
378
|
-
echo "caproom: watch: free mem ${pct}% >= ${wake_pct}% — waking pid $pid" >&2
|
|
379
|
-
fi
|
|
380
|
-
fi
|
|
381
|
-
done
|
|
382
|
-
parked_by_us=()
|
|
383
|
-
fi
|
|
384
|
-
fi
|
|
385
|
-
|
|
386
|
-
read_snapshot
|
|
387
|
-
for pid in "${pids[@]}"; do
|
|
388
|
-
local found=""
|
|
389
|
-
for i in "${!SNAP_PID[@]}"; do
|
|
390
|
-
[[ "${SNAP_PID[$i]}" == "$pid" ]] && { found="$i"; break; }
|
|
391
|
-
done
|
|
392
|
-
[[ -z "$found" ]] && continue
|
|
393
|
-
st0="${SNAP_ST[$found]:0:1}"
|
|
394
|
-
[[ "$st0" == "T" || "$st0" == "Z" ]] && continue # already parked/dead
|
|
395
|
-
walk_tree "$pid"
|
|
396
|
-
if [[ "$TREE_RSS_KB" -ge "$threshold_kb" ]]; then
|
|
397
|
-
local is_breaching=""
|
|
398
|
-
local ev1
|
|
399
|
-
for ev1 in ${breaching[@]+"${breaching[@]}"}; do [[ "$ev1" == "$pid" ]] && is_breaching=1 && break; done
|
|
400
|
-
if [[ -n "$is_breaching" ]]; then continue; fi
|
|
401
|
-
breaching+=("$pid")
|
|
402
|
-
if [[ $auto -eq 1 ]]; then
|
|
403
|
-
local tp stopped=0
|
|
404
|
-
for tp in "${TREE_PIDS[@]}"; do
|
|
405
|
-
kill -STOP "$tp" 2>/dev/null && { parked_by_us+=("$tp"); stopped=$(( stopped + 1 )); }
|
|
406
|
-
done
|
|
407
|
-
if [[ $json -eq 1 ]]; then
|
|
408
|
-
printf '{"schema":1,"event":"parked","ts":%s,"pid":%s,"tree_rss_kb":%s,"tree_pids":[%s],"stopped":%s}\n' "$(date +%s)" "$pid" "$TREE_RSS_KB" "$(printf '%s,' "${TREE_PIDS[@]}" | sed 's/,$//')" "$stopped"
|
|
409
|
-
else
|
|
410
|
-
echo "caproom: watch: tree of pid $pid hit $(( TREE_RSS_KB / 1024 ))MB (>= $(( threshold_kb / 1024 ))MB) — PARKED tree (${stopped} pids, wake: caproom wake $pid)" >&2
|
|
411
|
-
fi
|
|
412
|
-
else
|
|
413
|
-
if [[ $json -eq 1 ]]; then
|
|
414
|
-
printf '{"schema":1,"event":"breach","ts":%s,"pid":%s,"tree_rss_kb":%s}\n' "$(date +%s)" "$pid" "$TREE_RSS_KB"
|
|
415
|
-
else
|
|
416
|
-
echo "caproom: watch: tree of pid $pid hit $(( TREE_RSS_KB / 1024 ))MB (>= $(( threshold_kb / 1024 ))MB) — no --auto-park, reporting only" >&2
|
|
417
|
-
fi
|
|
418
|
-
fi
|
|
419
|
-
else
|
|
420
|
-
local -a keep=()
|
|
421
|
-
local was_breaching=0 ev2
|
|
422
|
-
for ev2 in ${breaching[@]+"${breaching[@]}"}; do
|
|
423
|
-
if [[ "$ev2" == "$pid" ]]; then was_breaching=1; else keep+=("$ev2"); fi
|
|
424
|
-
done
|
|
425
|
-
if [[ $was_breaching -eq 1 ]]; then
|
|
426
|
-
breaching=()
|
|
427
|
-
local k2
|
|
428
|
-
for k2 in ${keep[@]+"${keep[@]}"}; do breaching+=("$k2"); done
|
|
429
|
-
if [[ $json -eq 1 ]]; then
|
|
430
|
-
printf '{"schema":1,"event":"recovered","ts":%s,"pid":%s,"tree_rss_kb":%s}\n' "$(date +%s)" "$pid" "$TREE_RSS_KB"
|
|
431
|
-
else
|
|
432
|
-
echo "caproom: watch: pid $pid back under threshold ($(( TREE_RSS_KB / 1024 ))MB)" >&2
|
|
433
|
-
fi
|
|
434
|
-
fi
|
|
435
|
-
fi
|
|
436
|
-
done
|
|
437
|
-
sleep "$interval"
|
|
438
|
-
done
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
mem_free_pct() { if [[ "$(uname)" == "Darwin" ]]; then
|
|
442
|
-
local page_size free inactive total_bytes avail_bytes
|
|
443
|
-
page_size=$(vm_stat | awk '/page size of/ {print $8}')
|
|
444
|
-
free=$(vm_stat | awk '/Pages free/ {gsub("\\.","",$3); print $3}')
|
|
445
|
-
inactive=$(vm_stat | awk '/Pages inactive/ {gsub("\\.","",$3); print $3}')
|
|
446
|
-
total_bytes=$(sysctl -n hw.memsize)
|
|
447
|
-
avail_bytes=$(( (free + inactive) * page_size ))
|
|
448
|
-
echo $(( avail_bytes * 100 / total_bytes ))
|
|
449
|
-
else
|
|
450
|
-
local avail_kb total_kb
|
|
451
|
-
avail_kb=$(awk '/MemAvailable/ {print $2}' /proc/meminfo)
|
|
452
|
-
total_kb=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
|
|
453
|
-
echo $(( avail_kb * 100 / total_kb ))
|
|
454
|
-
fi
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
cmd_guard() {
|
|
458
|
-
local threshold=10
|
|
459
|
-
local interval=5
|
|
460
|
-
local pids=()
|
|
461
|
-
while [[ $# -gt 0 ]]; do
|
|
462
|
-
case "$1" in
|
|
463
|
-
--threshold) threshold="$2"; shift 2 ;;
|
|
464
|
-
--interval) interval="$2"; shift 2 ;;
|
|
465
|
-
--) shift ;;
|
|
466
|
-
*) pids+=("$1"); shift ;;
|
|
467
|
-
esac
|
|
468
|
-
done
|
|
469
|
-
[[ ${#pids[@]} -eq 0 ]] && { echo "usage: caproom guard [--threshold <pct>] [--interval <sec>] <pid...>" >&2; exit 1; }
|
|
470
|
-
echo "caproom: guarding ${#pids[@]} pid(s), park when system free mem < ${threshold}% (poll ${interval}s)" >&2
|
|
471
|
-
local parked=()
|
|
472
|
-
while :; do
|
|
473
|
-
local alive=()
|
|
474
|
-
local pid
|
|
475
|
-
for pid in "${pids[@]}"; do
|
|
476
|
-
kill -0 "$pid" 2>/dev/null && alive+=("$pid")
|
|
477
|
-
done
|
|
478
|
-
if [[ ${#alive[@]} -eq 0 ]]; then
|
|
479
|
-
echo "caproom: guard: all watched pids exited" >&2
|
|
480
|
-
exit 0
|
|
481
|
-
fi
|
|
482
|
-
pids=("${alive[@]}")
|
|
483
|
-
local pct
|
|
484
|
-
pct=$(mem_free_pct)
|
|
485
|
-
if [[ "$pct" -lt "$threshold" ]]; then
|
|
486
|
-
for pid in "${pids[@]}"; do
|
|
487
|
-
if [[ ! " ${parked[*]:-} " == *" $pid "* ]]; then
|
|
488
|
-
echo "caproom: system free mem ${pct}% < ${threshold}% threshold — about to blow, parking pid $pid (SIGSTOP)" >&2
|
|
489
|
-
kill -STOP "$pid" 2>/dev/null && parked+=("$pid")
|
|
490
|
-
fi
|
|
491
|
-
done
|
|
492
|
-
fi
|
|
493
|
-
sleep "$interval"
|
|
494
|
-
done
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
# ---- terminal bind: setup / unbind ------------------------------------
|
|
498
|
-
# Binds headroom management to every interactive shell in ANY terminal
|
|
499
|
-
# (Terminal.app, iTerm2, Ghostty, ...) by writing ONE integration file
|
|
500
|
-
# per shell under ~/.caproom/ and marker-patching the rc files. Idempotent,
|
|
501
|
-
# backed up, reversible with `caproom unbind`. Never runs automatically:
|
|
502
|
-
# npm postinstall only prints a hint.
|
|
503
|
-
|
|
504
|
-
CAPROOM_DIR="${CAPROOM_DIR:-$HOME/.caproom}"
|
|
505
|
-
|
|
506
|
-
setup_shell_sh() {
|
|
507
|
-
cat > "$CAPROOM_DIR/shell.sh" << 'EOF'
|
|
508
|
-
# caproom shell integration — regenerated by `caproom setup`; edits here
|
|
509
|
-
# are overwritten. Source of truth: bin/caproom (setup_shell_sh).
|
|
510
|
-
|
|
511
|
-
caproom_freemem_pct() { command caproom freemem 2>/dev/null; }
|
|
512
|
-
|
|
513
|
-
caproom_headroom_check() {
|
|
514
|
-
local pct last now
|
|
515
|
-
pct=$(caproom_freemem_pct) || return 0
|
|
516
|
-
[ -n "$pct" ] || return 0
|
|
517
|
-
[ "$pct" -lt "${CAPROOM_HEADROOM_WARN:-20}" ] || return 0
|
|
518
|
-
last=$(cat "${TMPDIR:-/tmp}/caproom-headroom-last" 2>/dev/null || echo 0)
|
|
519
|
-
now=$(date +%s)
|
|
520
|
-
[ $(( now - ${last:-0} )) -ge 60 ] || return 0
|
|
521
|
-
echo "$now" > "${TMPDIR:-/tmp}/caproom-headroom-last" 2>/dev/null
|
|
522
|
-
echo "caproom: headroom low (${pct}% free) — check 'caproom top' before launching heavy work"
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
if [ -n "$ZSH_VERSION" ]; then
|
|
526
|
-
autoload -Uz add-zsh-hook
|
|
527
|
-
add-zsh-hook precmd caproom_headroom_check
|
|
528
|
-
elif [ -n "$BASH_VERSION" ]; then
|
|
529
|
-
case ";$PROMPT_COMMAND;" in
|
|
530
|
-
*caproom_headroom_check*) ;;
|
|
531
|
-
*) PROMPT_COMMAND="caproom_headroom_check${PROMPT_COMMAND:+;$PROMPT_COMMAND}" ;;
|
|
532
|
-
esac
|
|
533
|
-
fi
|
|
534
|
-
|
|
535
|
-
# Opt-in auto-wrap: CAPROOM_AUTO_WRAP="claude,codex,opencode" gives every
|
|
536
|
-
# listed command a <name>_capped twin running under $CAPROOM_LIMIT_MB.
|
|
537
|
-
# The bare name is aliased ONLY with CAPROOM_AUTO_ALIAS=1 — never hijack a
|
|
538
|
-
# command the user did not consent to wrap.
|
|
539
|
-
if [ -n "${CAPROOM_AUTO_WRAP:-}" ]; then
|
|
540
|
-
for _cr_cmd in $(echo "${CAPROOM_AUTO_WRAP}" | tr ',' ' '); do
|
|
541
|
-
_cr_fn="$(printf '%s' "$_cr_cmd" | sed 's/[-.]/_/g')_capped"
|
|
542
|
-
eval "$(printf "%s() { command caproom --limit \"\${CAPROOM_LIMIT_MB:-4096}\" --grace \"\${CAPROOM_GRACE:-5}\" -- '%s' \"\$@\"; }" "$_cr_fn" "$(printf '%s' "$_cr_cmd" | sed "s/'/'\\\\''/g")")"
|
|
543
|
-
if [ "${CAPROOM_AUTO_ALIAS:-0}" = "1" ]; then
|
|
544
|
-
alias "$_cr_cmd=$_cr_fn"
|
|
545
|
-
fi
|
|
546
|
-
done
|
|
547
|
-
unset _cr_cmd _cr_fn
|
|
548
|
-
fi
|
|
549
|
-
EOF
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
setup_shell_fish() {
|
|
553
|
-
cat > "$CAPROOM_DIR/shell.fish" << 'EOF'
|
|
554
|
-
# caproom fish integration — regenerated by `caproom setup`.
|
|
555
|
-
function __caproom_freemem
|
|
556
|
-
command caproom freemem 2>/dev/null
|
|
557
|
-
end
|
|
558
|
-
|
|
559
|
-
function __caproom_headroom_check --on-event fish_prompt
|
|
560
|
-
set -l pct (__caproom_freemem)
|
|
561
|
-
or return
|
|
562
|
-
test -n "$pct"; or return
|
|
563
|
-
set -l warn 20
|
|
564
|
-
if set -q CAPROOM_HEADROOM_WARN
|
|
565
|
-
set warn $CAPROOM_HEADROOM_WARN
|
|
566
|
-
end
|
|
567
|
-
if test "$pct" -lt "$warn"
|
|
568
|
-
set -l stamp /tmp/caproom-headroom-last
|
|
569
|
-
set -l now (date +%s)
|
|
570
|
-
set -l last 0
|
|
571
|
-
if test -f $stamp
|
|
572
|
-
set last (cat $stamp)
|
|
573
|
-
end
|
|
574
|
-
if test (math "$now - $last") -ge 60
|
|
575
|
-
echo $now > $stamp
|
|
576
|
-
echo "caproom: headroom low ($pct% free) — check 'caproom top' before launching heavy work"
|
|
577
|
-
end
|
|
578
|
-
end
|
|
579
|
-
end
|
|
580
|
-
EOF
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
setup_shell_ps1() {
|
|
584
|
-
cat > "$CAPROOM_DIR/shell.ps1" << 'EOF'
|
|
585
|
-
# caproom PowerShell integration — regenerated by `caproom setup` (Windows).
|
|
586
|
-
function global:caproom_freemem_pct {
|
|
587
|
-
$os = Get-CimInstance Win32_OperatingSystem
|
|
588
|
-
[int]($os.FreePhysicalMemory * 100 / $os.TotalVisibleMemorySize)
|
|
589
|
-
}
|
|
590
|
-
$global:__caproomLastWarn = 0
|
|
591
|
-
function global:prompt {
|
|
592
|
-
$pct = caproom_freemem_pct
|
|
593
|
-
$now = [DateTimeOffset]::Now.ToUnixTimeSeconds()
|
|
594
|
-
if ($pct -lt (${CAPROOM_HEADROOM_WARN:-20}) -and ($now - $script:__caproomLastWarn) -ge 60) {
|
|
595
|
-
$script:__caproomLastWarn = $now
|
|
596
|
-
Write-Host "caproom: headroom low ($pct% free) — check 'caproom top' before launching heavy work" -ForegroundColor Yellow
|
|
597
|
-
}
|
|
598
|
-
"PS $($executionContext.SessionState.Path.CurrentLocation)> "
|
|
599
|
-
}
|
|
600
|
-
EOF
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
rc_targets() {
|
|
604
|
-
# Prints "path<TAB>required" pairs for every rc we manage. Only rcs that
|
|
605
|
-
# already exist are patched, EXCEPT the login shell's own rc which is
|
|
606
|
-
# created if missing — never invent configs for shells you don't use.
|
|
607
|
-
local zshrc="${ZDOTDIR:-$HOME/.zshrc}"
|
|
608
|
-
printf '%s\t%s\n' "$zshrc" "shell"
|
|
609
|
-
[[ -f "$HOME/.bashrc" ]] && printf '%s\t%s\n' "$HOME/.bashrc" "optional"
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
patch_rc_file() {
|
|
613
|
-
local rc="$1"
|
|
614
|
-
[[ -f "$rc" ]] || touch "$rc"
|
|
615
|
-
grep -q "# >>> caproom >>>" "$rc" && return 0
|
|
616
|
-
cp "$rc" "$rc.caproom.bak.$(date +%Y%m%d%H%M%S)"
|
|
617
|
-
{
|
|
618
|
-
echo ""
|
|
619
|
-
echo "# >>> caproom >>>"
|
|
620
|
-
echo '[ -f ~/.caproom/shell.sh ] && source ~/.caproom/shell.sh'
|
|
621
|
-
echo "# <<< caproom <<<"
|
|
622
|
-
} >> "$rc"
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
patch_rc_file_fish() {
|
|
626
|
-
local rc="$HOME/.config/fish/config.fish"
|
|
627
|
-
mkdir -p "$(dirname "$rc")" 2>/dev/null
|
|
628
|
-
[[ -f "$rc" ]] || return 0 # don't invent fish config unless it exists
|
|
629
|
-
grep -q "# caproom (fish)" "$rc" && return 0
|
|
630
|
-
cp "$rc" "$rc.caproom.bak.$(date +%Y%m%d%H%M%S)"
|
|
631
|
-
{
|
|
632
|
-
echo ""
|
|
633
|
-
echo "# caproom (fish)"
|
|
634
|
-
echo '[ -f ~/.caproom/shell.fish ] && source ~/.caproom/shell.fish'
|
|
635
|
-
} >> "$rc"
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
install_guard_daemon() {
|
|
639
|
-
local threshold="$1"
|
|
640
|
-
local bin_path
|
|
641
|
-
bin_path=$(command -v caproom || true)
|
|
642
|
-
[[ -n "$bin_path" ]] || { echo "caproom setup: cannot resolve caproom binary for daemon" >&2; return 1; }
|
|
643
|
-
if [[ "$(uname)" == "Darwin" ]]; then
|
|
644
|
-
local plist="$HOME/Library/LaunchAgents/com.caproom.guard.plist"
|
|
645
|
-
mkdir -p "$HOME/Library/LaunchAgents"
|
|
646
|
-
cat > "$plist" << EOF
|
|
647
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
648
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
649
|
-
<plist version="1.0"><dict>
|
|
650
|
-
<key>Label</key><string>com.caproom.guard</string>
|
|
651
|
-
<key>ProgramArguments</key><array>
|
|
652
|
-
<string>/usr/bin/env</string><string>bash</string><string>$bin_path</string>
|
|
653
|
-
<string>guard</string><string>--threshold</string><string>$threshold</string><string>--interval</string><string>15</string>
|
|
654
|
-
</array>
|
|
655
|
-
<key>RunAtLoad</key><true/>
|
|
656
|
-
<key>KeepAlive</key><true/>
|
|
657
|
-
</dict></plist>
|
|
658
|
-
EOF
|
|
659
|
-
echo "caproom setup: guard daemon installed -> $plist"
|
|
660
|
-
echo " load now: launchctl load $plist"
|
|
661
|
-
echo " unload: launchctl unload $plist"
|
|
662
|
-
else
|
|
663
|
-
local unit="$HOME/.config/systemd/user/caproom-guard.service"
|
|
664
|
-
mkdir -p "$HOME/.config/systemd/user"
|
|
665
|
-
cat > "$unit" << EOF
|
|
666
|
-
[Unit]
|
|
667
|
-
Description=caproom memory guard
|
|
668
|
-
|
|
669
|
-
[Service]
|
|
670
|
-
ExecStart=/usr/bin/env bash $bin_path guard --threshold $threshold --interval 15
|
|
671
|
-
Restart=on-failure
|
|
672
|
-
|
|
673
|
-
[Install]
|
|
674
|
-
WantedBy=default.target
|
|
675
|
-
EOF
|
|
676
|
-
echo "caproom setup: guard service installed -> $unit"
|
|
677
|
-
echo " start now: systemctl --user enable --now caproom-guard.service"
|
|
678
|
-
echo " stop: systemctl --user disable --now caproom-guard.service"
|
|
679
|
-
fi
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
cmd_setup() {
|
|
683
|
-
local guard="" threshold=10 do_unbind=0
|
|
684
|
-
while [[ $# -gt 0 ]]; do
|
|
685
|
-
case "$1" in
|
|
686
|
-
--guard) guard="$threshold"; shift ;;
|
|
687
|
-
--threshold) threshold="$2"; shift 2 ;;
|
|
688
|
-
--uninstall|--unbind) do_unbind=1; shift ;;
|
|
689
|
-
*) echo "caproom setup: unknown option $1" >&2; exit 1 ;;
|
|
690
|
-
esac
|
|
691
|
-
done
|
|
692
|
-
|
|
693
|
-
if [[ "$do_unbind" -eq 1 ]]; then
|
|
694
|
-
local rc removed=0
|
|
695
|
-
while IFS=$'\t' read -r rc _req; do
|
|
696
|
-
[[ -f "$rc" ]] || continue
|
|
697
|
-
if grep -q "# >>> caproom >>>" "$rc"; then
|
|
698
|
-
awk '/^# >>> caproom >>>$/{skip=1;next} /^# <<< caproom <<<$/{skip=0;next} !skip' "$rc" > "$rc.cr.tmp" && mv "$rc.cr.tmp" "$rc"
|
|
699
|
-
removed=$(( removed + 1 ))
|
|
700
|
-
fi
|
|
701
|
-
done < <(rc_targets)
|
|
702
|
-
if grep -q "# caproom (fish)" "$HOME/.config/fish/config.fish" 2>/dev/null; then
|
|
703
|
-
awk '/^# caproom \(fish\)$/{getline; skip=1; next} !skip' "$HOME/.config/fish/config.fish" > /tmp/cr-fish.tmp 2>/dev/null \
|
|
704
|
-
&& mv /tmp/cr-fish.tmp "$HOME/.config/fish/config.fish"
|
|
705
|
-
removed=$(( removed + 1 ))
|
|
706
|
-
fi
|
|
707
|
-
echo "caproom unbind: markers removed from $removed file(s); backups kept as *.caproom.bak.*"
|
|
708
|
-
echo " integration files left in $CAPROOM_DIR (rm -rf to purge)"
|
|
709
|
-
return 0
|
|
710
|
-
fi
|
|
711
|
-
|
|
712
|
-
mkdir -p "$CAPROOM_DIR"
|
|
713
|
-
setup_shell_sh
|
|
714
|
-
setup_shell_fish
|
|
715
|
-
[[ "$(uname)" != "Darwin" ]] || setup_shell_ps1
|
|
716
|
-
|
|
717
|
-
local rc req patched=0
|
|
718
|
-
while IFS=$'\t' read -r rc req; do
|
|
719
|
-
if patch_rc_file "$rc"; then patched=$(( patched + 1 )); fi
|
|
720
|
-
done < <(rc_targets)
|
|
721
|
-
patch_rc_file_fish
|
|
722
|
-
|
|
723
|
-
echo "caproom setup: bound to your shells via $CAPROOM_DIR/"
|
|
724
|
-
echo " shell.sh zsh + bash (headroom warning on every prompt, opt-in auto-wrap)"
|
|
725
|
-
echo " shell.fish fish equivalent"
|
|
726
|
-
echo " patched rc files: $patched (backups alongside as *.caproom.bak.*)"
|
|
727
|
-
echo ""
|
|
728
|
-
echo "auto-wrap usage:"
|
|
729
|
-
echo ' export CAPROOM_AUTO_WRAP="claude,codex,opencode" # creates <cmd>_capped twins'
|
|
730
|
-
echo ' export CAPROOM_AUTO_ALIAS=1 # ALSO shadow bare names (explicit consent)'
|
|
731
|
-
echo " export CAPROOM_LIMIT_MB=8192 # per-shell budget"
|
|
732
|
-
echo ""
|
|
733
|
-
echo "new terminals pick this up immediately; current ones: source ~/.caproom/shell.sh"
|
|
734
|
-
|
|
735
|
-
if [[ -n "$guard" ]]; then
|
|
736
|
-
echo ""
|
|
737
|
-
install_guard_daemon "$threshold"
|
|
738
|
-
fi
|
|
739
|
-
}
|
|
740
|
-
case "${1:-}" in
|
|
741
|
-
park) shift; cmd_park "$@"; exit 0 ;;
|
|
742
|
-
wake) shift; cmd_wake "$@"; exit 0 ;;
|
|
743
|
-
freemem) mem_free_pct; exit 0 ;;
|
|
744
|
-
setup|bind|unbind) shift; cmd_setup "$@"; exit 0 ;;
|
|
745
|
-
status) shift; cmd_status "$@"; exit 0 ;;
|
|
746
|
-
top) shift; cmd_top "$@"; exit 0 ;;
|
|
747
|
-
watch) shift; cmd_watch "$@"; exit 0 ;;
|
|
748
|
-
guard) shift; cmd_guard "$@"; exit 0 ;;
|
|
749
|
-
init) shift; cmd_init "$@"; exit 0 ;;
|
|
750
|
-
help|-h|--help) usage help ;;
|
|
751
|
-
esac
|
|
752
|
-
|
|
753
|
-
LIMIT_MB="${CAPROOM_LIMIT_MB:-4096}"
|
|
754
|
-
IMAGE="${CAPROOM_IMAGE:-node:22-slim}"
|
|
755
|
-
INTERVAL="${CAPROOM_INTERVAL:-0.2}"
|
|
756
|
-
GRACE="${CAPROOM_GRACE:-5}"
|
|
757
|
-
USE_DOCKER=0
|
|
758
|
-
BYPASS_TTY=0
|
|
759
|
-
USE_PTY=0
|
|
760
|
-
[[ "${CAPROOM_BYPASS_TTY:-0}" == "1" ]] && BYPASS_TTY=1
|
|
761
|
-
[[ "${CAPROOM_PTY:-0}" == "1" ]] && USE_PTY=1
|
|
762
|
-
|
|
763
|
-
while [[ $# -gt 0 ]]; do
|
|
764
|
-
case "$1" in
|
|
765
|
-
--limit) LIMIT_MB="$2"; shift 2 ;;
|
|
766
|
-
--image) IMAGE="$2"; shift 2 ;;
|
|
767
|
-
--interval) INTERVAL="$2"; shift 2 ;;
|
|
768
|
-
--grace) GRACE="$2"; shift 2 ;;
|
|
769
|
-
--docker) USE_DOCKER=1; shift ;;
|
|
770
|
-
# legacy no-op: the watchdog IS the default now; accepted so old
|
|
771
|
-
# scripts and init snippets keep working
|
|
772
|
-
--force-watchdog) shift ;;
|
|
773
|
-
--no-intercept-tty) BYPASS_TTY=1; shift ;;
|
|
774
|
-
--pty) USE_PTY=1; shift ;;
|
|
775
|
-
--no-pty) USE_PTY=0; shift ;;
|
|
776
|
-
--) shift; break ;;
|
|
777
|
-
-h|--help) usage help ;;
|
|
778
|
-
*) break ;;
|
|
779
|
-
esac
|
|
780
|
-
done
|
|
781
|
-
|
|
782
|
-
[[ $# -eq 0 ]] && usage
|
|
783
|
-
|
|
784
|
-
run_docker() {
|
|
785
|
-
echo "caproom: docker cgroup backend, limit=${LIMIT_MB}m image=${IMAGE}" >&2
|
|
786
|
-
exec docker run --rm -i \
|
|
787
|
-
--memory="${LIMIT_MB}m" --memory-swap="${LIMIT_MB}m" \
|
|
788
|
-
-v "$PWD:/work" -w /work "$IMAGE" "$@"
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
collect_tree() {
|
|
792
|
-
# Snapshot ps once and walk the descendant tree of $1 into TREE_PIDS /
|
|
793
|
-
# TREE_RSS_KB (see read_snapshot / walk_tree). Plain indexed arrays only
|
|
794
|
-
# so macOS's stock bash 3.2 works.
|
|
795
|
-
read_snapshot
|
|
796
|
-
walk_tree "$1"
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
is_known_tui() {
|
|
800
|
-
case "${1##*/}" in
|
|
801
|
-
opencode|claude|codex|vim|nvim|htop|less|fzf|nano|emacs) return 0 ;;
|
|
802
|
-
*) return 1 ;;
|
|
803
|
-
esac
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
SPAWN_PID=""
|
|
807
|
-
spawn_with_pty() {
|
|
808
|
-
# Try python3 pty_wrapper.py, then script fallback. Sets SPAWN_PID on success.
|
|
809
|
-
local wrapper
|
|
810
|
-
wrapper="$(dirname "${BASH_SOURCE[0]:-$0}")/../scripts/pty_wrapper.py"
|
|
811
|
-
if [[ ! -f "$wrapper" ]]; then
|
|
812
|
-
local cap_path
|
|
813
|
-
cap_path="$(command -v caproom 2>/dev/null || echo "")"
|
|
814
|
-
if [[ -n "$cap_path" ]]; then
|
|
815
|
-
wrapper="$(dirname "$cap_path")/../scripts/pty_wrapper.py"
|
|
816
|
-
[[ ! -f "$wrapper" ]] && wrapper="$(dirname "$(dirname "$cap_path")")/scripts/pty_wrapper.py"
|
|
817
|
-
fi
|
|
818
|
-
fi
|
|
819
|
-
[[ ! -f "$wrapper" ]] && wrapper="$HOME/Developer/caproom/scripts/pty_wrapper.py"
|
|
820
|
-
if [[ -f "$wrapper" ]] && command -v python3 >/dev/null 2>&1 && python3 -c "import pty" 2>/dev/null; then
|
|
821
|
-
# Avoid stdout capture issue: run in a way that doesn't hold the caller's command-substitution pipe.
|
|
822
|
-
# Use a temp file to pass pid back if needed, but we set global.
|
|
823
|
-
python3 "$wrapper" "$@" &
|
|
824
|
-
SPAWN_PID=$!
|
|
825
|
-
return 0
|
|
826
|
-
fi
|
|
827
|
-
if command -v script >/dev/null 2>&1; then
|
|
828
|
-
if [[ "$(uname)" == "Darwin" ]]; then
|
|
829
|
-
script -q /dev/null "$@" &
|
|
830
|
-
SPAWN_PID=$!
|
|
831
|
-
return 0
|
|
832
|
-
else
|
|
833
|
-
local cmd
|
|
834
|
-
cmd=$(printf '%q ' "$@")
|
|
835
|
-
script -q -c "$cmd" /dev/null &
|
|
836
|
-
SPAWN_PID=$!
|
|
837
|
-
return 0
|
|
838
|
-
fi
|
|
839
|
-
fi
|
|
840
|
-
return 1
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
run_watchdog() {
|
|
844
|
-
# PTY mode: allocate a real pty (forkpty) and forward bytes verbatim.
|
|
845
|
-
# Full terminal fidelity — OSC 10/11, DSR CPR, mouse DEC 1003 all work
|
|
846
|
-
# because the TUI talks to a real pty, not a pipe. Watchdog still enforces
|
|
847
|
-
# the cap on the pty tree (same kill logic as normal).
|
|
848
|
-
if [[ "$USE_PTY" -eq 1 ]]; then
|
|
849
|
-
echo "caproom: pty mode — allocating pty via forkpty for '${1##*/}' (limit ${LIMIT_MB}m, verbatim forwarding)" >&2
|
|
850
|
-
stty -tostop 2>/dev/null || true
|
|
851
|
-
printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[?25h' >/dev/tty 2>/dev/null || true
|
|
852
|
-
local saved_stty=""
|
|
853
|
-
if [[ -t 0 ]]; then
|
|
854
|
-
saved_stty=$(stty -g </dev/tty 2>/dev/null || true)
|
|
855
|
-
fi
|
|
856
|
-
restore_pty() {
|
|
857
|
-
[[ -n "$saved_stty" ]] && stty "$saved_stty" </dev/tty 2>/dev/null \
|
|
858
|
-
|| { [[ -t 0 ]] && stty sane </dev/tty 2>/dev/null || true; }
|
|
859
|
-
printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[?25h' >/dev/tty 2>/dev/null || true
|
|
860
|
-
}
|
|
861
|
-
local pty_pid
|
|
862
|
-
if spawn_with_pty "$@"; then
|
|
863
|
-
pty_pid=$SPAWN_PID
|
|
864
|
-
echo "caproom: pty allocated, pid $pty_pid — monitoring tree RSS" >&2
|
|
865
|
-
local limit_kb=$(( LIMIT_MB * 1024 ))
|
|
866
|
-
local exit_code=0
|
|
867
|
-
while kill -0 "$pty_pid" 2>/dev/null; do
|
|
868
|
-
collect_tree "$pty_pid"
|
|
869
|
-
if [[ "${#TREE_PIDS[@]}" -gt 0 && "$TREE_RSS_KB" -gt "$limit_kb" ]]; then
|
|
870
|
-
local overshoot=$(( TREE_RSS_KB * 100 / limit_kb ))
|
|
871
|
-
echo "caproom: pid $pty_pid tree RSS ${TREE_RSS_KB}KB exceeded ${limit_kb}KB cap (+${overshoot}%) — killing pty tree (grace ${GRACE}s)" >&2
|
|
872
|
-
kill -TERM "${TREE_PIDS[@]}" 2>/dev/null || true
|
|
873
|
-
local -a breach_pids=("${TREE_PIDS[@]}")
|
|
874
|
-
local waited=0
|
|
875
|
-
while kill -0 "$pty_pid" 2>/dev/null && [[ "$waited" -lt "$GRACE" ]]; do
|
|
876
|
-
sleep 1
|
|
877
|
-
waited=$(( waited + 1 ))
|
|
878
|
-
done
|
|
879
|
-
local sp sweep=0
|
|
880
|
-
for sp in "${breach_pids[@]}"; do
|
|
881
|
-
if kill -0 "$sp" 2>/dev/null; then
|
|
882
|
-
kill -9 "$sp" 2>/dev/null || true
|
|
883
|
-
sweep=$(( sweep + 1 ))
|
|
884
|
-
fi
|
|
885
|
-
done
|
|
886
|
-
if [[ "$sweep" -gt 0 ]]; then
|
|
887
|
-
echo "caproom: SIGKILLed ${sweep} survivor(s) after grace — exit 137" >&2
|
|
888
|
-
wait "$pty_pid" 2>/dev/null || true
|
|
889
|
-
restore_pty
|
|
890
|
-
exit 137
|
|
891
|
-
fi
|
|
892
|
-
wait "$pty_pid" 2>/dev/null || exit_code=$?
|
|
893
|
-
echo "caproom: pid $pty_pid exited cleanly (code $exit_code) during grace period" >&2
|
|
894
|
-
restore_pty
|
|
895
|
-
exit "$exit_code"
|
|
896
|
-
fi
|
|
897
|
-
sleep "$INTERVAL"
|
|
898
|
-
done
|
|
899
|
-
wait "$pty_pid" 2>/dev/null || exit_code=$?
|
|
900
|
-
restore_pty 2>/dev/null || true
|
|
901
|
-
exit "$exit_code"
|
|
902
|
-
else
|
|
903
|
-
echo "caproom: pty alloc failed — falling back to bypass/watchdog" >&2
|
|
904
|
-
restore_pty 2>/dev/null || true
|
|
905
|
-
fi
|
|
906
|
-
fi
|
|
907
|
-
# TUI bypass: if stdio is a tty and target is a known TUI (or bypass flag),
|
|
908
|
-
# don't sit on stdio — exec directly and monitor via detached watch.
|
|
909
|
-
# This preserves pty semantics (OSC 10/11, DSR CPR, DEC 1003 mouse) that
|
|
910
|
-
# break when a wrapper backgrounds the TUI and steals foreground pgrp.
|
|
911
|
-
if [[ "$BYPASS_TTY" -eq 1 ]] || { [[ -t 0 && -t 1 ]] && is_known_tui "${1:-}"; }; then
|
|
912
|
-
if [[ "$BYPASS_TTY" -eq 1 ]]; then
|
|
913
|
-
echo "caproom: bypass-tty active — exec directly, no stdio interposition (limit ${LIMIT_MB}m advisory via detached watch)" >&2
|
|
914
|
-
else
|
|
915
|
-
echo "caproom: tty TUI detected ('${1##*/}') — bypassing stdio interposition, monitoring via detached watch (limit ${LIMIT_MB}m)" >&2
|
|
916
|
-
fi
|
|
917
|
-
# Clean stale tty modes before handing off, same snapshot hygiene as
|
|
918
|
-
# the normal watchdog path but without taking foreground away.
|
|
919
|
-
stty -tostop 2>/dev/null || true
|
|
920
|
-
printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[?25h' >/dev/tty 2>/dev/null || true
|
|
921
|
-
# Detached watchdog monitors the TUI's pid tree without owning stdio.
|
|
922
|
-
# Resolve our own path (works via shim and direct invocation).
|
|
923
|
-
local _cap_bin="${BASH_SOURCE[0]:-$0}"
|
|
924
|
-
[[ "$_cap_bin" != /* ]] && _cap_bin="$PWD/$_cap_bin"
|
|
925
|
-
if [[ ! -f "$_cap_bin" ]]; then
|
|
926
|
-
_cap_bin="$(command -v caproom 2>/dev/null || echo "$_cap_bin")"
|
|
927
|
-
fi
|
|
928
|
-
set +m 2>/dev/null || true
|
|
929
|
-
"$@" &
|
|
930
|
-
local _tui_pid=$!
|
|
931
|
-
# Fire-and-forget watch; it exits when _tui_pid exits. Suppressed so it
|
|
932
|
-
# never leaks into the TUI's stdout. Threshold is the cap itself.
|
|
933
|
-
bash "$_cap_bin" watch --threshold-mb "$LIMIT_MB" --interval "$INTERVAL" --auto-park --json "$_tui_pid" >/dev/null 2>&1 &
|
|
934
|
-
local _watch_pid=$!
|
|
935
|
-
wait "$_tui_pid"
|
|
936
|
-
set -m 2>/dev/null || true
|
|
937
|
-
local _rc=$?
|
|
938
|
-
# Best-effort cleanup of the detached watcher (it should already exit
|
|
939
|
-
# via all-exited, but kill is cheap if TUI exited fast).
|
|
940
|
-
kill "$_watch_pid" 2>/dev/null || true
|
|
941
|
-
wait "$_watch_pid" 2>/dev/null || true
|
|
942
|
-
exit "$_rc"
|
|
943
|
-
fi
|
|
944
|
-
echo "caproom: watchdog backend (host-native), limit=${LIMIT_MB}m poll=${INTERVAL}s (process-tree RSS)" >&2
|
|
945
|
-
local limit_kb=$(( LIMIT_MB * 1024 ))
|
|
946
|
-
# Terminal hygiene: a TUI child (opencode, claude, ...) puts the tty in
|
|
947
|
-
# raw + mouse-tracking mode. If WE kill it, it never restores, and the
|
|
948
|
-
# user's shell then prints mouse reports like [[<35;25;15M as garbage.
|
|
949
|
-
# Clear stale state before snapshot — disable tostop to avoid SIGTTOU suspend
|
|
950
|
-
# when caproom backgrounds the TUI (ghostty reports [[<... as suspended tty output).
|
|
951
|
-
stty -tostop 2>/dev/null || true
|
|
952
|
-
printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[?25h' >/dev/tty 2>/dev/null || true
|
|
953
|
-
local saved_stty=""
|
|
954
|
-
if [[ -t 0 ]]; then
|
|
955
|
-
saved_stty=$(stty -g </dev/tty 2>/dev/null || true)
|
|
956
|
-
fi
|
|
957
|
-
restore_tty() {
|
|
958
|
-
# Only on OUR kill — a clean exit already restored its own state, and
|
|
959
|
-
# re-emitting resets there could clobber whatever the NEXT program drew.
|
|
960
|
-
[[ -n "$saved_stty" ]] && stty "$saved_stty" </dev/tty 2>/dev/null \
|
|
961
|
-
|| { [[ -t 0 ]] && stty sane </dev/tty 2>/dev/null || true; }
|
|
962
|
-
printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[?25h' >/dev/tty 2>/dev/null || true
|
|
963
|
-
}
|
|
964
|
-
set +m 2>/dev/null || true
|
|
965
|
-
"$@" &
|
|
966
|
-
local pid=$!
|
|
967
|
-
local exit_code=0
|
|
968
|
-
while kill -0 "$pid" 2>/dev/null; do
|
|
969
|
-
collect_tree "$pid"
|
|
970
|
-
if [[ "${#TREE_PIDS[@]}" -gt 0 && "$TREE_RSS_KB" -gt "$limit_kb" ]]; then
|
|
971
|
-
local overshoot=$(( TREE_RSS_KB * 100 / limit_kb ))
|
|
972
|
-
echo "caproom: pid $pid tree RSS ${TREE_RSS_KB}KB exceeded ${limit_kb}KB cap (+${overshoot}%) — killing tree (grace ${GRACE}s)" >&2
|
|
973
|
-
# Signal EVERY pid in the tree, not just the root: children that
|
|
974
|
-
# survive a root-only TERM get orphaned and keep allocating past
|
|
975
|
-
# the cap after caproom exits.
|
|
976
|
-
kill -TERM "${TREE_PIDS[@]}" 2>/dev/null || true
|
|
977
|
-
local -a breach_pids=("${TREE_PIDS[@]}")
|
|
978
|
-
local waited=0
|
|
979
|
-
while kill -0 "$pid" 2>/dev/null && [[ "$waited" -lt "$GRACE" ]]; do
|
|
980
|
-
sleep 1
|
|
981
|
-
waited=$(( waited + 1 ))
|
|
982
|
-
done
|
|
983
|
-
# Escalate against anything that ignored TERM — root OR child.
|
|
984
|
-
# Scanning the breach-time snapshot (not re-walking from the root)
|
|
985
|
-
# also catches the case where the root died but a stubborn child
|
|
986
|
-
# survived it. Children spawned DURING the grace window are not in
|
|
987
|
-
# the snapshot; same accepted gap as detached daemons generally.
|
|
988
|
-
local sp sweep=0
|
|
989
|
-
for sp in "${breach_pids[@]}"; do
|
|
990
|
-
if kill -0 "$sp" 2>/dev/null; then
|
|
991
|
-
kill -9 "$sp" 2>/dev/null || true
|
|
992
|
-
sweep=$(( sweep + 1 ))
|
|
993
|
-
fi
|
|
994
|
-
done
|
|
995
|
-
if [[ "$sweep" -gt 0 ]]; then
|
|
996
|
-
echo "caproom: SIGKILLed ${sweep} survivor(s) after grace — exit 137" >&2
|
|
997
|
-
wait "$pid" 2>/dev/null || true
|
|
998
|
-
restore_tty
|
|
999
|
-
exit 137
|
|
1000
|
-
fi
|
|
1001
|
-
wait "$pid" 2>/dev/null || exit_code=$?
|
|
1002
|
-
echo "caproom: pid $pid exited cleanly (code $exit_code) during grace period" >&2
|
|
1003
|
-
restore_tty
|
|
1004
|
-
exit "$exit_code"
|
|
1005
|
-
fi
|
|
1006
|
-
sleep "$INTERVAL"
|
|
1007
|
-
done
|
|
1008
|
-
wait "$pid" || exit_code=$?
|
|
1009
|
-
set -m 2>/dev/null || true
|
|
1010
|
-
exit "$exit_code"
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
if [[ "$USE_DOCKER" -eq 1 ]]; then
|
|
1014
|
-
# Explicit opt-in must fail loudly rather than silently downgrade —
|
|
1015
|
-
# the caller asked for a hard cap, a silent watchdog switch would
|
|
1016
|
-
# quietly change the guarantee they asked for.
|
|
1017
|
-
if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
|
|
1018
|
-
run_docker "$@"
|
|
1019
|
-
else
|
|
1020
|
-
echo "caproom: --docker requested but the docker daemon is not reachable" >&2
|
|
1021
|
-
exit 1
|
|
1022
|
-
fi
|
|
1023
|
-
else
|
|
1024
|
-
run_watchdog "$@"
|
|
1025
|
-
fi
|