230 lines
6.6 KiB
Bash
Executable File
230 lines
6.6 KiB
Bash
Executable File
#!/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/tcp/127.0.0.1/${VNC_PORT}) 2>/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
|