File size: 3,426 Bytes
fb38ec5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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<string[]> {
    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<void> {
    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<void>((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),
    });
  }
}