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:
parent
6799a57405
commit
a011e4b03c
6 changed files with 133 additions and 9 deletions
|
|
@ -119,6 +119,7 @@ Three honest caveats, with the full detail in `man party`:
|
|||
| `party who [--short]` | Show invited and attached users for the current party. |
|
||||
| `party status` | Show the caller's own state: hosting, attached, or idle. |
|
||||
| `party close` | Tear down the party server and its roster entry. Host-only. |
|
||||
| `party clean` | Remove the caller's own dead party dirs: crash leftovers whose tmux server is gone. |
|
||||
| `party join [name] [--passive]` | Join a party. Auto-attaches when one is running; picker otherwise. `--passive` attaches read-only to the host's view (watcher mode). Read-only invitees always join passive. |
|
||||
| `party leave` | Detach and clean up the per-guest session. |
|
||||
| `party knock <name>` | Ask the host of an invite-only party for an invite (write(1) ping with the exact invite command). |
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ Direction, not promises. Two rules govern everything below (README
|
|||
per-party dir (group-readable so every attendee can grab a copy);
|
||||
`party log --stop` ends it. "What did we do last night?" for
|
||||
collectives, and the audit trail human+AI co-work needs.
|
||||
- **`party clean`:** remove the caller's own crash leftovers (dirs
|
||||
they own whose socket is dead), turning the manual `rm -rf` recovery
|
||||
in host's error message into a verb.
|
||||
|
||||
## Later
|
||||
|
||||
|
|
|
|||
47
party
47
party
|
|
@ -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 "$@" ;;
|
||||
|
|
|
|||
6
party.1
6
party.1
|
|
@ -14,6 +14,8 @@
|
|||
.Cm close
|
||||
.Op Ar name
|
||||
.Nm
|
||||
.Cm clean
|
||||
.Nm
|
||||
.Cm join
|
||||
.Op Ar name
|
||||
.Op Fl -passive
|
||||
|
|
@ -136,6 +138,10 @@ With no
|
|||
.Ar name ,
|
||||
closes the only party the caller hosts.
|
||||
Host-only.
|
||||
.It Cm clean
|
||||
Remove the caller's own dead party directories: crash leftovers whose
|
||||
tmux server no longer answers.
|
||||
Never touches live parties, other users' directories, or symlinks.
|
||||
.It Cm join Op Ar name Op Fl -passive
|
||||
Join a party.
|
||||
With no
|
||||
|
|
|
|||
81
tests/85-clean.bats
Normal file
81
tests/85-clean.bats
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env bats
|
||||
#
|
||||
# party clean: remove the caller's own dead party dirs. Mistake-proofing
|
||||
# gates mirror cmd_close: own-basename glob, no symlinks, [ -O ], and a
|
||||
# dead socket. Liveness is faked through PARTY_TMUX stubs so no real
|
||||
# server or group membership is needed.
|
||||
|
||||
load 'helpers'
|
||||
|
||||
setup() {
|
||||
setup_party_sandbox
|
||||
load_party_lib
|
||||
}
|
||||
|
||||
teardown() { teardown_party_sandbox; }
|
||||
|
||||
@test "clean removes a dead leftover dir" {
|
||||
ensure_party_dir "$USER" ghost
|
||||
run "$PARTY_BIN" clean
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"removed"*"ghost"* ]]
|
||||
[ ! -d "$PARTY_SOCKET_DIR/party-$USER:ghost.d" ]
|
||||
}
|
||||
|
||||
@test "clean removes a crash-before-bind dir (no sock at all)" {
|
||||
mkdir "$PARTY_SOCKET_DIR/party-$USER:stillborn.d"
|
||||
run "$PARTY_BIN" clean
|
||||
[ "$status" -eq 0 ]
|
||||
[ ! -d "$PARTY_SOCKET_DIR/party-$USER:stillborn.d" ]
|
||||
}
|
||||
|
||||
@test "clean skips a live party and says to use close" {
|
||||
cat > "$PARTY_TMP/tmux-ok" <<'EOF'
|
||||
#!/bin/sh
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$PARTY_TMP/tmux-ok"
|
||||
export PARTY_TMUX="$PARTY_TMP/tmux-ok"
|
||||
ensure_party_dir "$USER" running
|
||||
run "$PARTY_BIN" clean
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"skipping"*"running"* ]]
|
||||
[[ "$output" == *"party close"* ]]
|
||||
[ -d "$PARTY_SOCKET_DIR/party-$USER:running.d" ]
|
||||
}
|
||||
|
||||
@test "clean never follows a planted symlink" {
|
||||
mkdir "$PARTY_TMP/target"
|
||||
: > "$PARTY_TMP/target/precious"
|
||||
ln -s "$PARTY_TMP/target" "$PARTY_SOCKET_DIR/party-$USER:lnk.d"
|
||||
run "$PARTY_BIN" clean
|
||||
[ "$status" -eq 0 ]
|
||||
[ -e "$PARTY_TMP/target/precious" ]
|
||||
[ -L "$PARTY_SOCKET_DIR/party-$USER:lnk.d" ]
|
||||
}
|
||||
|
||||
@test "clean ignores other users' dirs" {
|
||||
ensure_party_dir nobody theirparty
|
||||
run "$PARTY_BIN" clean
|
||||
[ "$status" -eq 0 ]
|
||||
[ -d "$PARTY_SOCKET_DIR/party-nobody:theirparty.d" ]
|
||||
}
|
||||
|
||||
@test "clean with nothing to do says so" {
|
||||
run "$PARTY_BIN" clean
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"nothing to clean"* ]]
|
||||
}
|
||||
|
||||
@test "clean rejects arguments" {
|
||||
run "$PARTY_BIN" clean extra
|
||||
[ "$status" -eq 2 ]
|
||||
}
|
||||
|
||||
@test "host's EEXIST message recommends party clean" {
|
||||
ensure_party_dir "$USER" blocked
|
||||
export TMUX_PARTY_GROUP="$(id -gn)"
|
||||
run "$PARTY_BIN" host blocked
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"party clean"* ]]
|
||||
}
|
||||
|
|
@ -564,7 +564,7 @@ STUB
|
|||
# deliberately do NOT auto-clean: the same code path triggers on a
|
||||
# concurrent in-flight host attempt for the same name, and racing
|
||||
# rm -rf calls would wipe each other's fresh dirs.
|
||||
# Recovery is a manual `rm -rf` per the error message.
|
||||
# Recovery is `party clean` per the error message.
|
||||
party_dir="$PARTY_SOCKET_DIR/party-$USER:resurrect.d"
|
||||
mkdir -p "$party_dir"
|
||||
: > "$party_dir/sock"
|
||||
|
|
@ -572,7 +572,7 @@ STUB
|
|||
run "$PARTY_BIN" host resurrect
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"already exists"* ]]
|
||||
[[ "$output" == *"rm -rf $party_dir"* ]]
|
||||
[[ "$output" == *"party clean"* ]]
|
||||
|
||||
# The leftover dir is untouched — proves we did not race-clean.
|
||||
[ -d "$party_dir" ]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue