diff --git a/lib/www/server/lib/selenium/index.js b/lib/www/server/lib/selenium/index.js index fe6d38a..e79f18e 100644 --- a/lib/www/server/lib/selenium/index.js +++ b/lib/www/server/lib/selenium/index.js @@ -1,52 +1,110 @@ -// TODO Append location to PATH - const path = require('path'); const fs = require('fs'); -const {Builder, By, Key, until} = require('selenium-webdriver'); -const firefox = require('selenium-webdriver/firefox'); +const { Builder, By, Key, until } = require('selenium-webdriver'); +const firefox = require('selenium-webdriver/firefox'); +const { execSync } = require('child_process'); const geckodriverPath = path.resolve(__dirname, "geckodriver"); -// We launch a browser instance and then start an activity timer. -// We shut down the browser after a period of inactivity, to -// save memory. +// State to prevent race conditions let driver = null; let timer = null; +let isShuttingDown = false; -function resetTimer () { - clearTimeout(timer); - timer = setTimeout(shutdown, 120000); // Yup, hardcoded to two minutes. For now anyway +// Verify GeckoDriver exists +if (!fs.existsSync(geckodriverPath)) { + throw new Error(`GeckoDriver not found at ${geckodriverPath}`); } -async function launch () { +function resetTimer() { + clearTimeout(timer); + timer = setTimeout(shutdown, 120000); // 2 minutes inactivity timeout +} + +async function launch() { + if (isShuttingDown) { + console.log("Shutdown in progress, waiting..."); + await new Promise(resolve => setTimeout(resolve, 1000)); + return launch(); // Retry after delay + } resetTimer(); if (!driver) { console.log("Launching Firefox"); const options = new firefox.Options(); + // Explicitly set headless mode and optimize for server + options.addArguments('--headless', '--no-sandbox', '--disable-gpu'); + // Limit content processes to reduce resource usage + options.setPreference('dom.ipc.processCount', 1); + + const service = new firefox.ServiceBuilder(geckodriverPath); driver = await new Builder() .forBrowser('firefox') - .setFirefoxService(new firefox.ServiceBuilder(geckodriverPath)) - .setFirefoxOptions(options.headless()) + .setFirefoxService(service) + .setFirefoxOptions(options) .build(); } } -async function shutdown () { - if (driver) { +async function shutdown() { + if (driver && !isShuttingDown) { + isShuttingDown = true; console.log("Shutting down Firefox"); - // This is an attempt at avoiding a race condition if someone - // makes a call and resets the timer while the shutdown is in - // progress. - const d = driver; - driver = null; - await d.quit(); + try { + const d = driver; + driver = null; + await d.quit(); + // Explicitly stop the service + const service = d.service; + if (service) { + service.stop(); + } + console.log("Firefox shutdown complete"); + } catch (error) { + console.error("Error during shutdown:", error); + // Forcefully kill lingering processes (Linux/Unix) + try { + execSync('pkill -u $USER firefox || true'); + execSync('pkill -u $USER geckodriver || true'); + console.log("Terminated lingering Firefox/GeckoDriver processes"); + } catch (killError) { + console.error("Error killing processes:", killError); + } + } finally { + isShuttingDown = false; + } } } -async function url2pdf (url) { +async function url2pdf(url) { await launch(); - await driver.get(url); - return await driver.printPage({width: 21.0, height: 29.7}); + try { + console.log(`Navigating to ${url}`); + await driver.get(url); + // Add delay to stabilize Marionette communication + await driver.sleep(3000); + const pdf = await driver.printPage({ width: 21.0, height: 29.7 }); + resetTimer(); // Reset timer after successful operation + return pdf; + } catch (error) { + console.error("Error in url2pdf:", error); + await shutdown(); // Force shutdown on error + throw error; + } } +// Periodically clean up orphaned processes (every 5 minutes) +setInterval(() => { + try { + const firefoxCount = execSync('pgrep -c firefox || echo 0').toString().trim(); + if (parseInt(firefoxCount) > 0 && !driver) { + console.log(`Found ${firefoxCount} orphaned Firefox processes, cleaning up...`); + execSync('pkill -u $USER firefox || true'); + execSync('pkill -u $USER geckodriver || true'); + console.log("Cleaned up orphaned processes"); + } + } catch (error) { + console.error("Error checking orphaned processes:", error); + } +}, 300000); + module.exports = { url2pdf };