# ╔══════════════════════════════════════════════════════════════════╗ # ║ arch-dev roaming :: engine ║ # ║ Roaming ~ over the tailnet. Source this from .zshrc. ║ # ║ ║ # ║ Reads ~/.config/roaming/hosts and generates a function per ║ # ║ host so you can run: ║ # ║ mail → interactive shell on mail ║ # ║ mail tail -f /var/log/x → run with TTY (works for -f, sudo) ║ # ║ mount_host mail → sshfs the host's mount at ║ # ║ /workspace/mail ║ # ╚══════════════════════════════════════════════════════════════════╝ export ROAMING_DIR="${ROAMING_DIR:-$HOME/.config/roaming}" export ROAMING_HOSTS="${ROAMING_HOSTS:-$ROAMING_DIR/hosts}" export ROAMING_MOUNT_BASE="${ROAMING_MOUNT_BASE:-/workspace}" export ROAMING_DEFAULT_USER="${ROAMING_DEFAULT_USER:-$USER}" # sshfs options that prevent the dreaded indefinite-freeze on network blips. # ServerAliveInterval=15 + CountMax=3 → I/O errors out after ~45s instead of # hanging forever. reconnect re-establishes when the host comes back. export ROAMING_SSHFS_OPTS="reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,follow_symlinks" # ── Internal: resolve a host name to its registry fields ────────────────────── # Sets globals: _RH_NAME _RH_FQDN _RH_USER _RH_MOUNT # Returns 1 if not found. _roaming_lookup() { local want="$1" [[ -f "$ROAMING_HOSTS" ]] || return 1 local name fqdn user mount while read -r name fqdn user mount; do [[ -z "$name" || "$name" == \#* ]] && continue if [[ "$name" == "$want" ]]; then _RH_NAME="$name" _RH_FQDN="$fqdn" _RH_USER="${user:-$ROAMING_DEFAULT_USER}" _RH_MOUNT="$mount" return 0 fi done < "$ROAMING_HOSTS" return 1 } # ── Internal: the actual remote-exec logic ──────────────────────────────────── # No args → interactive shell. Args → run with a TTY (so tail -f / sudo work). # "Dumb" by design: pipes/redirects run LOCALLY, exactly like plain ssh. _roaming_exec() { local name="$1"; shift if ! _roaming_lookup "$name"; then echo "roaming: unknown host '$name' (check $ROAMING_HOSTS)" >&2 return 1 fi local target="${_RH_USER}@${_RH_FQDN}" if [[ $# -eq 0 ]]; then ssh -t "$target" else ssh -t "$target" "$@" fi } # ── mount_host ───────────────────────────────────────────────────────── mount_host() { local name="$1" if [[ -z "$name" ]]; then echo "usage: mount_host " >&2 return 1 fi if ! _roaming_lookup "$name"; then echo "roaming: unknown host '$name'" >&2 return 1 fi if [[ -z "$_RH_MOUNT" ]]; then echo "roaming: '$name' has no mount path in registry" >&2 return 1 fi local mp="$ROAMING_MOUNT_BASE/$name" # Already mounted? if mountpoint -q "$mp" 2>/dev/null; then echo "roaming: $name already mounted at $mp" return 0 fi mkdir -p "$mp" echo "roaming: mounting ${_RH_USER}@${_RH_FQDN}:${_RH_MOUNT} → $mp" sshfs "${_RH_USER}@${_RH_FQDN}:${_RH_MOUNT}" "$mp" \ -o "$ROAMING_SSHFS_OPTS" \ && echo "roaming: ✓ mounted" \ || echo "roaming: ✗ mount failed" >&2 } # ── unmount_host ─────────────────────────────────────────────────────── unmount_host() { local name="$1" if [[ -z "$name" ]]; then echo "usage: unmount_host " >&2 return 1 fi local mp="$ROAMING_MOUNT_BASE/$name" if ! mountpoint -q "$mp" 2>/dev/null; then echo "roaming: $name not mounted" return 0 fi # fusermount3 first; -z (lazy) as fallback for stale mounts fusermount3 -u "$mp" 2>/dev/null \ || fusermount3 -uz "$mp" 2>/dev/null \ || umount "$mp" 2>/dev/null if mountpoint -q "$mp" 2>/dev/null; then echo "roaming: ✗ failed to unmount $name (stale? try: fusermount3 -uz $mp)" >&2 return 1 fi rmdir "$mp" 2>/dev/null echo "roaming: ✓ unmounted $name" } # ── roaming-status ──────────────────────────────────────────────────────────── roaming-status() { echo "── registered hosts ──" if [[ -f "$ROAMING_HOSTS" ]]; then local name fqdn user mount while read -r name fqdn user mount; do [[ -z "$name" || "$name" == \#* ]] && continue local mp="$ROAMING_MOUNT_BASE/$name" local mflag="" mountpoint -q "$mp" 2>/dev/null && mflag=" [mounted: $mp]" printf " %-10s %s@%s%s\n" "$name" "${user:-$ROAMING_DEFAULT_USER}" "$fqdn" "$mflag" done < "$ROAMING_HOSTS" else echo " (no registry at $ROAMING_HOSTS)" fi } # ── Generate a function per host ────────────────────────────────────────────── # After this loop, each registry entry `mail` becomes a `mail` command. _roaming_generate() { [[ -f "$ROAMING_HOSTS" ]] || return 0 local name fqdn user mount while read -r name fqdn user mount; do [[ -z "$name" || "$name" == \#* ]] && continue # Skip if it would clobber an existing command (safety) if command -v "$name" >/dev/null 2>&1 && ! typeset -f "$name" >/dev/null 2>&1; then echo "roaming: skipping '$name' — conflicts with existing command" >&2 continue fi eval "$name() { _roaming_exec '$name' \"\$@\"; }" done < "$ROAMING_HOSTS" } # Reload registry + regenerate functions (call after editing hosts) roaming-reload() { _roaming_generate echo "roaming: reloaded $(grep -cvE '^\s*#|^\s*$' "$ROAMING_HOSTS" 2>/dev/null || echo 0) host(s)" } # Generate on source _roaming_generate