From 62e9f13a45a8dcec6716d13ee86750025b9da29e Mon Sep 17 00:00:00 2001 From: Hermes Agent service account Date: Sat, 1 Aug 2026 20:14:49 -0500 Subject: [PATCH] jmri: replace TigerVNC with Xpra for JMRI GUI display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VNC had window management issues and some dialogs wouldn't open correctly. Xpra runs in rootless mode — each JMRI window appears natively on the client without a VNC client or XQuartz required. Changes: - Remove tigervnc-standalone-server, jmri-vnc.service, .vnc/ directory - Install xpra, deploy jmri-xpra.service.j2 (systemd unit) - Update jmri-gui wrapper: DISPLAY=:100, attach instructions printed on launch - Update defaults: drop VNC vars, add jmri_xpra_display=100 - Rename handler: Restart jmri-vnc -> Restart jmri-xpra Connect from macOS/Linux: xpra attach ssh://jmri@main-street-station/100 --- ansible/roles/jmri/defaults/main.yml | 13 ++-- ansible/roles/jmri/handlers/main.yml | 4 +- ansible/roles/jmri/tasks/main.yml | 66 ++++++++++--------- ansible/roles/jmri/templates/jmri-gui.j2 | 47 +++++++------ .../roles/jmri/templates/jmri-vnc.service.j2 | 28 -------- .../roles/jmri/templates/jmri-xpra.service.j2 | 26 ++++++++ 6 files changed, 91 insertions(+), 93 deletions(-) delete mode 100644 ansible/roles/jmri/templates/jmri-vnc.service.j2 create mode 100644 ansible/roles/jmri/templates/jmri-xpra.service.j2 diff --git a/ansible/roles/jmri/defaults/main.yml b/ansible/roles/jmri/defaults/main.yml index e3965b2..835db49 100644 --- a/ansible/roles/jmri/defaults/main.yml +++ b/ansible/roles/jmri/defaults/main.yml @@ -34,15 +34,12 @@ jmri_json_port: 12080 jmri_withrottle_port: 12090 # --------------------------------------------------------------------------- -# Phase 4 — VNC server (TigerVNC) -# Replaces X11 forwarding with a persistent virtual display. -# Connect with any VNC client to :5901 +# Phase 4 — Xpra virtual display +# Replaces TigerVNC with rootless Xpra — proper window management, +# persistent sessions, SSH-native attach (no VNC client needed). +# Connect from macOS/Linux: xpra attach ssh://jmri@main-street-station/100 # --------------------------------------------------------------------------- -jmri_vnc_display: "1" -jmri_vnc_port: 5901 -jmri_vnc_geometry: "1280x800" -jmri_vnc_depth: "24" -jmri_vnc_password: "" # set via group_vars (vault-backed) +jmri_xpra_display: "100" # --------------------------------------------------------------------------- # Phase 2 — Layout power monitor (Leviton Decora Smart Wi-Fi) diff --git a/ansible/roles/jmri/handlers/main.yml b/ansible/roles/jmri/handlers/main.yml index eff21e3..90c1765 100644 --- a/ansible/roles/jmri/handlers/main.yml +++ b/ansible/roles/jmri/handlers/main.yml @@ -17,9 +17,9 @@ name: jmri state: restarted -- name: Restart jmri-vnc +- name: Restart jmri-xpra ansible.builtin.systemd: - name: jmri-vnc + name: jmri-xpra state: restarted - name: Restart sshd diff --git a/ansible/roles/jmri/tasks/main.yml b/ansible/roles/jmri/tasks/main.yml index ee2026b..8170370 100644 --- a/ansible/roles/jmri/tasks/main.yml +++ b/ansible/roles/jmri/tasks/main.yml @@ -272,51 +272,55 @@ notify: Restart sshd # --------------------------------------------------------------------------- -# Phase 4 — VNC server (TigerVNC) -# Persistent virtual display on :1 (port 5901). Replaces X11 forwarding. +# Phase 4 — Xpra virtual display +# Replaces TigerVNC. Rootless mode: each JMRI window appears as a native +# window on the client. Sessions are persistent across disconnects. +# Connect: xpra attach ssh://jmri@main-street-station/{{ jmri_xpra_display }} # --------------------------------------------------------------------------- -- name: Install TigerVNC server +- name: Remove TigerVNC (replaced by Xpra) ansible.builtin.apt: name: tigervnc-standalone-server + state: absent + notify: Reload systemd + +- name: Disable and stop jmri-vnc service if present + ansible.builtin.systemd: + name: jmri-vnc + enabled: false + state: stopped + failed_when: false + +- name: Remove jmri-vnc systemd unit if present + ansible.builtin.file: + path: /etc/systemd/system/jmri-vnc.service + state: absent + notify: Reload systemd + +- name: Remove jmri VNC password directory if present + ansible.builtin.file: + path: "{{ jmri_home }}/.vnc" + state: absent + +- name: Install Xpra + ansible.builtin.apt: + name: xpra state: present update_cache: false -- name: Create .vnc directory for jmri user - ansible.builtin.file: - path: "{{ jmri_home }}/.vnc" - state: directory - owner: "{{ jmri_user }}" - group: "{{ jmri_group }}" - mode: '0700' - -- name: Set VNC password for jmri user - ansible.builtin.shell: - cmd: "printf '{{ jmri_vnc_password }}' | vncpasswd -f > {{ jmri_home }}/.vnc/passwd" - become_user: "{{ jmri_user }}" - no_log: true - changed_when: true - -- name: Set correct permissions on VNC password file - ansible.builtin.file: - path: "{{ jmri_home }}/.vnc/passwd" - owner: "{{ jmri_user }}" - group: "{{ jmri_group }}" - mode: '0600' - -- name: Deploy jmri-vnc systemd unit +- name: Deploy jmri-xpra systemd unit ansible.builtin.template: - src: jmri-vnc.service.j2 - dest: /etc/systemd/system/jmri-vnc.service + src: jmri-xpra.service.j2 + dest: /etc/systemd/system/jmri-xpra.service owner: root group: root mode: '0644' notify: - Reload systemd - - Restart jmri-vnc + - Restart jmri-xpra -- name: Enable and start jmri-vnc service +- name: Enable and start jmri-xpra service ansible.builtin.systemd: - name: jmri-vnc + name: jmri-xpra enabled: true state: started daemon_reload: true diff --git a/ansible/roles/jmri/templates/jmri-gui.j2 b/ansible/roles/jmri/templates/jmri-gui.j2 index e4ffdfd..fb0b450 100644 --- a/ansible/roles/jmri/templates/jmri-gui.j2 +++ b/ansible/roles/jmri/templates/jmri-gui.j2 @@ -1,22 +1,22 @@ #!/bin/bash -# jmri-gui — launch JMRI GUI on VNC virtual display +# jmri-gui — launch JMRI GUI on Xpra virtual display # Managed by Ansible — do not edit manually. # # Usage (connect as jmri user): -# ssh jmri@main-street-station jmri-gui panelpro -# ssh jmri@main-street-station jmri-gui decoderpro -# ssh jmri@main-street-station jmri-gui status +# jmri-gui panelpro Launch PanelPro on Xpra display +# jmri-gui decoderpro Launch DecoderPro on Xpra display +# jmri-gui status Show service status and attach command # -# Then connect a VNC client to main-street-station.local.mk-labs.cloud:5901 -# VNC password is in the Ansible vault (vault_jmri_vnc_password) +# Then attach from macOS/Linux: +# xpra attach ssh://jmri@main-street-station/{{ jmri_xpra_display }} set -euo pipefail JMRI_DIR="{{ jmri_install_dir }}" JMRI_SERVICE="jmri.service" MONITOR_SERVICE="jmri-monitor.service" -VNC_SERVICE="jmri-vnc.service" -DISPLAY=":{{ jmri_vnc_display }}" +XPRA_SERVICE="jmri-xpra.service" +DISPLAY=":{{ jmri_xpra_display }}" export DISPLAY usage() { @@ -28,13 +28,13 @@ status() { echo "=== JMRI daemon ===" systemctl status "$JMRI_SERVICE" --no-pager -l 2>&1 | head -8 echo "" - echo "=== VNC server ===" - systemctl status "$VNC_SERVICE" --no-pager -l 2>&1 | head -5 + echo "=== Xpra display ===" + systemctl status "$XPRA_SERVICE" --no-pager -l 2>&1 | head -5 echo "" echo "=== Layout monitor ===" systemctl status "$MONITOR_SERVICE" --no-pager -l 2>&1 | head -5 echo "" - echo "Connect VNC client to: $(hostname -f):{{ jmri_vnc_port }}" + echo "To attach: xpra attach ssh://jmri@$(hostname -f)/{{ jmri_xpra_display }}" } launch() { @@ -47,32 +47,31 @@ launch() { *) usage ;; esac - # Ensure VNC server is running - if ! systemctl is-active --quiet "$VNC_SERVICE" 2>/dev/null; then - echo "Starting VNC server..." - sudo systemctl start "$VNC_SERVICE" + # Ensure Xpra display is running + if ! systemctl is-active --quiet "$XPRA_SERVICE" 2>/dev/null; then + echo "Starting Xpra display..." + sudo systemctl start "$XPRA_SERVICE" sleep 2 fi - # Stop the JMRI daemon if running + # Stop the JMRI daemon if running (we're taking over the hardware connections) if systemctl is-active --quiet "$JMRI_SERVICE" 2>/dev/null; then echo "Stopping JMRI daemon..." sudo systemctl stop "$JMRI_SERVICE" fi - echo "Launching $binary on VNC display $DISPLAY..." - echo "Connect your VNC client to: $(hostname -f):{{ jmri_vnc_port }}" - echo "VNC password: see Ansible vault (vault_jmri_vnc_password)" - echo "" - # Force AWT out of headless mode export JMRI_OPTIONS="-Djava.awt.headless=false" - # Launch on the VNC display — no --profile so JMRI uses last-session preference + echo "Launching $binary on Xpra display $DISPLAY..." + echo "" + echo "Attach from your workstation:" + echo " xpra attach ssh://jmri@$(hostname -f)/{{ jmri_xpra_display }}" + echo "" + "$JMRI_DIR/$binary" & - echo "$binary launched in background on VNC display." - echo "Close it from the VNC client, then run 'jmri-gui status' to check state." + echo "$binary launched. Attach with xpra to see windows." echo "" # Restart daemon if layout switch is still on diff --git a/ansible/roles/jmri/templates/jmri-vnc.service.j2 b/ansible/roles/jmri/templates/jmri-vnc.service.j2 deleted file mode 100644 index 0635832..0000000 --- a/ansible/roles/jmri/templates/jmri-vnc.service.j2 +++ /dev/null @@ -1,28 +0,0 @@ -[Unit] -Description=JMRI VNC virtual display (:{{ jmri_vnc_display }}) -After=network.target -# Start before jmri.service so the display is ready when JMRI launches -Before=jmri.service - -[Service] -Type=simple -User={{ jmri_user }} -Group={{ jmri_group }} -ExecStart=/usr/bin/Xtigervnc \ - :{{ jmri_vnc_display }} \ - -rfbport {{ jmri_vnc_port }} \ - -geometry {{ jmri_vnc_geometry }} \ - -depth {{ jmri_vnc_depth }} \ - -SecurityTypes VncAuth \ - -PasswordFile {{ jmri_home }}/.vnc/passwd \ - -localhost no \ - -AlwaysShared \ - -NeverShared no \ - -AcceptKeyEvents \ - -AcceptPointerEvents \ - -desktop "JMRI" -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target diff --git a/ansible/roles/jmri/templates/jmri-xpra.service.j2 b/ansible/roles/jmri/templates/jmri-xpra.service.j2 new file mode 100644 index 0000000..6971187 --- /dev/null +++ b/ansible/roles/jmri/templates/jmri-xpra.service.j2 @@ -0,0 +1,26 @@ +[Unit] +Description=Xpra virtual display for JMRI (:{{ jmri_xpra_display }}) +After=network.target +# Start before jmri.service so the display is ready when JMRI launches +Before=jmri.service + +[Service] +Type=simple +User={{ jmri_user }} +Group={{ jmri_group }} +ExecStart=/usr/bin/xpra start \ + :{{ jmri_xpra_display }} \ + --daemon=no \ + --mdns=no \ + --notifications=no \ + --systemd-run=no \ + --pulseaudio=no \ + --speaker=off \ + --microphone=off \ + --video-encoders=none \ + --start-via-proxy=no +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target