| #!/usr/bin/env bash |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| set -u |
|
|
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| PORT="${PORT:-10000}" |
|
|
| NH_DIR="${NH_DIR:-/opt/9hits}" |
| NH_BIN="${NH_BIN:-$NH_DIR/nhviewer}" |
| NH_DISPLAY="${NH_DISPLAY:-:99}" |
| NH_RESOLUTION="${NH_RESOLUTION:-auto}" |
| INIT_TIMEOUT="${INIT_TIMEOUT:-300}" |
| NH_WATCHDOG="${NH_WATCHDOG:-yes}" |
| NH_WATCHDOG_STUCK="${NH_WATCHDOG_STUCK:-600}" |
| NH_RENDER_TO_TERMINAL="${NH_RENDER_TO_TERMINAL:-yes}" |
|
|
| export PORT |
| export DISPLAY="$NH_DISPLAY" |
| export HOME="${HOME:-/root}" |
| |
| |
| |
| export MALLOC_ARENA_MAX="${MALLOC_ARENA_MAX:-2}" |
| export MALLOC_TRIM_THRESHOLD_="${MALLOC_TRIM_THRESHOLD_:-65536}" |
| export MALLOC_MMAP_THRESHOLD_="${MALLOC_MMAP_THRESHOLD_:-131072}" |
|
|
| log() { printf '[start] %s\n' "$*"; } |
|
|
| _yes() { case "${1:-}" in 1|yes|true|on) return 0 ;; *) return 1 ;; esac; } |
|
|
| |
| |
| |
| detect_mem_limit_mb() { |
| local v="" path |
| for path in /sys/fs/cgroup/memory.max /sys/fs/cgroup/memory/memory.limit_in_bytes; do |
| [ -f "$path" ] || continue |
| v=$(cat "$path" 2>/dev/null || echo "") |
| case "$v" in |
| ""|max) v="" ;; |
| *[!0-9]*) v="" ;; |
| esac |
| |
| if [ -n "$v" ] && [ "$v" -ge 1099511627776 ] 2>/dev/null; then v=""; fi |
| [ -n "$v" ] && { echo $((v / 1024 / 1024)); return 0; } |
| done |
| local kb=0 |
| [ -f /proc/meminfo ] && kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null || echo 0) |
| echo $((kb / 1024)) |
| } |
| MEM_LIMIT_MB=$(detect_mem_limit_mb) |
| export MEM_LIMIT_MB |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| DUAL_VIEWER_MODE="${DUAL_VIEWER_MODE:-auto}" |
| TIME_SLICE="${TIME_SLICE:-1500}" |
| LOW_MEMORY="${LOW_MEMORY:-auto}" |
| CREATE_SWAP="${CREATE_SWAP:-}" |
| NH_SP="${NH_SP:-yes}" |
| FS_SP="${FS_SP:-yes}" |
| FS_GL_MODE="${FS_GL_MODE:-swiftshader}" |
| export DUAL_VIEWER_MODE TIME_SLICE LOW_MEMORY CREATE_SWAP NH_SP FS_SP FS_GL_MODE |
|
|
| |
| LOW_MEM_ON=0 |
| case "$LOW_MEMORY" in |
| off) : ;; |
| balanced|extreme) LOW_MEM_ON=1 ;; |
| auto) |
| if [ "${MEM_LIMIT_MB:-0}" -gt 0 ] && [ "$MEM_LIMIT_MB" -lt 1024 ]; then |
| LOW_MEM_ON=1 |
| fi |
| ;; |
| esac |
| if [ "$LOW_MEM_ON" -eq 1 ]; then |
| log "LOW_MEMORY=$LOW_MEMORY: applying Chromium memory flags (detected ~${MEM_LIMIT_MB} MB limit)" |
| else |
| log "LOW_MEMORY=$LOW_MEMORY: memory flags off (detected ~${MEM_LIMIT_MB} MB limit)" |
| fi |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| NH_MEM_FLAGS=() |
| FS_MEM_FLAGS=() |
| if [ "$LOW_MEM_ON" -eq 1 ]; then |
| NH_MEM_FLAGS=( |
| --disable-gpu --disable-dev-shm-usage --disable-extensions |
| --disable-background-networking --disable-sync |
| --disable-component-extensions-with-background-pages |
| --renderer-process-limit=1 --enable-low-end-device-mode --memory-model=low |
| --js-flags=--max-old-space-size=64 |
| --disk-cache-size=1048576 --media-cache-size=1048576 |
| --disable-features=Translate,BackForwardCache,MediaRouter,OptimizationHints |
| ) |
| |
| |
| |
| |
| |
| FS_MEM_FLAGS=( |
| --disable-extensions --disable-background-networking --disable-sync |
| --disable-component-extensions-with-background-pages |
| --renderer-process-limit=1 --enable-low-end-device-mode --memory-model=low |
| --js-flags=--max-old-space-size=64 |
| --disk-cache-size=1048576 --media-cache-size=1048576 |
| --disable-features=Translate,BackForwardCache,MediaRouter,OptimizationHints |
| ) |
| fi |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| NH_EXTREME_FLAGS=() |
| FS_EXTREME_FLAGS=() |
| if [ "$LOW_MEMORY" = "extreme" ]; then |
| log "LOW_MEMORY=extreme: enabling single-process mode (NH_SP=$NH_SP FS_SP=$FS_SP, FS_GL_MODE=$FS_GL_MODE)" |
| if _yes "$NH_SP"; then |
| NH_EXTREME_FLAGS+=(--single-process --in-process-gpu) |
| fi |
| if _yes "$FS_SP"; then |
| FS_EXTREME_FLAGS+=(--single-process --in-process-gpu) |
| fi |
| case "$FS_GL_MODE" in |
| disable-gpu) log "FS_GL_MODE=disable-gpu: replacing swiftshader with --disable-gpu for FeelingSurf" ;; |
| *) FS_GL_MODE=swiftshader ;; |
| esac |
| fi |
|
|
| |
| |
| FS_RESOLUTION="${FS_RESOLUTION:-}" |
| if [ -z "$FS_RESOLUTION" ]; then |
| if [ "$LOW_MEM_ON" -eq 1 ]; then FS_RESOLUTION=1280x720x24; else FS_RESOLUTION=1920x1080x24; fi |
| fi |
| export FS_RESOLUTION |
| export FS_MEM_FLAGS="${FS_MEM_FLAGS[*]}" |
| export FS_EXTRA_FLAGS="${FS_EXTRA_FLAGS:-}" |
|
|
| |
| |
| |
| crash_streak() { |
| local file="/tmp/$1.crash" now last start |
| now=$(date +%s 2>/dev/null || echo 0) |
| last=$(cat "$file" 2>/dev/null || echo 0) |
| start=$(cat "/tmp/$1.start" 2>/dev/null || echo 0) |
| if [ "$start" -gt 0 ] && [ $((now - start)) -lt 90 ]; then |
| echo $((last + 1)) > "$file" |
| else |
| echo 1 > "$file" |
| fi |
| echo "$(date +%s 2>/dev/null || echo 0)" > "/tmp/$1.start" |
| cat "$file" |
| } |
|
|
| |
| |
| maybe_create_swap() { |
| local size="${1:-256M}" f="/tmp/9hits_swap" |
| command -v swapon >/dev/null 2>&1 || { log "NOTE: swapon not available - skipping swap"; return 0; } |
| if ! fallocate -l "$size" "$f" 2>/dev/null; then |
| local mb="${size%[a-zA-Z]*}" |
| dd if=/dev/zero of="$f" bs=1M count="${mb:-256}" 2>/dev/null || return 0 |
| fi |
| chmod 600 "$f" |
| if mkswap "$f" >/dev/null 2>&1 && swapon "$f" 2>/dev/null; then |
| log "swap enabled: $size at $f (helps Chromium survive peaks)" |
| else |
| rm -f "$f" |
| log "NOTE: swap ($size) requested but swapon is not permitted on this host" |
| fi |
| } |
| if [ -n "$CREATE_SWAP" ]; then |
| maybe_create_swap "$CREATE_SWAP" |
| elif [ "${MEM_LIMIT_MB:-0}" -gt 0 ] && [ "$MEM_LIMIT_MB" -lt 1024 ]; then |
| log "small instance (~${MEM_LIMIT_MB} MB): trying a 256M swap (best-effort)" |
| maybe_create_swap "256M" |
| fi |
|
|
| |
| |
| |
| |
| wait_for_turn() { |
| local name="$1" tries=0 active="" age=0 |
| case "$DUAL_VIEWER_MODE" in |
| concurrent|off) return 0 ;; |
| esac |
| while :; do |
| if [ ! -f /tmp/active_viewer ] || [ ! -f /tmp/memguard.alive ]; then |
| tries=$((tries + 1)) |
| if [ "$tries" -ge 3 ]; then |
| log "memguard turn file not present - running $name anyway (fail-open)" |
| return 0 |
| fi |
| sleep 3 & wait $! |
| continue |
| fi |
| if [ -f /tmp/memguard.alive ]; then |
| age=$(($(date +%s 2>/dev/null || echo 0) - $(stat -c %Y /tmp/memguard.alive 2>/dev/null || echo 0))) |
| if [ "$age" -gt "${MEMGUARD_DEAD_AFTER:-90}" ]; then |
| log "memguard heartbeat stale (${age}s) - running $name anyway (fail-open)" |
| return 0 |
| fi |
| fi |
| active=$(cat /tmp/active_viewer 2>/dev/null || echo both) |
| case "$active" in |
| both|"$name") return 0 ;; |
| esac |
| sleep 5 & wait $! |
| done |
| } |
|
|
| |
| |
| SUPERVISOR_DELAY="${SUPERVISOR_DELAY:-${RESTART_DELAY:-10}}" |
|
|
| |
| NINEHITS_ENABLED="${NINEHITS_ENABLED:-no}" |
| if _yes "$NINEHITS_ENABLED"; then |
| NINEHITS_ENABLED=yes |
| log "9Hits enabled (NINEHITS_ENABLED=yes)" |
| else |
| NINEHITS_ENABLED=no |
| fi |
| export NINEHITS_ENABLED |
|
|
| if _yes "$NINEHITS_ENABLED"; then |
| |
| if [ -n "${BULK_ADD_PROXY_LIST_URL:-}" ] && [ -z "${BULK_ADD_PROXY_LIST:-}" ]; then |
| fetch_err_file=$(mktemp 2>/dev/null || echo "/tmp/fetch_proxy_err.$$") |
| if fetch_out=$(python3 "$SCRIPT_DIR/fetch_proxy_list.py" "$BULK_ADD_PROXY_LIST_URL" 2>"$fetch_err_file"); then |
| BULK_ADD_PROXY_LIST="$fetch_out" |
| BULK_ADD_PROXY_TYPE="${BULK_ADD_PROXY_TYPE:-socks5}" |
| proxy_count=$(awk -F'|' '{print NF}' <<< "$BULK_ADD_PROXY_LIST") |
| log "fetched $proxy_count proxies from BULK_ADD_PROXY_LIST_URL" |
| else |
| err_msg=$(cat "$fetch_err_file" 2>/dev/null || true) |
| [ -z "$err_msg" ] && err_msg="$fetch_out" |
| [ -z "$err_msg" ] && err_msg="unknown error" |
| log "WARNING: failed to fetch proxy list from BULK_ADD_PROXY_LIST_URL ($err_msg)" |
| fi |
| rm -f "$fetch_err_file" |
| fi |
|
|
| |
| |
| NH_ARGS=() |
| kv() { [ -n "${2:-}" ] && NH_ARGS+=("--$1=$2"); } |
| flag() { |
| case "${!1:-}" in |
| 1|yes|true|on) NH_ARGS+=("--$2") ;; |
| esac |
| } |
|
|
| kv access-key "${ACCESS_KEY:-}" |
| kv allow-popups "${ALLOW_POPUPS:-}" |
| kv allow-adult "${ALLOW_ADULT:-}" |
| kv allow-crypto "${ALLOW_CRYPTO:-}" |
| kv hide-browser "${HIDE_BROWSER:-}" |
| kv ex-proxy-sessions "${EX_PROXY_SESSIONS:-}" |
| kv ex-proxy-url "${EX_PROXY_URL:-}" |
| kv bulk-add-proxy-list "${BULK_ADD_PROXY_LIST:-}" |
| kv bulk-add-proxy-type "${BULK_ADD_PROXY_TYPE:-}" |
| kv session-note "${SESSION_NOTE:-}" |
| kv note "${NOTE:-}" |
| kv cache-path "${CACHE_PATH:-}" |
| kv hide-columns "${HIDE_COLUMNS:-}" |
| flag SYSTEM_SESSION system-session |
| flag CLEAR_ALL_SESSIONS clear-all-sessions |
|
|
| |
| |
| NH_ARGS+=("--cache-limit=${CACHE_LIMIT:-209715200}") |
|
|
| |
| |
| NH_ARGS+=("${NH_MEM_FLAGS[@]}") |
|
|
| |
| if [ -n "${EXTRA_ARGS:-}" ]; then |
| read -r -a extra_args <<< "$EXTRA_ARGS" |
| NH_ARGS+=("${extra_args[@]}") |
| fi |
|
|
| |
| NH_ARGS+=("$@") |
|
|
| |
| |
| if [ -n "${INSTALL_DIR:-}" ] && [ "$INSTALL_DIR" != "$NH_DIR" ]; then |
| log "NOTE: --install-dir is ignored; the viewer is baked into the image at $NH_DIR (override with NH_DIR at your own risk)." |
| fi |
| |
| if _yes "${RE_INSTALL:-}" && [ -z "${DEFAULT_DL:-}" ]; then |
| log "NOTE: RE_INSTALL is a no-op now (the viewer is extracted at build time). Use DEFAULT_DL=<url> to swap builds." |
| fi |
|
|
| if [ -z "${ACCESS_KEY:-}" ]; then |
| log "WARNING: ACCESS_KEY is not set - the viewer will report 'User not found!'" |
| fi |
|
|
| |
| if [[ "${EX_PROXY_SESSIONS:-0}" =~ ^[1-9][0-9]*$ ]] && [ -z "${EX_PROXY_URL:-}" ] && [ -z "${BULK_ADD_PROXY_LIST:-}" ]; then |
| log "WARNING: EX_PROXY_SESSIONS is set (${EX_PROXY_SESSIONS}) but EX_PROXY_URL and BULK_ADD_PROXY_LIST are empty. The 9Hits public pool is CLOSED! Every pool session will fail with 'Pool error: The public pool is closed!'. Configure your own pool at https://dash.9hits.com/pool (EX_PROXY_URL) or use BULK_ADD_PROXY_LIST / BULK_ADD_PROXY_LIST_URL." |
| fi |
|
|
| |
| est_sessions=0 |
| if [[ "${EX_PROXY_SESSIONS:-0}" =~ ^[1-9][0-9]*$ ]]; then |
| est_sessions=$((est_sessions + EX_PROXY_SESSIONS)) |
| fi |
|
|
| if [ -n "${BULK_ADD_PROXY_LIST:-}" ]; then |
| bulk_proxies=$(awk -F'|' '{print NF}' <<< "$BULK_ADD_PROXY_LIST") |
| est_sessions=$((est_sessions + bulk_proxies)) |
| fi |
|
|
| _yes "${SYSTEM_SESSION:-}" && est_sessions=$((est_sessions + 1)) |
|
|
| mem_total_mb=0 |
| if [ -f /proc/meminfo ]; then |
| mem_total_kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null || echo 0) |
| mem_total_mb=$((mem_total_kb / 1024)) |
| fi |
|
|
| if [ "$mem_total_mb" -gt 0 ] && [ "$mem_total_mb" -lt 1024 ] && [ "$est_sessions" -ge 4 ]; then |
| log "WARNING: detected ${mem_total_mb} MB RAM with ~${est_sessions} estimated sessions. Instances with < 1024 MB RAM (e.g. Render free tier 512 MB) may experience OOM kills with 4+ sessions. Consider lowering the session count (5-6 is recommended maximum) or using a bigger instance." |
| fi |
| if [ "$mem_total_mb" -gt 0 ] && [ "$mem_total_mb" -lt 2048 ]; then |
| log "NOTE: 9Hits v6 officially recommends >= 2 GB RAM per instance; this box has ~${mem_total_mb} MB. If sessions crash-loop, disable FeelingSurf (FEELINGSURF_ENABLED=no), run fewer sessions, or use a bigger instance." |
| fi |
|
|
| |
| display_args() { |
| local out=() a |
| for a in "${NH_ARGS[@]}"; do |
| case "$a" in |
| --access-key=*) out+=("--access-key=****") ;; |
| --bulk-add-proxy-list=*) out+=("--bulk-add-proxy-list=****") ;; |
| *) out+=("$a") ;; |
| esac |
| done |
| echo "${out[*]}" |
| } |
|
|
| |
| |
| |
| |
| fix_shm() { |
| local cur |
| cur=$(df -m /dev/shm 2>/dev/null | awk 'NR==2 {print $2}') |
| if [ -n "${cur:-}" ] && [ "$cur" -lt 512 ]; then |
| if mount -o remount,size=1g /dev/shm 2>/dev/null; then |
| log "enlarged /dev/shm to 1g" |
| else |
| log "NOTE: /dev/shm is only ${cur:-?} MB and cannot be resized here (no CAP_SYS_ADMIN). Where supported, run with --shm-size=2g (already set in docker-compose.yml)." |
| fi |
| fi |
| } |
|
|
| |
| pick_resolution() { |
| if [ "$NH_RESOLUTION" != "auto" ]; then |
| echo "$NH_RESOLUTION"; return |
| fi |
| |
| if [ "$LOW_MEM_ON" -eq 1 ]; then |
| echo "1280x720x24"; return |
| fi |
| local cores |
| cores=$(nproc 2>/dev/null || echo 1) |
| if [ "$cores" -ge 4 ] && [ "$mem_total_mb" -ge 4000 ]; then |
| echo "2560x1440x24" |
| else |
| echo "1920x1080x24" |
| fi |
| } |
|
|
| xvfb_supervisor() { |
| trap 'log "Xvfb supervisor stopped"; exit 0' TERM INT |
| if ! command -v Xvfb >/dev/null 2>&1; then |
| |
| log "Xvfb not installed on this runtime - 9Hits display supervisor disabled" |
| while :; do sleep 300 & wait $!; done |
| fi |
| local dnum="${NH_DISPLAY#:}"; dnum="${dnum%%.*}" |
| local res; res=$(pick_resolution) |
| while :; do |
| rm -f "/tmp/.X${dnum}-lock" "/tmp/.X11-unix/X${dnum}" 2>/dev/null || true |
| log "starting Xvfb $NH_DISPLAY ($res) for 9Hits" |
| Xvfb "$NH_DISPLAY" -screen 0 "$res" -nolisten tcp & |
| local xpid=$! |
| echo "$xpid" > /tmp/xvfb.pid |
| wait "$xpid" |
| local code=$? |
| rm -f /tmp/xvfb.pid |
| log "Xvfb exited (code $code) - restarting in 3s" |
| sleep 3 & wait $! |
| done |
| } |
|
|
| wait_display() { |
| local i |
| for i in $(seq 1 60); do |
| if command -v xdpyinfo >/dev/null 2>&1; then |
| xdpyinfo -display "$NH_DISPLAY" >/dev/null 2>&1 && return 0 |
| else |
| [ -S "/tmp/.X11-unix/X${NH_DISPLAY#:}" ] && return 0 |
| fi |
| sleep 0.5 |
| done |
| log "WARNING: X display $NH_DISPLAY not ready after 30s (continuing anyway)" |
| return 1 |
| } |
|
|
| |
| vnc_supervisor() { |
| trap 'log "VNC supervisor stopped"; exit 0' TERM INT |
| local port="${VNC_PORT:-5901}" auth_args=() |
| if [ -n "${VNC_PW:-}" ]; then |
| mkdir -p "$HOME/.x11vnc" |
| x11vnc -storepasswd "$VNC_PW" "$HOME/.x11vnc/passwd" >/dev/null 2>&1 |
| chmod 600 "$HOME/.x11vnc/passwd" |
| auth_args=(-rfbauth "$HOME/.x11vnc/passwd") |
| elif _yes "${NO_VNC_PW:-}"; then |
| auth_args=(-nopw) |
| log "WARNING: VNC has NO password (--no-vnc-pw). Anyone can watch/control the viewer display." |
| else |
| VNC_PW=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom 2>/dev/null | head -c 8) |
| mkdir -p "$HOME/.x11vnc" |
| x11vnc -storepasswd "$VNC_PW" "$HOME/.x11vnc/passwd" >/dev/null 2>&1 |
| chmod 600 "$HOME/.x11vnc/passwd" |
| auth_args=(-rfbauth "$HOME/.x11vnc/passwd") |
| log "VNC auto-generated password: $VNC_PW (set your own with VNC_PW=...)" |
| fi |
| while :; do |
| log "starting x11vnc on port $port (mirroring $NH_DISPLAY)" |
| x11vnc -display "$NH_DISPLAY" -rfbport "$port" -forever -shared -noxdamage -quiet "${auth_args[@]}" & |
| local vpid=$! |
| wait "$vpid" |
| log "x11vnc exited (code $?) - restarting in 3s" |
| sleep 3 & wait $! |
| done |
| } |
|
|
| |
| |
| |
| maybe_update_viewer() { |
| [ -n "${DEFAULT_DL:-}" ] || return 0 |
| local target="/tmp/nhviewer-download" src="" sub="" |
| log "DEFAULT_DL set - downloading viewer from $DEFAULT_DL" |
| if wget -q --tries=3 --timeout=60 -O "$target" "$DEFAULT_DL"; then |
| rm -rf /tmp/nh.new |
| mkdir -p /tmp/nh.new |
| if tar -xf "$target" -C /tmp/nh.new 2>/dev/null; then |
| |
| if [ -f /tmp/nh.new/nhviewer ]; then |
| src=/tmp/nh.new |
| else |
| sub=$(find /tmp/nh.new -mindepth 1 -maxdepth 1 -type d | head -n 1) |
| if [ -n "$sub" ] && [ -f "$sub/nhviewer" ]; then |
| src="$sub" |
| fi |
| fi |
| fi |
| fi |
| rm -f "$target" |
| if [ -z "$src" ]; then |
| log "WARNING: DEFAULT_DL download/extract failed - keeping the baked-in viewer" |
| rm -rf /tmp/nh.new |
| return 1 |
| fi |
| rm -rf "${NH_DIR}.old" |
| if [ -d "$NH_DIR" ]; then |
| mv "$NH_DIR" "${NH_DIR}.old" |
| fi |
| if [ "$src" != /tmp/nh.new ]; then |
| mv "$src" "$NH_DIR" |
| rm -rf /tmp/nh.new |
| else |
| mv /tmp/nh.new "$NH_DIR" |
| fi |
| chmod -R a+rwX "$NH_DIR" |
| chmod +x "$NH_BIN" 2>/dev/null || true |
| rm -rf "${NH_DIR}.old" |
| log "viewer replaced from DEFAULT_DL" |
| } |
|
|
| |
| |
| |
| run_init_pass() { |
| log "init pass: nhviewer $(display_args) --exit-on-init" |
| ( |
| cd "$NH_DIR" 2>/dev/null || exit 1 |
| timeout --kill-after=15s "${INIT_TIMEOUT}s" \ |
| "$NH_BIN" "${NH_ARGS[@]}" --exit-on-init |
| ) |
| } |
|
|
| nh_supervisor() { |
| trap 'log "9Hits supervisor stopped"; exit 0' TERM INT |
|
|
| maybe_update_viewer |
|
|
| if [ ! -x "$NH_BIN" ]; then |
| |
| |
| |
| log "ERROR: $NH_BIN is missing - 9Hits cannot start on this runtime (use the Docker image). Checking again every 5 min." |
| while [ ! -x "$NH_BIN" ]; do |
| echo "down" > /tmp/viewer.state |
| sleep 300 & wait $! |
| done |
| fi |
|
|
| wait_display || true |
|
|
| local stuck=0 |
| _yes "$NH_WATCHDOG" && stuck="$NH_WATCHDOG_STUCK" |
| NH_SP_ON="$NH_SP" |
|
|
| while :; do |
| |
| wait_for_turn ninehits |
| wait_display || true |
|
|
| local run_args=(--auto-start --in-loop) |
| _yes "$NH_RENDER_TO_TERMINAL" && run_args+=(--render-to-terminal) |
| [ -n "${RESET_INTERVAL:-}" ] && run_args+=("--reset-interval=${RESET_INTERVAL}") |
| |
| |
| run_args+=("${NH_MEM_FLAGS[@]}") |
| |
| |
| if [ "$LOW_MEMORY" = "extreme" ] && _yes "$NH_SP_ON"; then |
| if [ -f /tmp/viewer.restarts ]; then |
| local streak |
| streak=$(crash_streak viewer) |
| if [ "$streak" -ge 3 ]; then |
| NH_SP_ON=no |
| log "9Hits crashed $streak times at startup - disabling single-process for 9Hits (balanced flags only)" |
| fi |
| fi |
| if _yes "$NH_SP_ON"; then |
| run_args+=("${NH_EXTREME_FLAGS[@]}") |
| fi |
| fi |
| |
| if [ -n "${NH_RUN_EXTRA_ARGS:-}" ]; then |
| read -r -a _run_extra <<< "$NH_RUN_EXTRA_ARGS" |
| run_args+=("${_run_extra[@]}") |
| fi |
|
|
| echo "init" > /tmp/viewer.state |
| local attempt rc init_ok=0 |
| for attempt in 1 2 3; do |
| run_init_pass |
| rc=$? |
| if [ "$rc" -eq 0 ]; then |
| init_ok=1 |
| break |
| fi |
| log "WARNING: 9Hits init pass failed/timed out (attempt $attempt/3, code $rc) - retrying in ${SUPERVISOR_DELAY}s" |
| sleep "${SUPERVISOR_DELAY}" & wait $! |
| wait_display || true |
| done |
| [ "$init_ok" -ne 1 ] && log "WARNING: init pass failed 3x - launching run pass anyway (will re-init on next restart)" |
|
|
| |
| |
| wait_for_turn ninehits |
|
|
| echo "run" > /tmp/viewer.state |
| touch /tmp/viewer.lastoutput 2>/dev/null || true |
| log "launching 9Hits viewer: nhviewer ${run_args[*]} (config: $(display_args))" |
| ( |
| cd "$NH_DIR" 2>/dev/null || exit 1 |
| exec python3 "$SCRIPT_DIR/run_pty.py" \ |
| --heartbeat-file /tmp/viewer.lastoutput \ |
| --watchdog-stuck "$stuck" \ |
| -- "$NH_BIN" "${run_args[@]}" |
| ) & |
| local vpid=$! |
| echo "$vpid" > /tmp/viewer.pid |
| wait "$vpid" |
| local code=$? |
| rm -f /tmp/viewer.pid |
| echo "down" > /tmp/viewer.state |
| local restarts=0 |
| [ -f /tmp/viewer.restarts ] && restarts=$(cat /tmp/viewer.restarts 2>/dev/null || echo 0) |
| echo $((restarts + 1)) > /tmp/viewer.restarts |
| log "9Hits viewer exited (code $code) - restarting in ${SUPERVISOR_DELAY}s" |
| |
| |
| sleep "${SUPERVISOR_DELAY}" & wait $! |
| done |
| } |
|
|
| fix_shm |
|
|
| xvfb_supervisor & |
| export XVFB_SUPERVISOR_PID=$! |
|
|
| nh_supervisor & |
| export SUPERVISOR_PID=$! |
|
|
| VNC_SUPERVISOR_PID=0 |
| if _yes "${VNC:-}" || [ -n "${VNC_PW:-}" ] || _yes "${NO_VNC_PW:-}"; then |
| vnc_supervisor & |
| VNC_SUPERVISOR_PID=$! |
| fi |
| export VNC_SUPERVISOR_PID |
| else |
| |
| |
| |
| |
| log "9Hits disabled (NINEHITS_ENABLED=no) - running FeelingSurf-only + /health; set NINEHITS_ENABLED=yes to run both viewers (LOW_MEMORY + memguard keep them inside small plans)" |
| export SUPERVISOR_PID=0 XVFB_SUPERVISOR_PID=0 VNC_SUPERVISOR_PID=0 |
| fi |
|
|
| |
| |
| feelingsurf_supervisor() { |
| trap 'log "FeelingSurf supervisor stopped"; exit 0' TERM INT |
| FS_SP_ON="$FS_SP" |
| while :; do |
| |
| wait_for_turn feelingsurf |
| |
| |
| if [ "$LOW_MEMORY" = "extreme" ] && _yes "$FS_SP_ON"; then |
| if [ -f /tmp/feelingsurf.restarts ]; then |
| local streak |
| streak=$(crash_streak feelingsurf) |
| if [ "$streak" -ge 3 ]; then |
| FS_SP_ON=no |
| log "FeelingSurf crashed $streak times at startup - disabling single-process for FeelingSurf (balanced flags only)" |
| fi |
| fi |
| fi |
| export FS_SP="$FS_SP_ON" |
| log "launching FeelingSurf viewer (FS_SP=$FS_SP_ON FS_GL_MODE=$FS_GL_MODE)" |
| if [ "$(id -u)" = "0" ] && id fsviewer >/dev/null 2>&1; then |
| runuser -u fsviewer -- "$SCRIPT_DIR/feelingsurf-run.sh" & |
| else |
| |
| "$SCRIPT_DIR/feelingsurf-run.sh" & |
| fi |
| local fpid=$! |
| echo "$fpid" > /tmp/feelingsurf.pid |
| wait "$fpid" |
| local code=$? |
| rm -f /tmp/feelingsurf.pid |
| local restarts=0 |
| [ -f /tmp/feelingsurf.restarts ] && restarts=$(cat /tmp/feelingsurf.restarts 2>/dev/null || echo 0) |
| echo $((restarts + 1)) > /tmp/feelingsurf.restarts |
| log "FeelingSurf exited (code $code) - restarting in ${SUPERVISOR_DELAY}s" |
| sleep "${SUPERVISOR_DELAY}" & wait $! |
| done |
| } |
|
|
| case "${FEELINGSURF_ENABLED:-yes}" in |
| 0|no|false|off) |
| export FEELINGSURF_SUPERVISOR_PID=0 |
| log "FeelingSurf disabled by FEELINGSURF_ENABLED=${FEELINGSURF_ENABLED}" |
| ;; |
| *) |
| if [ ! -x /usr/bin/FeelingSurfViewer ]; then |
| |
| export FEELINGSURF_ENABLED=no FEELINGSURF_SUPERVISOR_PID=0 |
| log "FeelingSurf binary not installed on this runtime - disabling FeelingSurf" |
| else |
| if [ -z "${access_token:-${ACCESS_TOKEN:-}}" ]; then |
| log "WARNING: ACCESS_TOKEN is not set - FeelingSurf cannot authenticate" |
| fi |
| feelingsurf_supervisor & |
| export FEELINGSURF_SUPERVISOR_PID=$! |
| fi |
| ;; |
| esac |
|
|
| |
| |
| if ! _yes "$NINEHITS_ENABLED" && [ "${FEELINGSURF_SUPERVISOR_PID:-0}" -eq 0 ]; then |
| log "WARNING: both viewers are disabled (NINEHITS_ENABLED=no and FEELINGSURF_ENABLED=no/absent) - only /health will run" |
| fi |
|
|
| if _yes "$NINEHITS_ENABLED"; then |
| log "9Hits supervisor pid=$SUPERVISOR_PID (Xvfb $NH_DISPLAY pid=$XVFB_SUPERVISOR_PID)" |
| [ "$VNC_SUPERVISOR_PID" -gt 1 ] && log "VNC supervisor pid=$VNC_SUPERVISOR_PID (port ${VNC_PORT:-5901})" |
| fi |
| [ "${FEELINGSURF_SUPERVISOR_PID:-0}" -gt 1 ] && log "FeelingSurf supervisor pid=$FEELINGSURF_SUPERVISOR_PID" |
|
|
| |
| |
| |
| |
| if [ "$DUAL_VIEWER_MODE" != "off" ] && [ -f "$SCRIPT_DIR/memguard.py" ]; then |
| python3 "$SCRIPT_DIR/memguard.py" & |
| export MEMGUARD_PID=$! |
| log "memguard started (DUAL_VIEWER_MODE=$DUAL_VIEWER_MODE, TIME_SLICE=${TIME_SLICE}s, pid=$MEMGUARD_PID)" |
| else |
| export MEMGUARD_PID=0 |
| log "memguard disabled (DUAL_VIEWER_MODE=$DUAL_VIEWER_MODE) - no OOM protection; both viewers may not fit small plans" |
| fi |
|
|
| log "combined health endpoint on 0.0.0.0:$PORT (GET /health)" |
| exec python3 "$SCRIPT_DIR/health_server.py" |
|
|