150 lines
5.6 KiB
Bash
Executable File
150 lines
5.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Replace every shell playpen keeps in herdr-core with a fresh one, keeping the
|
|
# layout.
|
|
#
|
|
# tools/fresh-shells.sh list what will be ended, ask, then do it
|
|
# tools/fresh-shells.sh -y don't ask
|
|
#
|
|
# Acts on the hc session in $HC_SESSION (the default one when unset), and
|
|
# relaunches with $PLAYPEN_LAUNCHER (the installed launcher when unset).
|
|
#
|
|
# A program's environment is fixed when it starts. Shells that came from a core
|
|
# with something wrong in its environment keep it for good, and since playpen
|
|
# reattaches to them on every launch, restarting playpen never clears it. And
|
|
# `hc server restart` cannot either: it is a planned restart that keeps every
|
|
# program running for the next core to adopt.
|
|
#
|
|
# So, in order:
|
|
# 1. Quit playpen with SIGTERM, which saves the session first. This has to
|
|
# come before the shells go: a pane whose shell exits closes, and closing
|
|
# the last tab deletes the session.
|
|
# 2. Kill every terminal playpen started in the core (label playpen=1).
|
|
# 3. Start playpen again. It restores the session, finds those terminals gone,
|
|
# and opens a fresh shell in each pane's last directory.
|
|
#
|
|
# Whatever is running in those shells ends with them — agents included.
|
|
|
|
set -euo pipefail
|
|
|
|
unit_prefix="playpen-fresh-shells"
|
|
launcher="${PLAYPEN_LAUNCHER:-$HOME/.local/bin/playpen}"
|
|
|
|
# By path: the detached half runs under systemd, whose PATH has no ~/.local/bin.
|
|
hc_bin=$(command -v hc || echo "$HOME/.local/bin/hc")
|
|
|
|
die() { echo "fresh-shells: $*" >&2; exit 1; }
|
|
say() { echo "fresh-shells: $*"; }
|
|
|
|
# Every hc call runs without this process's Claude Code markers. Any hc command
|
|
# starts a core when none is running, and a core started with them hands them
|
|
# to every terminal it ever runs — which is how this script came to be needed.
|
|
hc() {
|
|
local unset=()
|
|
local name
|
|
for name in $(compgen -e); do
|
|
case "$name" in CLAUDE*) unset+=(-u "$name") ;; esac
|
|
done
|
|
env "${unset[@]}" "$hc_bin" "$@"
|
|
}
|
|
|
|
[ -x "$hc_bin" ] || die "hc is not installed"
|
|
command -v jq >/dev/null || die "needs jq"
|
|
[ -x "$launcher" ] || die "no playpen launcher at $launcher (run mise run install)"
|
|
|
|
yes=false
|
|
[ "${1:-}" = "-y" ] && yes=true
|
|
|
|
terminals() {
|
|
hc ls --label playpen=1 --json | jq -r '.terminals[].address'
|
|
}
|
|
|
|
# Whether this script is running in something step 1 or 2 would end: a pane of
|
|
# playpen's own (a local shell, which quitting hangs up), or a terminal in the
|
|
# core that step 2 kills.
|
|
inside_playpen() {
|
|
if [ -n "${HC_TERMINAL_ID:-}" ] &&
|
|
[ "$(hc get "$HC_TERMINAL_ID" --json 2>/dev/null | jq -r '.labels.playpen // empty')" = 1 ]; then
|
|
return 0
|
|
fi
|
|
local pid=$$
|
|
while [ "$pid" -gt 1 ]; do
|
|
[ "$(cat "/proc/$pid/comm" 2>/dev/null)" = playpen ] && return 0
|
|
pid=$(awk '{print $4}' "/proc/$pid/stat" 2>/dev/null) || return 1
|
|
done
|
|
return 1
|
|
}
|
|
|
|
if [ -z "${PLAYPEN_FRESH_SHELLS_DETACHED:-}" ]; then
|
|
mapfile -t addrs < <(terminals)
|
|
if [ "${#addrs[@]}" -eq 0 ]; then
|
|
say "playpen has no terminals in herdr-core; nothing to do"
|
|
exit 0
|
|
fi
|
|
|
|
echo "These ${#addrs[@]} terminals will be ended, and everything running in them:"
|
|
for a in "${addrs[@]}"; do
|
|
hc get "$a" --json | jq -r '" \(.address) \((.foreground.name // "?"))\t\(.foreground.cwd // .cwd)"'
|
|
done
|
|
if ! $yes; then
|
|
read -r -p "Quit playpen, end them, and reopen with fresh shells? [y/N] " answer
|
|
case "$answer" in y | Y | yes) ;; *) say "nothing done"; exit 0 ;; esac
|
|
fi
|
|
|
|
# Hand the rest to systemd, which is not inside anything about to end.
|
|
if inside_playpen; then
|
|
unit="$unit_prefix-$$"
|
|
systemd-run --user --collect --quiet --unit="$unit" \
|
|
--setenv=PLAYPEN_FRESH_SHELLS_DETACHED=1 \
|
|
${HC_SESSION:+--setenv=HC_SESSION="$HC_SESSION"} \
|
|
${PLAYPEN_LAUNCHER:+--setenv=PLAYPEN_LAUNCHER="$PLAYPEN_LAUNCHER"} \
|
|
"$(readlink -f "$0")" -y
|
|
say "this shell is one of them, so the rest runs on its own as $unit."
|
|
say "playpen will close and come back in a few seconds."
|
|
say "if it doesn't: journalctl --user -u $unit"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# 1. Quit playpen, saving the session. Only the ones on this hc session: a
|
|
# playpen talking to another core has nothing to do with these terminals.
|
|
mapfile -t pids < <(
|
|
for p in $(pgrep -u "$(id -u)" -x playpen); do
|
|
theirs=$(tr '\0' '\n' <"/proc/$p/environ" 2>/dev/null | sed -n 's/^HC_SESSION=//p')
|
|
[ "$theirs" = "${HC_SESSION:-}" ] && echo "$p"
|
|
done
|
|
)
|
|
if [ "${#pids[@]}" -gt 0 ]; then
|
|
say "quitting playpen (${pids[*]})"
|
|
kill -TERM "${pids[@]}"
|
|
for _ in $(seq 1 100); do
|
|
alive=false
|
|
for p in "${pids[@]}"; do kill -0 "$p" 2>/dev/null && alive=true; done
|
|
$alive || break
|
|
sleep 0.1
|
|
done
|
|
# Better to leave the shells alone than to end them under a playpen that is
|
|
# still up, which would close its panes and lose the layout.
|
|
$alive && die "playpen did not quit within 10s; nothing else done"
|
|
fi
|
|
|
|
# 2. End playpen's terminals.
|
|
mapfile -t addrs < <(terminals)
|
|
say "ending ${#addrs[@]} terminals"
|
|
for a in "${addrs[@]}"; do
|
|
# `rm` terminals are gone the moment they exit, so kill reports them as
|
|
# "removed" even when it worked.
|
|
hc kill "$a" >/dev/null 2>&1 || true
|
|
done
|
|
for _ in $(seq 1 100); do
|
|
[ -z "$(terminals)" ] && break
|
|
sleep 0.1
|
|
done
|
|
left=$(terminals)
|
|
[ -n "$left" ] && die "still running: $(echo "$left" | tr '\n' ' ')— not relaunching"
|
|
|
|
# 3. Start playpen again, from systemd rather than from this shell, so it gets
|
|
# the session's environment and not whatever this script was run from.
|
|
systemd-run --user --collect --quiet --unit="app-playpen-$$" \
|
|
${HC_SESSION:+--setenv=HC_SESSION="$HC_SESSION"} "$launcher"
|
|
say "playpen relaunched with fresh shells"
|