feat: party clean removes the caller's own crash leftovers

Turns the manual rm -rf recovery from host's EEXIST message into a
verb, behind the same gates cmd_close uses: own-basename glob, no
symlinks, ownership check, and a dead socket. Live parties are skipped
with a pointer to party close.
This commit is contained in:
veg 2026-07-11 12:32:39 +00:00
parent 6799a57405
commit a011e4b03c
6 changed files with 133 additions and 9 deletions

47
party
View file

@ -49,6 +49,7 @@ Hosting:
Names: letters, digits, '_', '-' (max 63).
close [name] Tear down the party you host (name it when
you host several).
clean Remove your own dead party dirs (crash leftovers).
Joining:
join [name] [--passive] Join a party. Without name: auto-pick or numbered prompt.
@ -764,14 +765,14 @@ EOF
# races with a concurrent in-flight setup: two processes both seeing a
# stale dir would both rm -rf it, and the loser's late rm -rf would
# wipe the winner's freshly-created replacement.
# The recovery path for genuine crash leftovers is a one-shot manual
# `rm -rf $party_dir` followed by retry; the error message says so.
# The recovery path for genuine crash leftovers is `party clean`
# followed by retry; the error message says so.
if ! mkdir -m 0700 "$party_dir" 2>/dev/null; then
cat >&2 <<EOF
party: $party_dir already exists. Either another host is starting '$name'
right now, or a prior crashed attempt left this directory behind. If no
other host is in flight, remove the directory and retry:
rm -rf $party_dir
other host is in flight, clean up and retry:
party clean
EOF
exit 1
fi
@ -946,6 +947,43 @@ cmd_close() {
rm -rf "$expected_dir"
echo "Party '$name' closed."
}
cmd_clean() {
case "${1:-}" in
-h|--help)
cat <<'EOF'
Usage: party clean
Remove your own dead party directories (crash leftovers whose tmux
server is gone). Live parties and other users' directories are never
touched; end a live party with `party close`.
EOF
exit 0 ;;
'') ;;
*) echo "party clean: unexpected arg '$1'" >&2; exit 2 ;;
esac
# Same mistake-proofing gates as cmd_close: the glob only matches
# the caller's own basename prefix, symlinks are never followed,
# [ -O ] pins ownership, and a socket answering the tmux protocol
# means the party is live and gets skipped, `party close` is the
# verb for those. An unmatched glob stays a literal string and
# fails [ -d ].
removed=0
for d in "$PARTY_SOCKET_DIR"/party-"$USER":*.d; do
[ -d "$d" ] || continue
[ -L "$d" ] && continue
[ -O "$d" ] || continue
if [ -e "$d/sock" ] \
&& [ "$(party_conn_state "$d/sock")" != dead ]; then
echo "party clean: skipping $d (server is live; use party close)." >&2
continue
fi
rm -rf "$d"
echo "removed $d"
removed=$((removed+1))
done
[ "$removed" -gt 0 ] || echo "nothing to clean."
}
cmd_join() {
passive=0
name=
@ -1547,6 +1585,7 @@ dispatch() {
--version) echo "party $PARTY_VERSION"; exit 0 ;;
host) cmd_host "$@" ;;
close) cmd_close "$@" ;;
clean) cmd_clean "$@" ;;
join) cmd_join "$@" ;;
leave) cmd_leave "$@" ;;
knock) cmd_knock "$@" ;;