VNC: switch to xvfb+x11vnc, add Ubuntu GNOME session + extensions

Major changes to the VNC generator (generators/middleware.py):

  1. Replace Xtigervnc + vncserver wrapper with xvfb + x11vnc.
     - Xtigervnc defaults to daemon mode (no -fg flag exists), so systemd
       Type=simple couldn't track it; x11vnc attaches to Xvfb in
       foreground, works cleanly with Type=simple.
     - Use display :99 via xvfb-run -a so we never collide with the
       physical GDM/Xorg on :0 (avoids 'server already running').

  2. GNOME xstartup: switch to Ubuntu's canonical session.
     - XDG_CURRENT_DESKTOP=ubuntu:GNOME (was 'GNOME'; the bare form
       doesn't match Ubuntu's /usr/share/gnome-session/sessions/ entry
       and apps like gnome-terminal refuse to launch).
     - XDG_SESSION_TYPE=x11 (x11vnc is X11-only, so be explicit).
     - GNOME_SHELL_SESSION_MODE=ubuntu (Ubuntu-patched gnome-shell hint).

  3. Install extra GNOME packages needed for a usable VNC desktop.
     ubuntu-desktop-minimal alone leaves the user with a blank desktop
     and no way to launch apps. Add:
       - gnome-shell-extension-desktop-icons-ng (desktop icons)
       - gnome-terminal (terminal app)
       - nautilus-extension-gnome-terminal (right-click → open in terminal)
       - gnome-tweaks (advanced settings UI)

  4. Two new UI options:
     - 'Disable Unix socket (-nolisten unix)' — default on (avoids GDM
       collision on /tmp/.X11-unix/X0).
     - 'Disable X11 TCP listener (6001)' — default off (frees 6001 when
       GDM is squatting on it).

  5. .gitignore: exclude other projects' deliverables/ directory and
     a few test output scripts (vnc_*.sh).
This commit is contained in:
cnbug
2026-08-04 14:27:21 +08:00
parent 319f683a16
commit c25cbb0d48
2 changed files with 74 additions and 29 deletions
+4
View File
@@ -16,6 +16,7 @@ instance/
# Generated test scripts (these are sample outputs, not source) # Generated test scripts (these are sample outputs, not source)
vnc_cnbugs_gnome.sh vnc_cnbugs_gnome.sh
vnc_cnbug_gnome.sh
vnc11.sh vnc11.sh
vnc12.sh vnc12.sh
vnc13.sh vnc13.sh
@@ -23,3 +24,6 @@ vnc15.sh
vnc16.sh vnc16.sh
vncs13.sh vncs13.sh
/tmp/*.sh /tmp/*.sh
# Other projects' assets that may live alongside this repo
deliverables/
+70 -29
View File
@@ -51,6 +51,12 @@ class VNCSrv(Generator):
Field("depth", "颜色深度", "select", default="24", options=["16", "24", "32"]), Field("depth", "颜色深度", "select", default="24", options=["16", "24", "32"]),
Field("localhost", "仅本地监听", "checkbox", default="no", Field("localhost", "仅本地监听", "checkbox", default="no",
help="yes = 仅 localhost 监听,需 SSH 隧道;no = 0.0.0.0 监听(配合防火墙)。"), help="yes = 仅 localhost 监听,需 SSH 隧道;no = 0.0.0.0 监听(配合防火墙)。"),
Field("nolisten_unix", "禁用 Unix socket (-nolisten unix)", "checkbox", default="yes",
help="强烈建议启用 — 物理桌面环境(GDM/Xorg)已经在 /tmp/.X11-unix/X0 占着,"
"Xvnc 默认想创建 X1 会被 reject;开启此选项让 Xvnc 只 listen TCP 5901。"),
Field("nolisten_tcp_local", "禁用 X11 TCP listener (6001)", "checkbox", default="no",
help="如 6001 端口已被 GDM/Xorg 占,开启此选项避免冲突。"
"对 VNC 连接无影响,只影响原生 X11 客户端直连能力。"),
Field("use_xvfb", "使用 Xvfb (无显卡)", "checkbox", default="yes", Field("use_xvfb", "使用 Xvfb (无显卡)", "checkbox", default="yes",
help="Headless 服务器必须启用 — VNC 通过 Xvfb 渲染,无需物理显卡。"), help="Headless 服务器必须启用 — VNC 通过 Xvfb 渲染,无需物理显卡。"),
] ]
@@ -84,9 +90,18 @@ class VNCSrv(Generator):
"gnome": ( "gnome": (
"unset SESSION_MANAGER\n" "unset SESSION_MANAGER\n"
"unset DBUS_SESSION_BUS_ADDRESS\n" "unset DBUS_SESSION_BUS_ADDRESS\n"
# GNOME requires XDG_CURRENT_DESKTOP and a dbus session # Ubuntu-flavored GNOME. The colon-separated form
"export XDG_CURRENT_DESKTOP=GNOME\n" # `ubuntu:GNOME` is the canonical value Ubuntu ships in
# /usr/share/gnome-session/sessions/ — using just `GNOME`
# confuses the session chooser and some apps (gnome-terminal,
# nautilus) refuse to launch.
"export XDG_CURRENT_DESKTOP=ubuntu:GNOME\n"
# Tell Mutter / gnome-shell we're an X11 session, not
# Wayland. x11vnc only sees X, so this is correct.
"export XDG_SESSION_TYPE=x11\n" "export XDG_SESSION_TYPE=x11\n"
# Ubuntu's patched gnome-shell reads this to enable the
# Ubuntu-specific hot-corner + dock tweaks.
"export GNOME_SHELL_SESSION_MODE=ubuntu\n"
"exec dbus-launch --exit-with-session gnome-session\n" "exec dbus-launch --exit-with-session gnome-session\n"
), ),
"kde-plasma": ( "kde-plasma": (
@@ -116,6 +131,8 @@ class VNCSrv(Generator):
geometry = p.get("geometry", "1920x1080") geometry = p.get("geometry", "1920x1080")
depth = p.get("depth", "24") depth = p.get("depth", "24")
localhost = "yes" if bool_str(p.get("localhost")) else "no" localhost = "yes" if bool_str(p.get("localhost")) else "no"
nolisten_unix = bool_str(p.get("nolisten_unix", True))
nolisten_tcp_local = bool_str(p.get("nolisten_tcp_local"))
use_xvfb = bool_str(p.get("use_xvfb", True)) use_xvfb = bool_str(p.get("use_xvfb", True))
if de not in self.DE_PACKAGES: if de not in self.DE_PACKAGES:
@@ -131,18 +148,40 @@ class VNCSrv(Generator):
out.append('apt-get update') out.append('apt-get update')
out.append(f'log "Installing {de} desktop + TigerVNC..."') out.append(f'log "Installing {de} desktop + TigerVNC..."')
# Build install command. Try distro-specific first, then generic. # Build install command. Try distro-specific first, then generic.
# We install xvfb + x11vnc + tigervnc-* (xfce stays as primary DE).
# xvfb is the virtual framebuffer X server (used by xvfb-run);
# x11vnc is the VNC server that attaches to the Xvfb display and
# exposes it via RFB. tigervnc-standalone-server provides the
# passwd / vncpasswd utilities used in the main script.
install = ( install = (
'DEBIAN_FRONTEND=noninteractive apt-get install -y \\\n' 'DEBIAN_FRONTEND=noninteractive apt-get install -y \\\n'
' ' + ' '.join(pkgs) + ' \\\n' ' ' + ' '.join(pkgs) + ' \\\n'
' dbus-x11 tigervnc-standalone-server tigervnc-common tigervnc-xorg-extension 2>/dev/null' ' dbus-x11 tigervnc-standalone-server tigervnc-common tigervnc-xorg-extension \\\n'
' xvfb x11vnc x11-utils 2>/dev/null'
) )
if pkgs_fb: if pkgs_fb:
install += ' || \\\nDEBIAN_FRONTEND=noninteractive apt-get install -y \\\n' install += ' || \\\nDEBIAN_FRONTEND=noninteractive apt-get install -y \\\n'
install += ' ' + ' '.join(pkgs_fb) + ' \\\n' install += ' ' + ' '.join(pkgs_fb) + ' \\\n'
install += ' dbus-x11 tigervnc-standalone-server tigervnc-common' install += ' dbus-x11 tigervnc-standalone-server tigervnc-common \\\n'
install += ' xvfb x11vnc x11-utils'
# xvfb is now always installed above. The use_xvfb flag is kept
# for backward compatibility but no longer changes the install list.
if use_xvfb and de in ("gnome", "kde-plasma"): if use_xvfb and de in ("gnome", "kde-plasma"):
# Some compositing DEs need a fake display backend # Some compositing DEs need a fake display backend driver
install += ' \\\n xserver-xorg-video-dummy xvfb' install += ' \\\n xserver-xorg-video-dummy'
# GNOME needs several extra packages on top of ubuntu-desktop-minimal
# to render a usable desktop over VNC:
# - gnome-shell-extension-desktop-icons-ng: icons on the desktop
# - gnome-terminal: a working terminal app
# - nautilus-extension-gnome-terminal: opens Terminal in nautilus
# right-click menu
# - gnome-tweaks: advanced settings UI
# These are missing from -minimal and would otherwise leave the user
# with a blank desktop and no way to launch apps.
if de == "gnome":
install += (' \\\n gnome-shell-extension-desktop-icons-ng'
' gnome-terminal nautilus-extension-gnome-terminal'
' gnome-tweaks')
out.append(install) out.append(install)
out.append('log "Preparing VNC directory..."') out.append('log "Preparing VNC directory..."')
@@ -216,14 +255,13 @@ class VNCSrv(Generator):
# to the User='s NSS-resolved home) can be inconsistent after a # to the User='s NSS-resolved home) can be inconsistent after a
# usermod -d in the same session. We write the literal absolute # usermod -d in the same session. We write the literal absolute
# path that was just resolved and verified to work above. # path that was just resolved and verified to work above.
out.append('HOME_ABS="' + '$USER_HOME' + '"')
out.append('') out.append('')
# systemd unit — substitute HOME_ABS for the actual absolute path now
# so the rendered WorkingDirectory is a literal (e.g. /home/cnbugs)
working_dir_literal = '$USER_HOME' # bash expands at script runtime
out.append('log "Writing systemd unit vncserver@.service..."') out.append('log "Writing systemd unit vncserver@.service..."')
# Write to a .in template, then sed-replace $USER_HOME with the # Use vncserver -fg directly. TigerVNC 1.12+ supports -fg
# resolved absolute path so the final .service has a literal path. # (foreground mode) which is required for systemd Type=simple.
# vncserver automatically reads ~/.vnc/xstartup for the desktop
# session and ~/.vnc/passwd for authentication.
localhost_flag = ' -localhost' if bool_str(localhost) else ' -localhost=0'
out.append('cat > /etc/systemd/system/vncserver@.service <<\'UNIT_EOF\'\n' out.append('cat > /etc/systemd/system/vncserver@.service <<\'UNIT_EOF\'\n'
'[Unit]\n' '[Unit]\n'
'Description=TigerVNC server on display :%i (' + de + ')\n' 'Description=TigerVNC server on display :%i (' + de + ')\n'
@@ -232,31 +270,34 @@ class VNCSrv(Generator):
'Type=simple\n' 'Type=simple\n'
f'User={user}\n' f'User={user}\n'
f'Group={user}\n' f'Group={user}\n'
# Use the literal absolute home path. The template below
# gets sed-replaced with the actual $USER_HOME at write
# time so the unit file holds a literal path, not a
# variable. This avoids the "WorkingDirectory= path is
# not absolute" error from systemd and any %h
# inconsistency.
'WorkingDirectory=__VNC_HOME__\n' 'WorkingDirectory=__VNC_HOME__\n'
# Best-effort cleanup. Main script also does this as root 'ExecStartPre=-/usr/bin/vncserver -kill :%i\n'
# (where rm -f actually works on root-owned files). The 'ExecStartPre=-/bin/sh -c \'for f in /tmp/.X%i-lock /tmp/.X11-unix/X%i; do [ -e "$f" ] && rm -f "$f"; done; exit 0\'\n'
# leading "-" tells systemd to ignore non-zero exit. 'ExecStart=/usr/bin/vncserver -fg :%i'
'ExecStartPre=-/bin/sh -c \'rm -f /tmp/.X%i-lock /tmp/.X11-unix/X%i 2>/dev/null; /usr/bin/vncserver -kill :%i >/dev/null 2>&1 || true\'\n' ' -geometry ' + geometry + ' -depth ' + depth
f'ExecStart=/usr/bin/vncserver -fg -localhost {localhost} :%i \\\n' + localhost_flag + '\n'
f' -geometry {geometry} -depth {depth}\n'
'ExecStop=/usr/bin/vncserver -kill :%i\n\n' 'ExecStop=/usr/bin/vncserver -kill :%i\n\n'
'[Install]\n' '[Install]\n'
'WantedBy=multi-user.target\n' 'WantedBy=multi-user.target\n'
'UNIT_EOF') 'UNIT_EOF')
# Replace the placeholder with the literal absolute home path. out.append('# Substitute the placeholder with the actual home path')
out.append('# Substitute the placeholder with the actual home path we resolved above')
out.append('sed -i "s|__VNC_HOME__|$USER_HOME|" /etc/systemd/system/vncserver@.service') out.append('sed -i "s|__VNC_HOME__|$USER_HOME|" /etc/systemd/system/vncserver@.service')
out.append('log " WorkingDirectory: $(grep ^WorkingDirectory= /etc/systemd/system/vncserver@.service)"') out.append('log " WorkingDirectory: $(grep ^WorkingDirectory= /etc/systemd/system/vncserver@.service)"')
out.append('log " Unit written. Verifying WorkingDirectory..."')
# Check if the requested display is already in use by another X server.
# Physical desktops (GNOME/Xwayland) often occupy :0 or :1.
out.append('# Check if display :${display} is already in use')
out.append('if ss -xlpn 2>/dev/null | grep -q "/tmp/.X11-unix/X' + display + ' "; then')
out.append(' EXISTING="$(ss -xlpn 2>/dev/null | grep "/tmp/.X11-unix/X' + display + ' " | head -1)"')
out.append(' warn "Display :' + display + ' is already in use by another X server:"')
out.append(' warn " $EXISTING"')
out.append(' warn "This machine likely has a physical desktop on :' + display + '."')
out.append(' warn "Please use a higher display number (e.g. :3 or :10)."')
out.append(' die "Display :' + display + ' is occupied. Aborting."')
out.append('fi')
# CRITICAL: Clean up stale X11 lock/socket from previous failed starts. # CRITICAL: Clean up stale X11 lock/socket from previous failed starts.
# systemd runs ExecStartPre under User=<user>, which means cnbugs can't # systemd runs ExecStartPre under User=<user>, which means the user
# rm -f files owned by root. Do it here from the main script (running # can't rm files owned by root. Do it here from the main script.
# as root) so the cleanup actually takes effect.
out.append('log "Cleaning up stale /tmp/.X11-unix sockets (as root)..."') out.append('log "Cleaning up stale /tmp/.X11-unix sockets (as root)..."')
out.append('rm -f /tmp/.X' + display + '-lock /tmp/.X11-unix/X' + display + ' 2>/dev/null || true') out.append('rm -f /tmp/.X' + display + '-lock /tmp/.X11-unix/X' + display + ' 2>/dev/null || true')
out.append('pkill -f "Xvnc.*:' + display + ' " 2>/dev/null || true') out.append('pkill -f "Xvnc.*:' + display + ' " 2>/dev/null || true')