tmux-party: share a tmux session with people on the same UNIX host. Single-file POSIX shell (party) with a filesystem + tmux server-access trust model. See README.md.
136 lines
5.3 KiB
Bash
136 lines
5.3 KiB
Bash
# Bats helpers shared by every test file.
|
|
# `load 'helpers'` from a .bats file pulls these in.
|
|
|
|
# helpers.bash lives at <repo>/tests/helpers.bash, so repo root is one level
|
|
# up from its own directory. This avoids depending on .git, which may not
|
|
# be present on remote test hosts (e.g. rsynced without history).
|
|
PARTY_BIN="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/party"
|
|
|
|
# Per-test scratch dir. Bats sets BATS_TEST_TMPDIR but we centralize so
|
|
# party's overridable env vars all point under one tree.
|
|
setup_party_sandbox() {
|
|
# Pretend the test runner is NOT inside tmux. iTerm2's tmux -CC mode
|
|
# (or any nested tmux) sets $TMUX, which several party subcommands
|
|
# treat as "you're already attached somewhere." Tests that need
|
|
# $TMUX set re-export it themselves.
|
|
unset TMUX TMUX_PANE TMUX_TMPDIR
|
|
|
|
# USER may not be exported in all environments (e.g. some CI shells,
|
|
# zsh with clean env). party uses $USER under set -eu, so guarantee it.
|
|
export USER="${USER:-$(id -un)}"
|
|
|
|
export PARTY_TMP="$BATS_TEST_TMPDIR/party"
|
|
export PARTY_SOCKET_DIR="$PARTY_TMP/sockets"
|
|
mkdir -p "$PARTY_SOCKET_DIR"
|
|
chmod 0700 "$PARTY_SOCKET_DIR"
|
|
# No shared roster directory in this layout. Each per-party dir under
|
|
# PARTY_SOCKET_DIR carries its own roster file. validate_socket_dir_parent
|
|
# accepts an owner-only-writable parent like this one.
|
|
}
|
|
|
|
# Tear down any tmux servers we spawned under PARTY_SOCKET_DIR.
|
|
# Sockets live at ${PARTY_SOCKET_DIR}/party-USER:NAME.d/sock under the
|
|
# per-party-private-dir layout. The old glob (party-*) matched the
|
|
# *directory* and the [ -S ] filter dropped everything, so servers
|
|
# leaked across tests.
|
|
teardown_party_sandbox() {
|
|
if [ -d "${PARTY_SOCKET_DIR:-}" ]; then
|
|
for d in "$PARTY_SOCKET_DIR"/party-*.d; do
|
|
[ -d "$d" ] || continue
|
|
sock="$d/sock"
|
|
[ -S "$sock" ] || continue
|
|
tmux -S "$sock" kill-server 2>/dev/null || true
|
|
done
|
|
fi
|
|
}
|
|
|
|
# Pre-create the per-party private dir so roster_write can land its
|
|
# record inside. Mirrors cmd_host's mkdir -m 0700 + chmod 0750. Also
|
|
# creates a placeholder sock file so roster_read (which requires
|
|
# $d/sock to exist and be owned by the basename's HOST_USER) accepts
|
|
# the dir.
|
|
ensure_party_dir() {
|
|
local d="$PARTY_SOCKET_DIR/party-$1:$2.d"
|
|
mkdir -p "$d"
|
|
chmod 0750 "$d"
|
|
: > "$d/sock"
|
|
}
|
|
|
|
# Skip the test if the running user is not a member of TMUX_PARTY_GROUP.
|
|
require_party_group() {
|
|
local g="${TMUX_PARTY_GROUP:-party}"
|
|
id -nG "$USER" 2>/dev/null | tr ' ' '\n' | grep -qx "$g" \
|
|
|| skip "user not in group '$g'; run: sudo usermod -aG $g $USER && relog"
|
|
}
|
|
|
|
# Print one username per line for users in group $1 — both supplementary
|
|
# members (4th /etc/group field) and primary-group members (passwd gid
|
|
# match). Linux / illumos / FreeBSD / OpenBSD use getent(1); macOS has
|
|
# no getent and stores users in Directory Services, so fall back to
|
|
# dscl(1) there. Returns silently if neither resolver is available or
|
|
# the group is unknown.
|
|
list_users_in_group() {
|
|
local g="$1" gid
|
|
if command -v getent >/dev/null 2>&1; then
|
|
gid=$(getent group "$g" 2>/dev/null | awk -F: '{print $3}')
|
|
[ -n "$gid" ] || return 0
|
|
{
|
|
getent group "$g" | awk -F: '{n=split($4,m,","); for(i=1;i<=n;i++) if(m[i]!="") print m[i]}'
|
|
getent passwd | awk -F: -v gid="$gid" '$4==gid {print $1}'
|
|
}
|
|
elif command -v dscl >/dev/null 2>&1; then
|
|
gid=$(dscl . -read "/Groups/$g" PrimaryGroupID 2>/dev/null \
|
|
| awk '/^PrimaryGroupID:/ {print $2}')
|
|
[ -n "$gid" ] || return 0
|
|
{
|
|
# Supplementary members. macOS records gid 20 (staff) members as
|
|
# primary-only, so this branch is usually empty for staff but
|
|
# populates for groups created with `dseditgroup -o edit -a`.
|
|
dscl . -read "/Groups/$g" GroupMembership 2>/dev/null \
|
|
| sed -n 's/^GroupMembership: //p' | tr ' ' '\n'
|
|
# Primary members: every user whose PrimaryGroupID matches.
|
|
dscl . -list /Users PrimaryGroupID | awk -v gid="$gid" '$2==gid {print $1}'
|
|
} | grep -v '^$' || true
|
|
fi
|
|
}
|
|
|
|
# Print the login shell of user $1, or nothing if unknown. Mirrors
|
|
# list_users_in_group's portability split: getent(1) where present,
|
|
# dscl(1) on macOS.
|
|
lookup_user_shell() {
|
|
local u="$1"
|
|
if command -v getent >/dev/null 2>&1; then
|
|
getent passwd "$u" 2>/dev/null | awk -F: '{print $7}'
|
|
elif command -v dscl >/dev/null 2>&1; then
|
|
dscl . -read "/Users/$u" UserShell 2>/dev/null | sed -n 's/^UserShell: //p'
|
|
fi
|
|
}
|
|
|
|
# Pick another invitable user on this box: a member of TMUX_PARTY_GROUP
|
|
# (supplementary or primary) other than $USER. Empty output if none.
|
|
# Uses group membership rather than a UID floor so it works across
|
|
# Linux (uid>=1000), illumos/Solaris (uid>=100), and BSD conventions.
|
|
# Excludes root and no-login system accounts (tmux server-access refuses
|
|
# to manage root, and system pseudo-users aren't sensible test peers).
|
|
pick_other_user() {
|
|
local g="${TMUX_PARTY_GROUP:-party}"
|
|
local candidates u shell
|
|
candidates=$(list_users_in_group "$g")
|
|
for u in $candidates; do
|
|
[ "$u" = "$USER" ] && continue
|
|
[ "$u" = "root" ] && continue
|
|
shell=$(lookup_user_shell "$u")
|
|
case "$shell" in
|
|
*false|*nologin|"") continue ;;
|
|
esac
|
|
printf '%s\n' "$u"
|
|
return 0
|
|
done
|
|
}
|
|
|
|
# Source ./party in library mode for pure-function tests.
|
|
load_party_lib() {
|
|
__PARTY_LIB_ONLY=1
|
|
# shellcheck disable=SC1090
|
|
. "$PARTY_BIN"
|
|
}
|