diff --git a/kiosk/Dockerfile b/kiosk/Dockerfile index 78ad295..9c341ba 100644 --- a/kiosk/Dockerfile +++ b/kiosk/Dockerfile @@ -1,12 +1,16 @@ -FROM node:18-bullseye-slim +FROM node:18-bookworm-slim -# Install dependencies for Chromium and X11 +# Install dependencies for WPE WebKit (Cog), X11, and DBus RUN apt-get update && apt-get install -y \ - chromium \ + cog \ + wpewebkit \ x11-xserver-utils \ + wmctrl \ + xdotool \ scrot \ jq \ curl \ + dbus-x11 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -18,7 +22,4 @@ COPY server.js ./ COPY start.sh /start.sh RUN chmod +x /start.sh -# Provide a default user data directory -RUN mkdir -p /data/chromium && chown -R node:node /data - CMD ["/start.sh"] diff --git a/kiosk/package.json b/kiosk/package.json index 7b1c569..1e16996 100644 --- a/kiosk/package.json +++ b/kiosk/package.json @@ -1,14 +1,13 @@ { "name": "kiosk-api", "version": "1.0.0", - "description": "Multi-monitor kiosk API based on balena browser block", + "description": "Multi-monitor kiosk API using WPE WebKit", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "express": "^4.18.2", - "puppeteer-core": "^20.0.0", "body-parser": "^1.20.2" } } diff --git a/kiosk/server.js b/kiosk/server.js index 748f32f..94932da 100644 --- a/kiosk/server.js +++ b/kiosk/server.js @@ -1,9 +1,7 @@ const express = require('express'); const bodyParser = require('body-parser'); -const puppeteer = require('puppeteer-core'); -const { spawn, exec, execSync } = require('child_process'); +const { spawn, execSync, exec } = require('child_process'); const fs = require('fs'); -const path = require('path'); const app = express(); app.use(bodyParser.urlencoded({ extended: true })); @@ -14,14 +12,11 @@ const MONITOR = process.env.MONITOR || 'HDMI-1'; // State variables let browserProcess = null; -let browser = null; -let page = null; let autorefreshInterval = null; -let currentUrl = process.env.LAUNCH_URL || 'about:blank'; -let isKiosk = process.env.KIOSK !== '0'; // default 1 -let isGpu = process.env.GPU !== '0'; // default 1 -let chromiumFlags = []; +let currentUrl = process.env.LAUNCH_URL || 'https://duckduckgo.com'; +let isKiosk = 1; // Cog is always full-screen/kiosk implicitly +let isGpu = 1; // WPE is inherently hardware-accelerated function getMonitorGeometry(monitorName) { try { @@ -29,7 +24,6 @@ function getMonitorGeometry(monitorName) { const lines = stdout.split('\n'); for (const line of lines) { if (line.startsWith(monitorName + ' ') && line.includes(' connected')) { - // Match geometry string like 1920x1080+1920+0 const match = line.match(/(\d+)x(\d+)\+(\d+)\+(\d+)/); if (match) { return { @@ -50,7 +44,6 @@ function getMonitorGeometry(monitorName) { function ensureMonitorConnected() { return new Promise(resolve => { const check = () => { - // Run xrandr --auto to ensure any newly connected displays are configured try { execSync('xrandr --auto'); } catch(e) {} @@ -68,91 +61,70 @@ function ensureMonitorConnected() { }); } -function buildFlags(geometry) { - 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', - `--user-data-dir=/data/chromium-${MONITOR}`, - `--window-position=${geometry.x},${geometry.y}`, - `--window-size=${geometry.width},${geometry.height}`, - '--autoplay-policy=no-user-gesture-required' - ]; - - if (isKiosk) { - flags.push('--kiosk'); - // To ensure full screen stays on the target monitor - flags.push(`--app=${currentUrl}`); - } - - if (!isGpu) { - flags.push('--disable-gpu'); - flags.push('--disable-software-rasterizer'); - } else { - flags.push('--enable-gpu'); - } - - chromiumFlags = flags; - return flags; +function positionCogWindow(geometry) { + // Wait for the window to appear, then use wmctrl to move it + console.log('Attempting to position Cog window...'); + let attempts = 10; + const tryMove = () => { + try { + // Find the window ID of the Cog browser. + // Cog's window title or class is usually 'cog' + const stdout = execSync('wmctrl -l').toString(); + if (stdout.toLowerCase().includes('cog')) { + // Find WID + const line = stdout.split('\n').find(l => l.toLowerCase().includes('cog')); + if (line) { + const wid = line.split(' ')[0]; + console.log(`Found Cog window: ${wid}. Positioning to ${geometry.x},${geometry.y}...`); + // Remove maximization first + try { execSync(`wmctrl -i -r ${wid} -b remove,maximized_vert,maximized_horz`); } catch (e) {} + // Move and resize: gravity,x,y,w,h + execSync(`wmctrl -i -r ${wid} -e 0,${geometry.x},${geometry.y},${geometry.width},${geometry.height}`); + console.log('Window positioned successfully.'); + return; // Done + } + } + } catch (e) { + console.log('wmctrl check failed', e.message); + } + + attempts--; + if (attempts > 0) { + setTimeout(tryMove, 1000); + } else { + console.error('Failed to find and position Cog window.'); + } + }; + + setTimeout(tryMove, 2000); } -async function startChromium() { +async function startBrowser() { console.log(`Checking connection status for ${MONITOR}...`); const geometry = await ensureMonitorConnected(); if (browserProcess) { - console.log('Stopping existing Chromium...'); + console.log('Stopping existing Cog...'); browserProcess.kill('SIGTERM'); await new Promise(resolve => setTimeout(resolve, 2000)); } - - const flags = buildFlags(geometry); - console.log('Starting Chromium with flags:', flags.join(' ')); + console.log(`Starting Cog to navigate to ${currentUrl}`); - // 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); - } + // Launch Cog. We use the X11 platform if available. + const args = [currentUrl]; + const env = Object.assign({}, process.env, { + COG_PLATFORM_NAME: 'x11', + // Ensures background opacity might work and standard behaviors + }); - browserProcess = spawn('/usr/bin/chromium', args, { - stdio: 'inherit' + browserProcess = spawn('cog', args, { + stdio: 'inherit', + env: env }); - // Connect Puppeteer with retries - let retries = 15; - const tryConnect = async () => { - try { - console.log('Connecting puppeteer...'); - browser = await puppeteer.connect({ - browserURL: 'http://127.0.0.1:9222', - defaultViewport: null - }); - - const pages = await browser.pages(); - page = pages.length > 0 ? pages[0] : await browser.newPage(); - - if (!isKiosk) { - console.log(`Navigating to ${currentUrl}`); - await page.goto(currentUrl); - } - console.log('Puppeteer connected successfully!'); - } catch (e) { - if (retries > 0) { - console.log(`Puppeteer connect failed, retrying in 2s... (${retries} attempts left)`); - retries--; - setTimeout(tryConnect, 2000); - } else { - console.error('Puppeteer connect error (fatal):', e); - } - } - }; - - setTimeout(tryConnect, 2000); + // Call position function which will wait for window to exist and move it + positionCogWindow(geometry); } // API Endpoints @@ -161,11 +133,11 @@ app.get('/ping', (req, res) => { res.status(200).send('OK'); }); -app.post('/refresh', async (req, res) => { - if (page) { - await page.reload(); +app.post('/refresh', (req, res) => { + try { + execSync('cogctl reload'); res.status(200).send('Refreshed'); - } else { + } catch (e) { res.status(503).send('Browser not ready'); } }); @@ -179,7 +151,7 @@ app.post('/autorefresh/:interval', (req, res) => { if (interval > 0 && interval <= 60) { autorefreshInterval = setInterval(() => { - if (page) page.reload().catch(console.error); + try { execSync('cogctl reload'); } catch(e) {} }, interval * 1000); res.status(200).send(`Autorefresh enabled: ${interval}s`); } else { @@ -195,70 +167,44 @@ app.get('/url', (req, res) => { res.status(200).send(currentUrl); }); -app.post('/url', async (req, res) => { - let needRestart = false; - +app.post('/url', (req, res) => { if (req.body.url) currentUrl = req.body.url; - if (req.body.gpu !== undefined) { - const newGpu = req.body.gpu === '1'; - if (newGpu !== isGpu) { - isGpu = newGpu; - needRestart = true; - } + // Cogctl can directly tell the browser to open a URL without restarting it + try { + console.log(`Sending cogctl open ${currentUrl}`); + execSync(`cogctl open "${currentUrl}"`); + res.status(200).send(`URL set to ${currentUrl}`); + } catch (e) { + console.error('Failed to set URL via cogctl. Attempting restart...', e.message); + startBrowser().then(() => { + res.status(200).send(`URL set to ${currentUrl} (restarted)`); + }); } - - if (req.body.kiosk !== undefined) { - const newKiosk = req.body.kiosk === '1'; - if (newKiosk !== isKiosk) { - isKiosk = newKiosk; - needRestart = true; - } - } - - // 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); - } - - res.status(200).send(`URL set to ${currentUrl}`); }); app.get('/gpu', (req, res) => { - res.status(200).send(isGpu ? '1' : '0'); + res.status(200).send('1'); // Always 1 for WPE }); -app.put('/gpu/:value', async (req, res) => { - const val = req.params.value === '1'; - if (val !== isGpu) { - isGpu = val; - await startChromium(); - } - res.status(200).send(isGpu ? '1' : '0'); +app.put('/gpu/:value', (req, res) => { + res.status(200).send('1'); // Ignored for WPE }); app.get('/kiosk', (req, res) => { - res.status(200).send(isKiosk ? '1' : '0'); + res.status(200).send('1'); // Always 1 for WPE }); -app.put('/kiosk/:value', async (req, res) => { - const val = req.params.value === '1'; - if (val !== isKiosk) { - isKiosk = val; - await startChromium(); - } - res.status(200).send(isKiosk ? '1' : '0'); +app.put('/kiosk/:value', (req, res) => { + res.status(200).send('1'); // Ignored for WPE }); app.get('/flags', (req, res) => { - res.status(200).json(chromiumFlags); + res.status(200).json(['WPE WebKit (Cog)']); }); app.get('/version', (req, res) => { - exec('/usr/bin/chromium --version', (err, stdout) => { + exec('cog --version', (err, stdout) => { if (err) res.status(500).send('Unknown'); else res.status(200).send(stdout.trim()); }); @@ -266,7 +212,6 @@ app.get('/version', (req, res) => { app.get('/screenshot', (req, res) => { const file = `/tmp/screenshot-${Date.now()}.png`; - // We use scrot for X11 screenshots. exec(`scrot ${file}`, (err) => { if (err) return res.status(500).send('Screenshot failed'); res.sendFile(file); @@ -274,7 +219,7 @@ app.get('/screenshot', (req, res) => { }); // Start initially -startChromium(); +startBrowser(); app.listen(PORT, () => { console.log(`Kiosk API running on port ${PORT} for monitor ${MONITOR}`); diff --git a/kiosk/start.sh b/kiosk/start.sh index 6438ee9..fa60f78 100644 --- a/kiosk/start.sh +++ b/kiosk/start.sh @@ -6,10 +6,13 @@ while [ ! -S /tmp/.X11-unix/X0 ]; do sleep 1 done -echo "X11 display found. Starting API server..." +echo "X11 display found. Initializing D-Bus and starting API server..." -# Disable screen blanking for this display as well just in case +# Disable screen blanking xset s off -dpms || true +# Initialize a local D-Bus session. This is required for cogctl to communicate with cog. +export DBUS_SESSION_BUS_ADDRESS=$(dbus-daemon --session --print-address --fork) + # Start the Node.js API server exec node server.js