Refactor Selenium to make it more robust.

It should stop runaway Firefox processes.
This commit is contained in:
D. Berge
2025-08-17 13:18:04 +02:00
parent 24dcebd0d9
commit 4dadffbbe7

View File

@@ -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 };