const net = require('net') const CDP = require('chrome-remote-interface') const chromeFinder = require('chrome-finder') const { exec, spawn, } = require('child_process') const path = require('node:path') const os = require('node:os') class WebDriver { constructor (params = { args: [], host: '127.0.0.1', port: 0, chromePath: '', userDataDir: '', }) { this.args = params.args || [] this.host = params.host || '127.0.0.1' this.port = params.port || 0 this.chromePath = params.chromePath || '' this.userDataDir = params.userDataDir || '' } headless () { this.args.push('--headless=old') } getCDP () { return this.cdp } setProxy (proxy) { this.args.push(`--proxy-server=${proxy}`) } addArguments (arg) { if (arg) { this.args.push(arg) } else { throw new Error('Arguments cannot be null') } } setChromeBinaryPath (binaryPath) { this.chromePath = binaryPath } setUserDataDirPath (binaryPath) { this.userDataDir = binaryPath } async builder () { try { if (!this.port) { this.port = await this.isPortTaken(this.getRandomNumber()) } if (!this.chromePath) { this.chromePath = chromeFinder() } if (!this.userDataDir) { this.userDataDir = path.join(os.tmpdir(), `scoped_dir${new Date().getTime()}`) } console.info('启动端口:', this.port) const command = `${this.chromePath}` const args = [ `--user-data-dir=${this.userDataDir}`, `--disk-cache-dir=${this.userDataDir}`, `--remote-debugging-port=${this.port}`, '--no-first-run', '--no-dleck', '--disable-plugins', '--no-default-browser-check', '--new-tab', // 'data:,', ] const mergedArray = args.concat(this.args) const { spawn } = require('child_process') return new Promise((resolve, reject) => { const childProcess = spawn(command, mergedArray) childProcess.stderr.on('data', async (data) => { await this.connectToChrome(1, this.port) resolve(true) }) childProcess.on('close', (code) => { console.info(`子进程退出成功,退出码: ${code}`) }) childProcess.on('error', (err) => { console.error(`子进程错误:${err.message}`) }) }) } catch (e) { console.error(e) } } createCDPConnection () { return { host: this.host, port: this.port, } } checkBrowser () { return !!(this.chromePath || chromeFinder()) } isPortTaken (port) { return new Promise((resolve) => { const server = net.createServer() server.on('listening', () => { server.close(() => { resolve(port) }) }) server.on('error', async () => { this.isPortTaken(port + 1).then(resolve) }) server.listen(port, '127.0.0.1') }) } async connectToChrome (time, port) { const waitingTime = { 1: 1000, 2: 3000, 3: 5000, 4: 10000, } if (!this.cdp) { try { this.cdp = await CDP({ port }) console.info('CDP 连接成功') } catch (error) { console.error('连接失败,正在重试...') await new Promise(resolve => setTimeout(resolve, waitingTime[time])) if (time < 4) { await this.connectToChrome(time + 1, port) } else { await this.quit() await this.builder() } } } } getRandomNumber () { return Math.floor(Math.random() * (65535 - 10000 + 1)) + 10000 } quit () { const command = `netstat -ano | findstr LISTENING | findstr :${this.port}` return new Promise((resolve, reject) => { exec(command, (error, stdout, stderr) => { if (error) { resolve(`执行命令时出错: ${error}`) return } const lines = stdout.split('\n') const processIds = [] lines.forEach((line) => { const parts = line.trim().split(/\s+/) if (parts.length >= 2) { const pid = parts[parts.length - 1] processIds.push(pid) } }) if (processIds.length === 0) { resolve(`未找到在端口 ${this.port} 上运行的进程`) return } if (processIds.length > 0) { processIds.forEach((pid) => { exec(`taskkill /F /PID ${pid}`, (error) => { if (error) { resolve(`关闭进程时出错: ${error}`) } else { resolve(this.port) } }) }) } }) }) } } module.exports = { WebDriver: WebDriver }