Add script to get fresh set of shells.
This commit is contained in:
1 parent
6f4492e3cb
commit
3db4123319
3 files changed
+160
No files matched your search
@@ -498,6 +498,13 @@ a terminal leaf). A terminal that has since exited reopens as a fresh shell in
|
||||
the recorded directory, as it would without herdr-core. Shells in the core can
|
||||
also be reached from anywhere else with `hc ls` and `hc attach`.
|
||||
|
||||
Since the shells outlive every restart, so does anything wrong with them — an
|
||||
environment variable they started with, say. `mise run fresh-shells` replaces
|
||||
them all while keeping the layout: it quits playpen (saving the session), ends
|
||||
every terminal playpen started in the core, and relaunches, so each pane comes
|
||||
back as a new shell in its last directory. It lists what is running and asks
|
||||
first, and it is safe to run from inside one of those panes.
|
||||
|
||||
The file is `session.json`, and it lives in the **state** directory rather than
|
||||
beside `layouts.json` and `settings.json`:
|
||||
|
||||
|
||||
@@ -184,6 +184,10 @@ done
|
||||
rmdir "$backups" 2>/dev/null || true
|
||||
'''
|
||||
|
||||
[tasks.fresh-shells]
|
||||
description = "Replace every herdr-core shell playpen keeps with a fresh one, keeping the layout"
|
||||
run = "tools/fresh-shells.sh"
|
||||
|
||||
[tasks.install]
|
||||
description = "Install playpen into ~/.local with a desktop entry and icon"
|
||||
depends = ["build"]
|
||||
|
||||
Executable
+149
@@ -0,0 +1,149 @@
|
||||
#!/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"
|
||||
Reference in new issue
Block a user