From 3d5aa94aa0ffe23a79ed51f8203a135cd92fba3b Mon Sep 17 00:00:00 2001 From: Tueem Date: Sun, 5 Jul 2026 23:54:18 +0200 Subject: [PATCH] refactor(*): move to x11 --- display-manager/Dockerfile | 18 +++----- display-manager/start.sh | 22 ++++----- display-manager/sway.conf | 29 ------------ docker-compose.yml | 28 ++++++------ kiosk/Dockerfile | 6 +-- kiosk/server.js | 93 ++++++++++++++++++++++---------------- kiosk/start.sh | 11 +++-- 7 files changed, 95 insertions(+), 112 deletions(-) delete mode 100644 display-manager/sway.conf diff --git a/display-manager/Dockerfile b/display-manager/Dockerfile index 89e47b0..c5cbd87 100644 --- a/display-manager/Dockerfile +++ b/display-manager/Dockerfile @@ -1,23 +1,19 @@ FROM debian:bullseye-slim RUN apt-get update && apt-get install -y \ - sway \ - xwayland \ - dbus-x11 \ - libwayland-client0 \ - libwayland-server0 \ - libwayland-egl1 \ - mesa-vulkan-drivers \ - libvulkan1 \ - fonts-liberation \ + xserver-xorg-core \ + xserver-xorg-video-fbdev \ + xserver-xorg-video-fbturbo \ + xserver-xorg-video-all \ x11-xserver-utils \ + xinit \ + dbus-x11 \ && rm -rf /var/lib/apt/lists/* -COPY sway.conf /etc/sway/config COPY start.sh /start.sh RUN chmod +x /start.sh -# Need to run as root to access DRM devices on many kiosk setups +# Xorg needs root to access DRM and VT USER root CMD ["/start.sh"] diff --git a/display-manager/start.sh b/display-manager/start.sh index 289a56b..1f755f9 100644 --- a/display-manager/start.sh +++ b/display-manager/start.sh @@ -1,16 +1,12 @@ #!/bin/bash -# Ensure runtime directory exists -mkdir -p /run/wayland -chmod 0700 /run/wayland +# Ensure X11 socket directory exists +mkdir -p /tmp/.X11-unix +chmod 1777 /tmp/.X11-unix -# Set env vars for Wayland/Sway -export XDG_RUNTIME_DIR=/run/wayland -export WAYLAND_DISPLAY=wayland-1 -export WLR_BACKENDS=drm,libinput -# Fallback to headless if DRM is not available (useful for testing on mac/desktop without actual DRM) -# export WLR_BACKENDS=headless - -# Run sway -echo "Starting Sway..." -exec sway -d -c /etc/sway/config +echo "Starting Xorg server..." +# Run Xorg on DISPLAY :0. +# We disable DPMS (screen blanking) and screen saver (-s 0). +# We also avoid requiring a virtual terminal using vt7 and -sharevts if needed, +# though running as root with devices mapped usually succeeds. +exec Xorg :0 -nocursor -s 0 -dpms vt7 diff --git a/display-manager/sway.conf b/display-manager/sway.conf deleted file mode 100644 index 50826c5..0000000 --- a/display-manager/sway.conf +++ /dev/null @@ -1,29 +0,0 @@ -# Default sway config for Kiosk - -# Disable default bar -bar { - mode invisible -} - -# Remove window borders and title bars -default_border none -default_floating_border none -hide_edge_borders both - -# Map workspaces to specific outputs -# Note: In a real Pi 4 environment, the outputs might be named HDMI-A-1 and HDMI-A-2. -# You can customize these output names based on `swaymsg -t get_outputs`. -workspace 1 output HDMI-A-1 -workspace 2 output HDMI-A-2 - -# Set a black background -output * bg #000000 solid_color - -# Idle configuration -# We don't want the screen to turn off in a kiosk. -exec swayidle -w \ - timeout 3600 'swaymsg "output * dpms on"' \ - resume 'swaymsg "output * dpms on"' - -# Include any other specific configurations -include /etc/sway/config.d/* diff --git a/docker-compose.yml b/docker-compose.yml index 6411c95..a773df0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,12 +5,16 @@ services: build: ./display-manager privileged: true restart: always + tty: true + stdin_open: true volumes: - - wayland-socket:/run/wayland + - x11-socket:/tmp/.X11-unix - /dev:/dev + devices: + - "/dev/tty7:/dev/tty7" + - "/dev/dri:/dev/dri" environment: - - XDG_RUNTIME_DIR=/run/wayland - - WAYLAND_DISPLAY=wayland-1 + - DISPLAY=:0 kiosk-1: build: ./kiosk @@ -20,12 +24,10 @@ services: ports: - "5011:5011" volumes: - - wayland-socket:/run/wayland + - x11-socket:/tmp/.X11-unix environment: - - XDG_RUNTIME_DIR=/run/wayland - - WAYLAND_DISPLAY=wayland-1 - - SWAYSOCK=/run/wayland/sway-ipc.sock - - WORKSPACE=1 + - DISPLAY=:0 + - MONITOR=HDMI-1 - PORT=5011 - LAUNCH_URL=https://github.com - KIOSK=1 @@ -39,16 +41,14 @@ services: ports: - "5012:5012" volumes: - - wayland-socket:/run/wayland + - x11-socket:/tmp/.X11-unix environment: - - XDG_RUNTIME_DIR=/run/wayland - - WAYLAND_DISPLAY=wayland-1 - - SWAYSOCK=/run/wayland/sway-ipc.sock - - WORKSPACE=2 + - DISPLAY=:0 + - MONITOR=HDMI-2 - PORT=5012 - LAUNCH_URL=https://google.com - KIOSK=1 - GPU=1 volumes: - wayland-socket: + x11-socket: diff --git a/kiosk/Dockerfile b/kiosk/Dockerfile index aa69225..78ad295 100644 --- a/kiosk/Dockerfile +++ b/kiosk/Dockerfile @@ -1,10 +1,10 @@ FROM node:18-bullseye-slim -# Install dependencies for Chromium, Wayland, and screenshot tool +# Install dependencies for Chromium and X11 RUN apt-get update && apt-get install -y \ chromium \ - sway \ - grim \ + x11-xserver-utils \ + scrot \ jq \ curl \ && rm -rf /var/lib/apt/lists/* diff --git a/kiosk/server.js b/kiosk/server.js index b4b7b0d..5df5342 100644 --- a/kiosk/server.js +++ b/kiosk/server.js @@ -1,7 +1,7 @@ const express = require('express'); const bodyParser = require('body-parser'); const puppeteer = require('puppeteer-core'); -const { spawn, exec } = require('child_process'); +const { spawn, exec, execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); @@ -10,7 +10,7 @@ app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); const PORT = process.env.PORT || 5011; -const WORKSPACE = process.env.WORKSPACE || '1'; +const MONITOR = process.env.MONITOR || 'HDMI-1'; // State variables let browserProcess = null; @@ -23,19 +23,48 @@ let isKiosk = process.env.KIOSK !== '0'; // default 1 let isGpu = process.env.GPU !== '0'; // default 1 let chromiumFlags = []; +function getMonitorGeometry(monitorName) { + try { + const stdout = execSync('xrandr').toString(); + const lines = stdout.split('\n'); + for (const line of lines) { + if (line.includes(monitorName) && line.includes('connected')) { + // Match geometry string like 1920x1080+1920+0 + const match = line.match(/(\d+)x(\d+)\+(\d+)\+(\d+)/); + if (match) { + return { + width: match[1], + height: match[2], + x: match[3], + y: match[4] + }; + } + } + } + } catch (e) { + console.error('Failed to parse xrandr:', e); + } + // Fallback geometry if xrandr fails + return { width: 1920, height: 1080, x: 0, y: 0 }; +} + function buildFlags() { + const geometry = getMonitorGeometry(MONITOR); + console.log(`Monitor ${MONITOR} resolved to position ${geometry.x},${geometry.y} with size ${geometry.width}x${geometry.height}`); + const flags = [ '--no-sandbox', '--disable-dev-shm-usage', '--remote-debugging-port=9222', - '--enable-features=UseOzonePlatform', - '--ozone-platform=wayland', - `--user-data-dir=/data/chromium-${WORKSPACE}`, - '--window-position=0,0' + `--user-data-dir=/data/chromium-${MONITOR}`, + `--window-position=${geometry.x},${geometry.y}`, + `--window-size=${geometry.width},${geometry.height}` ]; if (isKiosk) { flags.push('--kiosk'); + // To ensure full screen stays on the target monitor + flags.push(`--app=${currentUrl}`); } if (!isGpu) { @@ -57,24 +86,20 @@ async function startChromium() { } const flags = buildFlags(); - // Start with a specific title so we can target it with swaymsg - const initialUrl = `data:text/html,KioskInit${WORKSPACE}Loading...`; console.log('Starting Chromium with flags:', flags.join(' ')); - browserProcess = spawn('/usr/bin/chromium', [...flags, initialUrl], { + // If kiosk is enabled, we pass the URL via --app to enforce window boundaries + // Otherwise we just pass the URL as a positional argument. + const args = [...flags]; + if (!isKiosk) { + args.push(currentUrl); + } + + browserProcess = spawn('/usr/bin/chromium', args, { stdio: 'inherit' }); - // Wait a bit for the window to appear in Sway - setTimeout(() => { - console.log(`Assigning window to workspace ${WORKSPACE}...`); - exec(`swaymsg "[title=KioskInit${WORKSPACE}] move to workspace ${WORKSPACE}"`, (err, stdout, stderr) => { - if (err) console.error('Swaymsg error:', stderr); - else console.log('Swaymsg success:', stdout); - }); - }, 2000); - // Connect Puppeteer setTimeout(async () => { try { @@ -87,8 +112,10 @@ async function startChromium() { const pages = await browser.pages(); page = pages.length > 0 ? pages[0] : await browser.newPage(); - console.log(`Navigating to ${currentUrl}`); - await page.goto(currentUrl); + if (!isKiosk) { + console.log(`Navigating to ${currentUrl}`); + await page.goto(currentUrl); + } } catch (e) { console.error('Puppeteer connect error:', e); } @@ -128,7 +155,6 @@ app.post('/autorefresh/:interval', (req, res) => { }); app.post('/scan', (req, res) => { - // Stub for local service discovery res.status(200).send('Scanned'); }); @@ -157,7 +183,9 @@ app.post('/url', async (req, res) => { } } - if (needRestart) { + // If we're in kiosk mode, --app flag is used so changing URL requires restart to stay full screen + // If we're not in kiosk mode, we can just navigate the page + if (needRestart || isKiosk) { await startChromium(); } else if (page) { await page.goto(currentUrl); @@ -205,21 +233,10 @@ app.get('/version', (req, res) => { app.get('/screenshot', (req, res) => { const file = `/tmp/screenshot-${Date.now()}.png`; - // Using grim instead of scrot for Wayland - // We specify the output by workspace name if needed, but grim captures the whole screen if no output is specified. - // To capture just the workspace, we can use grim with swaymsg to find the coordinates, - // or simply rely on grim which by default captures everything. - // To be safe and capture just the output for this workspace: - exec(`grim -o HDMI-A-${WORKSPACE} ${file}`, (err) => { - if (err) { - // Fallback if HDMI-A-X is not found (e.g. testing locally) - exec(`grim ${file}`, (err2) => { - if (err2) return res.status(500).send('Screenshot failed'); - res.sendFile(file); - }); - } else { - res.sendFile(file); - } + // We use scrot for X11 screenshots. + exec(`scrot ${file}`, (err) => { + if (err) return res.status(500).send('Screenshot failed'); + res.sendFile(file); }); }); @@ -227,5 +244,5 @@ app.get('/screenshot', (req, res) => { startChromium(); app.listen(PORT, () => { - console.log(`Kiosk API running on port ${PORT}`); + console.log(`Kiosk API running on port ${PORT} for monitor ${MONITOR}`); }); diff --git a/kiosk/start.sh b/kiosk/start.sh index b8a3eb3..6438ee9 100644 --- a/kiosk/start.sh +++ b/kiosk/start.sh @@ -1,12 +1,15 @@ #!/bin/bash -# Wait for Wayland socket to be available -echo "Waiting for Wayland display $WAYLAND_DISPLAY..." -while [ ! -e "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]; do +# Wait for X11 socket to be available +echo "Waiting for X11 display $DISPLAY..." +while [ ! -S /tmp/.X11-unix/X0 ]; do sleep 1 done -echo "Wayland display found. Starting API server..." +echo "X11 display found. Starting API server..." + +# Disable screen blanking for this display as well just in case +xset s off -dpms || true # Start the Node.js API server exec node server.js