From 1e789b4c449cb8ff440d0f0c6d4b6ce1c422d520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 25 Jul 2026 15:32:28 +0000 Subject: [PATCH] setup: Ubuntu GNOME-on-Xorg desktop + setup.sh hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setup-desktop.sh now installs ubuntu-desktop + gdm3 + x11vnc and forces the Xorg session (WaylandEnable=false) with auto-login — x11vnc can only mirror an Xorg :0, not Wayland. vnc-manager.ts resolves the X authority from the GDM per-session path (/run/user//gdm/Xauthority) with a ~/.Xauthority fallback. setup.sh fixes: - desktop step gates on `dpkg -s ubuntu-desktop` (was the decommissioned officer-vnc service, which never matched so setup-desktop re-ran every time) - remove Pi (install, --list-models validation, verification check) - export GOPATH before the cliamp build so `go install` lands where it's checked even when Go was already present this run - write PUBLIC_BUILD_ENV=production and quote all .env values - guard the interactive .env block behind a TTY check so non-interactive runs skip cleanly instead of aborting on read EOF under set -e - restart systemd-logind only when a key actually changed - sed prefix-strip instead of `tr -d` (which deletes characters, not a prefix) Co-Authored-By: Claude Opus 4.8 --- scripts/provision-existing-users.sh | 205 ------------------------- scripts/setup-desktop.sh | 72 ++++++--- scripts/setup.sh | 79 +++++----- src/servers/sidecar/vnc/vnc-manager.ts | 23 ++- 4 files changed, 113 insertions(+), 266 deletions(-) delete mode 100755 scripts/provision-existing-users.sh diff --git a/scripts/provision-existing-users.sh b/scripts/provision-existing-users.sh deleted file mode 100755 index 4aee797d..00000000 --- a/scripts/provision-existing-users.sh +++ /dev/null @@ -1,205 +0,0 @@ -#!/bin/bash -# One-time migration: provision Linux users for existing database members. -# Reads users from PostgreSQL, skips Super Admins (they use the service account), -# and creates Linux users + seeds shell configs for everyone else. -# -# Usage: bash scripts/provision-existing-users.sh -# Requires: sudoers entry from setup.sh, POSTGRES_URL in .env - -set -euo pipefail - -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' - -ok() { echo -e " ${GREEN}✓${NC} $1"; } -warn() { echo -e " ${YELLOW}!${NC} $1"; } -fail() { echo -e " ${RED}✗${NC} $1"; } -skip() { echo -e " - $1 (skipped)"; } - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -ENV_FILE="$PROJECT_DIR/.env" -TEMPLATE_DIR="$PROJECT_DIR/src/servers/api/terminal/templates" - -# Load .env -if [ ! -f "$ENV_FILE" ]; then - fail ".env not found at $ENV_FILE" - exit 1 -fi -source <(grep -E '^[A-Z_]+=.*' "$ENV_FILE" | sed 's/^/export /') - -# Resolve DATA_PATH -DATA_PATH="${DATA_PATH:-$PROJECT_DIR/data}" - -# Parse POSTGRES_URL for psql -if [ -z "${POSTGRES_URL:-}" ]; then - fail "POSTGRES_URL not set in .env" - exit 1 -fi - -# Extract components from postgresql://user:pass@host:port/dbname -PG_USER=$(echo "$POSTGRES_URL" | sed -n 's|.*://\([^:]*\):.*|\1|p') -PG_PASS=$(echo "$POSTGRES_URL" | sed -n 's|.*://[^:]*:\([^@]*\)@.*|\1|p') -PG_HOST=$(echo "$POSTGRES_URL" | sed -n 's|.*@\([^:]*\):.*|\1|p') -PG_PORT=$(echo "$POSTGRES_URL" | sed -n 's|.*:\([0-9]*\)/.*|\1|p') -PG_DB=$(echo "$POSTGRES_URL" | sed -n 's|.*/\([^?]*\).*|\1|p') - -echo "" -echo "═══════════════════════════════════════════" -echo " Provision existing users" -echo "═══════════════════════════════════════════" -echo "" -echo "DATA_PATH: $DATA_PATH" -echo "Database: $PG_DB @ $PG_HOST:$PG_PORT" -echo "" - -# Query non-Super Admin active users -USERS=$(PGPASSWORD="$PG_PASS" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -F '|' \ - -c "SELECT email, COALESCE(username, '') FROM users WHERE role != 'Super Admin' AND status = 'Active';" 2>&1) - -if [ $? -ne 0 ]; then - fail "Failed to query database: $USERS" - exit 1 -fi - -if [ -z "$USERS" ]; then - echo "No non-admin active users found. Nothing to do." - exit 0 -fi - -echo "── Found users ──" -echo "$USERS" | while IFS='|' read -r email username; do - echo " $email (username: ${username:-})" -done -echo "" - -echo "── Provisioning ──" -while IFS='|' read -r email username; do - # Derive shell username (same logic as toShellUsername in data-path.ts) - if [ -n "$username" ]; then - shell_user=$(echo "$username" | sed 's/@.*$//' | sed 's/[^a-zA-Z0-9._-]/_/g' | tr '[:upper:]' '[:lower:]' | cut -c1-32) - else - shell_user=$(echo "$email" | sed 's/@.*$//' | sed 's/[^a-zA-Z0-9._-]/_/g' | tr '[:upper:]' '[:lower:]' | cut -c1-32) - fi - - USER_ROOT="$DATA_PATH/$email" - HOME_DIR="$USER_ROOT/home" - - echo "" - echo " [$email → $shell_user]" - - # Ensure data dirs exist (sudo in case dir is owned by a previous provisioning run) - sudo mkdir -p "$USER_ROOT" "$HOME_DIR" - - # Create Linux user if needed - if id "$shell_user" &>/dev/null; then - skip "Linux user $shell_user already exists" - else - if sudo useradd -d "$HOME_DIR" -s /bin/zsh -M "$shell_user"; then - ok "Created Linux user $shell_user" - else - fail "Failed to create Linux user $shell_user" - continue - fi - fi - - # Seed shell configs (only if not already present) - if [ ! -f "$HOME_DIR/.zshenv" ] && [ -f "$TEMPLATE_DIR/.zshenv" ]; then - sudo cp "$TEMPLATE_DIR/.zshenv" "$HOME_DIR/.zshenv" - ok "Seeded .zshenv" - else - skip ".zshenv" - fi - - if [ ! -f "$HOME_DIR/.zshrc" ] && [ -f "$TEMPLATE_DIR/.zshrc" ]; then - sudo cp "$TEMPLATE_DIR/.zshrc" "$HOME_DIR/.zshrc" - ok "Seeded .zshrc" - else - skip ".zshrc" - fi - - if [ ! -f "$HOME_DIR/.tmux.conf" ] && [ -f "$TEMPLATE_DIR/.tmux.conf" ]; then - sudo cp "$TEMPLATE_DIR/.tmux.conf" "$HOME_DIR/.tmux.conf" - ok "Seeded .tmux.conf" - else - skip ".tmux.conf" - fi - - sudo mkdir -p "$HOME_DIR/.config" - if [ ! -f "$HOME_DIR/.config/starship-officer.toml" ] && [ -f "$TEMPLATE_DIR/starship-officer.toml" ]; then - sudo cp "$TEMPLATE_DIR/starship-officer.toml" "$HOME_DIR/.config/starship-officer.toml" - ok "Seeded starship config" - else - skip "starship config" - fi - - # Oh My Zsh - if [ ! -d "$HOME_DIR/.oh-my-zsh" ]; then - if [ -d "$HOME/.oh-my-zsh" ]; then - sudo cp -r "$HOME/.oh-my-zsh" "$HOME_DIR/.oh-my-zsh" - ok "Copied oh-my-zsh from host" - fi - else - skip "oh-my-zsh" - fi - - # LazyVim - sudo mkdir -p "$HOME_DIR/.config" - if [ ! -d "$HOME_DIR/.config/nvim" ]; then - if [ -d "$HOME/.config/nvim" ]; then - sudo cp -r "$HOME/.config/nvim" "$HOME_DIR/.config/nvim" - ok "Copied nvim config from host" - fi - else - skip "nvim config" - fi - - # Ensure dirs - sudo mkdir -p "$HOME_DIR/.local/bin" - sudo mkdir -p "$HOME_DIR/.pi/agent/sessions" - - # VNC environment - VNC_DIR="$HOME_DIR/.vnc" - sudo mkdir -p "$VNC_DIR" - if [ ! -f "$VNC_DIR/passwd" ]; then - VNC_PASS=$(head -c 32 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 8) - echo -n "$VNC_PASS" | sudo tee "$VNC_DIR/password" > /dev/null - echo -n "$VNC_PASS" | vncpasswd -f | sudo tee "$VNC_DIR/passwd" > /dev/null - sudo tee "$VNC_DIR/xstartup" > /dev/null << 'XSTARTUP' -#!/bin/sh -unset SESSION_MANAGER -unset DBUS_SESSION_BUS_ADDRESS -eval $(dbus-launch --sh-syntax) -export DBUS_SESSION_BUS_ADDRESS -exec startxfce4 -XSTARTUP - sudo chmod +x "$VNC_DIR/xstartup" - sudo chmod 600 "$VNC_DIR/passwd" - sudo chmod 600 "$VNC_DIR/password" - ok "Provisioned VNC environment" - else - skip "VNC environment" - fi - - # Set ownership and permissions last - # chmod 770 so only owner and group can access (service user is added to group above) - sudo chown -R "$shell_user:$shell_user" "$USER_ROOT" - sudo chmod -R 770 "$USER_ROOT" - - # Add service user to this user's group so server jobs can access user data - SERVICE_USER="${SUDO_USER:-$(whoami)}" - if [ "$SERVICE_USER" != "$shell_user" ]; then - sudo usermod -aG "$shell_user" "$SERVICE_USER" - ok "Added $SERVICE_USER to group $shell_user" - fi - - ok "Provisioning complete" -done <<< "$USERS" - -echo "" -echo "═══════════════════════════════════════════" -echo " Done!" -echo "═══════════════════════════════════════════" -echo "" diff --git a/scripts/setup-desktop.sh b/scripts/setup-desktop.sh index 961b3f91..32887aa1 100755 --- a/scripts/setup-desktop.sh +++ b/scripts/setup-desktop.sh @@ -1,24 +1,63 @@ #!/bin/bash set -euo pipefail -# Officer Remote Desktop Setup — System packages only. -# Per-user VNC config is handled by user provisioning (provision.ts). +# Officer Remote Desktop Setup — Ubuntu GNOME desktop on Xorg, mirrored over VNC. +# +# The platform mirrors the single physical display :0 with x11vnc (see vnc-manager.ts). x11vnc can only +# capture an Xorg server, NOT a Wayland compositor — so we install the full GNOME desktop but force GDM +# onto the Xorg session (WaylandEnable=false). Auto-login is enabled so a user session owns :0 for the +# mirror to attach to. Switching the display manager takes effect on the next reboot. # Usage: ./scripts/setup-desktop.sh -echo "=== Officer Remote Desktop Setup ===" +echo "=== Officer Remote Desktop Setup (Ubuntu GNOME on Xorg) ===" echo "" -# --- Step 1: Install system packages --- -echo "[1/4] Installing system packages..." +DESKTOP_USER="$(whoami)" + +# --- Step 1: Install GNOME desktop + GDM + x11vnc --- +echo "[1/5] Installing ubuntu-desktop, GDM, x11vnc..." sudo apt update -qq sudo DEBIAN_FRONTEND=noninteractive apt install -y -qq \ - xfce4 xfce4-goodies \ - tigervnc-standalone-server tigervnc-common \ + ubuntu-desktop \ + gdm3 \ + x11vnc \ dbus-x11 echo " Done." -# --- Step 2: Install Brave browser (native .deb, not snap) --- -echo "[2/4] Installing Brave browser..." +# --- Step 2: Force GDM onto Xorg + enable auto-login (x11vnc cannot mirror Wayland) --- +echo "[2/5] Forcing Xorg session and auto-login in GDM..." +GDM_CONF=/etc/gdm3/custom.conf +sudo mkdir -p /etc/gdm3 +[ -f "$GDM_CONF" ] || echo "[daemon]" | sudo tee "$GDM_CONF" > /dev/null +# Ensure a [daemon] section exists to anchor the keys under. +sudo grep -qE '^\[daemon\]' "$GDM_CONF" || echo "[daemon]" | sudo tee -a "$GDM_CONF" > /dev/null + +# Set key=value under [daemon]: rewrite an existing (possibly commented) line, else insert after [daemon]. +set_gdm_key() { + local key="$1" val="$2" + if sudo grep -qE "^[[:space:]]*#?[[:space:]]*${key}=" "$GDM_CONF"; then + sudo sed -i "s|^[[:space:]]*#\?[[:space:]]*${key}=.*|${key}=${val}|" "$GDM_CONF" + else + sudo sed -i "/^\[daemon\]/a ${key}=${val}" "$GDM_CONF" + fi +} + +set_gdm_key WaylandEnable false +set_gdm_key AutomaticLoginEnable true +set_gdm_key AutomaticLogin "$DESKTOP_USER" +echo " Xorg forced (WaylandEnable=false), auto-login as $DESKTOP_USER." + +# --- Step 3: Make GDM the default display manager --- +echo "[3/5] Setting GDM as the default display manager..." +echo "/usr/sbin/gdm3" | sudo tee /etc/X11/default-display-manager > /dev/null +sudo systemctl enable gdm3 >/dev/null 2>&1 || sudo systemctl enable gdm >/dev/null 2>&1 || true +sudo systemctl set-default graphical.target >/dev/null 2>&1 || true +# Disable any prior display manager (e.g. lightdm from an XFCE setup) so it doesn't fight GDM. +sudo systemctl disable lightdm >/dev/null 2>&1 || true +echo " Done (takes effect on next reboot)." + +# --- Step 4: Install Brave browser (native .deb, not snap) --- +echo "[4/5] Installing Brave browser..." if ! command -v brave-browser-stable > /dev/null 2>&1; then sudo curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg \ https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg @@ -32,24 +71,20 @@ if [ -f /opt/brave.com/brave/brave-browser ]; then sudo rm -f /usr/bin/brave-browser-stable sudo ln -s /opt/brave.com/brave/brave-browser /usr/bin/brave-browser-stable fi -# Tell Brave to use basic password store (no GNOME Keyring prompts) +# Tell Brave to use basic password store (no keyring prompts) sudo mkdir -p /etc/brave echo '--password-store=basic' | sudo tee /etc/brave/brave-flags.conf > /dev/null echo " Done." -# --- Step 3: Remove GNOME Keyring (prevents password prompts on login) --- -echo "[3/4] Removing GNOME Keyring..." +# --- Step 5: Remove GNOME Keyring + set default browser (prevents password prompts on login) --- +echo "[5/5] Removing GNOME Keyring and setting default browser..." sudo apt remove -y --purge gnome-keyring > /dev/null 2>&1 || true rm -rf ~/.local/share/keyrings -echo " Done." - -# --- Step 4: Set default browser --- -echo "[4/4] Setting default browser..." if command -v brave-browser-stable > /dev/null 2>&1; then sudo update-alternatives --set x-www-browser /opt/brave.com/brave/brave 2>/dev/null || true echo " Brave set as default." else - echo " No supported browser found, skipping." + echo " No supported browser found, skipping default." fi # --- Cleanup old systemd service if it exists --- @@ -64,4 +99,5 @@ if systemctl list-unit-files officer-vnc.service &>/dev/null; then fi echo "" -echo "Setup complete. Per-user VNC sessions are managed by the VNC sidecar." +echo "Setup complete. The screen mirror (x11vnc on :0) is managed by the VNC sidecar." +echo "REBOOT to switch into the GNOME-on-Xorg session with auto-login." diff --git a/scripts/setup.sh b/scripts/setup.sh index 87c9fe58..668187ef 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -299,8 +299,13 @@ else fi # Configure logind to ignore idle — patch individual keys, don't overwrite the file +LOGIND_CHANGED=0 set_logind_key() { local key="$1" val="$2" file="/etc/systemd/logind.conf" + # Already set (uncommented) to the desired value → nothing to do. + if grep -qE "^${key}=${val}$" "$file" 2>/dev/null; then + return + fi if grep -qE "^${key}=" "$file" 2>/dev/null; then sudo sed -i "s|^${key}=.*|${key}=${val}|" "$file" elif grep -qE "^#${key}=" "$file" 2>/dev/null; then @@ -308,6 +313,7 @@ set_logind_key() { else echo "${key}=${val}" | sudo tee -a "$file" > /dev/null fi + LOGIND_CHANGED=1 } set_logind_key HandleLidSwitch ignore @@ -316,8 +322,13 @@ set_logind_key HandlePowerKey ignore set_logind_key IdleAction none set_logind_key RuntimeDirectorySize 10% -sudo systemctl restart systemd-logind -ok "Configured logind to disable auto-suspend" +# Only restart logind when something actually changed — a needless restart can disrupt live sessions. +if [ "$LOGIND_CHANGED" = "1" ]; then + sudo systemctl restart systemd-logind + ok "Configured logind to disable auto-suspend" +else + skip "logind auto-suspend settings" +fi # ─── 5. Node.js 22 (system-wide) ───────────────────────────────────────────── echo "" @@ -337,7 +348,7 @@ case $PM in apt) NEED_INSTALL=0 if [ -f "$SYSTEM_NODE" ]; then - SYS_MAJOR=$("$SYSTEM_NODE" -v 2>/dev/null | cut -d. -f1 | tr -d 'v') + SYS_MAJOR=$("$SYSTEM_NODE" -v 2>/dev/null | cut -d. -f1 | sed 's/^v//') if [ "$SYS_MAJOR" = "22" ]; then skip "node v$("$SYSTEM_NODE" -v) (system-wide at $SYSTEM_NODE)" else @@ -354,7 +365,7 @@ case $PM in fi if [ "$NEED_INSTALL" = "1" ]; then install_node22_apt - if [ -f "$SYSTEM_NODE" ] && [ "$("$SYSTEM_NODE" -v 2>/dev/null | cut -d. -f1 | tr -d 'v')" = "22" ]; then + if [ -f "$SYSTEM_NODE" ] && [ "$("$SYSTEM_NODE" -v 2>/dev/null | cut -d. -f1 | sed 's/^v//')" = "22" ]; then ok "node v$("$SYSTEM_NODE" -v) installed at $SYSTEM_NODE" if has node && [ "$(command -v node)" != "$SYSTEM_NODE" ]; then warn "Shell resolves 'node' to $(command -v node) — system node is at $SYSTEM_NODE" @@ -367,7 +378,7 @@ case $PM in fi ;; pacman) - if has node && [ "$(node -v 2>/dev/null | cut -d. -f1 | tr -d 'v')" = "22" ]; then + if has node && [ "$(node -v 2>/dev/null | cut -d. -f1 | sed 's/^v//')" = "22" ]; then skip "node v$(node -v)" else install_pkg nodejs npm @@ -375,7 +386,7 @@ case $PM in fi ;; brew) - if has node && [ "$(node -v 2>/dev/null | cut -d. -f1 | tr -d 'v')" = "22" ]; then + if has node && [ "$(node -v 2>/dev/null | cut -d. -f1 | sed 's/^v//')" = "22" ]; then skip "node v$(node -v)" else install_pkg node @@ -412,7 +423,7 @@ echo "" echo "── Go ──" echo " Fetching latest Go version..." -GOLANG_VERSION=$(curl -fsSL "https://go.dev/dl/?mode=json" | jq -r '.[0].version' | tr -d 'go') +GOLANG_VERSION=$(curl -fsSL "https://go.dev/dl/?mode=json" | jq -r '.[0].version' | sed 's/^go//') if [ -z "$GOLANG_VERSION" ]; then warn "Could not fetch latest Go version — falling back to 1.23.6" GOLANG_VERSION=1.23.6 @@ -445,7 +456,7 @@ install_go() { } if has go; then - INSTALLED_GO=$(go version 2>/dev/null | awk '{print $3}' | tr -d 'go') + INSTALLED_GO=$(go version 2>/dev/null | awk '{print $3}' | sed 's/^go//') if [ "$INSTALLED_GO" = "$GOLANG_VERSION" ]; then skip "go $INSTALLED_GO" else @@ -528,7 +539,11 @@ fi echo "" echo "── cliamp ──" -GOPATH_BIN="${GOPATH:-$HOME/.local/go-path}/bin" +# Export GOPATH (not just PATH) so `go install` lands in GOPATH_BIN — even when Go was already present +# this run and install_go (which sets GOPATH) never ran. Otherwise go uses its default ~/go/bin and the +# check below wrongly reports a build failure. +export GOPATH="${GOPATH:-$HOME/.local/go-path}" +GOPATH_BIN="$GOPATH/bin" export PATH="$GOPATH_BIN:$PATH" if has cliamp; then @@ -619,7 +634,7 @@ else case $PM in apt) echo " Fetching latest eza version..." - EZA_VERSION=$(curl -fsSL "https://api.github.com/repos/eza-community/eza/releases/latest" | jq -r '.tag_name' | tr -d 'v') + EZA_VERSION=$(curl -fsSL "https://api.github.com/repos/eza-community/eza/releases/latest" | jq -r '.tag_name' | sed 's/^v//') if [ -z "$EZA_VERSION" ]; then warn "Could not fetch eza version — skipping"; else ARCH=$(uname -m) case $ARCH in @@ -647,7 +662,7 @@ else case $PM in apt) echo " Fetching latest lazygit version..." - LAZYGIT_VERSION=$(curl -fsSL "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | jq -r '.tag_name' | tr -d 'v') + LAZYGIT_VERSION=$(curl -fsSL "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | jq -r '.tag_name' | sed 's/^v//') if [ -z "$LAZYGIT_VERSION" ]; then warn "Could not fetch lazygit version — skipping"; else ARCH=$(uname -m) case $ARCH in @@ -710,24 +725,6 @@ else npm config set prefix "$HOME/.local" ok "npm prefix set to $HOME/.local" - # Pi (coding agent) - if has pi; then - skip "pi (@mariozechner/pi-coding-agent)" - else - echo " Installing pi..." - npm install -g @mariozechner/pi-coding-agent - if has pi; then ok "pi installed"; else warn "pi install failed"; fi - fi - - # Validate Pi works - if has pi; then - if pi --list-models > /dev/null 2>&1; then - ok "pi validated (--list-models works)" - else - warn "pi installed but --list-models failed — check API keys in ~/.pi/agent/auth.json" - fi - fi - # Claude Code (uses Anthropic's own installer for auto-update support) if has claude; then skip "claude (claude-code)" @@ -765,7 +762,15 @@ echo "── Environment (.env) ──" GENERATE_ENV=true -if [ -f "$PROJECT_DIR/.env" ]; then +if [ ! -t 0 ]; then + # Non-interactive shell: the prompts below would hit EOF and abort the whole script under `set -e`. + GENERATE_ENV=false + if [ -f "$PROJECT_DIR/.env" ]; then + skip ".env (kept existing — non-interactive shell)" + else + warn "No .env and not a terminal — re-run setup.sh interactively to generate it" + fi +elif [ -f "$PROJECT_DIR/.env" ]; then echo -n " .env already exists. Regenerate? (y/n) [n]: " read -r REGEN if [[ "$REGEN" != "y" && "$REGEN" != "Y" ]]; then @@ -820,12 +825,13 @@ if [ "$GENERATE_ENV" = true ]; then # Write .env cat > "$PROJECT_DIR/.env" </dev/null; then - skip "remote desktop (officer-vnc service already running)" +if dpkg -s ubuntu-desktop &>/dev/null 2>&1; then + skip "remote desktop (ubuntu-desktop already installed)" else case $PM in apt) @@ -901,7 +907,6 @@ check cliamp echo "" echo "AI agents:" -check pi check claude echo "" diff --git a/src/servers/sidecar/vnc/vnc-manager.ts b/src/servers/sidecar/vnc/vnc-manager.ts index d13696f9..37f028c5 100644 --- a/src/servers/sidecar/vnc/vnc-manager.ts +++ b/src/servers/sidecar/vnc/vnc-manager.ts @@ -11,9 +11,19 @@ const MIRROR_DISPLAY_NUM = 0; const MIRROR_PORT = 5900; const READY_TIMEOUT_MS = 5000; -// x11vnc reads :0's cookie from the logged-in user's own .Xauthority, so no root is needed. -// While the greeter owns :0 the cookie lives in lightdm's file instead and mirroring fails. -const XAUTHORITY = join(process.env.HOME ?? '', '.Xauthority'); +// x11vnc reads :0's cookie from the logged-in user's X authority, so no root is needed. Where that +// cookie lives depends on the display manager: GDM (Ubuntu GNOME on Xorg) keeps it in the per-session +// dir /run/user//gdm/Xauthority; LightDM (or a manual startx) uses the classic ~/.Xauthority. Try +// the GDM path first, then fall back. While the greeter owns :0 the user's cookie doesn't exist yet and +// mirroring fails until someone is logged in. +function resolveXauthority(): string | null { + const uid = typeof process.getuid === 'function' ? process.getuid() : null; + const candidates = [ + uid != null ? `/run/user/${uid}/gdm/Xauthority` : null, + join(process.env.HOME ?? '', '.Xauthority'), + ].filter((p): p is string => Boolean(p)); + return candidates.find((p) => existsSync(p)) ?? null; +} type MirrorSession = { email: string; @@ -99,8 +109,9 @@ export async function startSession(params: VncStartParams): Promise<{ port: numb const homeDir = getHomeDirForRole(params.email, params.role); const passwdFile = await ensureVncPassword(homeDir); - if (!existsSync(XAUTHORITY)) { - throw new Error(`No X authority at ${XAUTHORITY} — nobody is logged in on ${MIRROR_DISPLAY}`); + const xauthority = resolveXauthority(); + if (!xauthority) { + throw new Error(`No X authority found (GDM or ~/.Xauthority) — nobody is logged in on ${MIRROR_DISPLAY}`); } const proc = Bun.spawn({ @@ -109,7 +120,7 @@ export async function startSession(params: VncStartParams): Promise<{ port: numb '-display', MIRROR_DISPLAY, '-auth', - XAUTHORITY, + xauthority, '-rfbport', String(MIRROR_PORT), '-rfbauth',