const express = require('express'); const bodyParser = require('body-parser'); const { spawn, execSync, exec } = require('child_process'); const fs = require('fs'); const app = express(); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); const PORT = process.env.PORT || 5011; const MONITOR = process.env.MONITOR || 'HDMI-1'; // State variables let browserProcess = null; let autorefreshInterval = null; 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 { const stdout = execSync('xrandr').toString(); const lines = stdout.split('\n'); for (const line of lines) { if (line.startsWith(monitorName + ' ') && line.includes(' connected')) { 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 execute xrandr:', e.message); } return null; } function ensureMonitorConnected() { return new Promise(resolve => { const check = () => { try { execSync('xrandr --auto'); } catch(e) {} const geom = getMonitorGeometry(MONITOR); if (geom) { console.log(`Monitor ${MONITOR} is connected and ready.`); resolve(geom); } else { console.log(`Waiting for monitor ${MONITOR} to be connected...`); setTimeout(check, 5000); } }; check(); }); } 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 startBrowser() { console.log(`Checking connection status for ${MONITOR}...`); const geometry = await ensureMonitorConnected(); if (browserProcess) { console.log('Stopping existing Cog...'); browserProcess.kill('SIGTERM'); await new Promise(resolve => setTimeout(resolve, 2000)); } console.log(`Starting Cog to navigate to ${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('cog', args, { stdio: 'inherit', env: env }); // Call position function which will wait for window to exist and move it positionCogWindow(geometry); } // API Endpoints app.get('/ping', (req, res) => { res.status(200).send('OK'); }); app.post('/refresh', (req, res) => { try { execSync('cogctl reload'); res.status(200).send('Refreshed'); } catch (e) { res.status(503).send('Browser not ready'); } }); app.post('/autorefresh/:interval', (req, res) => { const interval = parseInt(req.params.interval, 10); if (autorefreshInterval) { clearInterval(autorefreshInterval); autorefreshInterval = null; } if (interval > 0 && interval <= 60) { autorefreshInterval = setInterval(() => { try { execSync('cogctl reload'); } catch(e) {} }, interval * 1000); res.status(200).send(`Autorefresh enabled: ${interval}s`); } else { res.status(200).send('Autorefresh disabled'); } }); app.post('/scan', (req, res) => { res.status(200).send('Scanned'); }); app.get('/url', (req, res) => { res.status(200).send(currentUrl); }); app.post('/url', (req, res) => { if (req.body.url) currentUrl = req.body.url; // 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)`); }); } }); app.get('/gpu', (req, res) => { res.status(200).send('1'); // Always 1 for WPE }); app.put('/gpu/:value', (req, res) => { res.status(200).send('1'); // Ignored for WPE }); app.get('/kiosk', (req, res) => { res.status(200).send('1'); // Always 1 for WPE }); app.put('/kiosk/:value', (req, res) => { res.status(200).send('1'); // Ignored for WPE }); app.get('/flags', (req, res) => { res.status(200).json(['WPE WebKit (Cog)']); }); app.get('/version', (req, res) => { exec('cog --version', (err, stdout) => { if (err) res.status(500).send('Unknown'); else res.status(200).send(stdout.trim()); }); }); app.get('/screenshot', (req, res) => { const file = `/tmp/screenshot-${Date.now()}.png`; exec(`scrot ${file}`, (err) => { if (err) return res.status(500).send('Screenshot failed'); res.sendFile(file); }); }); // Start initially startBrowser(); app.listen(PORT, () => { console.log(`Kiosk API running on port ${PORT} for monitor ${MONITOR}`); });