178 lines
6.2 KiB
Bash
Executable File
178 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Automate the rustnet VHS recording: produces assets/rustnet.gif (demo)
|
|
# and assets/screenshots/*.png (README) in one pass.
|
|
#
|
|
# Prerequisites (Linux):
|
|
# vhs # required (https://github.com/charmbracelet/vhs)
|
|
# gifsicle # optional, for GIF size optimization
|
|
# cargo build --release # auto-run if target/release/rustnet missing
|
|
#
|
|
# Usage:
|
|
# scripts/record-rustnet-demo.sh
|
|
#
|
|
# What it does:
|
|
# 1. Verifies vhs and cargo are installed.
|
|
# 2. Builds target/release/rustnet if missing or stale vs Cargo.lock.
|
|
# 3. Grants the binary capture capabilities via `sudo setcap`, so the
|
|
# recording runs rustnet UNPRIVILEGED — the Security sidebar shows
|
|
# the sandboxed non-root posture instead of a root warning.
|
|
# 4. Spawns a background traffic generator (curl/dig/ping) so the
|
|
# connection table is busy and sparklines move.
|
|
# 5. Runs `vhs demo.tape` -> assets/rustnet.gif.
|
|
# 6. Runs `vhs screenshots.tape` -> assets/screenshots/*.png.
|
|
# 7. Optimizes the GIF with gifsicle -O3 --lossy=80 if available
|
|
# (terminal flat colors are unaffected by the lossy pass).
|
|
# 8. Cleans up the throwaway GIF and background processes.
|
|
# 9. Prints a summary of the produced assets.
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
RUSTNET_DIR="$(dirname "$SCRIPT_DIR")"
|
|
DEMO_TAPE="${RUSTNET_DIR}/demo.tape"
|
|
SCREENSHOTS_TAPE="${RUSTNET_DIR}/screenshots.tape"
|
|
GIF_FILE="${RUSTNET_DIR}/assets/rustnet.gif"
|
|
SCREENSHOTS_DIR="${RUSTNET_DIR}/assets/screenshots"
|
|
THROWAWAY_GIF="${SCREENSHOTS_DIR}/_throwaway.gif"
|
|
RELEASE_BIN="${RUSTNET_DIR}/target/release/rustnet"
|
|
|
|
TRAFFIC_PID=""
|
|
|
|
# Background traffic generator: HTTPS, DNS, ICMP, multiple processes.
|
|
# Started fresh for each tape with a delay so that every generated
|
|
# connection is born AFTER the tape's rustnet instance (and its eBPF
|
|
# hooks) is up — connections whose process exits before rustnet starts
|
|
# can never be attributed and would show as dead "-" rows.
|
|
start_traffic() {
|
|
(
|
|
sleep 6
|
|
while true; do
|
|
curl -s -o /dev/null --max-time 4 https://example.com || true
|
|
curl -s -o /dev/null --max-time 4 https://github.com || true
|
|
curl -s -o /dev/null --max-time 4 https://wikipedia.org || true
|
|
curl -s -o /dev/null --max-time 4 https://ratatui.rs || true
|
|
dig +short +time=2 +tries=1 example.com >/dev/null 2>&1 || true
|
|
dig +short +time=2 +tries=1 github.com >/dev/null 2>&1 || true
|
|
ping -c 1 -W 1 1.1.1.1 >/dev/null 2>&1 || true
|
|
sleep 1
|
|
done
|
|
) &
|
|
TRAFFIC_PID=$!
|
|
}
|
|
|
|
stop_traffic() {
|
|
if [[ -n "${TRAFFIC_PID}" ]] && kill -0 "${TRAFFIC_PID}" 2>/dev/null; then
|
|
kill "${TRAFFIC_PID}" 2>/dev/null || true
|
|
wait "${TRAFFIC_PID}" 2>/dev/null || true
|
|
fi
|
|
TRAFFIC_PID=""
|
|
}
|
|
|
|
cleanup() {
|
|
stop_traffic
|
|
rm -f "${THROWAWAY_GIF}"
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# Kill stale ttyd / headless-Chrome processes left over from prior failed
|
|
# vhs runs. VHS uses go-rod, which writes its Chrome profile to a
|
|
# `rod/user-data` tmpdir; if a prior run died, the locked profile causes
|
|
# the next run to fail with `could not open ttyd: ERR_CONNECTION_REFUSED`.
|
|
preflight_cleanup_vhs() {
|
|
local stale=0
|
|
if pgrep -f "ttyd --port" >/dev/null 2>&1; then
|
|
pkill -f "ttyd --port" 2>/dev/null || true
|
|
stale=1
|
|
fi
|
|
if pgrep -f "rod/user-data" >/dev/null 2>&1; then
|
|
pkill -f "rod/user-data" 2>/dev/null || true
|
|
stale=1
|
|
fi
|
|
if [[ "${stale}" -eq 1 ]]; then
|
|
echo "Cleaned up stale ttyd / Chrome processes from prior vhs run."
|
|
sleep 1
|
|
fi
|
|
rm -rf "${TMPDIR:-/tmp}/rod" 2>/dev/null || true
|
|
}
|
|
preflight_cleanup_vhs
|
|
|
|
# Dependency checks
|
|
for cmd in vhs cargo setcap; do
|
|
if ! command -v "$cmd" &> /dev/null; then
|
|
echo "Error: $cmd is not installed."
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
if ! command -v gifsicle &> /dev/null; then
|
|
echo "Note: gifsicle not found; skipping GIF optimization."
|
|
HAS_GIFSICLE=0
|
|
else
|
|
HAS_GIFSICLE=1
|
|
fi
|
|
|
|
# Tape files must exist
|
|
[[ -f "${DEMO_TAPE}" ]] || { echo "Error: ${DEMO_TAPE} missing."; exit 1; }
|
|
[[ -f "${SCREENSHOTS_TAPE}" ]] || { echo "Error: ${SCREENSHOTS_TAPE} missing."; exit 1; }
|
|
|
|
# Validate sudo up front (before the potentially long build) so the run
|
|
# doesn't die at the setcap step minutes in. Honors SUDO_ASKPASS when
|
|
# the caller exports one; otherwise prompts on the terminal as usual.
|
|
SUDO=(sudo)
|
|
[[ -n "${SUDO_ASKPASS:-}" ]] && SUDO=(sudo -A)
|
|
echo "setcap needs sudo; validating credentials..."
|
|
"${SUDO[@]}" -v || { echo "Error: sudo authentication failed."; exit 1; }
|
|
|
|
# Build release binary if missing or stale
|
|
if [[ ! -x "${RELEASE_BIN}" ]] || [[ "${RUSTNET_DIR}/Cargo.lock" -nt "${RELEASE_BIN}" ]]; then
|
|
echo "Building release binary..."
|
|
(cd "${RUSTNET_DIR}" && cargo build --release)
|
|
fi
|
|
|
|
# Grant capture capabilities so the tapes can run rustnet unprivileged.
|
|
# File capabilities are dropped whenever cargo rewrites the binary, so
|
|
# (re)apply them on every run.
|
|
echo "Granting capture capabilities (sudo setcap)..."
|
|
"${SUDO[@]}" setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' "${RELEASE_BIN}"
|
|
|
|
mkdir -p "${SCREENSHOTS_DIR}"
|
|
|
|
echo ""
|
|
echo "Rendering demo GIF (vhs demo.tape)..."
|
|
start_traffic
|
|
(cd "${RUSTNET_DIR}" && vhs "${DEMO_TAPE}")
|
|
stop_traffic
|
|
|
|
echo ""
|
|
echo "Rendering screenshots (vhs screenshots.tape)..."
|
|
start_traffic
|
|
(cd "${RUSTNET_DIR}" && vhs "${SCREENSHOTS_TAPE}")
|
|
stop_traffic
|
|
|
|
# Optimize the GIF. The lossy pass shrinks dithered/AA edges; flat
|
|
# terminal colors are visually unaffected.
|
|
if [[ "${HAS_GIFSICLE}" -eq 1 ]] && [[ -f "${GIF_FILE}" ]]; then
|
|
echo ""
|
|
echo "Optimizing GIF with gifsicle -O3 --lossy=80..."
|
|
BEFORE_SIZE=$(stat -f%z "${GIF_FILE}" 2>/dev/null || stat -c%s "${GIF_FILE}")
|
|
gifsicle -O3 --lossy=80 --batch "${GIF_FILE}"
|
|
AFTER_SIZE=$(stat -f%z "${GIF_FILE}" 2>/dev/null || stat -c%s "${GIF_FILE}")
|
|
echo " ${BEFORE_SIZE} -> ${AFTER_SIZE} bytes"
|
|
fi
|
|
|
|
# Summary.
|
|
echo ""
|
|
echo "Done."
|
|
echo ""
|
|
echo "GIF:"
|
|
ls -lh "${GIF_FILE}" 2>/dev/null | awk '{print " " $9 " (" $5 ")"}'
|
|
echo ""
|
|
echo "Screenshots:"
|
|
for png in "${SCREENSHOTS_DIR}"/*.png; do
|
|
[[ -f "${png}" ]] && ls -lh "${png}" | awk '{print " " $9 " (" $5 ")"}'
|
|
done
|
|
echo ""
|
|
echo "Preview:"
|
|
echo " open ${GIF_FILE}"
|
|
echo " open ${SCREENSHOTS_DIR}"
|