#!/usr/bin/env bash # Per-bot X11 screens inside one Team computer. # Slot 0 is DISPLAY :1 / VNC 5900 / view 6080 (primary). # Slot N is DISPLAY :(N+1) / VNC 5900+N / view 6080+N. set -euo pipefail export HOME="${HOME:-/home/lazyboy}" export SHELL=/bin/zsh export TERM="${TERM:-xterm-256color}" export LANG="${LANG:-zh_TW.UTF-8}" export LC_ALL="${LC_ALL:-zh_TW.UTF-8}" export LANGUAGE="${LANGUAGE:-zh_TW:zh:en}" export GTK_MODULES="${GTK_MODULES:-atk-bridge}" export GTK_A11Y="${GTK_A11Y:-atspi}" export GNOME_ACCESSIBILITY="${GNOME_ACCESSIBILITY:-1}" export NO_AT_BRIDGE="${NO_AT_BRIDGE:-0}" export PATH="$HOME/.local/bin:/usr/local/bin:$PATH" ROOT=/tmp/lazyboy LIMIT=8 cmd="${1:-}" shift || true mkdir -p "$ROOT/screens" /tmp/.X11-unix "$HOME/.browser-profiles" hydrate_home() { mkdir -p "$HOME/.config/xfce4/terminal" "$HOME/.cache" "$HOME/.local/bin" if [[ ! -f "$HOME/.zshrc" && -f /usr/share/lazyboy/skel/zshrc ]]; then cp /usr/share/lazyboy/skel/zshrc "$HOME/.zshrc" fi if [[ ! -f "$HOME/.p10k.zsh" && -f /usr/share/lazyboy/skel/p10k.zsh ]]; then cp /usr/share/lazyboy/skel/p10k.zsh "$HOME/.p10k.zsh" fi if [[ ! -f "$HOME/.config/xfce4/terminal/terminalrc" && -f /usr/share/lazyboy/skel/terminalrc ]]; then cp /usr/share/lazyboy/skel/terminalrc "$HOME/.config/xfce4/terminal/terminalrc" fi } hydrate_xfce() { local xfce_home="$1" mkdir -p \ "$xfce_home/.config/xfce4/xfconf/xfce-perchannel-xml" \ "$xfce_home/.config/xfce4/panel" \ "$xfce_home/.cache" \ "$xfce_home/.local/share" \ "$xfce_home/runtime" chmod 700 "$xfce_home/runtime" 2>/dev/null || true local xml="$xfce_home/.config/xfce4/xfconf/xfce-perchannel-xml" local skel_xml=/usr/share/lazyboy/xfce-skel/xfce4/xfconf/xfce-perchannel-xml local f for f in xfce4-panel.xml xfwm4.xml xfce4-desktop.xml thunar.xml; do if [[ -r "$skel_xml/$f" ]]; then cp -f "$skel_xml/$f" "$xml/$f" || true fi done if [[ -r /etc/xdg/xfce4/helpers.rc ]]; then cp -f /etc/xdg/xfce4/helpers.rc "$xfce_home/.config/xfce4/helpers.rc" || true fi } wait_display() { local display="$1" local n for n in $(seq 1 100); do if xdpyinfo -display "$display" >/dev/null 2>&1; then return 0 fi sleep 0.1 done return 1 } port_open() { local port="$1" python3 - "$port" <<'PY' import socket, sys port = int(sys.argv[1]) try: s = socket.create_connection(("127.0.0.1", port), 0.2) s.close() except OSError: sys.exit(1) PY } wait_port() { local port="$1" local n for n in $(seq 1 50); do if port_open "$port"; then return 0 fi sleep 0.1 done return 1 } start_xvfb() { local display="$1" local number="$2" local log="$3" if xdpyinfo -display "$display" >/dev/null 2>&1; then return 0 fi rm -f "/tmp/.X${number}-lock" "/tmp/.X11-unix/X${number}" Xvfb "$display" -screen 0 1280x800x24 -ac +extension RANDR +render -noreset >>"${log}-xvfb.log" 2>&1 & echo $! > "${log}-xvfb.pid" wait_display "$display" } alive_pidfile() { local file="$1" [[ -f "$file" ]] || return 1 local pid pid="$(cat "$file" 2>/dev/null || true)" [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null } start_atspi() { local display="$1" local log="$2" local number="${display#:}" if [[ -n "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then printf '%s\n' "$DBUS_SESSION_BUS_ADDRESS" >"$ROOT/screen-${number}.dbus" fi if [[ -n "${XDG_RUNTIME_DIR:-}" ]]; then printf '%s\n' "$XDG_RUNTIME_DIR" >"$ROOT/screen-${number}.runtime" fi local launcher="" registry="" local c for c in /usr/libexec/at-spi-bus-launcher /usr/lib/at-spi2-core/at-spi-bus-launcher at-spi-bus-launcher; do if [[ -x "$c" ]] || command -v "$c" >/dev/null 2>&1; then launcher="$c" break fi done for c in /usr/libexec/at-spi2-registryd /usr/lib/at-spi2-core/at-spi2-registryd at-spi2-registryd; do if [[ -x "$c" ]] || command -v "$c" >/dev/null 2>&1; then registry="$c" break fi done if [[ -n "$launcher" ]]; then DISPLAY="$display" "$launcher" --launch-immediately >>"${log}-atspi-bus.log" 2>&1 & echo $! >"${log}-atspi-bus.pid" fi if [[ -n "$registry" ]]; then DISPLAY="$display" "$registry" >>"${log}-atspi-registry.log" 2>&1 & echo $! >"${log}-atspi-registry.pid" fi sleep 0.2 } start_desktop() { local display="$1" local xfce_home="$2" local log="$3" if alive_pidfile "${log}-panel.pid"; then return 0 fi hydrate_xfce "$xfce_home" DISPLAY="$display" xsetroot -solid "#0f172a" >/dev/null 2>&1 || true export DISPLAY="$display" export XDG_CONFIG_HOME="$xfce_home/.config" export XDG_CACHE_HOME="$xfce_home/.cache" export XDG_DATA_HOME="$xfce_home/.local/share" export XDG_RUNTIME_DIR="$xfce_home/runtime" export XDG_CURRENT_DESKTOP=XFCE if command -v dbus-launch >/dev/null 2>&1; then eval "$(dbus-launch --sh-syntax)" echo "${DBUS_SESSION_BUS_PID:-}" >"${log}-dbus.pid" fi start_atspi "$display" "$log" if command -v xfconfd >/dev/null 2>&1; then xfconfd --daemon >/dev/null 2>&1 || true fi if command -v xfwm4 >/dev/null 2>&1; then xfwm4 --compositor=off --display="$display" --sm-client-disable >>"${log}-wm.log" 2>&1 & echo $! >"${log}-wm.pid" sleep 0.3 xfdesktop --disable-wm-check --sm-client-disable >>"${log}-desktop.log" 2>&1 & echo $! >"${log}-desktop.pid" xfce4-panel --disable-wm-check --sm-client-disable >>"${log}-panel.log" 2>&1 & echo $! >"${log}-panel.pid" else echo "XFCE is missing" >&2 return 1 fi sleep 0.4 } start_vnc() { local display="$1" local vnc_port="$2" local view_port="$3" local log="$4" if ! port_open "$vnc_port"; then x11vnc -display "$display" -forever -shared -nopw -listen 127.0.0.1 -rfbport "$vnc_port" \ -xkb -repeat -cursor arrow -noxdamage -ncache 0 >>"${log}-x11vnc.log" 2>&1 & fi if ! port_open "$view_port"; then local novnc=/usr/share/novnc if [[ ! -d "$novnc" ]]; then echo "noVNC is missing" >&2 return 1 fi websockify --heartbeat=30 --web="$novnc" "0.0.0.0:${view_port}" "127.0.0.1:${vnc_port}" \ >>"${log}-novnc.log" 2>&1 & fi wait_port "$vnc_port" wait_port "$view_port" } start_cua_driver() { local display="$1" local log="$2" local number="${display#:}" [[ "${LAZYBOY_COMPUTER_DRIVER:-legacy}" == "cua" ]] || return 0 if ! command -v cua-driver >/dev/null 2>&1; then echo "Cua backend selected but cua-driver is not installed" >&2 return 1 fi local sock="$ROOT/cua-${number}.sock" if [[ "$number" == "1" ]]; then sock="$ROOT/cua.sock"; fi if alive_pidfile "${log}-cua.pid"; then if [[ -S "$sock" ]]; then return 0; fi echo "Cua process exists but its display socket is missing" >&2 return 1 fi export CUA_DRIVER_RS_HOME="$ROOT/cua-home-${number}" if [[ -s "$ROOT/screen-${number}.dbus" ]]; then export DBUS_SESSION_BUS_ADDRESS="$(cat "$ROOT/screen-${number}.dbus")" fi if [[ -s "$ROOT/screen-${number}.runtime" ]]; then export XDG_RUNTIME_DIR="$(cat "$ROOT/screen-${number}.runtime")" fi mkdir -p "$CUA_DRIVER_RS_HOME" cua-driver telemetry disable >>"${log}-cua.log" 2>&1 || true rm -f "$sock" DISPLAY="$display" cua-driver serve \ --grant existing-profile \ --socket "$sock" \ --pid-file "${log}-cua.pid" \ --no-overlay \ >>"${log}-cua.log" 2>&1 & local bg=$! local n for n in $(seq 1 50); do if [[ -S "$sock" ]]; then return 0 fi if ! kill -0 "$bg" 2>/dev/null; then echo "cua-driver serve exited" >&2 cat "${log}-cua.log" >&2 || true return 1 fi sleep 0.1 done echo "cua-driver socket was not ready" >&2 cat "${log}-cua.log" >&2 || true return 1 } start_xterm() { local display="$1" local log="$2" if alive_pidfile "${log}-xterm.pid"; then return 0 fi DISPLAY="$display" SHELL=/bin/zsh lazyboy-terminal >>"${log}-xterm.log" 2>&1 & echo $! > "${log}-xterm.pid" } start_browser() { local display="$1" local profile="$2" local log="$3" local number="${display#:}" mkdir -p "$profile" "$ROOT" if [[ -n "$profile" ]]; then printf '%s\n' "$profile" >"$ROOT/screen-${number}.profile" fi if pgrep -f "chromium.*--user-data-dir=${profile}" >/dev/null 2>&1; then return 0 fi rm -f "$profile/SingletonLock" "$profile/SingletonCookie" "$profile/SingletonSocket" DISPLAY="$display" HOME="$HOME" LAZYBOY_BROWSER_PROFILE="$profile" \ lazyboy-browser https://duckduckgo.com >>"${log}-browser.log" 2>&1 & echo $! > "${log}-browser.pid" } ensure_slot() { local slot="$1" local profile="${2:-}" if ! [[ "$slot" =~ ^[0-9]+$ ]] || (( slot < 0 || slot >= LIMIT )); then echo "invalid screen slot" >&2 return 1 fi local number=$((slot + 1)) local display=":${number}" local vnc_port=$((5900 + slot)) local view_port=$((6080 + slot)) local log="$ROOT/screen-${number}" local xfce_home="/tmp/xfce-home" if (( slot != 0 )); then xfce_home="/tmp/xfce-home-${number}" fi mkdir -p "$ROOT" if [[ -n "$profile" ]]; then printf '%s\n' "$profile" >"$ROOT/screen-${number}.profile" fi if xdpyinfo -display "$display" >/dev/null 2>&1 && port_open "$vnc_port" && port_open "$view_port"; then start_cua_driver "$display" "$log" || return 1 if [[ -n "$profile" ]]; then start_browser "$display" "$profile" "$log" fi printf '{"ok":true,"slot":%s,"display":"%s","viewPort":%s,"vncPort":%s}\n' \ "$slot" "$display" "$view_port" "$vnc_port" return 0 fi ( flock -w 25 9 || { echo "screen ${slot} is busy starting" >&2 exit 1 } if xdpyinfo -display "$display" >/dev/null 2>&1 && port_open "$vnc_port" && port_open "$view_port"; then start_cua_driver "$display" "$log" || exit 1 if [[ -n "$profile" ]]; then start_browser "$display" "$profile" "$log" fi printf '{"ok":true,"slot":%s,"display":"%s","viewPort":%s,"vncPort":%s}\n' \ "$slot" "$display" "$view_port" "$vnc_port" exit 0 fi start_xvfb "$display" "$number" "$log" || { echo "Xvfb failed on ${display}" >&2 cat "${log}-xvfb.log" >&2 || true exit 1 } start_desktop "$display" "$xfce_home" "$log" start_cua_driver "$display" "$log" || exit 1 start_xterm "$display" "$log" start_vnc "$display" "$vnc_port" "$view_port" "$log" || exit 1 if [[ -n "$profile" ]]; then start_browser "$display" "$profile" "$log" fi if (( slot == 0 )) && [[ -f "${log}-xvfb.pid" ]]; then cp "${log}-xvfb.pid" "$ROOT/xvfb-1.pid" fi printf '{"ok":true,"slot":%s,"display":"%s","viewPort":%s,"vncPort":%s}\n' \ "$slot" "$display" "$view_port" "$vnc_port" ) 9>"$ROOT/screen-${slot}.lock" } boot_primary() { mkdir -p "$HOME" "$HOME/.local/bin" "$HOME/.config" "$HOME/.browser-profiles" "$ROOT" cd "$HOME" hydrate_home printf '這是 Docker 裡的 Debian 桌面。家目錄會保存在 /home/lazyboy。\n' > "$HOME/README.txt" mkdir -p "$HOME/shared" "$HOME/bots" ensure_slot 0 "" } case "$cmd" in boot-primary) boot_primary ;; ensure) ensure_slot "${1:-0}" "${2:-}" ;; *) echo "usage: lazyboy-screen boot-primary|ensure [profile]" >&2 exit 2 ;; esac