feat: finalize camoufox integration

This commit is contained in:
zhom
2025-08-03 14:38:44 +04:00
parent 54fd9b7282
commit b088ae675b
10 changed files with 385 additions and 351 deletions
+36 -10
View File
@@ -176,29 +176,48 @@ export async function stopCamoufoxProcess(id: string): Promise<boolean> {
}
try {
const killByPattern = spawn("pkill", ["-f", `camoufox-worker.*${id}`], {
stdio: "ignore",
});
console.log(`Stopping Camoufox process ${id} (PID: ${config.processId})`);
// Method 2: If we have a process ID, kill by PID
// Method 1: If we have a process ID, kill by PID with proper signal sequence
if (config.processId) {
try {
// First try SIGTERM for graceful shutdown
process.kill(config.processId, "SIGTERM");
console.log(`Sent SIGTERM to Camoufox process ${config.processId}`);
// Give it a moment to terminate gracefully
await new Promise((resolve) => setTimeout(resolve, 2000));
// Give it more time to terminate gracefully (increased from 2s to 5s)
await new Promise((resolve) => setTimeout(resolve, 5000));
// Force kill if still running
// Check if process is still running
try {
process.kill(config.processId, 0); // Signal 0 checks if process exists
// Process still exists, force kill
console.log(
`Camoufox process ${config.processId} still running, sending SIGKILL`,
);
process.kill(config.processId, "SIGKILL");
} catch {
// Process already terminated
console.log(
`Camoufox process ${config.processId} terminated gracefully`,
);
}
} catch (error) {
// Process not found or already terminated
} catch {
console.log(
`Camoufox process ${config.processId} not found or already terminated`,
);
}
}
// Method 2: Pattern-based kill as fallback
const killByPattern = spawn(
"pkill",
["-TERM", "-f", `camoufox-worker.*${id}`],
{
stdio: "ignore",
},
);
// Wait for pattern-based kill command to complete
await new Promise<void>((resolve) => {
killByPattern.on("exit", () => resolve());
@@ -206,10 +225,17 @@ export async function stopCamoufoxProcess(id: string): Promise<boolean> {
setTimeout(() => resolve(), 3000);
});
// Final cleanup with SIGKILL if needed
setTimeout(() => {
spawn("pkill", ["-KILL", "-f", `camoufox-worker.*${id}`], {
stdio: "ignore",
});
}, 1000);
// Delete the configuration
deleteCamoufoxConfig(id);
return true;
} catch (error) {
} catch {
// Delete the configuration even if stopping failed
deleteCamoufoxConfig(id);
return false;
+146 -32
View File
@@ -1,5 +1,5 @@
import { Camoufox } from "camoufox-js";
import type { Page } from "playwright-core";
import { launchServer } from "camoufox-js";
import { type Browser, type BrowserServer, firefox } from "playwright-core";
import { getCamoufoxConfig, saveCamoufoxConfig } from "./camoufox-storage.js";
/**
@@ -20,34 +20,56 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
process.exit(1);
}
// Return success immediately - before any async operations
const processId = process.pid;
config.processId = process.pid;
saveCamoufoxConfig(config);
console.log(
JSON.stringify({
success: true,
id: id,
processId,
processId: process.pid,
profilePath: config.profilePath,
message: "Camoufox worker started successfully",
}),
);
// Update config with process details
config.processId = processId;
saveCamoufoxConfig(config);
// Handle process termination gracefully
const gracefulShutdown = async () => {
process.exit(0);
};
process.on("SIGTERM", () => void gracefulShutdown());
process.on("SIGINT", () => void gracefulShutdown());
// Launch browser in background - this can take time and may fail
setImmediate(async () => {
let page: Page | null = null;
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
@@ -58,7 +80,7 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
camoufoxOptions.user_data_dir = config.profilePath;
}
// Remove custom properties before passing to Camoufox
// Theming
camoufoxOptions.disableTheming = true;
camoufoxOptions.showcursor = false;
@@ -72,24 +94,108 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
camoufoxOptions.headless = false;
}
const browser = await Camoufox(camoufoxOptions);
// Launch the server with proper options
server = await launchServer({
ws_path: `/ws_${config.id}`,
os: camoufoxOptions.os,
block_images: camoufoxOptions.block_images,
block_webrtc: camoufoxOptions.block_webrtc,
block_webgl: camoufoxOptions.block_webgl,
disable_coop: camoufoxOptions.disable_coop,
geoip: camoufoxOptions.geoip,
humanize: camoufoxOptions.humanize,
locale: camoufoxOptions.locale,
addons: camoufoxOptions.addons,
fonts: camoufoxOptions.fonts,
custom_fonts_only: camoufoxOptions.custom_fonts_only,
exclude_addons: camoufoxOptions.exclude_addons,
screen: camoufoxOptions.screen,
window: camoufoxOptions.window,
fingerprint: camoufoxOptions.fingerprint,
ff_version: camoufoxOptions.ff_version,
headless: camoufoxOptions.headless,
main_world_eval: camoufoxOptions.main_world_eval,
executable_path: camoufoxOptions.executable_path,
firefox_user_prefs: camoufoxOptions.firefox_user_prefs,
proxy: camoufoxOptions.proxy,
enable_cache: camoufoxOptions.enable_cache,
args: camoufoxOptions.args,
env: camoufoxOptions.env,
debug: camoufoxOptions.debug,
virtual_display: camoufoxOptions.virtual_display,
webgl_config: camoufoxOptions.webgl_config,
config: {
disableTheming: true,
showcursor: false,
timezone: camoufoxOptions.timezone,
},
});
// 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);
// Handle URL opening if provided
if (config.url && context) {
try {
if (!page) {
page = await context.newPage();
// Monitor for window closure to handle Command+Q properly
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,
});
} catch {
// 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
@@ -97,16 +203,24 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
try {
if (!browser || !browser.isConnected()) {
clearInterval(keepAlive);
process.exit(0);
await gracefulShutdown();
}
} catch {
} catch (error) {
console.error({
message: "Error in keepAlive check",
error,
});
clearInterval(keepAlive);
process.exit(0);
await gracefulShutdown();
}
}, 2000);
} catch {
// Browser launch failed, but worker is still "successful"
// Process will stay alive due to the main setInterval above
} catch (error) {
console.error({
message: "Failed to launch Camoufox",
error,
});
// Browser launch failed, attempt cleanup
await gracefulShutdown();
}
});
+4 -11
View File
@@ -233,8 +233,7 @@ program
// Firefox preferences
.option("--firefox-prefs <prefs>", "Firefox user preferences (JSON string)")
.option("--disable-theming", "disable Firefox theming")
.option("--no-showcursor", "disable cursor display")
// Note: theming and cursor options are hardcoded and not user-configurable
.description("manage Camoufox browser instances")
.action(
@@ -262,11 +261,12 @@ program
// Security options
if (options.disableCoop) camoufoxOptions.disable_coop = true;
// Geolocation
// Geolocation - always enable geoip for proper spoofing
if (options.geoip) {
camoufoxOptions.geoip =
options.geoip === "auto" ? true : (options.geoip as string);
}
if (options.latitude && options.longitude) {
camoufoxOptions.geolocation = {
latitude: options.latitude as number,
@@ -279,9 +279,8 @@ program
if (options.timezone)
camoufoxOptions.timezone = options.timezone as string;
// UI and behavior
if (options.humanize)
camoufoxOptions.humanize = options.humanize as boolean | number;
camoufoxOptions.humanize = options.humanize as boolean;
if (options.headless) camoufoxOptions.headless = true;
// Localization
@@ -388,11 +387,6 @@ program
}
}
// Theming and cursor - these are custom properties for camoufox-js
if (options.disableTheming) camoufoxOptions.disableTheming = true;
if (options.showcursor === false) camoufoxOptions.showcursor = false;
// Use the launcher to start Camoufox properly
const config = await startCamoufoxProcess(
camoufoxOptions,
typeof options.profilePath === "string"
@@ -401,7 +395,6 @@ program
typeof options.url === "string" ? options.url : undefined,
);
// Output the configuration as JSON for the Rust side to parse
console.log(
JSON.stringify({
id: config.id,