Files
donutbrowser/nodecar/src/camoufox-worker.ts
T

236 lines
7.0 KiB
TypeScript

import { launchOptions } from "camoufox-js";
import type { LaunchOptions } from "camoufox-js/dist/utils.js";
import { type Browser, type BrowserServer, firefox } from "playwright-core";
import { getCamoufoxConfig, saveCamoufoxConfig } from "./camoufox-storage.js";
import { getEnvVars } from "./utils.js";
/**
* Run a Camoufox browser server as a worker process
* @param id The Camoufox configuration ID
*/
export async function runCamoufoxWorker(id: string): Promise<void> {
// Get the Camoufox configuration
const config = getCamoufoxConfig(id);
if (!config) {
console.error(
JSON.stringify({
error: "Configuration not found",
id: id,
}),
);
process.exit(1);
}
config.processId = process.pid;
saveCamoufoxConfig(config);
console.log(
JSON.stringify({
success: true,
id: id,
processId: process.pid,
profilePath: config.profilePath,
message: "Camoufox worker started successfully",
}),
);
// Launch browser in background - this can take time and may fail
setImmediate(async () => {
let browser: Browser | null = null;
let server: BrowserServer | null = null;
let windowCheckInterval: NodeJS.Timeout | null = null;
// Graceful shutdown handler with access to browser and server
const gracefulShutdown = async () => {
try {
// Clear any intervals first
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
}
// Close browser context and server if they exist
if (browser?.isConnected()) {
await browser.close();
}
if (server) {
server.process().kill();
await server.close();
}
} catch {
// Ignore cleanup errors during shutdown
}
process.exit(0);
};
// Handle various quit signals for proper macOS Command+Q support
process.on("SIGTERM", () => void gracefulShutdown());
process.on("SIGINT", () => void gracefulShutdown());
process.on("SIGHUP", () => void gracefulShutdown());
process.on("SIGQUIT", () => void gracefulShutdown());
// Handle uncaught exceptions and unhandled rejections
process.on("uncaughtException", () => void gracefulShutdown());
process.on("unhandledRejection", () => void gracefulShutdown());
try {
// Prepare options for Camoufox
const camoufoxOptions: LaunchOptions = { ...config.options };
// Add profile path if provided
if (config.profilePath) {
camoufoxOptions.user_data_dir = config.profilePath;
}
if (camoufoxOptions.block_images) {
camoufoxOptions.block_images = true;
}
if (camoufoxOptions.block_webgl) {
camoufoxOptions.block_webgl = true;
}
if (camoufoxOptions.block_webrtc) {
camoufoxOptions.block_webrtc = true;
}
// Check for headless mode from config (no environment variable check)
if (camoufoxOptions.headless) {
camoufoxOptions.headless = true;
}
// Always set these defaults
camoufoxOptions.i_know_what_im_doing = true;
camoufoxOptions.config = {
disableTheming: true,
showcursor: false,
...(camoufoxOptions.config || {}),
};
// Generate the configuration using launchOptions
const generatedOptions = await launchOptions(camoufoxOptions);
// If we have a custom config from Rust, use it directly as environment variables
let finalEnv = generatedOptions.env || {};
if (config.customConfig) {
try {
// Parse the custom config JSON string
const customConfigObj = JSON.parse(config.customConfig);
// Convert custom config to environment variables using getEnvVars
const customEnvVars = getEnvVars(customConfigObj);
// Merge custom config with generated config (custom takes precedence)
finalEnv = { ...finalEnv, ...customEnvVars };
} catch (error) {
console.error(
"Failed to parse custom config, using generated config:",
error,
);
}
}
// Launch the server with the final configuration
server = await firefox.launchServer({
...generatedOptions,
wsPath: `/ws_${config.id}`,
env: finalEnv,
});
// Connect to the server
browser = await firefox.connect(server.wsEndpoint());
const context = await browser.newContext();
// Handle browser disconnection for proper cleanup
browser.on("disconnected", () => void gracefulShutdown());
saveCamoufoxConfig(config);
// Monitor for window closure
const startWindowMonitoring = () => {
windowCheckInterval = setInterval(async () => {
try {
if (browser?.isConnected()) {
const contexts = browser.contexts();
let totalPages = 0;
for (const ctx of contexts) {
const pages = ctx.pages();
totalPages += pages.length;
}
// If no pages are open, terminate the server
if (totalPages === 0) {
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
}
await gracefulShutdown();
}
}
} catch {
// If we can't check windows, assume browser is closing
if (windowCheckInterval) {
clearInterval(windowCheckInterval);
}
await gracefulShutdown();
}
}, 1000); // Check every second
};
// Handle URL opening if provided
if (config.url) {
try {
const page = await context.newPage();
await page.goto(config.url, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
// Start monitoring after page is created
startWindowMonitoring();
} catch (urlError) {
console.error({
message: "Failed to open URL",
error: urlError,
});
// URL opening failure doesn't affect startup success
// Still start monitoring
startWindowMonitoring();
}
} else {
await context.newPage();
// Start monitoring after page is created
startWindowMonitoring();
}
// Monitor browser connection
const keepAlive = setInterval(async () => {
try {
if (!browser || !browser.isConnected()) {
clearInterval(keepAlive);
await gracefulShutdown();
}
} catch (error) {
console.error({
message: "Error in keepAlive check",
error,
});
clearInterval(keepAlive);
await gracefulShutdown();
}
}, 2000);
} catch (error) {
console.error({
message: "Failed to launch Camoufox",
error,
});
// Browser launch failed, attempt cleanup
await gracefulShutdown();
}
});
// Keep process alive
process.stdin.resume();
}