#!/usr/bin/env bash # Start Xvfb + x11vnc + noVNC so a human can complete Reddit login # through the dashboard, then exec the Node bot which drives Chromium # on that virtual display. set -euo pipefail DISPLAY_NUM="${VNC_DISPLAY_NUM:-99}" VNC_PORT="${VNC_PORT:-5900}" NOVNC_PORT="${NOVNC_PORT:-6080}" GEOMETRY="${SCREEN_GEOMETRY:-1280x800x24}" BROWSER_SESSION_DIR="${BROWSER_SESSION_DIR:-/app/data/browser-session}" CHROME_CDP_PORT="${CHROME_CDP_PORT:-9222}" GOOGLE_CHROME_BIN="${GOOGLE_CHROME_BIN:-google-chrome}" NATIVE_LOGIN_EXIT_CODE=75 export CHROME_CDP_URL="${CHROME_CDP_URL:-http://127.0.0.1:${CHROME_CDP_PORT}}" export DISPLAY=":${DISPLAY_NUM}" # noVNC ships under different paths depending on the distro; find the web root once. if [ -d /usr/share/novnc ]; then NOVNC_WEB_DIR=/usr/share/novnc elif [ -d /usr/share/webapps/novnc ]; then NOVNC_WEB_DIR=/usr/share/webapps/novnc else echo "[entrypoint] could not locate noVNC web root" >&2 exit 1 fi # Make / on the noVNC server land on vnc.html with autoconnect. if [ ! -e "${NOVNC_WEB_DIR}/index.html" ] && [ -e "${NOVNC_WEB_DIR}/vnc.html" ]; then ln -sf vnc.html "${NOVNC_WEB_DIR}/index.html" fi # Clear any stale lock from a prior unclean exit — otherwise Xvfb refuses # to bind to the display and the bot starts up with no X server. rm -f "/tmp/.X${DISPLAY_NUM}-lock" "/tmp/.X11-unix/X${DISPLAY_NUM}" 2>/dev/null || true # Persistent profiles retain Chromium's singleton artifacts when a container # is replaced before Chromium exits cleanly. Remove only those locks; cookies, # local storage, and the rest of the authenticated profile remain untouched. rm -f \ "${BROWSER_SESSION_DIR}/SingletonLock" \ "${BROWSER_SESSION_DIR}/SingletonSocket" \ "${BROWSER_SESSION_DIR}/SingletonCookie" \ 2>/dev/null || true echo "[entrypoint] starting Xvfb on ${DISPLAY} (${GEOMETRY})" Xvfb "${DISPLAY}" -screen 0 "${GEOMETRY}" -ac +extension RANDR & XVFB_PID=$! # Give Xvfb a moment to come up. Requires x11-utils (xdpyinfo). XVFB_READY=0 for i in $(seq 1 50); do if xdpyinfo -display "${DISPLAY}" >/dev/null 2>&1; then echo "[entrypoint] Xvfb is ready after ${i} checks" XVFB_READY=1 break fi sleep 0.2 done if [ "${XVFB_READY}" != "1" ]; then echo "[entrypoint] FATAL: Xvfb did not respond after 10s — refusing to launch bot without a display" >&2 exit 1 fi # Minimal window manager so dialogs and tabs aren't stuck behind the root window. if command -v fluxbox >/dev/null 2>&1; then fluxbox >/dev/null 2>&1 & fi VNC_ARGS=(-display "${DISPLAY}" -forever -shared -rfbport "${VNC_PORT}") if [ -n "${VNC_PASSWORD:-}" ]; then mkdir -p /root/.vnc x11vnc -storepasswd "${VNC_PASSWORD}" /root/.vnc/passwd >/dev/null VNC_ARGS+=(-rfbauth /root/.vnc/passwd) echo "[entrypoint] x11vnc starting with password auth" else VNC_ARGS+=(-nopw) echo "[entrypoint] WARNING: VNC_PASSWORD not set — VNC has no auth" fi x11vnc "${VNC_ARGS[@]}" >/var/log/x11vnc.log 2>&1 & X11VNC_PID=$! VNC_READY=0 for i in $(seq 1 50); do if ! kill -0 "${X11VNC_PID}" 2>/dev/null; then echo "[entrypoint] FATAL: x11vnc exited before becoming ready" >&2 cat /var/log/x11vnc.log >&2 exit 1 fi if (: /dev/null; then echo "[entrypoint] VNC server is ready after ${i} checks" VNC_READY=1 break fi sleep 0.2 done if [ "${VNC_READY}" != "1" ]; then echo "[entrypoint] FATAL: x11vnc did not listen on port ${VNC_PORT} after 10s" >&2 cat /var/log/x11vnc.log >&2 exit 1 fi echo "[entrypoint] starting noVNC (websockify) on :${NOVNC_PORT}" websockify --web "${NOVNC_WEB_DIR}" "${NOVNC_PORT}" "localhost:${VNC_PORT}" \ >/var/log/websockify.log 2>&1 & NOVNC_PID=$! CHROME_PID="" BOT_PID="" GATE_PID="" clear_chrome_locks() { rm -f \ "${BROWSER_SESSION_DIR}/SingletonLock" \ "${BROWSER_SESSION_DIR}/SingletonSocket" \ "${BROWSER_SESSION_DIR}/SingletonCookie" \ 2>/dev/null || true } stop_chrome() { if [ -n "${CHROME_PID}" ]; then kill "${CHROME_PID}" 2>/dev/null || true wait "${CHROME_PID}" 2>/dev/null || true CHROME_PID="" fi clear_chrome_locks } start_native_login_chrome() { unset CHROME_CDP_URL echo "[entrypoint] starting native Google Chrome login with DevTools disabled" "${GOOGLE_CHROME_BIN}" \ --user-data-dir="${BROWSER_SESSION_DIR}" \ --no-first-run \ --no-default-browser-check \ --disable-dev-shm-usage \ --no-sandbox \ --start-maximized \ https://www.reddit.com/login/ \ >/var/log/google-chrome-native.log 2>&1 & CHROME_PID=$! } start_controlled_chrome() { export CHROME_CDP_URL="http://127.0.0.1:${CHROME_CDP_PORT}" local chrome_ready=0 echo "[entrypoint] starting Google Chrome for authenticated bot control" "${GOOGLE_CHROME_BIN}" \ --user-data-dir="${BROWSER_SESSION_DIR}" \ --remote-debugging-address=127.0.0.1 \ --remote-debugging-port="${CHROME_CDP_PORT}" \ --no-first-run \ --no-default-browser-check \ --disable-dev-shm-usage \ --no-sandbox \ --start-maximized \ about:blank \ >/var/log/google-chrome.log 2>&1 & CHROME_PID=$! for i in $(seq 1 100); do if ! kill -0 "${CHROME_PID}" 2>/dev/null; then break fi if curl -fsS "${CHROME_CDP_URL}/json/version" >/dev/null 2>&1; then echo "[entrypoint] Google Chrome is ready after ${i} checks" chrome_ready=1 break fi sleep 0.2 done if [ "${chrome_ready}" != "1" ]; then echo "[entrypoint] WARNING: Google Chrome failed to start; using Playwright fallback" >&2 stop_chrome unset CHROME_CDP_URL fi } cleanup() { echo "[entrypoint] shutting down" [ -n "${BOT_PID}" ] && kill "${BOT_PID}" 2>/dev/null || true [ -n "${GATE_PID}" ] && kill "${GATE_PID}" 2>/dev/null || true stop_chrome kill "${NOVNC_PID}" "${X11VNC_PID}" "${XVFB_PID}" 2>/dev/null || true wait || true } trap cleanup TERM INT EXIT if ! command -v "${GOOGLE_CHROME_BIN}" >/dev/null 2>&1; then echo "[entrypoint] WARNING: Google Chrome is unavailable; using Playwright fallback" >&2 unset CHROME_CDP_URL else start_controlled_chrome fi while true; do echo "[entrypoint] launching bot" set +e node dist/index.js & BOT_PID=$! wait "${BOT_PID}" BOT_EXIT=$? BOT_PID="" set -e if [ "${BOT_EXIT}" -ne "${NATIVE_LOGIN_EXIT_CODE}" ]; then exit "${BOT_EXIT}" fi echo "[entrypoint] bot requested native Reddit login mode" stop_chrome start_native_login_chrome node dist/native-login-gate.js & GATE_PID=$! wait "${GATE_PID}" GATE_EXIT=$? GATE_PID="" if [ "${GATE_EXIT}" -ne 0 ]; then exit "${GATE_EXIT}" fi echo "[entrypoint] human completed login handoff; enabling authenticated control" stop_chrome start_controlled_chrome done