import { EventEmitter } from "events"; import { ChildProcess, spawn } from "child_process"; import { BrowserLauncherOptions, BrowserEvent, BrowserEventType } from "../types/index.js"; import path, { dirname } from "path"; import { FastifyBaseLogger } from "fastify"; import { fileURLToPath } from "url"; export class SeleniumService extends EventEmitter { private seleniumProcess: ChildProcess | null = null; private seleniumServerUrl: string = "http://localhost:4444"; private port: number = 4444; private launchOptions?: BrowserLauncherOptions; private logger: FastifyBaseLogger; constructor(logger: FastifyBaseLogger) { super(); this.logger = logger; } public async getChromeArgs(): Promise { const { options, userAgent } = this.launchOptions ?? {}; return [ "disable-dev-shm-usage", "no-sandbox", "enable-javascript", userAgent ? `user-agent=${userAgent}` : "", options?.proxyUrl ? `proxy-server=${options.proxyUrl}` : "", ...(options?.args?.map((arg) => (arg.startsWith("--") ? arg.slice(2) : arg)) || []), ].filter(Boolean); } public async launch(launchOptions: BrowserLauncherOptions): Promise { this.launchOptions = launchOptions; if (this.seleniumProcess) { await this.close(); } const projectRoot = path.resolve(dirname(fileURLToPath(import.meta.url)), "../../"); const seleniumServerPath = path.join(projectRoot, "selenium", "server", "selenium-server.jar"); const seleniumArgs = ["-jar", seleniumServerPath, "standalone"]; this.seleniumProcess = spawn("java", seleniumArgs); this.seleniumServerUrl = `http://localhost:${this.port}`; this.seleniumProcess.stdout?.on("data", (data) => { this.logger.info(`Selenium stdout: ${data}`); this.postLog({ type: BrowserEventType.Console, text: JSON.stringify({ type: BrowserEventType.Console, message: `${data}` }), timestamp: new Date(), }); }); this.seleniumProcess.stderr?.on("data", (data) => { this.logger.error(`Selenium stderr: ${data}`); this.postLog({ type: BrowserEventType.Error, text: JSON.stringify({ type: BrowserEventType.Error, error: `${data}` }), timestamp: new Date(), }); }); this.seleniumProcess.on("close", (code) => { this.logger.info(`Selenium process exited with code ${code}`); this.seleniumProcess = null; }); await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error("Selenium server failed to start within the timeout period")); }, 15000); // 15 seconds timeout this.seleniumProcess!.stdout?.on("data", (data) => { if (data.toString().includes("Started Selenium Standalone")) { clearTimeout(timeout); resolve(); } }); }); } public close(): void { if (this.seleniumProcess) { this.seleniumProcess.kill("SIGINT"); this.seleniumProcess = null; } } public getSeleniumServerUrl(): string { return this.seleniumServerUrl; } private async postLog(browserLog: BrowserEvent) { if (!this.launchOptions?.logSinkUrl) { return; } await fetch(this.launchOptions.logSinkUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(browserLog), }); } }