feat: party clean rescues an orphaned transcript before deleting

A server crash mid-logged-party is the one case close's rescue can
never have run, and host's EEXIST message funnels the host straight
into clean. Deleting the night's log there is exactly the accident
class the close-time rescue exists for, so clean applies the same
rescue: no quiescence poll needed, the liveness gate just proved the
writers are gone.
This commit is contained in:
veg 2026-07-11 13:13:05 +00:00
parent f946c7e876
commit 8d10e62779
4 changed files with 31 additions and 1 deletions

View file

@ -120,7 +120,7 @@ Three honest caveats, with the full detail in `man party`:
| `party who [--short]` | Show invited and attached users for the current 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 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 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 clean` | Remove the caller's own dead party dirs: crash leftovers whose tmux server is gone. Rescues a non-empty transcript first. |
| `party log [--stop]` | Record the party into its private dir via `tmux pipe-pane` (host only), group-readable. `--stop` ends it. | | `party log [--stop]` | Record the party into its private dir via `tmux pipe-pane` (host only), group-readable. `--stop` ends it. |
| `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 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 leave` | Detach and clean up the per-guest session. |

15
party
View file

@ -1005,6 +1005,21 @@ EOF
echo "party clean: skipping $d (server is live; use party close)." >&2 echo "party clean: skipping $d (server is live; use party close)." >&2
continue continue
fi fi
# A crash-orphaned transcript is the same "lost the night's log"
# accident cmd_close's rescue exists for; the server here is
# already confirmed dead by the liveness check above, so unlike
# close there's no pipe-pane `cat` still draining and no
# quiescence poll is needed before moving it.
if [ -s "$d/log" ]; then
_cn_name=${d##*"party-$USER:"}
_cn_name=${_cn_name%.d}
_cn_dest="$HOME/party-$_cn_name-$(date -u '+%Y%m%d-%H%M%S').log"
if mv "$d/log" "$_cn_dest" 2>/dev/null; then
echo "party clean: transcript saved to $_cn_dest"
fi
fi
rm -rf "$d" rm -rf "$d"
echo "removed $d" echo "removed $d"
removed=$((removed+1)) removed=$((removed+1))

View file

@ -146,6 +146,9 @@ Host-only.
Remove the caller's own dead party directories: crash leftovers whose Remove the caller's own dead party directories: crash leftovers whose
tmux server no longer answers. tmux server no longer answers.
Never touches live parties, other users' directories, or symlinks. Never touches live parties, other users' directories, or symlinks.
A non-empty transcript left by
.Cm log
is saved to the caller's home directory first.
.It Cm log Op Fl -stop Op Fl -party Ar name .It Cm log Op Fl -stop Op Fl -party Ar name
Record the party into Record the party into
.Pa log .Pa log

View file

@ -22,6 +22,18 @@ teardown() { teardown_party_sandbox; }
[ ! -d "$PARTY_SOCKET_DIR/party-$USER:ghost.d" ] [ ! -d "$PARTY_SOCKET_DIR/party-$USER:ghost.d" ]
} }
@test "clean rescues a non-empty transcript before removing the dir" {
export HOME="$PARTY_TMP/home"
mkdir -p "$HOME"
ensure_party_dir "$USER" crashed
printf 'the night\n' > "$PARTY_SOCKET_DIR/party-$USER:crashed.d/log"
run "$PARTY_BIN" clean
[ "$status" -eq 0 ]
[[ "$output" == *"transcript saved to"* ]]
[ ! -d "$PARTY_SOCKET_DIR/party-$USER:crashed.d" ]
grep -q "the night" "$HOME"/party-crashed-*.log
}
@test "clean removes a crash-before-bind dir (no sock at all)" { @test "clean removes a crash-before-bind dir (no sock at all)" {
mkdir "$PARTY_SOCKET_DIR/party-$USER:stillborn.d" mkdir "$PARTY_SOCKET_DIR/party-$USER:stillborn.d"
run "$PARTY_BIN" clean run "$PARTY_BIN" clean