diff --git a/package-lock.json b/package-lock.json index 2391de1f..4354fe2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -363,6 +363,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.3.tgz", "integrity": "sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1001,6 +1002,7 @@ "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -1836,6 +1838,7 @@ "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", @@ -4587,6 +4590,7 @@ "integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^3.0.0", "@octokit/graphql": "^5.0.0", @@ -5486,6 +5490,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz", "integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.19.8" } @@ -5529,6 +5534,7 @@ "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5695,6 +5701,7 @@ "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -5750,6 +5757,7 @@ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -6603,6 +6611,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6677,6 +6686,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -8402,6 +8412,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", @@ -10779,16 +10790,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/encoding-sniffer": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", @@ -10814,19 +10815,6 @@ "node": ">=0.10.0" } }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -11155,6 +11143,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -18031,6 +18020,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", "@yarnpkg/lockfile": "^1.1.0", @@ -19437,6 +19427,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -19672,6 +19663,7 @@ "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -20235,6 +20227,7 @@ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -20758,6 +20751,7 @@ "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", "license": "Apache-2.0", + "peer": true, "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -21282,6 +21276,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -23305,6 +23300,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -24133,6 +24129,7 @@ "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -24180,6 +24177,7 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -25296,6 +25294,7 @@ "deepmerge": "4.3.1", "puppeteer-core": "22.3.0", "selenium-server": "3.141.59", + "tree-kill": "1.2.2", "webdriverio": "9.20.0" } }, diff --git a/packages/plugin-selenium-driver/package.json b/packages/plugin-selenium-driver/package.json index 94adf2b9..3956ad5c 100644 --- a/packages/plugin-selenium-driver/package.json +++ b/packages/plugin-selenium-driver/package.json @@ -25,6 +25,7 @@ "deepmerge": "4.3.1", "puppeteer-core": "22.3.0", "selenium-server": "3.141.59", + "tree-kill": "1.2.2", "webdriverio": "9.20.0" } } diff --git a/packages/plugin-selenium-driver/src/plugin/index.ts b/packages/plugin-selenium-driver/src/plugin/index.ts index ff157cfd..3509d726 100644 --- a/packages/plugin-selenium-driver/src/plugin/index.ts +++ b/packages/plugin-selenium-driver/src/plugin/index.ts @@ -20,6 +20,7 @@ import * as deepmerge from 'deepmerge'; import {spawnWithPipes} from '@testring/child-process'; import {loggerClient} from '@testring/logger'; import {getCrxBase64} from '@testring/dwnld-collector-crx'; +import kill from 'tree-kill'; import type {Cookie} from '@wdio/protocols'; import type {ClickOptions, MockFilterOptions, WaitUntilOptions} from 'webdriverio'; @@ -257,9 +258,15 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { } private forceKillSelenium() { - if (this.localSelenium && !this.localSelenium.killed) { - this.logger.debug('Force killing Selenium process due to signal'); - this.localSelenium.kill('SIGKILL'); + if (this.localSelenium && !this.localSelenium.killed && this.localSelenium.pid) { + this.logger.debug('Force killing Selenium process tree due to signal'); + kill(this.localSelenium.pid, 'SIGKILL', (err: Error | undefined) => { + if (err) { + this.logger.error(`Failed to force kill Selenium process tree: ${err.message}`); + } else { + this.logger.debug('Selenium process tree force killed'); + } + }); } } @@ -640,13 +647,7 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { this.customBrowserClientsConfigs.delete(applicant); } - public async kill() { - this.logger.debug('Kill command is called'); - - // Set killed flag to prevent new operations - this.killed = true; - - // Close all browser sessions + private async closeAllBrowserSessions() { for (const applicant of this.browserClients.keys()) { try { await this.end(applicant); @@ -654,6 +655,55 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { this.logger.error(e); } } + } + + private cleanupSeleniumProcess() { + if (!this.localSelenium) { + return; + } + + // remove listener + if (this.localSelenium.stderr) { + this.localSelenium.stderr.removeAllListeners('data'); + this.localSelenium.stdout?.removeAllListeners(); + } + + // Ensure all pipes are closed + this.localSelenium.stdout?.destroy(); + this.localSelenium.stderr?.destroy(); + this.localSelenium.stdin?.destroy(); + } + + private async killSeleniumProcessTree(pid: number): Promise { + return new Promise((resolve) => { + kill(pid, 'SIGTERM', (err: Error | undefined) => { + if (err) { + this.logger.warn(`Failed to kill process tree with SIGTERM: ${err.message}`); + // Fallback to SIGKILL if SIGTERM fails + kill(pid, 'SIGKILL', (killErr: Error | undefined) => { + if (killErr) { + this.logger.error(`Failed to kill process tree with SIGKILL: ${killErr.message}`); + } else { + this.logger.debug('Process tree killed with SIGKILL'); + } + resolve(); + }); + } else { + this.logger.debug('Process tree killed with SIGTERM'); + resolve(); + } + }); + }); + } + + public async kill() { + this.logger.debug('Kill command is called'); + + // Set killed flag to prevent new operations + this.killed = true; + + // Close all browser sessions + await this.closeAllBrowserSessions(); // If using 'local' mode, stop all active sessions if (this.config.workerLimit === 'local') { @@ -667,52 +717,25 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { return; } - // remove listener - if (this.localSelenium.stderr) { - this.localSelenium.stderr.removeAllListeners('data'); - this.localSelenium.stdout?.removeAllListeners(); + const pid = this.localSelenium.pid; + if (!pid) { + this.logger.warn('Selenium process has no PID, cannot kill process tree'); + return; } - // Ensure all pipes are closed - this.localSelenium.stdout?.destroy(); - this.localSelenium.stderr?.destroy(); - this.localSelenium.stdin?.destroy(); + this.cleanupSeleniumProcess(); this.logger.debug( - `Stopping local Selenium server (PID: ${this.localSelenium.pid})`, + `Stopping local Selenium server and all child processes (PID: ${pid})`, ); - // Try SIGTERM first - this.localSelenium.kill('SIGTERM'); - - // Wait for exit event with a timeout (ensures it does not hang forever) - const waitForExit = new Promise((resolve) => { - this.localSelenium?.once('exit', () => { - this.logger.debug('Selenium process exited.'); - resolve(); - }); - }); - - // Force kill if not exiting within 1 second (reduced from 3 seconds) - const forceKill = new Promise((resolve) => { - setTimeout(() => { - if (this.localSelenium && !this.localSelenium.killed) { - this.logger.warn( - `Selenium did not exit in time. Sending SIGKILL.`, - ); - this.localSelenium.kill('SIGKILL'); - } - resolve(); - }, 1000); // Reduced timeout for faster cleanup - }); - - // Wait for either normal exit or force kill - await Promise.race([waitForExit, forceKill]); + // Use tree-kill to properly terminate the process tree + await this.killSeleniumProcessTree(pid); this.localSelenium.removeAllListeners(); this.logger.debug( - 'Selenium process and all associated pipes closed.', + 'Selenium process tree and all associated pipes closed.', ); } }