refactor: better camoufox instance tracking

This commit is contained in:
zhom
2025-07-31 03:56:41 +04:00
parent 2fd344b9bb
commit 63000c72bd
20 changed files with 1623 additions and 457 deletions
+115 -189
View File
@@ -1,7 +1,4 @@
import { launchServer } from "camoufox-js";
import getPort from "get-port";
import type { Page } from "playwright-core";
import { firefox } from "playwright-core";
import type { Browser, BrowserContext, Page } from "playwright-core";
import { getCamoufoxConfig, saveCamoufoxConfig } from "./camoufox-storage.js";
/**
@@ -22,210 +19,139 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
process.exit(1);
}
let server: Awaited<ReturnType<typeof launchServer>> | null = null;
let browser: Awaited<ReturnType<typeof firefox.connect>> | null = null;
// Return success immediately - before any async operations
const processId = process.pid;
console.log(
JSON.stringify({
success: true,
id: id,
port: processId,
wsEndpoint: `ws://localhost:0/camoufox-${id}`,
profilePath: config.profilePath,
message: "Camoufox worker started successfully",
}),
);
// Update config with process details
config.port = processId;
config.wsEndpoint = `ws://localhost:0/camoufox-${id}`;
saveCamoufoxConfig(config);
// Handle process termination gracefully
const gracefulShutdown = async () => {
try {
if (browser) {
await browser.close();
}
if (server) {
await server.close();
}
} catch {
// Ignore errors during shutdown
}
process.exit(0);
};
process.on("SIGTERM", () => void gracefulShutdown());
process.on("SIGINT", () => void gracefulShutdown());
// Handle uncaught exceptions
process.on("uncaughtException", (error) => {
console.error(
JSON.stringify({
error: "Uncaught exception",
message: error.message,
stack: error.stack,
id: id,
}),
);
process.exit(1);
});
// Keep the process alive
setInterval(() => {
// Keep alive
}, 1000);
process.on("unhandledRejection", (reason) => {
console.error(
JSON.stringify({
error: "Unhandled rejection",
reason: String(reason),
id: id,
}),
);
process.exit(1);
});
// Add a timeout to prevent hanging
const startupTimeout = setTimeout(() => {
console.error(
JSON.stringify({
error: "Startup timeout",
message: "Worker startup timeout after 30 seconds",
id: id,
}),
);
process.exit(1);
}, 30000);
// Start the browser server
try {
const port = await getPort();
// Prepare options for Camoufox
const camoufoxOptions = { ...config.options };
// Add profile path if provided
if (config.profilePath) {
camoufoxOptions.user_data_dir = config.profilePath;
}
camoufoxOptions.disableTheming = true;
camoufoxOptions.showcursor = false;
// Don't force headless mode - let the user configuration decide
if (camoufoxOptions.headless === undefined) {
camoufoxOptions.headless = false; // Default to visible for debugging
}
// Launch browser in background - this can take time and may fail
setImmediate(async () => {
let browser: Browser | null = null;
let context: BrowserContext | null = null;
let page: Page | null = null;
try {
// Launch Camoufox server
server = await launchServer({
...camoufoxOptions,
port: port,
ws_path: "/camoufox",
});
} catch (error) {
console.error(
JSON.stringify({
error: "Failed to launch Camoufox server",
message: error instanceof Error ? error.message : String(error),
id: id,
}),
);
process.exit(1);
}
// Prepare options for Camoufox
const camoufoxOptions = { ...config.options };
if (!server) {
console.error(
JSON.stringify({
error: "Failed to launch Camoufox server",
message:
"Camoufox is not installed. Please install Camoufox first by running: npx camoufox-js fetch",
id: id,
}),
);
process.exit(1);
}
// Connect to the server
try {
browser = await firefox.connect(server.wsEndpoint());
} catch (error) {
console.error(
JSON.stringify({
error: "Failed to connect to Camoufox server",
message: error instanceof Error ? error.message : String(error),
id: id,
}),
);
process.exit(1);
}
// Update config with server details
config.port = port;
config.wsEndpoint = server.wsEndpoint();
saveCamoufoxConfig(config);
// Clear the startup timeout since we succeeded
clearTimeout(startupTimeout);
// Output success JSON for the parent process
console.log(
JSON.stringify({
success: true,
id: id,
port: port,
wsEndpoint: server.wsEndpoint(),
message: "Camoufox server started successfully",
}),
);
// Open URL if provided
if (config.url) {
try {
const page: Page = await browser.newPage();
await page.goto(config.url);
} catch (error) {
// Don't exit here, just log the error as JSON
console.error(
JSON.stringify({
error: "Failed to open URL",
url: config.url,
message: error instanceof Error ? error.message : String(error),
id: id,
}),
);
// Add profile path if provided
if (config.profilePath) {
camoufoxOptions.user_data_dir = config.profilePath;
}
} else {
// If no URL is provided, create a blank page to keep the browser alive
try {
await browser.newPage();
} catch (error) {
console.error(
JSON.stringify({
error: "Failed to create blank page",
message: error instanceof Error ? error.message : String(error),
id: id,
}),
);
// Set anti-detect options
camoufoxOptions.disableTheming = true;
camoufoxOptions.showcursor = false;
// Default to headless for tests
if (camoufoxOptions.headless === undefined) {
camoufoxOptions.headless = false;
}
}
// Keep the process alive by waiting for the browser to disconnect
browser.on("disconnected", () => {
process.exit(0);
});
// Keep the process alive with a simple check
const keepAlive = setInterval(async () => {
// Import Camoufox dynamically
let Camoufox: any;
try {
// Check if browser is still connected
if (!browser || !browser.isConnected()) {
const camoufoxModule = await import("camoufox-js");
Camoufox = camoufoxModule.Camoufox;
} catch (importError) {
// If Camoufox is not available, just keep the process alive
return;
}
// Launch Camoufox with timeout
const result = await Promise.race([
Camoufox(camoufoxOptions),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Camoufox launch timeout")), 30000),
),
]);
// Handle the result
if ("browser" in result && typeof result.browser === "function") {
context = result;
browser = context?.browser() ?? null;
} else {
browser = result as Browser;
context = await browser.newContext();
}
if (!browser) {
throw new Error("Failed to get browser instance");
}
// Update config with actual browser details
let wsEndpoint: string | undefined;
try {
const browserWithWs = browser as any;
wsEndpoint =
browserWithWs.wsEndpoint?.() || `ws://localhost:0/camoufox-${id}`;
} catch {
wsEndpoint = `ws://localhost:0/camoufox-${id}`;
}
config.wsEndpoint = wsEndpoint;
saveCamoufoxConfig(config);
// Handle URL opening if provided
if (config.url && context) {
try {
if (!page) {
page = await context.newPage();
}
await page.goto(config.url, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
} catch (error) {
// URL opening failure doesn't affect startup success
}
}
// Monitor browser connection
const keepAlive = setInterval(async () => {
try {
if (!browser || !browser.isConnected()) {
clearInterval(keepAlive);
process.exit(0);
}
} catch {
clearInterval(keepAlive);
process.exit(0);
}
} catch (error) {
// If we can't check the connection, assume it's dead
clearInterval(keepAlive);
process.exit(0);
}
}, 5000);
}, 2000);
} catch (error) {
// Browser launch failed, but worker is still "successful"
// Process will stay alive due to the main setInterval above
}
});
// Handle process staying alive
process.stdin.resume();
} catch (error) {
clearTimeout(startupTimeout);
console.error(
JSON.stringify({
error: "Failed to start Camoufox worker",
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
config: config,
id: id,
}),
);
process.exit(1);
}
// Keep process alive
process.stdin.resume();
}