const express = require('express');
const bodyParser = require('body-parser');
const puppeteer = require('puppeteer-core');
const { spawn, exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
const PORT = process.env.PORT || 5011;
const WORKSPACE = process.env.WORKSPACE || '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 = [];
function buildFlags() {
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'
];
if (isKiosk) {
flags.push('--kiosk');
}
if (!isGpu) {
flags.push('--disable-gpu');
flags.push('--disable-software-rasterizer');
} else {
flags.push('--enable-gpu');
}
chromiumFlags = flags;
return flags;
}
async function startChromium() {
if (browserProcess) {
console.log('Stopping existing Chromium...');
browserProcess.kill('SIGTERM');
await new Promise(resolve => setTimeout(resolve, 2000));
}
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], {
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 {
console.log('Connecting puppeteer...');
browser = await puppeteer.connect({
browserURL: 'http://localhost:9222',
defaultViewport: null
});
const pages = await browser.pages();
page = pages.length > 0 ? pages[0] : await browser.newPage();
console.log(`Navigating to ${currentUrl}`);
await page.goto(currentUrl);
} catch (e) {
console.error('Puppeteer connect error:', e);
}
}, 3000);
}
// API Endpoints
app.get('/ping', (req, res) => {
res.status(200).send('OK');
});
app.post('/refresh', async (req, res) => {
if (page) {
await page.reload();
res.status(200).send('Refreshed');
} else {
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(() => {
if (page) page.reload().catch(console.error);
}, interval * 1000);
res.status(200).send(`Autorefresh enabled: ${interval}s`);
} else {
res.status(200).send('Autorefresh disabled');
}
});
app.post('/scan', (req, res) => {
// Stub for local service discovery
res.status(200).send('Scanned');
});
app.get('/url', (req, res) => {
res.status(200).send(currentUrl);
});
app.post('/url', async (req, res) => {
let needRestart = false;
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;
}
}
if (req.body.kiosk !== undefined) {
const newKiosk = req.body.kiosk === '1';
if (newKiosk !== isKiosk) {
isKiosk = newKiosk;
needRestart = true;
}
}
if (needRestart) {
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');
});
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.get('/kiosk', (req, res) => {
res.status(200).send(isKiosk ? '1' : '0');
});
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.get('/flags', (req, res) => {
res.status(200).json(chromiumFlags);
});
app.get('/version', (req, res) => {
exec('/usr/bin/chromium --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`;
// 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);
}
});
});
// Start initially
startChromium();
app.listen(PORT, () => {
console.log(`Kiosk API running on port ${PORT}`);
});