From 8150921e8a4ccc69d6f881e24b14578940aaf087 Mon Sep 17 00:00:00 2001 From: "artem.sukhinin" Date: Wed, 22 Oct 2025 14:51:18 +0300 Subject: [PATCH 1/2] WAT-5278 --- core/api/src/index.ts | 3 +- core/child-process/src/fork.ts | 43 +++++++++++++------ core/child-process/src/index.ts | 1 + core/child-process/src/spawn-with-pipes.ts | 10 ++--- core/child-process/src/spawn.ts | 13 +++++- .../src/modules/test-run-controller.ts | 14 +++--- .../src/plugin/index.ts | 29 +++++++++++-- 7 files changed, 80 insertions(+), 33 deletions(-) diff --git a/core/api/src/index.ts b/core/api/src/index.ts index 25ad2e428..f548ab37a 100644 --- a/core/api/src/index.ts +++ b/core/api/src/index.ts @@ -1,5 +1,6 @@ import {WebApplication} from '@testring/web-application'; import {testAPIController, TestAPIController} from './test-api-controller'; +import {TestContext} from './test-context'; import {run} from './run'; -export {run, testAPIController, TestAPIController, WebApplication}; +export {run, testAPIController, TestAPIController, WebApplication, TestContext}; diff --git a/core/child-process/src/fork.ts b/core/child-process/src/fork.ts index a409d9a5a..a0a221205 100644 --- a/core/child-process/src/fork.ts +++ b/core/child-process/src/fork.ts @@ -3,7 +3,7 @@ import process from 'node:process'; import {getAvailablePort} from '@testring/utils'; import {IChildProcessForkOptions, IChildProcessFork} from '@testring/types'; import {resolveBinary} from './resolve-binary'; -import {spawn} from './spawn'; +import {spawn, spawnDebug} from './spawn'; import {ChildProcess} from 'child_process'; interface ChildProcessExtension extends ChildProcess { @@ -99,20 +99,35 @@ export async function fork( let childProcess: ChildProcess; if (IS_WIN) { - childProcess = spawn('node', [ - ...processArgs, - ...getAdditionalParameters(filePath), - filePath, - childArg, - ...args, - ]); + childProcess = mergedOptions.debug + ? spawnDebug('node', [ + ...processArgs, + ...getAdditionalParameters(filePath), + filePath, + childArg, + ...args, + ]) + : spawn('node', [ + ...processArgs, + ...getAdditionalParameters(filePath), + filePath, + childArg, + ...args, + ]); } else { - childProcess = spawn(getExecutor(filePath), [ - ...processArgs, - filePath, - childArg, - ...args, - ]); + childProcess = mergedOptions.debug + ? spawnDebug(getExecutor(filePath), [ + ...processArgs, + filePath, + childArg, + ...args, + ]) + : spawn(getExecutor(filePath), [ + ...processArgs, + filePath, + childArg, + ...args, + ]); } const childProcessExtended = childProcess as ChildProcessExtension; diff --git a/core/child-process/src/index.ts b/core/child-process/src/index.ts index 777de0f8b..fd0b8478a 100644 --- a/core/child-process/src/index.ts +++ b/core/child-process/src/index.ts @@ -1,4 +1,5 @@ export {spawn} from './spawn'; +export {spawnDebug} from './spawn'; export {spawnWithPipes} from './spawn-with-pipes'; export {fork} from './fork'; export {isChildProcess} from './utils'; diff --git a/core/child-process/src/spawn-with-pipes.ts b/core/child-process/src/spawn-with-pipes.ts index 03e1dd19d..50af4186a 100644 --- a/core/child-process/src/spawn-with-pipes.ts +++ b/core/child-process/src/spawn-with-pipes.ts @@ -5,15 +5,13 @@ export function spawnWithPipes( command: string, args: Array = [], ): childProcess.ChildProcess { - const child = childProcess.spawn(command, args, { + // Note: child.unref() removed to prevent orphaned processes + // Node.js will now wait for child process to exit before main process exits + + return childProcess.spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'], // Use pipes for proper control cwd: process.cwd(), detached: false, // Run attached to prevent orphan processes windowsHide: true, // Hide the console window on Windows }); - - // Ensure child does not keep the event loop active - child.unref(); - - return child; } diff --git a/core/child-process/src/spawn.ts b/core/child-process/src/spawn.ts index 446cfb398..f03095575 100644 --- a/core/child-process/src/spawn.ts +++ b/core/child-process/src/spawn.ts @@ -8,6 +8,17 @@ export function spawn( return childProcess.spawn(command, args, { stdio: [null, null, null, 'ipc'], cwd: process.cwd(), - detached: true, + detached: true, // Keep detached: true for normal operation + }); +} + +export function spawnDebug( + command: string, + args: Array = [], +): childProcess.ChildProcess { + return childProcess.spawn(command, args, { + stdio: [null, null, null, 'ipc'], + cwd: process.cwd(), + detached: false, // Not detached in debug mode to prevent orphaned processes }); } diff --git a/core/plugin-api/src/modules/test-run-controller.ts b/core/plugin-api/src/modules/test-run-controller.ts index 44da5990a..f7e606007 100644 --- a/core/plugin-api/src/modules/test-run-controller.ts +++ b/core/plugin-api/src/modules/test-run-controller.ts @@ -1,4 +1,4 @@ -import {TestRunControllerPlugins, IQueuedTest} from '@testring/types'; +import {TestRunControllerPlugins, IQueuedTest, ITestWorkerCallbackMeta} from '@testring/types'; import {AbstractAPI} from './abstract'; export class TestRunControllerAPI extends AbstractAPI { @@ -6,22 +6,22 @@ export class TestRunControllerAPI extends AbstractAPI { this.registryWritePlugin(TestRunControllerPlugins.beforeRun, handler); } - beforeTest(handler: (test: IQueuedTest) => Promise) { + beforeTest(handler: (test: IQueuedTest, meta: ITestWorkerCallbackMeta) => Promise) { this.registryWritePlugin(TestRunControllerPlugins.beforeTest, handler); } - beforeTestRetry(handler: (params: IQueuedTest) => Promise) { + beforeTestRetry(handler: (params: IQueuedTest, error: Error, meta: ITestWorkerCallbackMeta) => Promise) { this.registryWritePlugin( TestRunControllerPlugins.beforeTestRetry, handler, ); } - afterTest(handler: (params: IQueuedTest) => Promise) { + afterTest(handler: (params: IQueuedTest, error: Error | null, meta: ITestWorkerCallbackMeta) => Promise) { this.registryWritePlugin(TestRunControllerPlugins.afterTest, handler); } - afterRun(handler: (queue: IQueuedTest[]) => Promise) { + afterRun(handler: (error: Error | null) => Promise) { this.registryWritePlugin(TestRunControllerPlugins.afterRun, handler); } @@ -35,7 +35,7 @@ export class TestRunControllerAPI extends AbstractAPI { } shouldNotStart( - handler: (state: boolean, test: IQueuedTest) => Promise, + handler: (state: boolean, test: IQueuedTest, meta: ITestWorkerCallbackMeta) => Promise, ) { this.registryWritePlugin( TestRunControllerPlugins.shouldNotStart, @@ -44,7 +44,7 @@ export class TestRunControllerAPI extends AbstractAPI { } shouldNotRetry( - handler: (state: boolean, test: IQueuedTest) => Promise, + handler: (state: boolean, test: IQueuedTest, meta: ITestWorkerCallbackMeta) => Promise, ) { this.registryWritePlugin( TestRunControllerPlugins.shouldNotRetry, diff --git a/packages/plugin-selenium-driver/src/plugin/index.ts b/packages/plugin-selenium-driver/src/plugin/index.ts index 803238d5d..df6f12f19 100644 --- a/packages/plugin-selenium-driver/src/plugin/index.ts +++ b/packages/plugin-selenium-driver/src/plugin/index.ts @@ -186,6 +186,8 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { private incrementWinId = 0; + private killed = false; // Flag to prevent operations after kill + constructor(config: Partial = {}) { this.config = this.createConfig(config); @@ -246,8 +248,14 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { } private setupProcessCleanup() { + process.on('exit', () => this.forceKillSelenium()); process.on('SIGINT', () => this.forceKillSelenium()); process.on('SIGTERM', () => this.forceKillSelenium()); + + // Debug mode specific cleanup handlers + process.on('SIGUSR1', () => this.forceKillSelenium()); // Debugger disconnect + process.on('SIGUSR2', () => this.forceKillSelenium()); // Debugger disconnect alternative + // Note: SIGKILL cannot be caught or handled - it immediately terminates the process } @@ -302,8 +310,8 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { 'standalone', '--port', port.toString(), - '--bind-host', - 'false', + '--host', + '127.0.0.1', ); } else { args.push('-jar', seleniumJarPath, '-port', port.toString()); @@ -426,6 +434,10 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { applicant: string, config?: Partial, ): Promise { + if (this.killed) { + throw new Error('SeleniumPlugin is being killed'); + } + await this.waitForReadyState; const clientData = this.browserClients.get(applicant); @@ -674,6 +686,9 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { public async kill() { this.logger.debug('Kill command is called'); + + // Set killed flag to prevent new operations + this.killed = true; // Close all browser sessions for (const applicant of this.browserClients.keys()) { @@ -690,6 +705,12 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { } if (this.localSelenium) { + // Check if already killed + if (this.localSelenium.killed) { + this.logger.debug('Selenium process already killed'); + return; + } + // remove listener if (this.localSelenium.stderr) { this.localSelenium.stderr.removeAllListeners('data'); @@ -716,7 +737,7 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { }); }); - // Force kill if not exiting within 3 seconds + // Force kill if not exiting within 1 second (reduced from 3 seconds) const forceKill = new Promise((resolve) => { setTimeout(() => { if (this.localSelenium && !this.localSelenium.killed) { @@ -726,7 +747,7 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { this.localSelenium.kill('SIGKILL'); } resolve(); - }, 3000); + }, 1000); // Reduced timeout for faster cleanup }); // Wait for either normal exit or force kill From 27324aa7bb20665b94e6125c6bad20212fd24a24 Mon Sep 17 00:00:00 2001 From: "artem.sukhinin" Date: Wed, 22 Oct 2025 15:04:37 +0300 Subject: [PATCH 2/2] WAT-5278 --- packages/plugin-selenium-driver/package.json | 1 - .../src/plugin/index.ts | 44 ------------------- packages/plugin-selenium-driver/src/types.ts | 1 - 3 files changed, 46 deletions(-) diff --git a/packages/plugin-selenium-driver/package.json b/packages/plugin-selenium-driver/package.json index 6f3a41c19..b86487b3e 100644 --- a/packages/plugin-selenium-driver/package.json +++ b/packages/plugin-selenium-driver/package.json @@ -13,7 +13,6 @@ "author": "RingCentral", "license": "MIT", "dependencies": { - "@nullcc/code-coverage-client": "1.4.2", "@testring/child-process": "0.8.5", "@testring/dwnld-collector-crx": "0.8.5", "@testring/logger": "0.8.5", diff --git a/packages/plugin-selenium-driver/src/plugin/index.ts b/packages/plugin-selenium-driver/src/plugin/index.ts index df6f12f19..ff157cfdd 100644 --- a/packages/plugin-selenium-driver/src/plugin/index.ts +++ b/packages/plugin-selenium-driver/src/plugin/index.ts @@ -20,7 +20,6 @@ import * as deepmerge from 'deepmerge'; import {spawnWithPipes} from '@testring/child-process'; import {loggerClient} from '@testring/logger'; import {getCrxBase64} from '@testring/dwnld-collector-crx'; -import {CDPCoverageCollector} from '@nullcc/code-coverage-client'; import type {Cookie} from '@wdio/protocols'; import type {ClickOptions, MockFilterOptions, WaitUntilOptions} from 'webdriverio'; @@ -37,7 +36,6 @@ type browserClientItem = { client: BrowserObjectCustom; sessionId: string; initTime: number; - cdpCoverageCollector: CDPCoverageCollector | null; }; const DEFAULT_CONFIG: SeleniumPluginConfig = { @@ -53,7 +51,6 @@ const DEFAULT_CONFIG: SeleniumPluginConfig = { }, 'wdio:enforceWebDriverClassic': true, } as any, - cdpCoverage: false, disableClientPing: false, localVersion: 'v3' as SeleniumVersion, seleniumArgs: [], @@ -475,19 +472,10 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { client as BrowserObjectCustom, ); - let cdpCoverageCollector; - if (this.config.cdpCoverage) { - this.logger.debug('Started to init cdp coverage....'); - cdpCoverageCollector = await this.enableCDPCoverageClient(client); - this.logger.debug('ended to init cdp coverage....'); - } this.browserClients.set(applicant, { client: customClient, sessionId, initTime: Date.now(), - cdpCoverageCollector: cdpCoverageCollector - ? cdpCoverageCollector - : null, }); this.logger.debug( @@ -495,38 +483,6 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { ); } - private async enableCDPCoverageClient(client: BrowserObjectCustom) { - if (this.config.host === undefined) { - return null; - } - //accurate - if (!client.capabilities['se:cdp']) { - return null; - } - const cdpAddress = client.capabilities['se:cdp']; - const collector = new CDPCoverageCollector({ - wsEndpoint: cdpAddress, - }); - await collector.init(); - await collector.start(); - return collector; - } - - public async getCdpCoverageFile(applicant: string) { - const clientData = this.browserClients.get(applicant); - this.logger.debug(`start upload coverage for applicant ${applicant}`); - if (!clientData) { - return; - } - const coverageCollector = clientData.cdpCoverageCollector; - if (!coverageCollector) { - return; - } - const {coverage} = await coverageCollector.collect(); - await coverageCollector.stop(); - return [Buffer.from(JSON.stringify(coverage))]; - } - protected addCustromMethods( client: BrowserObjectCustom, ): BrowserObjectCustom { diff --git a/packages/plugin-selenium-driver/src/types.ts b/packages/plugin-selenium-driver/src/types.ts index 45342e548..dcf01a054 100644 --- a/packages/plugin-selenium-driver/src/types.ts +++ b/packages/plugin-selenium-driver/src/types.ts @@ -9,7 +9,6 @@ export type SeleniumPluginConfig = Capabilities.WebdriverIOConfig & { clientTimeout: number; host?: string; // fallback for configuration. In WebdriverIO 5 field host renamed to hostname desiredCapabilities?: Capabilities.RequestedStandaloneCapabilities[]; // fallback for configuration. In WebdriverIO 5 field renamed - cdpCoverage: boolean; workerLimit?: number | 'local'; disableClientPing?: boolean; delayAfterSessionClose?: number;