mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-17 00:20:47 +02:00
feat: finalize camoufox integration
This commit is contained in:
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user