#!/usr/bin/env bash
# nf: nohup, but the output goes to a link on nohup.foo instead of ./nohup.out
#
# Install:  curl -fsSL https://nohup.foo/nf -o ~/.local/bin/nf && chmod +x ~/.local/bin/nf

NOHUP_URL="${NOHUP_URL:-https://nohup.foo}"
NOHUP_URL="${NOHUP_URL%/}"

usage() {
  cat <<'EOF'
Usage: nf [-q] [-n NAME] COMMAND [ARG]...

Run COMMAND immune to hangups, like nohup, with its output streamed to a
live link on nohup.foo. When COMMAND finishes, its exit code is sent too.

  -q        don't echo output to this terminal
  -n NAME   name to show in the viewer (default: the command line)
  -h        show this help and exit

The link is printed before COMMAND starts. Close the terminal whenever
you like; COMMAND keeps running and the link keeps updating.

Set NOHUP_URL to use a different server.
EOF
}

quiet=0
name=""
while getopts ":qn:h" opt; do
  case $opt in
    q) quiet=1 ;;
    n) name=$OPTARG ;;
    h) usage; exit 0 ;;
    :) printf "nf: option requires an argument -- '%s'\nTry 'nf -h' for more information.\n" "$OPTARG" >&2; exit 125 ;;
    *) printf "nf: invalid option -- '%s'\nTry 'nf -h' for more information.\n" "$OPTARG" >&2; exit 125 ;;
  esac
done
shift $((OPTIND - 1))

if [ $# -eq 0 ]; then
  printf "nf: missing operand\nTry 'nf -h' for more information.\n" >&2
  exit 125
fi
if ! command -v curl >/dev/null 2>&1; then
  echo "nf: curl is required" >&2
  exit 125
fi

# Pick the id here so the link can be printed before anything runs.
id=$(LC_ALL=C tr -dc 'a-z0-9' </dev/urandom 2>/dev/null | head -c 12)
url="$NOHUP_URL/$id"
[ -n "$name" ] || name="$*"
name_b64=$(printf '%s' "$name" | head -c 300 | base64 | tr -d '\n')

# Like nohup: if input is a terminal, ignore it.
msg="appending output to '$url'"
if [ -t 0 ]; then
  exec </dev/null
  msg="ignoring input and $msg"
fi
echo "nohup: $msg" >&2

# The whole point: hangups are ignored by nf and everything it starts.
trap '' HUP
# Ctrl-C stops COMMAND, but nf and the upload stay alive long enough to report how it ended.
trap ':' INT

exec 3>&1 # this terminal, for the local copy of the output
if [ "$quiet" -eq 1 ]; then echo_to=/dev/null; else echo_to=/dev/fd/3; fi

run() {
  trap ':' INT
  # Keepalive pings, so quiet commands don't look like dead uploads. (sleep must not hold the pipe open.)
  ( trap '' PIPE; while sleep 25 >/dev/null 2>&1; do printf '\036nohup:ping\n' 2>/dev/null; done ) &
  local pinger=$!

  # tee ignores SIGPIPE, so a dropped upload never takes COMMAND down with it.
  "$@" 2>&1 | ( trap '' INT PIPE; exec tee -a "$echo_to" 2>/dev/null )
  local rc=${PIPESTATUS[0]}

  kill "$pinger" 2>/dev/null
  wait "$pinger" 2>/dev/null
  trap '' PIPE
  printf '\036nohup:exit=%d\n' "$rc" 2>/dev/null
  exit "$rc"
}

run "$@" | ( trap '' INT; exec curl -sS -T - -H "X-Nohup-Cmd-B64: $name_b64" "$url" >/dev/null )
statuses=("${PIPESTATUS[@]}")
rc=${statuses[0]}

if [ "${statuses[1]}" -ne 0 ]; then
  echo "nf: upload failed (curl exit ${statuses[1]}). $url may be missing output." >&2
fi
exit "$rc"
