From 2743dc84f6c3635ff20626d8b29d187dd9c9313e Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 18 Dec 2025 10:31:46 +0100 Subject: [PATCH 01/71] Abort async operations on SIGTERM/SIGINT --- cli/commands/site/list.ts | 8 ------ cli/lib/pm2-manager.ts | 3 ++- cli/lib/wordpress-server-manager.ts | 39 ++++++++++++++++++++++++----- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/cli/commands/site/list.ts b/cli/commands/site/list.ts index 0ce79da6a6..e4db8b48a1 100644 --- a/cli/commands/site/list.ts +++ b/cli/commands/site/list.ts @@ -71,14 +71,6 @@ function displaySiteList( sitesData: SiteListEntry[], format: 'table' | 'json' ) const logger = new Logger< LoggerAction >(); export async function runCommand( format: 'table' | 'json', watch: boolean ): Promise< void > { - const handleTermination = () => { - disconnect(); - process.exit( 0 ); - }; - process.on( 'SIGTERM', handleTermination ); - process.on( 'SIGHUP', handleTermination ); - process.on( 'disconnect', handleTermination ); - try { logger.reportStart( LoggerAction.LOAD_SITES, __( 'Loading sites…' ) ); const appdata = await readAppdata(); diff --git a/cli/lib/pm2-manager.ts b/cli/lib/pm2-manager.ts index fda5b41578..9a9184603f 100644 --- a/cli/lib/pm2-manager.ts +++ b/cli/lib/pm2-manager.ts @@ -60,7 +60,8 @@ export function disconnect(): void { } } -process.on( 'exit', disconnect ); +process.on( 'disconnect', disconnect ); +process.on( 'SIGHUP', disconnect ); process.on( 'SIGINT', disconnect ); process.on( 'SIGTERM', disconnect ); diff --git a/cli/lib/wordpress-server-manager.ts b/cli/lib/wordpress-server-manager.ts index 544ce17ac4..10f93c5d8b 100644 --- a/cli/lib/wordpress-server-manager.ts +++ b/cli/lib/wordpress-server-manager.ts @@ -31,6 +31,15 @@ import { Logger } from 'cli/logger'; const SITE_PROCESS_PREFIX = 'studio-site-'; +// Get an abort signal that's triggered on SIGINT/SIGTERM. This is useful for aborting and cleaning +// up async operations. +const abortController = new AbortController(); +function handleProcessTermination() { + abortController.abort(); +} +process.on( 'SIGINT', handleProcessTermination ); +process.on( 'SIGTERM', handleProcessTermination ); + function getProcessName( siteId: string ): string { return `${ SITE_PROCESS_PREFIX }${ siteId }`; } @@ -124,27 +133,37 @@ export async function startWordPressServer( async function waitForReadyMessage( pmId: number ): Promise< void > { const bus = await getPm2Bus(); + let timeoutId: NodeJS.Timeout; + let readyHandler: ( packet: unknown ) => void; - return new Promise( ( resolve, reject ) => { - const timeout = setTimeout( () => { - bus.off( 'process:msg', readyHandler ); + return new Promise< void >( ( resolve, reject ) => { + timeoutId = setTimeout( () => { reject( new Error( 'Timeout waiting for ready message from WordPress server child' ) ); }, PLAYGROUND_CLI_INACTIVITY_TIMEOUT ); - const readyHandler = ( packet: unknown ) => { + readyHandler = ( packet: unknown ) => { const result = childMessagePm2Schema.safeParse( packet ); if ( ! result.success ) { return; } if ( result.data.process.pm_id === pmId && result.data.raw.topic === 'ready' ) { - clearTimeout( timeout ); - bus.off( 'process:msg', readyHandler ); resolve(); } }; + abortController.signal.addEventListener( + 'abort', + () => { + reject( new Error( 'Operation aborted' ) ); + }, + { once: true } + ); + bus.on( 'process:msg', readyHandler ); + } ).finally( () => { + clearTimeout( timeoutId ); + bus.off( 'process:msg', readyHandler ); } ); } @@ -242,6 +261,14 @@ async function sendMessage( } }; + abortController.signal.addEventListener( + 'abort', + () => { + reject( new Error( 'Operation aborted' ) ); + }, + { once: true } + ); + bus.on( 'process:msg', responseHandler ); sendMessageToProcess( pmId, { ...message, messageId } ).catch( reject ); From d34ee1e97254b801111bfe0326d18132bacd1420 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 18 Dec 2025 10:46:05 +0100 Subject: [PATCH 02/71] Also send abort message to child server --- cli/lib/types/wordpress-server-ipc.ts | 6 ++ cli/lib/wordpress-server-manager.ts | 1 + cli/wordpress-server-child.ts | 85 +++++++++++++++++++-------- 3 files changed, 67 insertions(+), 25 deletions(-) diff --git a/cli/lib/types/wordpress-server-ipc.ts b/cli/lib/types/wordpress-server-ipc.ts index 3c4c0bda88..7276e42826 100644 --- a/cli/lib/types/wordpress-server-ipc.ts +++ b/cli/lib/types/wordpress-server-ipc.ts @@ -23,6 +23,10 @@ const serverConfig = z.object( { export type ServerConfig = z.infer< typeof serverConfig >; +const managerMessageAbort = z.object( { + topic: z.literal( 'abort' ), +} ); + const managerMessageStartServer = z.object( { topic: z.literal( 'start-server' ), data: z.object( { @@ -49,6 +53,7 @@ const managerMessageWpCliCommand = z.object( { } ); const _managerMessagePayloadSchema = z.discriminatedUnion( 'topic', [ + managerMessageAbort, managerMessageStartServer, managerMessageRunBlueprint, managerMessageStopServer, @@ -58,6 +63,7 @@ export type ManagerMessagePayload = z.infer< typeof _managerMessagePayloadSchema const managerMessageBase = z.object( { messageId: z.number() } ); export const managerMessageSchema = z.discriminatedUnion( 'topic', [ + managerMessageBase.merge( managerMessageAbort ), managerMessageBase.merge( managerMessageStartServer ), managerMessageBase.merge( managerMessageRunBlueprint ), managerMessageBase.merge( managerMessageStopServer ), diff --git a/cli/lib/wordpress-server-manager.ts b/cli/lib/wordpress-server-manager.ts index 10f93c5d8b..6f13127598 100644 --- a/cli/lib/wordpress-server-manager.ts +++ b/cli/lib/wordpress-server-manager.ts @@ -264,6 +264,7 @@ async function sendMessage( abortController.signal.addEventListener( 'abort', () => { + void sendMessageToProcess( pmId, { messageId, topic: 'abort' } ); reject( new Error( 'Operation aborted' ) ); }, { once: true } diff --git a/cli/wordpress-server-child.ts b/cli/wordpress-server-child.ts index efbdda1332..e42efad2f1 100644 --- a/cli/wordpress-server-child.ts +++ b/cli/wordpress-server-child.ts @@ -224,30 +224,40 @@ function wrapWithStartingPromise< Args extends unknown[], Return extends void >( }; } -const startServer = wrapWithStartingPromise( async ( config: ServerConfig ): Promise< void > => { - if ( server ) { - logToConsole( `Server already running for site ${ config.siteId }` ); - return; - } - - try { - const args = await getBaseRunCLIArgs( 'server', config ); - lastCliArgs = sanitizeRunCLIArgs( args ); - server = await runCLI( args ); - - if ( config.enableMultiWorker && server ) { - logToConsole( `Server started with ${ server.workerThreadCount } worker thread(s)` ); +const startServer = wrapWithStartingPromise( + async ( config: ServerConfig, signal: AbortSignal ): Promise< void > => { + if ( server ) { + logToConsole( `Server already running for site ${ config.siteId }` ); + return; } - if ( config.adminPassword ) { - await setAdminPassword( server, config.adminPassword ); + try { + signal.addEventListener( + 'abort', + () => { + throw new Error( 'Operation aborted' ); + }, + { once: true } + ); + + const args = await getBaseRunCLIArgs( 'server', config ); + lastCliArgs = sanitizeRunCLIArgs( args ); + server = await runCLI( args ); + + if ( config.enableMultiWorker && server ) { + logToConsole( `Server started with ${ server.workerThreadCount } worker thread(s)` ); + } + + if ( config.adminPassword ) { + await setAdminPassword( server, config.adminPassword ); + } + } catch ( error ) { + server = null; + errorToConsole( `Failed to start server:`, error ); + throw error; } - } catch ( error ) { - server = null; - errorToConsole( `Failed to start server:`, error ); - throw error; } -} ); +); const STOP_SERVER_TIMEOUT = 5000; @@ -272,8 +282,16 @@ async function stopServer(): Promise< void > { } } -async function runBlueprint( config: ServerConfig ): Promise< void > { +async function runBlueprint( config: ServerConfig, signal: AbortSignal ): Promise< void > { try { + signal.addEventListener( + 'abort', + () => { + throw new Error( 'Operation aborted' ); + }, + { once: true } + ); + const args = await getBaseRunCLIArgs( 'run-blueprint', config ); lastCliArgs = sanitizeRunCLIArgs( args ); await runCLI( args ); @@ -286,7 +304,8 @@ async function runBlueprint( config: ServerConfig ): Promise< void > { } async function runWpCliCommand( - args: string[] + args: string[], + signal: AbortSignal ): Promise< { stdout: string; stderr: string; exitCode: number } > { await Promise.allSettled( [ startingPromise ] ); @@ -294,6 +313,14 @@ async function runWpCliCommand( throw new Error( `Failed to run WP CLI command because server is not running` ); } + signal.addEventListener( + 'abort', + () => { + throw new Error( 'Operation aborted' ); + }, + { once: true } + ); + const response = await server.playground.cli( [ 'php', '/tmp/wp-cli.phar', @@ -319,6 +346,8 @@ function sendErrorMessage( messageId: number, error: unknown ) { process.send!( errorResponse ); } +const abortControllers: Record< number, AbortController > = {}; + async function ipcMessageHandler( packet: unknown ) { const messageResult = managerMessageSchema.safeParse( packet ); @@ -334,22 +363,28 @@ async function ipcMessageHandler( packet: unknown ) { } const validMessage = messageResult.data; + abortControllers[ validMessage.messageId ] ??= new AbortController(); + const abortController = abortControllers[ validMessage.messageId ]; try { let result: unknown; switch ( validMessage.topic ) { + case 'abort': + abortController?.abort(); + delete abortControllers[ validMessage.messageId ]; + return; case 'start-server': - result = await startServer( validMessage.data.config ); + result = await startServer( validMessage.data.config, abortController.signal ); break; case 'run-blueprint': - result = await runBlueprint( validMessage.data.config ); + result = await runBlueprint( validMessage.data.config, abortController.signal ); break; case 'stop-server': result = await stopServer(); break; case 'wp-cli-command': - result = await runWpCliCommand( validMessage.data.args ); + result = await runWpCliCommand( validMessage.data.args, abortController.signal ); break; default: throw new Error( `Unknown message.` ); From 9a31e582ece629afea86e834dbaf7836168725a8 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 18 Dec 2025 10:57:01 +0100 Subject: [PATCH 03/71] Only create AbortController when topic is not `abort` --- cli/wordpress-server-child.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/wordpress-server-child.ts b/cli/wordpress-server-child.ts index e42efad2f1..5456da94bb 100644 --- a/cli/wordpress-server-child.ts +++ b/cli/wordpress-server-child.ts @@ -363,7 +363,9 @@ async function ipcMessageHandler( packet: unknown ) { } const validMessage = messageResult.data; - abortControllers[ validMessage.messageId ] ??= new AbortController(); + if ( validMessage.topic !== 'abort' ) { + abortControllers[ validMessage.messageId ] = new AbortController(); + } const abortController = abortControllers[ validMessage.messageId ]; try { From 61f3256555f690df26de11dc1ffda5237cba1daa Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 18 Dec 2025 12:39:38 +0100 Subject: [PATCH 04/71] Fix --- cli/lib/pm2-manager.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/cli/lib/pm2-manager.ts b/cli/lib/pm2-manager.ts index 9a9184603f..37e34ab7d1 100644 --- a/cli/lib/pm2-manager.ts +++ b/cli/lib/pm2-manager.ts @@ -60,8 +60,6 @@ export function disconnect(): void { } } -process.on( 'disconnect', disconnect ); -process.on( 'SIGHUP', disconnect ); process.on( 'SIGINT', disconnect ); process.on( 'SIGTERM', disconnect ); From f81ba0cf163013f4f35665cfb1c9db2f069b58ef Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 18 Dec 2025 15:15:18 +0100 Subject: [PATCH 05/71] More fixes --- cli/lib/pm2-manager.ts | 3 --- cli/lib/types/wordpress-server-ipc.ts | 2 ++ cli/lib/wordpress-server-manager.ts | 8 +++----- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/cli/lib/pm2-manager.ts b/cli/lib/pm2-manager.ts index 37e34ab7d1..7d80f3d317 100644 --- a/cli/lib/pm2-manager.ts +++ b/cli/lib/pm2-manager.ts @@ -60,9 +60,6 @@ export function disconnect(): void { } } -process.on( 'SIGINT', disconnect ); -process.on( 'SIGTERM', disconnect ); - // Cache the return value of `pm2.list` for a very short time to make multiple calls in quick // succession more efficient const listProcesses = cacheFunctionTTL( () => { diff --git a/cli/lib/types/wordpress-server-ipc.ts b/cli/lib/types/wordpress-server-ipc.ts index 7276e42826..982ba83c59 100644 --- a/cli/lib/types/wordpress-server-ipc.ts +++ b/cli/lib/types/wordpress-server-ipc.ts @@ -25,6 +25,7 @@ export type ServerConfig = z.infer< typeof serverConfig >; const managerMessageAbort = z.object( { topic: z.literal( 'abort' ), + data: z.object( {} ), } ); const managerMessageStartServer = z.object( { @@ -43,6 +44,7 @@ const managerMessageRunBlueprint = z.object( { const managerMessageStopServer = z.object( { topic: z.literal( 'stop-server' ), + data: z.object( {} ), } ); const managerMessageWpCliCommand = z.object( { diff --git a/cli/lib/wordpress-server-manager.ts b/cli/lib/wordpress-server-manager.ts index 6f13127598..a043626431 100644 --- a/cli/lib/wordpress-server-manager.ts +++ b/cli/lib/wordpress-server-manager.ts @@ -264,7 +264,7 @@ async function sendMessage( abortController.signal.addEventListener( 'abort', () => { - void sendMessageToProcess( pmId, { messageId, topic: 'abort' } ); + void sendMessageToProcess( pmId, { messageId, topic: 'abort', data: {} } ); reject( new Error( 'Operation aborted' ) ); }, { once: true } @@ -294,10 +294,8 @@ export async function stopWordPressServer( siteId: string ): Promise< void > { try { await sendMessage( runningProcess.pmId, - { topic: 'stop-server' }, - { - maxTotalElapsedTime: GRACEFUL_STOP_TIMEOUT, - } + { topic: 'stop-server', data: {} }, + { maxTotalElapsedTime: GRACEFUL_STOP_TIMEOUT } ); } catch { // Graceful shutdown failed, PM2 delete will handle it From bd6b4d9f1b7c50b47beccae7fece96beb6e8904e Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 18 Dec 2025 15:24:14 +0100 Subject: [PATCH 06/71] Tweaks --- cli/lib/wordpress-server-manager.ts | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cli/lib/wordpress-server-manager.ts b/cli/lib/wordpress-server-manager.ts index a043626431..9c93a0a5c7 100644 --- a/cli/lib/wordpress-server-manager.ts +++ b/cli/lib/wordpress-server-manager.ts @@ -133,8 +133,10 @@ export async function startWordPressServer( async function waitForReadyMessage( pmId: number ): Promise< void > { const bus = await getPm2Bus(); + let timeoutId: NodeJS.Timeout; let readyHandler: ( packet: unknown ) => void; + let abortListener: () => void; return new Promise< void >( ( resolve, reject ) => { timeoutId = setTimeout( () => { @@ -152,17 +154,15 @@ async function waitForReadyMessage( pmId: number ): Promise< void > { } }; - abortController.signal.addEventListener( - 'abort', - () => { - reject( new Error( 'Operation aborted' ) ); - }, - { once: true } - ); + abortListener = () => { + reject( new Error( 'Operation aborted' ) ); + }; + abortController.signal.addEventListener( 'abort', abortListener ); bus.on( 'process:msg', readyHandler ); } ).finally( () => { clearTimeout( timeoutId ); + abortController.signal.removeEventListener( 'abort', abortListener ); bus.off( 'process:msg', readyHandler ); } ); } @@ -195,7 +195,9 @@ async function sendMessage( const { maxTotalElapsedTime = PLAYGROUND_CLI_MAX_TIMEOUT, logger } = options; const bus = await getPm2Bus(); const messageId = nextMessageId++; + let responseHandler: ( packet: unknown ) => void; + let abortListener: () => void; return new Promise( ( resolve, reject ) => { const startTime = Date.now(); @@ -261,19 +263,17 @@ async function sendMessage( } }; - abortController.signal.addEventListener( - 'abort', - () => { - void sendMessageToProcess( pmId, { messageId, topic: 'abort', data: {} } ); - reject( new Error( 'Operation aborted' ) ); - }, - { once: true } - ); + abortListener = () => { + void sendMessageToProcess( pmId, { messageId, topic: 'abort', data: {} } ); + reject( new Error( 'Operation aborted' ) ); + }; + abortController.signal.addEventListener( 'abort', abortListener ); bus.on( 'process:msg', responseHandler ); sendMessageToProcess( pmId, { ...message, messageId } ).catch( reject ); } ).finally( () => { + abortController.signal.removeEventListener( 'abort', abortListener ); bus.off( 'process:msg', responseHandler ); const tracker = messageActivityTrackers.get( messageId ); From a6a7e7a6039c3d09cf6d1caa44397a54e3c85a3d Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 18 Dec 2025 15:59:27 +0100 Subject: [PATCH 07/71] Fix unit tests --- cli/lib/tests/pm2-manager.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/lib/tests/pm2-manager.test.ts b/cli/lib/tests/pm2-manager.test.ts index 0eff6dd496..a8d8d2b13c 100644 --- a/cli/lib/tests/pm2-manager.test.ts +++ b/cli/lib/tests/pm2-manager.test.ts @@ -282,6 +282,7 @@ describe( 'PM2 Manager', () => { const message: ManagerMessage = { topic: 'stop-server', messageId: 1, + data: {}, }; await sendMessageToProcess( 42, message ); @@ -303,6 +304,7 @@ describe( 'PM2 Manager', () => { const message: ManagerMessage = { topic: 'stop-server', messageId: 1, + data: {}, }; await expect( sendMessageToProcess( 42, message ) ).rejects.toThrow( 'Send failed' ); From 569058fdb221640cc4d65059641023f5c2d7f319 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 08:51:46 +0100 Subject: [PATCH 08/71] Fix types --- cli/wordpress-server-child.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/wordpress-server-child.ts b/cli/wordpress-server-child.ts index a1b19f492c..5fd143ffe3 100644 --- a/cli/wordpress-server-child.ts +++ b/cli/wordpress-server-child.ts @@ -350,7 +350,7 @@ function sendErrorMessage( messageId: string, error: unknown ) { process.send!( errorResponse ); } -const abortControllers: Record< number, AbortController > = {}; +const abortControllers: Record< string, AbortController > = {}; async function ipcMessageHandler( packet: unknown ) { const messageResult = managerMessageSchema.safeParse( packet ); From 8786163da64915463e270a6e2beba67ae097acc2 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 10:01:43 +0100 Subject: [PATCH 09/71] Remove `this.sessionPath` files individually To help us diagnose which specific files or directories are causing trouble --- e2e/e2e-helpers.ts | 65 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index dcd4bbdedf..f166c36166 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -1,4 +1,5 @@ import { randomUUID } from 'crypto'; +import fsSync from 'fs'; import { tmpdir } from 'os'; import path from 'path'; import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers'; @@ -92,7 +93,67 @@ export class E2ESession { async cleanup() { await this.electronApp?.close(); - // Clean up temporary folder to hold application data - fs.rmSync( this.sessionPath, { recursive: true, force: true } ); + + // Attempt cleanup with retry logic to handle Windows file locking + let lastError: Error | null = null; + for ( let attempt = 0; attempt < 3; attempt++ ) { + try { + this.removeDirectoryRecursive( this.sessionPath ); + console.log( '[E2E Cleanup] Successfully cleaned up session directory' ); + return; + } catch ( error ) { + lastError = error as Error; + console.warn( + `[E2E Cleanup] Attempt ${ attempt + 1 } failed. Retrying in 1s...`, + lastError.message + ); + // Wait before retrying + await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); + } + } + + // Log detailed error information for diagnostics + console.error( '[E2E Cleanup] Failed to clean up session after 3 attempts' ); + throw new Error( + `[E2E Cleanup] Failed to clean up session directory: ${ lastError?.message }` + ); + } + + private removeDirectoryRecursive( dirPath: string ): void { + if ( ! fsSync.existsSync( dirPath ) ) { + return; + } + + let items: string[]; + try { + items = fsSync.readdirSync( dirPath ); + } catch ( error ) { + console.error( `[E2E Cleanup] Failed to read directory ${ dirPath }:`, error ); + throw error; + } + + // Remove each item individually to isolate failures + for ( const item of items ) { + const itemPath = path.join( dirPath, item ); + try { + const stat = fsSync.lstatSync( itemPath ); // Use lstatSync to handle symlinks + if ( stat.isDirectory() ) { + this.removeDirectoryRecursive( itemPath ); + } else { + fsSync.unlinkSync( itemPath ); + } + } catch ( error ) { + console.error( `[E2E Cleanup] Failed to remove ${ itemPath }:`, error ); + throw error; + } + } + + // Remove the now-empty directory + try { + fsSync.rmdirSync( dirPath ); + } catch ( error ) { + console.error( `[E2E Cleanup] Failed to remove directory ${ dirPath }:`, error ); + throw error; + } } } From abbcfc88e8d795905ba8a1737407507b053250ff Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 10:23:42 +0100 Subject: [PATCH 10/71] Stop running servers in a detached process --- src/modules/cli/lib/execute-command.ts | 14 ++++++++++---- src/site-server.ts | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 0cdb0d01df..fa7bf4036f 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -40,11 +40,12 @@ export interface ExecuteCliCommandOptions { * - 'capture': capture stdout/stderr, available in success/failure events */ output: 'ignore' | 'capture'; + detached?: boolean; } export function executeCliCommand( args: string[], - options: ExecuteCliCommandOptions = { output: 'ignore' } + options: ExecuteCliCommandOptions = { output: 'ignore', detached: false } ): [ CliCommandEventEmitter, ChildProcess ] { const cliPath = getCliPath(); @@ -58,6 +59,7 @@ export function executeCliCommand( // Using Electron's utilityProcess.fork API gave us issues with the child process never exiting const child = fork( cliPath, [ ...args, '--avoid-telemetry' ], { stdio, + detached: options.detached, env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', @@ -107,9 +109,13 @@ export function executeCliCommand( } } ); - process.on( 'exit', () => { - child.kill(); - } ); + if ( options.detached ) { + child.unref(); + } else { + process.on( 'exit', () => { + child.kill(); + } ); + } return [ eventEmitter, child ]; } diff --git a/src/site-server.ts b/src/site-server.ts index 18795213d4..63c69d9edb 100644 --- a/src/site-server.ts +++ b/src/site-server.ts @@ -43,6 +43,7 @@ export async function stopAllServersOnQuit() { return new Promise< void >( ( resolve ) => { const [ emitter ] = executeCliCommand( [ 'site', 'stop-all', '--auto-start' ], { output: 'ignore', + detached: true, } ); emitter.on( 'success', () => resolve() ); emitter.on( 'failure', () => resolve() ); From fa0537eddfcab2d77c0e5f26139532de16bed319 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 10:38:15 +0100 Subject: [PATCH 11/71] Revert "Remove `this.sessionPath` files individually" This reverts commit 8786163da64915463e270a6e2beba67ae097acc2. --- e2e/e2e-helpers.ts | 65 ++-------------------------------------------- 1 file changed, 2 insertions(+), 63 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index f166c36166..dcd4bbdedf 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -1,5 +1,4 @@ import { randomUUID } from 'crypto'; -import fsSync from 'fs'; import { tmpdir } from 'os'; import path from 'path'; import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers'; @@ -93,67 +92,7 @@ export class E2ESession { async cleanup() { await this.electronApp?.close(); - - // Attempt cleanup with retry logic to handle Windows file locking - let lastError: Error | null = null; - for ( let attempt = 0; attempt < 3; attempt++ ) { - try { - this.removeDirectoryRecursive( this.sessionPath ); - console.log( '[E2E Cleanup] Successfully cleaned up session directory' ); - return; - } catch ( error ) { - lastError = error as Error; - console.warn( - `[E2E Cleanup] Attempt ${ attempt + 1 } failed. Retrying in 1s...`, - lastError.message - ); - // Wait before retrying - await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); - } - } - - // Log detailed error information for diagnostics - console.error( '[E2E Cleanup] Failed to clean up session after 3 attempts' ); - throw new Error( - `[E2E Cleanup] Failed to clean up session directory: ${ lastError?.message }` - ); - } - - private removeDirectoryRecursive( dirPath: string ): void { - if ( ! fsSync.existsSync( dirPath ) ) { - return; - } - - let items: string[]; - try { - items = fsSync.readdirSync( dirPath ); - } catch ( error ) { - console.error( `[E2E Cleanup] Failed to read directory ${ dirPath }:`, error ); - throw error; - } - - // Remove each item individually to isolate failures - for ( const item of items ) { - const itemPath = path.join( dirPath, item ); - try { - const stat = fsSync.lstatSync( itemPath ); // Use lstatSync to handle symlinks - if ( stat.isDirectory() ) { - this.removeDirectoryRecursive( itemPath ); - } else { - fsSync.unlinkSync( itemPath ); - } - } catch ( error ) { - console.error( `[E2E Cleanup] Failed to remove ${ itemPath }:`, error ); - throw error; - } - } - - // Remove the now-empty directory - try { - fsSync.rmdirSync( dirPath ); - } catch ( error ) { - console.error( `[E2E Cleanup] Failed to remove directory ${ dirPath }:`, error ); - throw error; - } + // Clean up temporary folder to hold application data + fs.rmSync( this.sessionPath, { recursive: true, force: true } ); } } From c984234b268ad0bd9358f4eabe15d83113ee6e88 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 10:38:50 +0100 Subject: [PATCH 12/71] Retry --- e2e/e2e-helpers.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index dcd4bbdedf..c538d033ae 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -93,6 +93,11 @@ export class E2ESession { async cleanup() { await this.electronApp?.close(); // Clean up temporary folder to hold application data - fs.rmSync( this.sessionPath, { recursive: true, force: true } ); + fs.rmSync( this.sessionPath, { + recursive: true, + force: true, + maxRetries: 30, + retryDelay: 1000, + } ); } } From 87e62ec457dde92a237b49b70e305b5c7da01357 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 11:20:39 +0100 Subject: [PATCH 13/71] Increase timeouts --- e2e/blueprints.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/e2e/blueprints.test.ts b/e2e/blueprints.test.ts index 221910e44a..46d1cccf90 100644 --- a/e2e/blueprints.test.ts +++ b/e2e/blueprints.test.ts @@ -18,7 +18,7 @@ test.describe( 'Blueprints', () => { await onboarding.closeWhatsNew(); const siteContent = new SiteContent( session.mainWindow, DEFAULT_SITE_NAME ); - await expect( siteContent.siteNameHeading ).toBeVisible( { timeout: 120_000 } ); + await expect( siteContent.siteNameHeading ).toBeVisible( { timeout: 200_000 } ); } ); test.afterAll( async () => { @@ -49,7 +49,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to get admin URL const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -85,7 +85,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to get admin URL const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -123,7 +123,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to get admin URL const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -159,7 +159,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to get admin URL const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -197,7 +197,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to verify site is accessible const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -236,7 +236,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to verify site is accessible const settingsTab = await siteContent.navigateToTab( 'Settings' ); From d42d25ebec6df5c31aa619dbe91e828bcfb5ab4b Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 11:20:48 +0100 Subject: [PATCH 14/71] Fix deprecated blueprint syntax --- e2e/fixtures/blueprints/activate-plugin.json | 4 ++-- e2e/fixtures/blueprints/activate-theme.json | 4 ++-- e2e/fixtures/blueprints/install-plugin.json | 4 ++-- e2e/fixtures/blueprints/install-theme.json | 4 ++-- src/modules/cli/lib/execute-site-watch-command.ts | 3 --- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/e2e/fixtures/blueprints/activate-plugin.json b/e2e/fixtures/blueprints/activate-plugin.json index 6b4f6b326c..111bd4eefc 100644 --- a/e2e/fixtures/blueprints/activate-plugin.json +++ b/e2e/fixtures/blueprints/activate-plugin.json @@ -4,7 +4,7 @@ "steps": [ { "step": "installPlugin", - "pluginZipFile": { + "pluginData": { "resource": "wordpress.org/plugins", "slug": "hello-dolly" } @@ -14,4 +14,4 @@ "pluginPath": "hello-dolly/hello.php" } ] -} \ No newline at end of file +} diff --git a/e2e/fixtures/blueprints/activate-theme.json b/e2e/fixtures/blueprints/activate-theme.json index fe60126e93..2600eab205 100644 --- a/e2e/fixtures/blueprints/activate-theme.json +++ b/e2e/fixtures/blueprints/activate-theme.json @@ -4,7 +4,7 @@ "steps": [ { "step": "installTheme", - "themeZipFile": { + "themeData": { "resource": "wordpress.org/themes", "slug": "twentytwentyone" } @@ -14,4 +14,4 @@ "themeFolderName": "twentytwentyone" } ] -} \ No newline at end of file +} diff --git a/e2e/fixtures/blueprints/install-plugin.json b/e2e/fixtures/blueprints/install-plugin.json index 76c840c7f5..2dd6e971bf 100644 --- a/e2e/fixtures/blueprints/install-plugin.json +++ b/e2e/fixtures/blueprints/install-plugin.json @@ -4,10 +4,10 @@ "steps": [ { "step": "installPlugin", - "pluginZipFile": { + "pluginData": { "resource": "wordpress.org/plugins", "slug": "akismet" } } ] -} \ No newline at end of file +} diff --git a/e2e/fixtures/blueprints/install-theme.json b/e2e/fixtures/blueprints/install-theme.json index 5bb3492410..d718e55e37 100644 --- a/e2e/fixtures/blueprints/install-theme.json +++ b/e2e/fixtures/blueprints/install-theme.json @@ -4,10 +4,10 @@ "steps": [ { "step": "installTheme", - "themeZipFile": { + "themeData": { "resource": "wordpress.org/themes", "slug": "twentytwentytwo" } } ] -} \ No newline at end of file +} diff --git a/src/modules/cli/lib/execute-site-watch-command.ts b/src/modules/cli/lib/execute-site-watch-command.ts index 996643b620..ff69c7d7ba 100644 --- a/src/modules/cli/lib/execute-site-watch-command.ts +++ b/src/modules/cli/lib/execute-site-watch-command.ts @@ -108,9 +108,6 @@ export function startSiteWatcher(): void { export function stopSiteWatcher(): void { if ( watcher ) { const [ , childProcess ] = watcher; - if ( childProcess.connected ) { - childProcess.disconnect(); - } childProcess.kill(); watcher = null; } From 10955cfebebd715c308f91b2226e3d5878a393ba Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 14:01:12 +0100 Subject: [PATCH 15/71] Increase timeout --- e2e/localization.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/localization.test.ts b/e2e/localization.test.ts index cab3c30e63..572452977f 100644 --- a/e2e/localization.test.ts +++ b/e2e/localization.test.ts @@ -139,7 +139,7 @@ test.describe( 'Localization', () => { // Wait for site to be created const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 200_000 } ); const settingsTabButton = session.mainWindow.getByRole( 'tab', { name: /Settings|設定/i } ); await settingsTabButton.click(); From fc4fa56d6ce60e91d99383f22726d0c10b165911 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 14:34:21 +0100 Subject: [PATCH 16/71] Try adding a small delay --- e2e/e2e-helpers.ts | 8 ++++++-- src/modules/cli/lib/execute-site-watch-command.ts | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index c538d033ae..cf02d280d5 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -92,12 +92,16 @@ export class E2ESession { async cleanup() { await this.electronApp?.close(); + + // Give processes time to release file handles + await new Promise( ( resolve ) => setTimeout( resolve, 3000 ) ); + // Clean up temporary folder to hold application data fs.rmSync( this.sessionPath, { recursive: true, force: true, - maxRetries: 30, - retryDelay: 1000, + maxRetries: 5, + retryDelay: 500, } ); } } diff --git a/src/modules/cli/lib/execute-site-watch-command.ts b/src/modules/cli/lib/execute-site-watch-command.ts index ff69c7d7ba..996643b620 100644 --- a/src/modules/cli/lib/execute-site-watch-command.ts +++ b/src/modules/cli/lib/execute-site-watch-command.ts @@ -108,6 +108,9 @@ export function startSiteWatcher(): void { export function stopSiteWatcher(): void { if ( watcher ) { const [ , childProcess ] = watcher; + if ( childProcess.connected ) { + childProcess.disconnect(); + } childProcess.kill(); watcher = null; } From 814d4ae613283adf8a2e775f7c4a52965ee8d321 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 15:02:50 +0100 Subject: [PATCH 17/71] Kill `site list --watch` on SIGINT --- cli/commands/site/list.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/commands/site/list.ts b/cli/commands/site/list.ts index e4db8b48a1..196c26c3df 100644 --- a/cli/commands/site/list.ts +++ b/cli/commands/site/list.ts @@ -126,6 +126,8 @@ export async function runCommand( format: 'table' | 'json', watch: boolean ): Pr }, { debounceMs: 500 } ); + + process.on( 'SIGINT', disconnect ); } } finally { if ( ! watch ) { From 96e03aa7ccb9ebae6591d4972c2762982f8e3f81 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 15:09:53 +0100 Subject: [PATCH 18/71] Create main window after creating site watcher --- src/index.ts | 6 +- src/modules/cli/lib/execute-command.ts | 19 ++++--- .../cli/lib/execute-site-watch-command.ts | 57 +++++++++++-------- 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/index.ts b/src/index.ts index 09892facd7..c05f073b3c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -311,9 +311,11 @@ async function appBoot() { await renameLaunchUniquesStat(); - await createMainWindow(); await startUserDataWatcher(); - startSiteWatcher(); + + await startSiteWatcher(); + + await createMainWindow(); const userData = await loadUserData(); // Bump stats for the first time the app runs - this is when no lastBumpStats are available diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index fa7bf4036f..4ca81b8cda 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -10,8 +10,9 @@ export interface CliCommandResult { } type CliCommandEventMap = { - data: { data: unknown }; + started: void; error: { error: Error }; + data: { data: unknown }; success: { result?: CliCommandResult }; failure: { result?: CliCommandResult }; }; @@ -67,6 +68,16 @@ export function executeCliCommand( } ); const eventEmitter = new CliCommandEventEmitter(); + child.on( 'spawn', () => { + eventEmitter.emit( 'started' ); + } ); + + child.on( 'error', ( error ) => { + console.error( 'Child process error:', error ); + Sentry.captureException( error ); + eventEmitter.emit( 'error', { error } ); + } ); + let stdout = ''; let stderr = ''; @@ -83,12 +94,6 @@ export function executeCliCommand( eventEmitter.emit( 'data', { data: message } ); } ); - child.on( 'error', ( error ) => { - console.error( 'Child process error:', error ); - Sentry.captureException( error ); - eventEmitter.emit( 'error', { error } ); - } ); - let capturedExitCode: number | null = null; child.on( 'exit', ( code ) => { diff --git a/src/modules/cli/lib/execute-site-watch-command.ts b/src/modules/cli/lib/execute-site-watch-command.ts index 996643b620..b99b2a475c 100644 --- a/src/modules/cli/lib/execute-site-watch-command.ts +++ b/src/modules/cli/lib/execute-site-watch-command.ts @@ -71,37 +71,44 @@ async function updateSiteServerStatus( await current; } -export function startSiteWatcher(): void { - if ( watcher ) { - return; - } +export async function startSiteWatcher(): Promise< void > { + return new Promise( ( resolve, reject ) => { + if ( watcher ) { + return resolve(); + } - watcher = executeCliCommand( [ 'site', 'list', '--watch', '--format', 'json' ], { - output: 'ignore', - } ); - const [ eventEmitter ] = watcher; + watcher = executeCliCommand( [ 'site', 'list', '--watch', '--format', 'json' ], { + output: 'ignore', + } ); + const [ eventEmitter ] = watcher; - eventEmitter.on( 'data', ( { data } ) => { - const parsed = siteStatusEventSchema.safeParse( data ); - if ( ! parsed.success ) { - return; - } + eventEmitter.on( 'started', () => { + resolve(); + } ); - const { siteId, status, url } = parsed.data.value; - const isRunning = status === 'running'; + eventEmitter.on( 'error', ( { error } ) => { + reject(); + console.error( 'Site watcher error:', error ); + watcher = null; + } ); - void updateSiteServerStatus( siteId, isRunning, url ); - void sendIpcEventToRenderer( 'site-status-changed', parsed.data.value ); - } ); + eventEmitter.on( 'data', ( { data } ) => { + const parsed = siteStatusEventSchema.safeParse( data ); + if ( ! parsed.success ) { + return; + } - eventEmitter.on( 'error', ( { error } ) => { - console.error( 'Site watcher error:', error ); - watcher = null; - } ); + const { siteId, status, url } = parsed.data.value; + const isRunning = status === 'running'; - eventEmitter.on( 'failure', () => { - console.warn( 'Site watcher exited unexpectedly' ); - watcher = null; + void updateSiteServerStatus( siteId, isRunning, url ); + void sendIpcEventToRenderer( 'site-status-changed', parsed.data.value ); + } ); + + eventEmitter.on( 'failure', () => { + console.warn( 'Site watcher exited unexpectedly' ); + watcher = null; + } ); } ); } From ef932ca23922dc91f769dce9387e77c9c643b5a3 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Mon, 22 Dec 2025 09:25:34 +0100 Subject: [PATCH 19/71] Try using async fs method for cleanup --- e2e/e2e-helpers.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index cf02d280d5..684e9364fc 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -93,11 +93,8 @@ export class E2ESession { async cleanup() { await this.electronApp?.close(); - // Give processes time to release file handles - await new Promise( ( resolve ) => setTimeout( resolve, 3000 ) ); - // Clean up temporary folder to hold application data - fs.rmSync( this.sessionPath, { + await fs.promises.rm( this.sessionPath, { recursive: true, force: true, maxRetries: 5, From 03545121f875ebfe2637d2723f36b923358486c3 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Mon, 22 Dec 2025 09:57:18 +0100 Subject: [PATCH 20/71] Try rimraf (which has advanced retry strategies) --- e2e/e2e-helpers.ts | 8 +- package-lock.json | 199 +++++++++++++++++++++++++++++++++++++++++++-- package.json | 1 + 3 files changed, 193 insertions(+), 15 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 684e9364fc..4b7795bf3d 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -4,6 +4,7 @@ import path from 'path'; import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers'; import fs from 'fs-extra'; import { _electron as electron, Page, ElectronApplication } from 'playwright'; +import { rimraf } from 'rimraf'; export class E2ESession { electronApp: ElectronApplication; @@ -94,11 +95,6 @@ export class E2ESession { await this.electronApp?.close(); // Clean up temporary folder to hold application data - await fs.promises.rm( this.sessionPath, { - recursive: true, - force: true, - maxRetries: 5, - retryDelay: 500, - } ); + await rimraf( this.sessionPath ); } } diff --git a/package-lock.json b/package-lock.json index 70146c823e..2c2f048a10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -124,6 +124,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "resize-observer-polyfill": "^1.5.1", + "rimraf": "^6.1.2", "tailwindcss": "^3.3.6", "ts-jest": "^29.4.6", "typescript": "~5.9.3", @@ -5346,6 +5347,29 @@ } } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6427,6 +6451,23 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@npmcli/move-file/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@octokit/app": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.1.0.tgz", @@ -13414,6 +13455,69 @@ "node": ">=10" } }, + "node_modules/cacache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/cacache/node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -23602,10 +23706,11 @@ } }, "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" }, "node_modules/pako": { "version": "1.0.11", @@ -25408,15 +25513,91 @@ "license": "MIT" }, "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^7.1.3" + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" diff --git a/package.json b/package.json index 3a9a28cbdb..53ea5969a1 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "resize-observer-polyfill": "^1.5.1", + "rimraf": "^6.1.2", "tailwindcss": "^3.3.6", "ts-jest": "^29.4.6", "typescript": "~5.9.3", From 07fc67930103c33975a65fd705edeec7cd3559b2 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Wed, 7 Jan 2026 14:36:54 +0100 Subject: [PATCH 21/71] New approach: don't remove `sessionPath` dir --- e2e/e2e-helpers.ts | 6 +- package-lock.json | 197 ++++++++------------------------------------- package.json | 1 - 3 files changed, 37 insertions(+), 167 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 4b7795bf3d..65ace7ccfc 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -94,7 +94,9 @@ export class E2ESession { async cleanup() { await this.electronApp?.close(); - // Clean up temporary folder to hold application data - await rimraf( this.sessionPath ); + // Removing the `sessionPath` directory has proven to be difficult, especially on Windows. Since + // session paths are unique, the WordPress installations are relatively small, and the E2E tests + // primarily run in ephemeral CI workers, we've decided to fix this issue by simply not removing + // the `sessionPath` directory. } } diff --git a/package-lock.json b/package-lock.json index 659306655b..989f3ce96a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126,7 +126,6 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "resize-observer-polyfill": "^1.5.1", - "rimraf": "^6.1.2", "tailwindcss": "^3.3.6", "ts-jest": "^29.4.6", "typescript": "~5.9.3", @@ -476,7 +475,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2530,7 +2528,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2554,7 +2551,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -4153,7 +4149,6 @@ "version": "11.11.3", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz", "integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.11.0", @@ -5234,7 +5229,6 @@ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", @@ -5345,29 +5339,6 @@ } } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6634,7 +6605,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -6947,7 +6917,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -6969,7 +6938,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" }, @@ -6982,7 +6950,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, @@ -7007,7 +6974,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", @@ -7413,7 +7379,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" @@ -7439,7 +7404,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", @@ -7466,7 +7430,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz", "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -7503,6 +7466,7 @@ "hasInstallScript": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", @@ -7545,6 +7509,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7566,6 +7531,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7587,6 +7553,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7608,6 +7575,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7629,6 +7597,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7650,6 +7619,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7671,6 +7641,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7692,6 +7663,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7713,6 +7685,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7734,6 +7707,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7755,6 +7729,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7776,6 +7751,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7797,6 +7773,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7812,6 +7789,7 @@ "dev": true, "license": "Apache-2.0", "optional": true, + "peer": true, "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -9517,7 +9495,6 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.24" @@ -9742,8 +9719,7 @@ "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.13.5.tgz", "integrity": "sha512-ZBZcxieydxNwgEU9eFAXGMaDb1Xoh+ZkZcUQ27LNJzc2lPSByoL6CSVqnYiaVo+n9JgqbYyHlMq+i7z0wRNTfA==", "devOptional": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", @@ -9945,7 +9921,8 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/aws-lambda": { "version": "8.10.159", @@ -10312,7 +10289,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -10363,7 +10339,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -10374,7 +10349,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -10543,7 +10517,6 @@ "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", @@ -12322,7 +12295,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -13256,7 +13228,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -14894,7 +14865,8 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/dot-case": { "version": "3.0.4", @@ -15920,7 +15892,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -15980,7 +15951,6 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -16105,7 +16075,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -16246,7 +16215,6 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -18254,7 +18222,8 @@ "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/import-fresh": { "version": "3.3.0", @@ -19375,7 +19344,6 @@ "integrity": "sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.0.5", "@jest/types": "30.0.5", @@ -20913,7 +20881,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -21715,6 +21682,7 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -23151,7 +23119,8 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/node-api-version": { "version": "0.2.1", @@ -24256,7 +24225,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -24461,7 +24429,6 @@ "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-3.0.3.tgz", "integrity": "sha512-X4UlrxDTH8oom9qXlcjnydsjAOD2BmB6yFmvS4Z2zdTzqqpRWb+fbqrH412+l+OUXmbzJlSXjlMFYPgYG12IAA==", "dev": true, - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -24489,6 +24456,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -24503,6 +24471,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -24514,7 +24483,8 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "dev": true, + "peer": true }, "node_modules/proc-log": { "version": "2.0.1", @@ -24764,7 +24734,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -24816,7 +24785,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -24860,7 +24828,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -25109,8 +25076,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -25535,97 +25501,6 @@ "dev": true, "license": "MIT" }, - "node_modules/rimraf": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", - "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "glob": "^13.0.0", - "package-json-from-dist": "^1.0.1" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", - "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/rimraf/node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -25650,7 +25525,6 @@ "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -25802,6 +25676,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -25824,6 +25699,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -25841,6 +25717,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 14.18.0" }, @@ -27269,7 +27146,6 @@ "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", @@ -27449,7 +27325,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -27742,7 +27617,6 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -27970,7 +27844,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -28277,7 +28150,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -28541,7 +28413,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -29191,7 +29062,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -29357,7 +29227,6 @@ "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", diff --git a/package.json b/package.json index 7b1e931f0d..6bb3e72d4a 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,6 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "resize-observer-polyfill": "^1.5.1", - "rimraf": "^6.1.2", "tailwindcss": "^3.3.6", "ts-jest": "^29.4.6", "typescript": "~5.9.3", From 055d14cb0750449346a0a34e6dfaccc6e54fae52 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Wed, 7 Jan 2026 14:37:04 +0100 Subject: [PATCH 22/71] Unused import --- e2e/e2e-helpers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 65ace7ccfc..426167bd2b 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -4,7 +4,6 @@ import path from 'path'; import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers'; import fs from 'fs-extra'; import { _electron as electron, Page, ElectronApplication } from 'playwright'; -import { rimraf } from 'rimraf'; export class E2ESession { electronApp: ElectronApplication; From 0d0dc26761f9d46d2f4ec0caa561b995c4ee5362 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Wed, 7 Jan 2026 16:53:05 +0100 Subject: [PATCH 23/71] Force close app --- e2e/e2e-helpers.ts | 53 ++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 426167bd2b..f43fbfe57a 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -39,40 +39,42 @@ export class E2ESession { }; await fs.writeFile( appdataPath, JSON.stringify( initialAppdata, null, 2 ) ); - // find the latest build in the out directory - const latestBuild = findLatestBuild(); + await this.launchFirstWindow( testEnv ); + } - // parse the packaged Electron app and find paths and other info - const appInfo = parseElectronApp( latestBuild ); - let executablePath = appInfo.executable; - if ( appInfo.platform === 'win32' ) { - // `parseElectronApp` function obtains the executable path by finding the first executable from the build folder. - // We need to ensure that the executable is the Studio app. - executablePath = executablePath.replace( 'Squirrel.exe', 'Studio.exe' ); + // Close the app but keep the data for persistence testing + async restart() { + await this.forceCloseApp(); + await this.launchFirstWindow(); + } + + private async forceCloseApp() { + if ( ! this.electronApp ) { + return; } - this.electronApp = await electron.launch( { - args: [ appInfo.main ], // main file from package.json - executablePath, // path to the Electron executable - env: { - ...process.env, - ...testEnv, - E2E: 'true', // allow app to determine whether it's running as an end-to-end test - E2E_APP_DATA_PATH: this.appDataPath, - E2E_HOME_PATH: this.homePath, - }, - timeout: 60_000, + // We kill the Electron process instead of closing the app, because `electronApp.close()` hangs + // while waiting for child processes to exit. + const process = this.electronApp.process(); + process.kill( 'SIGKILL' ); + + // Wait for the process to actually exit + await new Promise< void >( ( resolve ) => { + if ( process.exitCode !== null ) { + resolve(); + return; + } + process.on( 'exit', () => resolve() ); } ); - this.mainWindow = await this.electronApp.firstWindow( { timeout: 60_000 } ); } - // Close the app but keep the data for persistence testing - async restart() { - await this.electronApp?.close(); + private async launchFirstWindow( testEnv: NodeJS.ProcessEnv = {} ) { const latestBuild = findLatestBuild(); const appInfo = parseElectronApp( latestBuild ); let executablePath = appInfo.executable; if ( appInfo.platform === 'win32' ) { + // `parseElectronApp` function obtains the executable path by finding the first executable from + // the build folder. We need to ensure that the executable is the Studio app. executablePath = executablePath.replace( 'Squirrel.exe', 'Studio.exe' ); } @@ -81,6 +83,7 @@ export class E2ESession { executablePath, env: { ...process.env, + ...testEnv, E2E: 'true', E2E_APP_DATA_PATH: this.appDataPath, E2E_HOME_PATH: this.homePath, @@ -91,7 +94,7 @@ export class E2ESession { } async cleanup() { - await this.electronApp?.close(); + await this.forceCloseApp(); // Removing the `sessionPath` directory has proven to be difficult, especially on Windows. Since // session paths are unique, the WordPress installations are relatively small, and the E2E tests From bc83d93ec817fd429d97db62efbf2c774332f667 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 08:44:02 +0100 Subject: [PATCH 24/71] disconnect from pm2 in response to SIGTERM --- cli/commands/site/list.ts | 1 + cli/lib/wordpress-server-manager.ts | 7 ++----- e2e/e2e-helpers.ts | 24 ++---------------------- 3 files changed, 5 insertions(+), 27 deletions(-) diff --git a/cli/commands/site/list.ts b/cli/commands/site/list.ts index 41d67baca8..50d3be6a93 100644 --- a/cli/commands/site/list.ts +++ b/cli/commands/site/list.ts @@ -128,6 +128,7 @@ export async function runCommand( format: 'table' | 'json', watch: boolean ): Pr ); process.on( 'SIGINT', disconnect ); + process.on( 'SIGTERM', disconnect ); } } finally { if ( ! watch ) { diff --git a/cli/lib/wordpress-server-manager.ts b/cli/lib/wordpress-server-manager.ts index 3443e68d0e..a76a4d3ae8 100644 --- a/cli/lib/wordpress-server-manager.ts +++ b/cli/lib/wordpress-server-manager.ts @@ -34,11 +34,8 @@ const SITE_PROCESS_PREFIX = 'studio-site-'; // Get an abort signal that's triggered on SIGINT/SIGTERM. This is useful for aborting and cleaning // up async operations. const abortController = new AbortController(); -function handleProcessTermination() { - abortController.abort(); -} -process.on( 'SIGINT', handleProcessTermination ); -process.on( 'SIGTERM', handleProcessTermination ); +process.on( 'SIGINT', () => abortController.abort() ); +process.on( 'SIGTERM', () => abortController.abort() ); function getProcessName( siteId: string ): string { return `${ SITE_PROCESS_PREFIX }${ siteId }`; diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index f43fbfe57a..7a68d18281 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -44,30 +44,10 @@ export class E2ESession { // Close the app but keep the data for persistence testing async restart() { - await this.forceCloseApp(); + await this.electronApp.close(); await this.launchFirstWindow(); } - private async forceCloseApp() { - if ( ! this.electronApp ) { - return; - } - - // We kill the Electron process instead of closing the app, because `electronApp.close()` hangs - // while waiting for child processes to exit. - const process = this.electronApp.process(); - process.kill( 'SIGKILL' ); - - // Wait for the process to actually exit - await new Promise< void >( ( resolve ) => { - if ( process.exitCode !== null ) { - resolve(); - return; - } - process.on( 'exit', () => resolve() ); - } ); - } - private async launchFirstWindow( testEnv: NodeJS.ProcessEnv = {} ) { const latestBuild = findLatestBuild(); const appInfo = parseElectronApp( latestBuild ); @@ -94,7 +74,7 @@ export class E2ESession { } async cleanup() { - await this.forceCloseApp(); + await this.electronApp.close(); // Removing the `sessionPath` directory has proven to be difficult, especially on Windows. Since // session paths are unique, the WordPress installations are relatively small, and the E2E tests From 8264bbe08626fae6c4eaedf9a61eca9e2a45e3e8 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 09:12:57 +0100 Subject: [PATCH 25/71] Revert user data watcher changes from #2313 --- src/hooks/use-site-details.tsx | 25 ----------------- src/ipc-handlers.ts | 14 +--------- .../cli/lib/execute-site-watch-command.ts | 28 ------------------- src/tests/ipc-handlers.test.ts | 6 ---- 4 files changed, 1 insertion(+), 72 deletions(-) diff --git a/src/hooks/use-site-details.tsx b/src/hooks/use-site-details.tsx index 706412a893..dee009d6d7 100644 --- a/src/hooks/use-site-details.tsx +++ b/src/hooks/use-site-details.tsx @@ -188,21 +188,6 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) { } } ); - /* - * Site Update Listeners - * - * Two complementary watchers keep the UI in sync with external changes: - * - * 1. 'site-status-changed' (from Site Status Watcher - execute-site-watch-command.ts): - * - Source: PM2 process events via `studio site list --watch` - * - Detects: Site start/stop/crash events - * - * 2. 'user-data-updated' (from User Data Watcher - user-data-watcher.ts): - * - Source: fs.watch on the appdata file - * - Detects: ALL changes (new sites, edits, deletions) - * - Used for: CLI site creation, property changes, external modifications - * - */ useIpcListener( 'site-status-changed', ( _, { siteId, status, url } ) => { setSites( ( prevSites ) => prevSites.map( ( site ) => @@ -211,16 +196,6 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) { ); } ); - useIpcListener( 'user-data-updated', async () => { - const updatedSites = await getIpcApi().getSiteDetails(); - setSites( updatedSites ); - - // Handle case where selected site was deleted externally - if ( selectedSiteId && ! updatedSites.find( ( site ) => site.id === selectedSiteId ) ) { - setSelectedSiteId( updatedSites.length ? updatedSites[ 0 ].id : '' ); - } - } ); - const toggleLoadingServerForSite = useCallback( ( siteId: string ) => { setLoadingServer( ( currentLoading ) => ( { ...currentLoading, diff --git a/src/ipc-handlers.ts b/src/ipc-handlers.ts index 8c9ce861d3..fcaa6e5601 100644 --- a/src/ipc-handlers.ts +++ b/src/ipc-handlers.ts @@ -173,19 +173,7 @@ function mergeSiteDetailsWithRunningDetails( sites: SiteDetails[] ): SiteDetails return sites.map( ( site ) => { const server = SiteServer.get( site.id ); if ( server ) { - // Merge fresh data from disk with running state from server - // This ensures external changes (e.g., from CLI) are reflected - if ( server.details.running ) { - return { - ...site, - running: true as const, - url: server.details.url, - }; - } - return { - ...site, - running: false as const, - }; + return server.details; } return site; } ); diff --git a/src/modules/cli/lib/execute-site-watch-command.ts b/src/modules/cli/lib/execute-site-watch-command.ts index 7e74f6f8ac..2b12d4ba50 100644 --- a/src/modules/cli/lib/execute-site-watch-command.ts +++ b/src/modules/cli/lib/execute-site-watch-command.ts @@ -1,31 +1,3 @@ -/** - * Site Status Watcher - * - * This module monitors site running/stopped status changes by subscribing to PM2 process events - * via `studio site list --watch`. It's primarily used to detect status changes that occur outside - * of Studio's direct control, such as: - * - Sites started/stopped via CLI commands - * - Site crashes or unexpected process terminations - * - * IMPORTANT: Architecture Notes - * ----------------------------- - * There are currently TWO separate watchers that update the UI with site changes: - * - * 1. Site Status Watcher (this file): - * - Monitors PM2 process events (start/stop/crash) - * - Only detects running/stopped status changes - * - Sends 'site-status-changed' IPC events to the renderer - * - * 2. User Data Watcher (src/lib/user-data-watcher.ts): - * - Monitors the appdata file directly via fs.watch - * - Detects ALL changes to site data (new sites, edits, deletions) - * - Sends 'user-data-updated' IPC events to the renderer - * - * The renderer (use-site-details.tsx) listens to BOTH: - * - 'site-status-changed': Updates running/stopped status for existing sites - * - 'user-data-updated': Refreshes the entire site list (handles new sites, edits, deletions) - * - */ import { z } from 'zod'; import { sendIpcEventToRenderer } from 'src/ipc-utils'; import { executeCliCommand } from 'src/modules/cli/lib/execute-command'; diff --git a/src/tests/ipc-handlers.test.ts b/src/tests/ipc-handlers.test.ts index b95905b902..67c0659484 100644 --- a/src/tests/ipc-handlers.test.ts +++ b/src/tests/ipc-handlers.test.ts @@ -283,11 +283,9 @@ describe( 'getXdebugEnabledSite', () => { const result = await getXdebugEnabledSite( mockIpcMainInvokeEvent ); expect( result ).toEqual( { - autoStart: false, id: 'site-2', name: 'Site 2', path: '/path/to/site-2', - phpVersion: '8.0', running: true, enableXdebug: true, } ); @@ -304,11 +302,9 @@ describe( 'getXdebugEnabledSite', () => { ( fs.existsSync as jest.Mock ).mockReturnValue( true ); ( SiteServer.get as jest.Mock ).mockReturnValue( { details: { - autoStart: false, id: 'site-1', name: 'Site 1', path: '/path/to/site-1', - phpVersion: '8.0', running: false, enableXdebug: true, }, @@ -317,11 +313,9 @@ describe( 'getXdebugEnabledSite', () => { const result = await getXdebugEnabledSite( mockIpcMainInvokeEvent ); expect( result ).toEqual( { - autoStart: false, id: 'site-1', name: 'Site 1', path: '/path/to/site-1', - phpVersion: '8.0', running: false, enableXdebug: true, } ); From 1c0cfda1ff411c9daadae7d6781cc3832ea8c188 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 09:14:19 +0100 Subject: [PATCH 26/71] Wait for running button --- e2e/site-navigation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/site-navigation.test.ts b/e2e/site-navigation.test.ts index cf7cd9ebdc..f123a9a7ac 100644 --- a/e2e/site-navigation.test.ts +++ b/e2e/site-navigation.test.ts @@ -49,7 +49,7 @@ test.describe( 'Site Navigation', () => { // Wait for default site to be ready and get URLs const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.siteNameHeading ).toBeVisible( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); // Get site URLs for tests const settingsTab = await siteContent.navigateToTab( 'Settings' ); From e12e4ad8816b9df3caf0e2558187f921962886d3 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 09:49:27 +0100 Subject: [PATCH 27/71] Use Electron's will-quit event in `execute-command.ts` --- src/modules/cli/lib/execute-command.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 0e3209bd4a..e46233336b 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -2,6 +2,7 @@ import { fork, ChildProcess, StdioOptions } from 'node:child_process'; import EventEmitter from 'node:events'; import * as Sentry from '@sentry/electron/main'; import { getBundledNodeBinaryPath, getCliPath } from 'src/storage/paths'; +import { app } from 'electron'; export interface CliCommandResult { stdout: string; @@ -120,7 +121,7 @@ export function executeCliCommand( if ( options.detached ) { child.unref(); } else { - process.on( 'exit', () => { + app.on( 'will-quit', () => { child.kill(); } ); } From 21b2e77ab2b2083dfa93c70564879df851d2bb4c Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 10:17:09 +0100 Subject: [PATCH 28/71] SIGINT and SIGTERM listeners in `wp` command --- cli/commands/wp.ts | 3 +++ src/modules/cli/lib/execute-command.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/commands/wp.ts b/cli/commands/wp.ts index d6d11abb8c..cc171ccad3 100644 --- a/cli/commands/wp.ts +++ b/cli/commands/wp.ts @@ -26,6 +26,9 @@ export async function runCommand( const useCustomPhpVersion = options.phpVersion && options.phpVersion !== site.phpVersion; if ( ! useCustomPhpVersion ) { + process.on( 'SIGINT', disconnect ); + process.on( 'SIGTERM', disconnect ); + try { await connect(); diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index e46233336b..1e8c3e9efc 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -1,8 +1,8 @@ +import { app } from 'electron'; import { fork, ChildProcess, StdioOptions } from 'node:child_process'; import EventEmitter from 'node:events'; import * as Sentry from '@sentry/electron/main'; import { getBundledNodeBinaryPath, getCliPath } from 'src/storage/paths'; -import { app } from 'electron'; export interface CliCommandResult { stdout: string; From 6355215aed43b433b552f67501eb73a5a9dfd11c Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 11:26:00 +0100 Subject: [PATCH 29/71] More SIGINT and SIGTERM listeners in `wp` command --- cli/commands/wp.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/commands/wp.ts b/cli/commands/wp.ts index cc171ccad3..a15da59487 100644 --- a/cli/commands/wp.ts +++ b/cli/commands/wp.ts @@ -43,6 +43,9 @@ export async function runCommand( } } + process.on( 'SIGINT', () => process.exit( 1 ) ); + process.on( 'SIGTERM', () => process.exit( 1 ) ); + // …If not, instantiate a new Playground instance const [ response, closeWpCliServer ] = await runWpCliCommand( siteFolder, From 0d16ae5d342722dcd67b18d35703ba34b4cc52c2 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 12:09:56 +0100 Subject: [PATCH 30/71] Temporarily skip blueprints test --- e2e/blueprints.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/blueprints.test.ts b/e2e/blueprints.test.ts index 46d1cccf90..d57df7503e 100644 --- a/e2e/blueprints.test.ts +++ b/e2e/blueprints.test.ts @@ -7,7 +7,7 @@ import Onboarding from './page-objects/onboarding'; import SiteContent from './page-objects/site-content'; import { getUrlWithAutoLogin } from './utils'; -test.describe( 'Blueprints', () => { +test.describe.skip( 'Blueprints', () => { const session = new E2ESession(); test.beforeAll( async () => { From d64e35a7f579d096899769fd3ee501fea87c74cc Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 12:28:07 +0100 Subject: [PATCH 31/71] Revert "Temporarily skip blueprints test" This reverts commit 0d16ae5d342722dcd67b18d35703ba34b4cc52c2. --- e2e/blueprints.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/blueprints.test.ts b/e2e/blueprints.test.ts index d57df7503e..46d1cccf90 100644 --- a/e2e/blueprints.test.ts +++ b/e2e/blueprints.test.ts @@ -7,7 +7,7 @@ import Onboarding from './page-objects/onboarding'; import SiteContent from './page-objects/site-content'; import { getUrlWithAutoLogin } from './utils'; -test.describe.skip( 'Blueprints', () => { +test.describe( 'Blueprints', () => { const session = new E2ESession(); test.beforeAll( async () => { From 07eaa56f6e7fea150c74cace781d001167b46b99 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 12:28:33 +0100 Subject: [PATCH 32/71] Try with force kill again --- e2e/e2e-helpers.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 7a68d18281..bf4485f46c 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -44,10 +44,32 @@ export class E2ESession { // Close the app but keep the data for persistence testing async restart() { - await this.electronApp.close(); + await this.forceCloseApp(); await this.launchFirstWindow(); } + private async forceCloseApp() { + if ( ! this.electronApp ) { + return; + } + + // Use process kill instead of close() because close() waits for the entire + // process tree to exit gracefully. With the architectural refactor moving + // site management to CLI, there are multiple child processes with IPC + // channels that can prevent graceful shutdown, especially on Windows CI. + const childProcess = this.electronApp.process(); + childProcess.kill( 'SIGKILL' ); + + // Wait for the process to actually exit + await new Promise< void >( ( resolve ) => { + if ( childProcess.exitCode !== null ) { + resolve(); + return; + } + childProcess.on( 'exit', () => resolve() ); + } ); + } + private async launchFirstWindow( testEnv: NodeJS.ProcessEnv = {} ) { const latestBuild = findLatestBuild(); const appInfo = parseElectronApp( latestBuild ); @@ -74,7 +96,7 @@ export class E2ESession { } async cleanup() { - await this.electronApp.close(); + await this.forceCloseApp(); // Removing the `sessionPath` directory has proven to be difficult, especially on Windows. Since // session paths are unique, the WordPress installations are relatively small, and the E2E tests From 1534b2ce747d53ad893b121f89d7a3969aed2622 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 12:47:33 +0100 Subject: [PATCH 33/71] Logging --- e2e/e2e-helpers.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index bf4485f46c..7508326fb4 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -49,7 +49,9 @@ export class E2ESession { } private async forceCloseApp() { + console.log( 'forceCloseApp: starting' ); if ( ! this.electronApp ) { + console.log( 'forceCloseApp: no electronApp, returning early' ); return; } @@ -58,16 +60,22 @@ export class E2ESession { // site management to CLI, there are multiple child processes with IPC // channels that can prevent graceful shutdown, especially on Windows CI. const childProcess = this.electronApp.process(); + console.log( 'forceCloseApp: killing pid', childProcess.pid ); childProcess.kill( 'SIGKILL' ); // Wait for the process to actually exit await new Promise< void >( ( resolve ) => { if ( childProcess.exitCode !== null ) { + console.log( 'forceCloseApp: process already exited with code', childProcess.exitCode ); resolve(); return; } - childProcess.on( 'exit', () => resolve() ); + childProcess.on( 'exit', ( code ) => { + console.log( 'forceCloseApp: process exited with code', code ); + resolve(); + } ); } ); + console.log( 'forceCloseApp: done' ); } private async launchFirstWindow( testEnv: NodeJS.ProcessEnv = {} ) { From f75e007360f33694e5102e030b690b75c7b61ed7 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 13:10:28 +0100 Subject: [PATCH 34/71] New approach to waiting for app close --- e2e/e2e-helpers.ts | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 7508326fb4..d980be1707 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -44,38 +44,27 @@ export class E2ESession { // Close the app but keep the data for persistence testing async restart() { - await this.forceCloseApp(); + await this.closeApp(); await this.launchFirstWindow(); } - private async forceCloseApp() { - console.log( 'forceCloseApp: starting' ); + private async closeApp() { if ( ! this.electronApp ) { - console.log( 'forceCloseApp: no electronApp, returning early' ); return; } - // Use process kill instead of close() because close() waits for the entire - // process tree to exit gracefully. With the architectural refactor moving - // site management to CLI, there are multiple child processes with IPC - // channels that can prevent graceful shutdown, especially on Windows CI. const childProcess = this.electronApp.process(); - console.log( 'forceCloseApp: killing pid', childProcess.pid ); - childProcess.kill( 'SIGKILL' ); + await this.electronApp.close(); - // Wait for the process to actually exit - await new Promise< void >( ( resolve ) => { - if ( childProcess.exitCode !== null ) { - console.log( 'forceCloseApp: process already exited with code', childProcess.exitCode ); - resolve(); - return; - } - childProcess.on( 'exit', ( code ) => { - console.log( 'forceCloseApp: process exited with code', code ); - resolve(); - } ); - } ); - console.log( 'forceCloseApp: done' ); + // Ensure process is fully dead (singleton lock released) before continuing. + // This prevents a race condition where the next test launches before the + // previous instance has fully exited and released the singleton lock. + if ( childProcess.exitCode === null ) { + await new Promise< void >( ( resolve ) => childProcess.on( 'exit', resolve ) ); + } + + // Clear the reference so Playwright doesn't try to close it again during teardown + this.electronApp = null as unknown as ElectronApplication; } private async launchFirstWindow( testEnv: NodeJS.ProcessEnv = {} ) { @@ -104,7 +93,7 @@ export class E2ESession { } async cleanup() { - await this.forceCloseApp(); + await this.closeApp(); // Removing the `sessionPath` directory has proven to be difficult, especially on Windows. Since // session paths are unique, the WordPress installations are relatively small, and the E2E tests From bc41e3878f3d6ce79039b9c1b5426f49ae535ce2 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 13:34:27 +0100 Subject: [PATCH 35/71] Logging again --- e2e/e2e-helpers.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index d980be1707..9d3b911f76 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -49,18 +49,27 @@ export class E2ESession { } private async closeApp() { + console.log( 'closeApp: starting' ); if ( ! this.electronApp ) { + console.log( 'closeApp: no electronApp' ); return; } const childProcess = this.electronApp.process(); + console.log( 'closeApp: calling electronApp.close() for pid', childProcess.pid ); await this.electronApp.close(); + console.log( 'closeApp: close() returned' ); // Ensure process is fully dead (singleton lock released) before continuing. // This prevents a race condition where the next test launches before the // previous instance has fully exited and released the singleton lock. if ( childProcess.exitCode === null ) { - await new Promise< void >( ( resolve ) => childProcess.on( 'exit', resolve ) ); + await new Promise< void >( ( resolve ) => + childProcess.on( 'exit', () => { + console.log( 'closeApp: process exited' ); + resolve(); + } ) + ); } // Clear the reference so Playwright doesn't try to close it again during teardown From f99f4b0851a1260fbd2f68f4a386446687c3b690 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 14:14:08 +0100 Subject: [PATCH 36/71] Try to make all child processes detached --- src/modules/cli/lib/execute-command.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 1e8c3e9efc..ddec7f8c24 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -61,7 +61,7 @@ export function executeCliCommand( const child = fork( cliPath, [ ...args, '--avoid-telemetry' ], { stdio, - detached: options.detached, + detached: options.detached ?? process.platform === 'win32', execPath: getBundledNodeBinaryPath(), } ); const eventEmitter = new CliCommandEventEmitter(); From 3596bac444b94e6e61dd673f01d90504d57da03d Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 14:40:09 +0100 Subject: [PATCH 37/71] Experiment --- e2e/e2e-helpers.ts | 47 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 9d3b911f76..4ef4c297a5 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -56,24 +56,43 @@ export class E2ESession { } const childProcess = this.electronApp.process(); - console.log( 'closeApp: calling electronApp.close() for pid', childProcess.pid ); - await this.electronApp.close(); - console.log( 'closeApp: close() returned' ); - - // Ensure process is fully dead (singleton lock released) before continuing. - // This prevents a race condition where the next test launches before the - // previous instance has fully exited and released the singleton lock. - if ( childProcess.exitCode === null ) { - await new Promise< void >( ( resolve ) => - childProcess.on( 'exit', () => { - console.log( 'closeApp: process exited' ); - resolve(); - } ) - ); + console.log( 'closeApp: closing pid', childProcess.pid ); + + // On Windows, Playwright's close() hangs indefinitely even though the process + // exits. This appears to be related to debugger/WebSocket cleanup issues. + // We bypass close() entirely and kill the process directly. + if ( process.platform === 'win32' ) { + childProcess.kill(); + + // Wait for process to exit + if ( childProcess.exitCode === null ) { + await new Promise< void >( ( resolve ) => { + childProcess.on( 'exit', () => { + console.log( 'closeApp: process exited' ); + resolve(); + } ); + } ); + } + } else { + await this.electronApp.close(); + console.log( 'closeApp: close() returned' ); + + // Ensure process is fully dead (singleton lock released) before continuing. + // This prevents a race condition where the next test launches before the + // previous instance has fully exited and released the singleton lock. + if ( childProcess.exitCode === null ) { + await new Promise< void >( ( resolve ) => + childProcess.on( 'exit', () => { + console.log( 'closeApp: process exited' ); + resolve(); + } ) + ); + } } // Clear the reference so Playwright doesn't try to close it again during teardown this.electronApp = null as unknown as ElectronApplication; + console.log( 'closeApp: done' ); } private async launchFirstWindow( testEnv: NodeJS.ProcessEnv = {} ) { From 4b7364afee5ef34ce1141db1fbc9b5005ce67362 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 15:18:39 +0100 Subject: [PATCH 38/71] Revert "Experiment" This reverts commit 3596bac444b94e6e61dd673f01d90504d57da03d. --- e2e/e2e-helpers.ts | 47 ++++++++++++++-------------------------------- 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 4ef4c297a5..9d3b911f76 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -56,43 +56,24 @@ export class E2ESession { } const childProcess = this.electronApp.process(); - console.log( 'closeApp: closing pid', childProcess.pid ); - - // On Windows, Playwright's close() hangs indefinitely even though the process - // exits. This appears to be related to debugger/WebSocket cleanup issues. - // We bypass close() entirely and kill the process directly. - if ( process.platform === 'win32' ) { - childProcess.kill(); - - // Wait for process to exit - if ( childProcess.exitCode === null ) { - await new Promise< void >( ( resolve ) => { - childProcess.on( 'exit', () => { - console.log( 'closeApp: process exited' ); - resolve(); - } ); - } ); - } - } else { - await this.electronApp.close(); - console.log( 'closeApp: close() returned' ); - - // Ensure process is fully dead (singleton lock released) before continuing. - // This prevents a race condition where the next test launches before the - // previous instance has fully exited and released the singleton lock. - if ( childProcess.exitCode === null ) { - await new Promise< void >( ( resolve ) => - childProcess.on( 'exit', () => { - console.log( 'closeApp: process exited' ); - resolve(); - } ) - ); - } + console.log( 'closeApp: calling electronApp.close() for pid', childProcess.pid ); + await this.electronApp.close(); + console.log( 'closeApp: close() returned' ); + + // Ensure process is fully dead (singleton lock released) before continuing. + // This prevents a race condition where the next test launches before the + // previous instance has fully exited and released the singleton lock. + if ( childProcess.exitCode === null ) { + await new Promise< void >( ( resolve ) => + childProcess.on( 'exit', () => { + console.log( 'closeApp: process exited' ); + resolve(); + } ) + ); } // Clear the reference so Playwright doesn't try to close it again during teardown this.electronApp = null as unknown as ElectronApplication; - console.log( 'closeApp: done' ); } private async launchFirstWindow( testEnv: NodeJS.ProcessEnv = {} ) { From c0ab6422de365e0e6319558faec2e0825608d795 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 15:19:54 +0100 Subject: [PATCH 39/71] Revert "Try to make all child processes detached" This reverts commit f99f4b0851a1260fbd2f68f4a386446687c3b690. --- src/modules/cli/lib/execute-command.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index ddec7f8c24..1e8c3e9efc 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -61,7 +61,7 @@ export function executeCliCommand( const child = fork( cliPath, [ ...args, '--avoid-telemetry' ], { stdio, - detached: options.detached ?? process.platform === 'win32', + detached: options.detached, execPath: getBundledNodeBinaryPath(), } ); const eventEmitter = new CliCommandEventEmitter(); From 1682341e3572eec7703e8d8947bdab05cf00bdef Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 15:38:47 +0100 Subject: [PATCH 40/71] Try a 5s timeout for closing the app --- e2e/e2e-helpers.ts | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 9d3b911f76..7220c80ffe 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -57,23 +57,16 @@ export class E2ESession { const childProcess = this.electronApp.process(); console.log( 'closeApp: calling electronApp.close() for pid', childProcess.pid ); - await this.electronApp.close(); + // Cap `ElectronApplication::close` call at 5s to prevent timeout issues on Windows + await new Promise( ( resolve, reject ) => { + Promise.race( [ + this.electronApp.close(), + new Promise( ( resolve ) => setTimeout( resolve, 5000 ) ), + ] ) + .then( resolve ) + .catch( reject ); + } ); console.log( 'closeApp: close() returned' ); - - // Ensure process is fully dead (singleton lock released) before continuing. - // This prevents a race condition where the next test launches before the - // previous instance has fully exited and released the singleton lock. - if ( childProcess.exitCode === null ) { - await new Promise< void >( ( resolve ) => - childProcess.on( 'exit', () => { - console.log( 'closeApp: process exited' ); - resolve(); - } ) - ); - } - - // Clear the reference so Playwright doesn't try to close it again during teardown - this.electronApp = null as unknown as ElectronApplication; } private async launchFirstWindow( testEnv: NodeJS.ProcessEnv = {} ) { From e2faab8aaa2b4796489f337724f9940ea6a1a66e Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 8 Jan 2026 16:08:22 +0100 Subject: [PATCH 41/71] Experiment with removing stopAllServersOnQuit --- src/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 69542b3b27..18e6c6c832 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,7 +45,6 @@ import { renameLaunchUniquesStat } from 'src/migrations/rename-launch-uniques-st import { startSiteWatcher, stopSiteWatcher } from 'src/modules/cli/lib/execute-site-watch-command'; import { updateWindowsCliVersionedPathIfNeeded } from 'src/modules/cli/lib/windows-installation-manager'; import { setupWPServerFiles, updateWPServerFiles } from 'src/setup-wp-server-files'; -import { stopAllServersOnQuit } from 'src/site-server'; import { loadUserData, lockAppdata, saveUserData, unlockAppdata } from 'src/storage/user-data'; import { setupUpdates } from 'src/updates'; // eslint-disable-next-line import/order @@ -437,7 +436,6 @@ async function appBoot() { } ); app.on( 'quit', () => { - void stopAllServersOnQuit(); stopUserDataWatcher(); stopSiteWatcher(); } ); From 9d56d0cd1852db7dfb437908fcea7975be17103e Mon Sep 17 00:00:00 2001 From: bcotrim Date: Thu, 8 Jan 2026 22:38:01 +0000 Subject: [PATCH 42/71] shutdown message --- cli/index.ts | 18 ++++++++++++++++++ src/index.ts | 2 ++ src/modules/cli/lib/execute-command.ts | 11 +++++++++++ 3 files changed, 31 insertions(+) diff --git a/cli/index.ts b/cli/index.ts index e1a96dd685..1dec872b64 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -4,6 +4,7 @@ import { bumpAggregatedUniqueStat, AppdataProvider, LastBumpStatsData } from 'co import { suppressPunycodeWarning } from 'common/lib/suppress-punycode-warning'; import { StatsGroup, StatsMetric } from 'common/types/stats'; import yargs from 'yargs'; +import { disconnect } from 'cli/lib/pm2-manager'; import { registerCommand as registerAuthLoginCommand } from 'cli/commands/auth/login'; import { registerCommand as registerAuthLogoutCommand } from 'cli/commands/auth/logout'; import { registerCommand as registerAuthStatusCommand } from 'cli/commands/auth/status'; @@ -28,6 +29,23 @@ import { version } from '../package.json'; suppressPunycodeWarning(); +// Handle shutdown message from parent process (Electron app). +// On Windows, child.kill() doesn't send SIGTERM, so we use IPC to notify +// the CLI to clean up (e.g., disconnect from PM2) before terminating. +// Only add this listener when running with IPC channel (from Electron app). +if ( process.send ) { + process.on( 'message', ( message: unknown ) => { + if ( message && typeof message === 'object' && 'type' in message && message.type === 'shutdown' ) { + disconnect(); + process.exit( 0 ); + } + } ); + // Allow the process to exit naturally when the main work is done, + // even though we have a message listener. The IPC channel will be + // cleaned up when the parent terminates. + process.channel?.unref(); +} + const cliAppdataProvider: AppdataProvider< LastBumpStatsData > = { load: readAppdata, lock: lockAppdata, diff --git a/src/index.ts b/src/index.ts index 18e6c6c832..69542b3b27 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,6 +45,7 @@ import { renameLaunchUniquesStat } from 'src/migrations/rename-launch-uniques-st import { startSiteWatcher, stopSiteWatcher } from 'src/modules/cli/lib/execute-site-watch-command'; import { updateWindowsCliVersionedPathIfNeeded } from 'src/modules/cli/lib/windows-installation-manager'; import { setupWPServerFiles, updateWPServerFiles } from 'src/setup-wp-server-files'; +import { stopAllServersOnQuit } from 'src/site-server'; import { loadUserData, lockAppdata, saveUserData, unlockAppdata } from 'src/storage/user-data'; import { setupUpdates } from 'src/updates'; // eslint-disable-next-line import/order @@ -436,6 +437,7 @@ async function appBoot() { } ); app.on( 'quit', () => { + void stopAllServersOnQuit(); stopUserDataWatcher(); stopSiteWatcher(); } ); diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 1e8c3e9efc..f23cbfd9f6 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -122,6 +122,17 @@ export function executeCliCommand( child.unref(); } else { app.on( 'will-quit', () => { + // On Windows, child.kill() immediately terminates the process without sending + // SIGTERM, so signal handlers in the child never run. Use IPC to notify the + // child to clean up (e.g., disconnect from PM2) before terminating. + if ( child.connected ) { + try { + child.send( { type: 'shutdown' } ); + } catch { + // Process may have already exited + } + child.disconnect(); + } child.kill(); } ); } From ed7991a225bdb184ebb3d67ef86d7634b78f0584 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 08:53:22 +0100 Subject: [PATCH 43/71] Revert "shutdown message" This reverts commit 9d56d0cd1852db7dfb437908fcea7975be17103e. --- cli/index.ts | 18 ------------------ src/index.ts | 2 -- src/modules/cli/lib/execute-command.ts | 11 ----------- 3 files changed, 31 deletions(-) diff --git a/cli/index.ts b/cli/index.ts index 1dec872b64..e1a96dd685 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -4,7 +4,6 @@ import { bumpAggregatedUniqueStat, AppdataProvider, LastBumpStatsData } from 'co import { suppressPunycodeWarning } from 'common/lib/suppress-punycode-warning'; import { StatsGroup, StatsMetric } from 'common/types/stats'; import yargs from 'yargs'; -import { disconnect } from 'cli/lib/pm2-manager'; import { registerCommand as registerAuthLoginCommand } from 'cli/commands/auth/login'; import { registerCommand as registerAuthLogoutCommand } from 'cli/commands/auth/logout'; import { registerCommand as registerAuthStatusCommand } from 'cli/commands/auth/status'; @@ -29,23 +28,6 @@ import { version } from '../package.json'; suppressPunycodeWarning(); -// Handle shutdown message from parent process (Electron app). -// On Windows, child.kill() doesn't send SIGTERM, so we use IPC to notify -// the CLI to clean up (e.g., disconnect from PM2) before terminating. -// Only add this listener when running with IPC channel (from Electron app). -if ( process.send ) { - process.on( 'message', ( message: unknown ) => { - if ( message && typeof message === 'object' && 'type' in message && message.type === 'shutdown' ) { - disconnect(); - process.exit( 0 ); - } - } ); - // Allow the process to exit naturally when the main work is done, - // even though we have a message listener. The IPC channel will be - // cleaned up when the parent terminates. - process.channel?.unref(); -} - const cliAppdataProvider: AppdataProvider< LastBumpStatsData > = { load: readAppdata, lock: lockAppdata, diff --git a/src/index.ts b/src/index.ts index 69542b3b27..18e6c6c832 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,7 +45,6 @@ import { renameLaunchUniquesStat } from 'src/migrations/rename-launch-uniques-st import { startSiteWatcher, stopSiteWatcher } from 'src/modules/cli/lib/execute-site-watch-command'; import { updateWindowsCliVersionedPathIfNeeded } from 'src/modules/cli/lib/windows-installation-manager'; import { setupWPServerFiles, updateWPServerFiles } from 'src/setup-wp-server-files'; -import { stopAllServersOnQuit } from 'src/site-server'; import { loadUserData, lockAppdata, saveUserData, unlockAppdata } from 'src/storage/user-data'; import { setupUpdates } from 'src/updates'; // eslint-disable-next-line import/order @@ -437,7 +436,6 @@ async function appBoot() { } ); app.on( 'quit', () => { - void stopAllServersOnQuit(); stopUserDataWatcher(); stopSiteWatcher(); } ); diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index f23cbfd9f6..1e8c3e9efc 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -122,17 +122,6 @@ export function executeCliCommand( child.unref(); } else { app.on( 'will-quit', () => { - // On Windows, child.kill() immediately terminates the process without sending - // SIGTERM, so signal handlers in the child never run. Use IPC to notify the - // child to clean up (e.g., disconnect from PM2) before terminating. - if ( child.connected ) { - try { - child.send( { type: 'shutdown' } ); - } catch { - // Process may have already exited - } - child.disconnect(); - } child.kill(); } ); } From 8c2e2baa06bd9a8cbb400987ad983d9a9b2f3d6c Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 08:53:50 +0100 Subject: [PATCH 44/71] Revert E2ESession::closeApp implementation --- e2e/e2e-helpers.ts | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 7220c80ffe..7a68d18281 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -44,31 +44,10 @@ export class E2ESession { // Close the app but keep the data for persistence testing async restart() { - await this.closeApp(); + await this.electronApp.close(); await this.launchFirstWindow(); } - private async closeApp() { - console.log( 'closeApp: starting' ); - if ( ! this.electronApp ) { - console.log( 'closeApp: no electronApp' ); - return; - } - - const childProcess = this.electronApp.process(); - console.log( 'closeApp: calling electronApp.close() for pid', childProcess.pid ); - // Cap `ElectronApplication::close` call at 5s to prevent timeout issues on Windows - await new Promise( ( resolve, reject ) => { - Promise.race( [ - this.electronApp.close(), - new Promise( ( resolve ) => setTimeout( resolve, 5000 ) ), - ] ) - .then( resolve ) - .catch( reject ); - } ); - console.log( 'closeApp: close() returned' ); - } - private async launchFirstWindow( testEnv: NodeJS.ProcessEnv = {} ) { const latestBuild = findLatestBuild(); const appInfo = parseElectronApp( latestBuild ); @@ -95,7 +74,7 @@ export class E2ESession { } async cleanup() { - await this.closeApp(); + await this.electronApp.close(); // Removing the `sessionPath` directory has proven to be difficult, especially on Windows. Since // session paths are unique, the WordPress installations are relatively small, and the E2E tests From 8307750c95a936f78e7d50f8e1df2f503afb34f6 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 08:53:59 +0100 Subject: [PATCH 45/71] Temporarily skip app.test.ts --- e2e/app.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/app.test.ts b/e2e/app.test.ts index 188ebc65ea..c071aabbb0 100644 --- a/e2e/app.test.ts +++ b/e2e/app.test.ts @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test'; import { E2ESession } from './e2e-helpers'; import Onboarding from './page-objects/onboarding'; -test.describe( 'Electron app', () => { +test.describe.skip( 'Electron app', () => { const session = new E2ESession(); test.beforeAll( async () => { From 9d7849203cfa0cffb227fabe91abc03115aedd0e Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 09:21:47 +0100 Subject: [PATCH 46/71] Temporarily skip blueprints.test.ts --- e2e/blueprints.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/blueprints.test.ts b/e2e/blueprints.test.ts index 46d1cccf90..d57df7503e 100644 --- a/e2e/blueprints.test.ts +++ b/e2e/blueprints.test.ts @@ -7,7 +7,7 @@ import Onboarding from './page-objects/onboarding'; import SiteContent from './page-objects/site-content'; import { getUrlWithAutoLogin } from './utils'; -test.describe( 'Blueprints', () => { +test.describe.skip( 'Blueprints', () => { const session = new E2ESession(); test.beforeAll( async () => { From 57878b99a58e558e2a7f3f71dc84b6c0f0038d36 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 09:41:54 +0100 Subject: [PATCH 47/71] Revert "Temporarily skip app.test.ts" This reverts commit 8307750c95a936f78e7d50f8e1df2f503afb34f6. --- e2e/app.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/app.test.ts b/e2e/app.test.ts index c071aabbb0..188ebc65ea 100644 --- a/e2e/app.test.ts +++ b/e2e/app.test.ts @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test'; import { E2ESession } from './e2e-helpers'; import Onboarding from './page-objects/onboarding'; -test.describe.skip( 'Electron app', () => { +test.describe( 'Electron app', () => { const session = new E2ESession(); test.beforeAll( async () => { From 0727a04c226449a4aefb687a7cfd71cdbb690cab Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 09:41:56 +0100 Subject: [PATCH 48/71] Revert "Temporarily skip blueprints.test.ts" This reverts commit 9d7849203cfa0cffb227fabe91abc03115aedd0e. --- e2e/blueprints.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/blueprints.test.ts b/e2e/blueprints.test.ts index d57df7503e..46d1cccf90 100644 --- a/e2e/blueprints.test.ts +++ b/e2e/blueprints.test.ts @@ -7,7 +7,7 @@ import Onboarding from './page-objects/onboarding'; import SiteContent from './page-objects/site-content'; import { getUrlWithAutoLogin } from './utils'; -test.describe.skip( 'Blueprints', () => { +test.describe( 'Blueprints', () => { const session = new E2ESession(); test.beforeAll( async () => { From 64e90d86274f24248fbff45f5cec94812b36118d Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 09:42:07 +0100 Subject: [PATCH 49/71] Add logging to Playwright source code --- patches/playwright-core+1.57.0.patch | 64 ++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 patches/playwright-core+1.57.0.patch diff --git a/patches/playwright-core+1.57.0.patch b/patches/playwright-core+1.57.0.patch new file mode 100644 index 0000000000..511b1ca9e2 --- /dev/null +++ b/patches/playwright-core+1.57.0.patch @@ -0,0 +1,64 @@ +diff --git a/node_modules/playwright-core/lib/server/browserContext.js b/node_modules/playwright-core/lib/server/browserContext.js +index f7a5ef2..9bd67a6 100644 +--- a/node_modules/playwright-core/lib/server/browserContext.js ++++ b/node_modules/playwright-core/lib/server/browserContext.js +@@ -394,14 +394,16 @@ if (navigator.serviceWorker) navigator.serviceWorker.register = async () => { co + async _deleteAllDownloads() { + await Promise.all(Array.from(this._downloads).map((download) => download.artifact.deleteOnContextClose())); + } +- async _deleteAllTempDirs() { ++ async _deleteAllTempDirs() { ++ console.log('BrowserContext::_deleteAllTempDirs', this._tempDirs); + await Promise.all(this._tempDirs.map(async (dir) => await import_fs.default.promises.unlink(dir).catch((e) => { + }))); + } + setCustomCloseHandler(handler) { + this._customCloseHandler = handler; + } +- async close(options) { ++ async close(options) { ++ console.log('BrowserContext::close start'); + if (this._closedStatus === "open") { + if (options.reason) + this._closeReason = options.reason; +@@ -415,13 +417,17 @@ if (navigator.serviceWorker) navigator.serviceWorker.register = async () => { co + if (context === this) + promises.push(artifact.finishedPromise()); + } ++ console.log('BrowserContext::close _customCloseHandler start'); + if (this._customCloseHandler) { + await this._customCloseHandler(); + } else { + await this.doClose(options.reason); + } ++ console.log('BrowserContext::close _customCloseHandler resolved'); + promises.push(this._deleteAllDownloads()); ++ console.log('BrowserContext::close deleted all downloads'); + promises.push(this._deleteAllTempDirs()); ++ console.log('BrowserContext::close deleted all temp dirs'); + await Promise.all(promises); + if (!this._customCloseHandler) + this._didCloseInternal(); +diff --git a/node_modules/playwright-core/lib/server/electron/electron.js b/node_modules/playwright-core/lib/server/electron/electron.js +index 3fb4963..54de988 100644 +--- a/node_modules/playwright-core/lib/server/electron/electron.js ++++ b/node_modules/playwright-core/lib/server/electron/electron.js +@@ -75,13 +75,17 @@ class ElectronApplication extends import_instrumentation.SdkObject { + }); + this._nodeSession.on("Runtime.consoleAPICalled", (event) => this._onConsoleAPI(event)); + const appClosePromise = new Promise((f) => this.once(ElectronApplication.Events.Close, f)); +- this._browserContext.setCustomCloseHandler(async () => { ++ this._browserContext.setCustomCloseHandler(async () => { ++ console.log('ElectronApplication customCloseHandler start'); + await this._browserContext.stopVideoRecording(); + const electronHandle = await this._nodeElectronHandlePromise; ++ console.log('ElectronApplication customCloseHandler got electron handle'); + await electronHandle.evaluate(({ app }) => app.quit()).catch(() => { + }); ++ console.log('ElectronApplication customCloseHandler evaluated `app.quit`'); + this._nodeConnection.close(); + await appClosePromise; ++ console.log('ElectronApplication customCloseHandler finished'); + }); + } + static { From 438fcc8dd80d1619e1382afa145cc21cb29ab630 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 10:19:23 +0100 Subject: [PATCH 50/71] pidtree --- e2e/e2e-helpers.ts | 20 ++++++++++++++++++-- package-lock.json | 14 ++++++++++++++ package.json | 1 + 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 7a68d18281..e65b40f31e 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'os'; import path from 'path'; import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers'; import fs from 'fs-extra'; +import pidtree from 'pidtree'; import { _electron as electron, Page, ElectronApplication } from 'playwright'; export class E2ESession { @@ -42,9 +43,24 @@ export class E2ESession { await this.launchFirstWindow( testEnv ); } + async closeApp() { + console.log( 'Closing app...' ); + const process = this.electronApp.process(); + if ( process.pid ) { + const children = pidtree( process.pid ); + console.log( 'process children', children ); + } else { + console.log( 'No process pid' ); + } + + console.log( 'Calling electronApp.close()' ); + await this.electronApp.close(); + console.log( 'electronApp.close() resolved' ); + } + // Close the app but keep the data for persistence testing async restart() { - await this.electronApp.close(); + await this.closeApp(); await this.launchFirstWindow(); } @@ -74,7 +90,7 @@ export class E2ESession { } async cleanup() { - await this.electronApp.close(); + await this.closeApp(); // Removing the `sessionPath` directory has proven to be difficult, especially on Windows. Since // session paths are unique, the WordPress installations are relatively small, and the E2E tests diff --git a/package-lock.json b/package-lock.json index 989f3ce96a..dfbde31a5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -121,6 +121,7 @@ "jest-watch-typeahead": "^3.0.1", "nock": "^13.5.6", "patch-package": "^8.0.0", + "pidtree": "^0.6.0", "postcss": "^8.4.32", "prettier": "npm:wp-prettier@3.0.3", "react": "^18.2.0", @@ -24052,6 +24053,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", diff --git a/package.json b/package.json index 6bb3e72d4a..c8984b478d 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "jest-watch-typeahead": "^3.0.1", "nock": "^13.5.6", "patch-package": "^8.0.0", + "pidtree": "^0.6.0", "postcss": "^8.4.32", "prettier": "npm:wp-prettier@3.0.3", "react": "^18.2.0", From 6b8f3d465c0d6e35bea240ef225400d5f7f5c8d0 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 10:19:30 +0100 Subject: [PATCH 51/71] More playwright logging --- patches/playwright-core+1.57.0.patch | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/patches/playwright-core+1.57.0.patch b/patches/playwright-core+1.57.0.patch index 511b1ca9e2..72c46934fa 100644 --- a/patches/playwright-core+1.57.0.patch +++ b/patches/playwright-core+1.57.0.patch @@ -1,8 +1,25 @@ diff --git a/node_modules/playwright-core/lib/server/browserContext.js b/node_modules/playwright-core/lib/server/browserContext.js -index f7a5ef2..9bd67a6 100644 +index f7a5ef2..87a2764 100644 --- a/node_modules/playwright-core/lib/server/browserContext.js +++ b/node_modules/playwright-core/lib/server/browserContext.js -@@ -394,14 +394,16 @@ if (navigator.serviceWorker) navigator.serviceWorker.register = async () => { co +@@ -181,12 +181,14 @@ if (navigator.serviceWorker) navigator.serviceWorker.register = async () => { co + await this.setStorageState(progress, this._options.storageState, "resetForReuse"); + await page?.resetForReuse(progress); + } +- _browserClosed() { ++ _browserClosed() { ++ console.trace('BrowserContext::_browserClosed') + for (const page of this.pages()) + page._didClose(); + this._didCloseInternal(); + } +- _didCloseInternal() { ++ _didCloseInternal() { ++ console.trace('BrowserContext::_didCloseInternal') + if (this._closedStatus === "closed") { + return; + } +@@ -394,14 +396,16 @@ if (navigator.serviceWorker) navigator.serviceWorker.register = async () => { co async _deleteAllDownloads() { await Promise.all(Array.from(this._downloads).map((download) => download.artifact.deleteOnContextClose())); } @@ -17,11 +34,11 @@ index f7a5ef2..9bd67a6 100644 } - async close(options) { + async close(options) { -+ console.log('BrowserContext::close start'); ++ console.log('BrowserContext::close start', this._closedStatus); if (this._closedStatus === "open") { if (options.reason) this._closeReason = options.reason; -@@ -415,13 +417,17 @@ if (navigator.serviceWorker) navigator.serviceWorker.register = async () => { co +@@ -415,13 +419,17 @@ if (navigator.serviceWorker) navigator.serviceWorker.register = async () => { co if (context === this) promises.push(artifact.finishedPromise()); } From 453fa90333838831dc19c296b24a8ebd2549b048 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 10:46:29 +0100 Subject: [PATCH 52/71] More logging and await pidtree --- e2e/e2e-helpers.ts | 2 +- patches/playwright-core+1.57.0.patch | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index e65b40f31e..e9db893665 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -47,7 +47,7 @@ export class E2ESession { console.log( 'Closing app...' ); const process = this.electronApp.process(); if ( process.pid ) { - const children = pidtree( process.pid ); + const children = await pidtree( process.pid ); console.log( 'process children', children ); } else { console.log( 'No process pid' ); diff --git a/patches/playwright-core+1.57.0.patch b/patches/playwright-core+1.57.0.patch index 72c46934fa..5ee3d900eb 100644 --- a/patches/playwright-core+1.57.0.patch +++ b/patches/playwright-core+1.57.0.patch @@ -79,3 +79,20 @@ index 3fb4963..54de988 100644 }); } static { +diff --git a/node_modules/playwright-core/lib/server/utils/processLauncher.js b/node_modules/playwright-core/lib/server/utils/processLauncher.js +index 954d5e6..df35158 100644 +--- a/node_modules/playwright-core/lib/server/utils/processLauncher.js ++++ b/node_modules/playwright-core/lib/server/utils/processLauncher.js +@@ -149,7 +149,11 @@ async function launchProcess(options) { + let processClosed = false; + let fulfillCleanup = () => { + }; +- const waitForCleanup = new Promise((f) => fulfillCleanup = f); ++ const waitForCleanup = new Promise((f) => fulfillCleanup = f); ++ spawnedProcess.once("exit", (exitCode, signal) => { ++ options.log(`[pid=${spawnedProcess.pid}] `); ++ }); ++ + spawnedProcess.once("close", (exitCode, signal) => { + options.log(`[pid=${spawnedProcess.pid}] `); + processClosed = true; From 2a77010d0350ddfc82e16c68163e870f958f2034 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 11:19:33 +0100 Subject: [PATCH 53/71] pidtree after close --- e2e/e2e-helpers.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index e9db893665..04daf1c103 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -48,7 +48,7 @@ export class E2ESession { const process = this.electronApp.process(); if ( process.pid ) { const children = await pidtree( process.pid ); - console.log( 'process children', children ); + console.log( 'process children before close', children ); } else { console.log( 'No process pid' ); } @@ -56,6 +56,11 @@ export class E2ESession { console.log( 'Calling electronApp.close()' ); await this.electronApp.close(); console.log( 'electronApp.close() resolved' ); + + if ( process.pid ) { + const children = await pidtree( process.pid ); + console.log( 'process children after close', children ); + } } // Close the app but keep the data for persistence testing From 368eec04bf95a8c4879cdf7b9e69e1977fcc6873 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 11:20:26 +0100 Subject: [PATCH 54/71] Remove stdio listeners --- src/modules/cli/lib/execute-command.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 1e8c3e9efc..2947bf5d6e 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -102,6 +102,8 @@ export function executeCliCommand( child.on( 'exit', ( code ) => { capturedExitCode = code; + child.stdout?.removeAllListeners(); + child.stderr?.removeAllListeners(); } ); child.on( 'close', ( code ) => { From 1ad92242e63b3fde59f2a8f2204416c47d6f9088 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 11:37:31 +0100 Subject: [PATCH 55/71] Fix pidtree logging after close --- e2e/e2e-helpers.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 04daf1c103..5e2f0c16dc 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -46,8 +46,10 @@ export class E2ESession { async closeApp() { console.log( 'Closing app...' ); const process = this.electronApp.process(); + let pid: number | undefined; if ( process.pid ) { - const children = await pidtree( process.pid ); + pid = process.pid; + const children = await pidtree( pid ); console.log( 'process children before close', children ); } else { console.log( 'No process pid' ); @@ -57,9 +59,13 @@ export class E2ESession { await this.electronApp.close(); console.log( 'electronApp.close() resolved' ); - if ( process.pid ) { - const children = await pidtree( process.pid ); - console.log( 'process children after close', children ); + if ( pid ) { + try { + const children = await pidtree( pid ); + console.log( 'process children after close', children ); + } catch ( error ) { + console.error( 'pidtree error after close', error ); + } } } From 6a21d369fe556f4f0ed757ebc589732463e396b5 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 11:37:43 +0100 Subject: [PATCH 56/71] destroy stdio streams on exit --- src/modules/cli/lib/execute-command.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 2947bf5d6e..8ee071f993 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -80,7 +80,9 @@ export function executeCliCommand( let stderr = ''; if ( options.output === 'capture' ) { - const logPrefix = options.logPrefix ? `[CLI - ${ options.logPrefix }]` : '[CLI]'; + const logPrefix = options.logPrefix + ? `[CLI - pid ${ child.pid } - site ID ${ options.logPrefix }]` + : '[CLI]'; child.stdout?.on( 'data', ( data: Buffer ) => { const text = data.toString(); stdout += text; @@ -102,8 +104,11 @@ export function executeCliCommand( child.on( 'exit', ( code ) => { capturedExitCode = code; + // Destroy streams immediately on exit to allow 'close' event to fire on Windows child.stdout?.removeAllListeners(); child.stderr?.removeAllListeners(); + child.stdout?.destroy(); + child.stderr?.destroy(); } ); child.on( 'close', ( code ) => { From 37fe3ee6cdf151a778f09dd2f2402c958c060956 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 12:21:01 +0100 Subject: [PATCH 57/71] Disconnect IPC --- src/modules/cli/lib/execute-command.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 8ee071f993..c069678173 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -129,6 +129,9 @@ export function executeCliCommand( child.unref(); } else { app.on( 'will-quit', () => { + if ( child.connected ) { + child.disconnect(); + } child.kill(); } ); } From 42099ce8f84b05874cc555c380f8174d2d84b7f6 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 12:53:37 +0100 Subject: [PATCH 58/71] Try teardown workaround --- e2e/e2e-helpers.ts | 99 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 14 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 5e2f0c16dc..ff288cd6b9 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -1,5 +1,6 @@ +import { ChildProcess, execSync } from 'child_process'; import { randomUUID } from 'crypto'; -import { tmpdir } from 'os'; +import { tmpdir, platform } from 'os'; import path from 'path'; import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers'; import fs from 'fs-extra'; @@ -45,26 +46,96 @@ export class E2ESession { async closeApp() { console.log( 'Closing app...' ); - const process = this.electronApp.process(); - let pid: number | undefined; - if ( process.pid ) { - pid = process.pid; - const children = await pidtree( pid ); - console.log( 'process children before close', children ); + const childProcess = this.electronApp.process(); + const pid = childProcess.pid; + + let childPids: number[] = []; + if ( pid ) { + try { + childPids = await pidtree( pid ); + console.log( 'process children before close', childPids ); + } catch { + // Process may have already exited + } } else { console.log( 'No process pid' ); } - console.log( 'Calling electronApp.close()' ); - await this.electronApp.close(); - console.log( 'electronApp.close() resolved' ); + // On Windows, Playwright's electronApp.close() can hang indefinitely because the 'close' + // event on the spawned process never fires. This happens when CLI child processes inherit + // stdio handles from the shell (cmd.exe) that Playwright uses to spawn Electron on Windows. + // The 'exit' event fires correctly, but 'close' waits for all stdio streams to close, + // which doesn't happen if inherited handles are held by child processes. + // + // Workaround: On Windows, manually trigger app.quit(), wait for the process exit event, + // then kill any remaining child processes instead of using electronApp.close(). + if ( platform() === 'win32' ) { + await this.closeAppWindows( childProcess, childPids ); + } else { + console.log( 'Calling electronApp.close()' ); + await this.electronApp.close(); + console.log( 'electronApp.close() resolved' ); + } if ( pid ) { try { - const children = await pidtree( pid ); - console.log( 'process children after close', children ); - } catch ( error ) { - console.error( 'pidtree error after close', error ); + const remainingChildren = await pidtree( pid ); + console.log( 'process children after close', remainingChildren ); + } catch { + // Expected - parent process should be gone + console.log( 'Parent process terminated (pidtree found no matching pid)' ); + } + } + } + + private async closeAppWindows( childProcess: ChildProcess, childPids: number[] ) { + console.log( 'Using Windows-specific close workaround' ); + + // Create a promise that resolves when the process exits + const exitPromise = new Promise< void >( ( resolve ) => { + childProcess.once( 'exit', ( code, signal ) => { + console.log( `Process exited with code=${ code }, signal=${ signal }` ); + resolve(); + } ); + } ); + + // Trigger app.quit() via Playwright's evaluate + try { + console.log( 'Evaluating app.quit()' ); + await this.electronApp.evaluate( ( { app } ) => app.quit() ); + console.log( 'app.quit() evaluated' ); + } catch ( error ) { + // The connection may close before we get a response, which is expected + console.log( 'app.quit() evaluation completed (connection may have closed)' ); + } + + // Wait for the exit event with a timeout + console.log( 'Waiting for process exit...' ); + const timeoutPromise = new Promise< void >( ( _, reject ) => { + setTimeout( () => reject( new Error( 'Process exit timeout' ) ), 30_000 ); + } ); + + try { + await Promise.race( [ exitPromise, timeoutPromise ] ); + console.log( 'Process exit event received' ); + } catch ( error ) { + console.log( 'Process exit timeout, will force kill' ); + } + + // Kill any remaining child processes + await this.killRemainingProcesses( childPids ); + + console.log( 'Windows close workaround completed' ); + } + + private async killRemainingProcesses( pids: number[] ) { + for ( const pid of pids ) { + try { + // On Windows, use taskkill to forcefully terminate the process + console.log( `Killing remaining process ${ pid }` ); + execSync( `taskkill /pid ${ pid } /f /t`, { stdio: 'ignore' } ); + } catch { + // Process may have already exited, ignore errors } } } From 96f48169723b8f2a0e344b8b9fa4ac6148c2be22 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 13:28:23 +0100 Subject: [PATCH 59/71] Log pid and result in will-quit handler --- src/modules/cli/lib/execute-command.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index c069678173..9a24131d73 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -129,10 +129,12 @@ export function executeCliCommand( child.unref(); } else { app.on( 'will-quit', () => { + const pid = child.pid; if ( child.connected ) { child.disconnect(); } - child.kill(); + const result = child.kill(); + console.log( `Child process with pid ${ pid } killed with result: ${ result }` ); } ); } From 5fd1f894b3de0d6f65826fc7a4ffae6600c7acc9 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 13:55:58 +0100 Subject: [PATCH 60/71] Unregister will-quit handlers --- src/modules/cli/lib/execute-command.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 9a24131d73..b4fa6ddfe2 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -111,8 +111,16 @@ export function executeCliCommand( child.stderr?.destroy(); } ); + function appQuitHandler() { + if ( child.connected ) { + child.disconnect(); + } + child.kill(); + } + child.on( 'close', ( code ) => { child.removeAllListeners(); + app.off( 'will-quit', appQuitHandler ); const exitCode = capturedExitCode ?? code ?? 1; const result: CliCommandResult | undefined = @@ -128,14 +136,7 @@ export function executeCliCommand( if ( options.detached ) { child.unref(); } else { - app.on( 'will-quit', () => { - const pid = child.pid; - if ( child.connected ) { - child.disconnect(); - } - const result = child.kill(); - console.log( `Child process with pid ${ pid } killed with result: ${ result }` ); - } ); + app.on( 'will-quit', appQuitHandler ); } return [ eventEmitter, child ]; From 8c5b80ecda937aa58b0493aec2da9f8642a72f48 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 13:56:39 +0100 Subject: [PATCH 61/71] Bring back logging --- src/modules/cli/lib/execute-command.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index b4fa6ddfe2..f4fd5b35a7 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -112,10 +112,12 @@ export function executeCliCommand( } ); function appQuitHandler() { + const pid = child.pid; if ( child.connected ) { child.disconnect(); } - child.kill(); + const result = child.kill(); + console.log( `Child process with pid ${ pid } killed with result: ${ result }` ); } child.on( 'close', ( code ) => { From 22ca2745d2893e7eae89b7a9f350f5f7801a5cda Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 13:58:23 +0100 Subject: [PATCH 62/71] Stop all servers on quit --- src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index 18e6c6c832..69542b3b27 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,6 +45,7 @@ import { renameLaunchUniquesStat } from 'src/migrations/rename-launch-uniques-st import { startSiteWatcher, stopSiteWatcher } from 'src/modules/cli/lib/execute-site-watch-command'; import { updateWindowsCliVersionedPathIfNeeded } from 'src/modules/cli/lib/windows-installation-manager'; import { setupWPServerFiles, updateWPServerFiles } from 'src/setup-wp-server-files'; +import { stopAllServersOnQuit } from 'src/site-server'; import { loadUserData, lockAppdata, saveUserData, unlockAppdata } from 'src/storage/user-data'; import { setupUpdates } from 'src/updates'; // eslint-disable-next-line import/order @@ -436,6 +437,7 @@ async function appBoot() { } ); app.on( 'quit', () => { + void stopAllServersOnQuit(); stopUserDataWatcher(); stopSiteWatcher(); } ); From cd06620d5da19536574c8526b710006f356c6d7e Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 14:13:50 +0100 Subject: [PATCH 63/71] Undo all Windows hacks in E2ESession --- e2e/e2e-helpers.ts | 100 ++------------------------------------------- 1 file changed, 4 insertions(+), 96 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index ff288cd6b9..4606c55b01 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -1,10 +1,8 @@ -import { ChildProcess, execSync } from 'child_process'; import { randomUUID } from 'crypto'; -import { tmpdir, platform } from 'os'; +import { tmpdir } from 'os'; import path from 'path'; import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers'; import fs from 'fs-extra'; -import pidtree from 'pidtree'; import { _electron as electron, Page, ElectronApplication } from 'playwright'; export class E2ESession { @@ -45,99 +43,9 @@ export class E2ESession { } async closeApp() { - console.log( 'Closing app...' ); - const childProcess = this.electronApp.process(); - const pid = childProcess.pid; - - let childPids: number[] = []; - if ( pid ) { - try { - childPids = await pidtree( pid ); - console.log( 'process children before close', childPids ); - } catch { - // Process may have already exited - } - } else { - console.log( 'No process pid' ); - } - - // On Windows, Playwright's electronApp.close() can hang indefinitely because the 'close' - // event on the spawned process never fires. This happens when CLI child processes inherit - // stdio handles from the shell (cmd.exe) that Playwright uses to spawn Electron on Windows. - // The 'exit' event fires correctly, but 'close' waits for all stdio streams to close, - // which doesn't happen if inherited handles are held by child processes. - // - // Workaround: On Windows, manually trigger app.quit(), wait for the process exit event, - // then kill any remaining child processes instead of using electronApp.close(). - if ( platform() === 'win32' ) { - await this.closeAppWindows( childProcess, childPids ); - } else { - console.log( 'Calling electronApp.close()' ); - await this.electronApp.close(); - console.log( 'electronApp.close() resolved' ); - } - - if ( pid ) { - try { - const remainingChildren = await pidtree( pid ); - console.log( 'process children after close', remainingChildren ); - } catch { - // Expected - parent process should be gone - console.log( 'Parent process terminated (pidtree found no matching pid)' ); - } - } - } - - private async closeAppWindows( childProcess: ChildProcess, childPids: number[] ) { - console.log( 'Using Windows-specific close workaround' ); - - // Create a promise that resolves when the process exits - const exitPromise = new Promise< void >( ( resolve ) => { - childProcess.once( 'exit', ( code, signal ) => { - console.log( `Process exited with code=${ code }, signal=${ signal }` ); - resolve(); - } ); - } ); - - // Trigger app.quit() via Playwright's evaluate - try { - console.log( 'Evaluating app.quit()' ); - await this.electronApp.evaluate( ( { app } ) => app.quit() ); - console.log( 'app.quit() evaluated' ); - } catch ( error ) { - // The connection may close before we get a response, which is expected - console.log( 'app.quit() evaluation completed (connection may have closed)' ); - } - - // Wait for the exit event with a timeout - console.log( 'Waiting for process exit...' ); - const timeoutPromise = new Promise< void >( ( _, reject ) => { - setTimeout( () => reject( new Error( 'Process exit timeout' ) ), 30_000 ); - } ); - - try { - await Promise.race( [ exitPromise, timeoutPromise ] ); - console.log( 'Process exit event received' ); - } catch ( error ) { - console.log( 'Process exit timeout, will force kill' ); - } - - // Kill any remaining child processes - await this.killRemainingProcesses( childPids ); - - console.log( 'Windows close workaround completed' ); - } - - private async killRemainingProcesses( pids: number[] ) { - for ( const pid of pids ) { - try { - // On Windows, use taskkill to forcefully terminate the process - console.log( `Killing remaining process ${ pid }` ); - execSync( `taskkill /pid ${ pid } /f /t`, { stdio: 'ignore' } ); - } catch { - // Process may have already exited, ignore errors - } - } + console.log( 'E2ESession: Closing app...' ); + await this.electronApp.close(); + console.log( 'E2ESession: App closed.' ); } // Close the app but keep the data for persistence testing From a6084b5b9e01a442b7453e846017e409c3f1e3b2 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 15:09:32 +0100 Subject: [PATCH 64/71] Bring back tree-kill --- e2e/e2e-helpers.ts | 44 ++++++++++++++++++++++++++------------- package-lock.json | 25 ++++++++++------------ package.json | 2 +- src/__mocks__/electron.ts | 1 + 4 files changed, 43 insertions(+), 29 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 4606c55b01..af74c2ea7c 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -1,9 +1,13 @@ import { randomUUID } from 'crypto'; -import { tmpdir } from 'os'; +import { tmpdir, platform } from 'os'; import path from 'path'; +import { promisify } from 'util'; import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers'; import fs from 'fs-extra'; import { _electron as electron, Page, ElectronApplication } from 'playwright'; +import kill from 'tree-kill'; + +const killAsync = promisify( kill ); export class E2ESession { electronApp: ElectronApplication; @@ -42,18 +46,20 @@ export class E2ESession { await this.launchFirstWindow( testEnv ); } - async closeApp() { - console.log( 'E2ESession: Closing app...' ); - await this.electronApp.close(); - console.log( 'E2ESession: App closed.' ); - } - - // Close the app but keep the data for persistence testing async restart() { await this.closeApp(); await this.launchFirstWindow(); } + async cleanup() { + await this.closeApp(); + + // Removing the `sessionPath` directory has proven to be difficult, especially on Windows. Since + // session paths are unique, the WordPress installations are relatively small, and the E2E tests + // primarily run in ephemeral CI workers, we've decided to fix this issue by simply not removing + // the `sessionPath` directory. + } + private async launchFirstWindow( testEnv: NodeJS.ProcessEnv = {} ) { const latestBuild = findLatestBuild(); const appInfo = parseElectronApp( latestBuild ); @@ -79,12 +85,22 @@ export class E2ESession { this.mainWindow = await this.electronApp.firstWindow( { timeout: 60_000 } ); } - async cleanup() { - await this.closeApp(); + private async closeApp() { + const pid = this.electronApp.process().pid; - // Removing the `sessionPath` directory has proven to be difficult, especially on Windows. Since - // session paths are unique, the WordPress installations are relatively small, and the E2E tests - // primarily run in ephemeral CI workers, we've decided to fix this issue by simply not removing - // the `sessionPath` directory. + // In Windows CI environments, Playwright's electronApp.close() can hang indefinitely because the + // 'close' event on the spawned process never fires. Because the 'exit' event fires correctly, we + // believe that there's an issue with stdio streams being inherited by child processes that don't + // quit. To expand on this, the difference between the 'exit' and 'close' events is just that: + // 'close' waits for stdio streams to also close. So why don't the children die? We don't know. + // + // Workaround: On Windows, manually trigger app.quit() and kill the process tree instead + // of using electronApp.close(). + if ( platform() === 'win32' && pid ) { + await this.electronApp.evaluate( ( { app } ) => app.quit() ).catch( () => {} ); + await killAsync( pid ); + } else { + await this.electronApp.close(); + } } } diff --git a/package-lock.json b/package-lock.json index dfbde31a5c..c3af8fc428 100644 --- a/package-lock.json +++ b/package-lock.json @@ -121,13 +121,13 @@ "jest-watch-typeahead": "^3.0.1", "nock": "^13.5.6", "patch-package": "^8.0.0", - "pidtree": "^0.6.0", "postcss": "^8.4.32", "prettier": "npm:wp-prettier@3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", "resize-observer-polyfill": "^1.5.1", "tailwindcss": "^3.3.6", + "tree-kill": "^1.2.2", "ts-jest": "^29.4.6", "typescript": "~5.9.3", "typescript-eslint": "^8.50.0", @@ -24053,19 +24053,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -27490,6 +27477,16 @@ "node": ">=18" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", diff --git a/package.json b/package.json index c8984b478d..4f64b34e9b 100644 --- a/package.json +++ b/package.json @@ -92,13 +92,13 @@ "jest-watch-typeahead": "^3.0.1", "nock": "^13.5.6", "patch-package": "^8.0.0", - "pidtree": "^0.6.0", "postcss": "^8.4.32", "prettier": "npm:wp-prettier@3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", "resize-observer-polyfill": "^1.5.1", "tailwindcss": "^3.3.6", + "tree-kill": "^1.2.2", "ts-jest": "^29.4.6", "typescript": "~5.9.3", "typescript-eslint": "^8.50.0", diff --git a/src/__mocks__/electron.ts b/src/__mocks__/electron.ts index 4fccacca5f..93752dff67 100644 --- a/src/__mocks__/electron.ts +++ b/src/__mocks__/electron.ts @@ -15,6 +15,7 @@ export const app = { getPreferredSystemLanguages: jest.fn( () => [ 'en-US' ] ), requestSingleInstanceLock: jest.fn( () => true ), on: jest.fn(), + off: jest.fn(), setAppLogsPath: jest.fn(), setAsDefaultProtocolClient: jest.fn(), enableSandbox: jest.fn(), From 601f39efa6d0350d82383fbff28e50ba1eaf72a6 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 15:13:27 +0100 Subject: [PATCH 65/71] Log stop-all pid and pidtree --- e2e/e2e-helpers.ts | 3 +++ package-lock.json | 14 ++++++++++++++ package.json | 1 + src/site-server.ts | 3 ++- 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index af74c2ea7c..8d25633222 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -4,6 +4,7 @@ import path from 'path'; import { promisify } from 'util'; import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers'; import fs from 'fs-extra'; +import pidtree from 'pidtree'; import { _electron as electron, Page, ElectronApplication } from 'playwright'; import kill from 'tree-kill'; @@ -97,6 +98,8 @@ export class E2ESession { // Workaround: On Windows, manually trigger app.quit() and kill the process tree instead // of using electronApp.close(). if ( platform() === 'win32' && pid ) { + const children = await pidtree( pid ); + console.log( 'Children pids before quitting', children ); await this.electronApp.evaluate( ( { app } ) => app.quit() ).catch( () => {} ); await killAsync( pid ); } else { diff --git a/package-lock.json b/package-lock.json index c3af8fc428..f38c38b68b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -121,6 +121,7 @@ "jest-watch-typeahead": "^3.0.1", "nock": "^13.5.6", "patch-package": "^8.0.0", + "pidtree": "^0.6.0", "postcss": "^8.4.32", "prettier": "npm:wp-prettier@3.0.3", "react": "^18.2.0", @@ -24053,6 +24054,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", diff --git a/package.json b/package.json index 4f64b34e9b..c874c43417 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "jest-watch-typeahead": "^3.0.1", "nock": "^13.5.6", "patch-package": "^8.0.0", + "pidtree": "^0.6.0", "postcss": "^8.4.32", "prettier": "npm:wp-prettier@3.0.3", "react": "^18.2.0", diff --git a/src/site-server.ts b/src/site-server.ts index b2c43e710a..ab3debb95b 100644 --- a/src/site-server.ts +++ b/src/site-server.ts @@ -39,10 +39,11 @@ export async function stopAllServersOnQuit() { // Preserve autoStart so sites will restart on next app launch. // Use silent mode to avoid terminal errors during quit. return new Promise< void >( ( resolve ) => { - const [ emitter ] = executeCliCommand( [ 'site', 'stop-all', '--auto-start' ], { + const [ emitter, childProcess ] = executeCliCommand( [ 'site', 'stop-all', '--auto-start' ], { output: 'ignore', detached: true, } ); + console.log( `Spawned stop-all child process with pid ${ childProcess.pid }` ); emitter.on( 'success', () => resolve() ); emitter.on( 'failure', () => resolve() ); emitter.on( 'error', () => resolve() ); From c10e414a15551e586e44db275a34e98101557c5b Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 15:47:51 +0100 Subject: [PATCH 66/71] Catch errors from tree-kill --- e2e/e2e-helpers.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 8d25633222..7fb7a2f506 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -4,11 +4,10 @@ import path from 'path'; import { promisify } from 'util'; import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers'; import fs from 'fs-extra'; -import pidtree from 'pidtree'; import { _electron as electron, Page, ElectronApplication } from 'playwright'; -import kill from 'tree-kill'; +import treeKill from 'tree-kill'; -const killAsync = promisify( kill ); +const treeKillAsync = promisify( treeKill ); export class E2ESession { electronApp: ElectronApplication; @@ -90,18 +89,19 @@ export class E2ESession { const pid = this.electronApp.process().pid; // In Windows CI environments, Playwright's electronApp.close() can hang indefinitely because the - // 'close' event on the spawned process never fires. Because the 'exit' event fires correctly, we - // believe that there's an issue with stdio streams being inherited by child processes that don't - // quit. To expand on this, the difference between the 'exit' and 'close' events is just that: - // 'close' waits for stdio streams to also close. So why don't the children die? We don't know. + // 'close' event on the spawned process never fires. The 'exit' event (which differs from 'close' + // in that it fires before the stdio stream have closed) fires correctly, which leads us to + // believe that there's an issue with stdio streams being inherited by child processes. // - // Workaround: On Windows, manually trigger app.quit() and kill the process tree instead - // of using electronApp.close(). + // The workaround is to manually trigger `app.quit()` and kill the process tree instead of using + // `electronApp.close()`. if ( platform() === 'win32' && pid ) { - const children = await pidtree( pid ); - console.log( 'Children pids before quitting', children ); await this.electronApp.evaluate( ( { app } ) => app.quit() ).catch( () => {} ); - await killAsync( pid ); + try { + await treeKillAsync( pid ); + } catch ( error ) { + console.error( 'Failed to kill process tree:', error ); + } } else { await this.electronApp.close(); } From d437c1de86f4e03d6603b0122939787a945ef245 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 15:48:01 +0100 Subject: [PATCH 67/71] Remove pidtree --- package-lock.json | 14 -------------- package.json | 1 - 2 files changed, 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index f38c38b68b..c3af8fc428 100644 --- a/package-lock.json +++ b/package-lock.json @@ -121,7 +121,6 @@ "jest-watch-typeahead": "^3.0.1", "nock": "^13.5.6", "patch-package": "^8.0.0", - "pidtree": "^0.6.0", "postcss": "^8.4.32", "prettier": "npm:wp-prettier@3.0.3", "react": "^18.2.0", @@ -24054,19 +24053,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", diff --git a/package.json b/package.json index c874c43417..4f64b34e9b 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,6 @@ "jest-watch-typeahead": "^3.0.1", "nock": "^13.5.6", "patch-package": "^8.0.0", - "pidtree": "^0.6.0", "postcss": "^8.4.32", "prettier": "npm:wp-prettier@3.0.3", "react": "^18.2.0", From 60906ffae6614abe24ae9bfa29ed6006974ac69f Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 9 Jan 2026 16:19:55 +0100 Subject: [PATCH 68/71] No IPC channel in stopAllServersOnQuit --- src/modules/cli/lib/cli-server-process.ts | 2 +- src/modules/cli/lib/cli-site-creator.ts | 2 +- src/modules/cli/lib/execute-command.ts | 19 ++++++++++--------- .../cli/lib/execute-site-watch-command.ts | 2 +- src/site-server.ts | 5 ++--- src/tests/execute-wp-cli.test.ts | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/modules/cli/lib/cli-server-process.ts b/src/modules/cli/lib/cli-server-process.ts index 3529bc3131..dbb00123cf 100644 --- a/src/modules/cli/lib/cli-server-process.ts +++ b/src/modules/cli/lib/cli-server-process.ts @@ -29,7 +29,7 @@ export class CliServerProcess implements WordPressServerProcess { return new Promise( ( resolve, reject ) => { const [ emitter ] = executeCliCommand( [ 'site', 'start', '--path', this.sitePath, '--skip-browser' ], - { output: 'capture', logPrefix: this.siteId } + { mode: 'capture-stdio', logPrefix: this.siteId } ); emitter.on( 'data', ( { data } ) => { diff --git a/src/modules/cli/lib/cli-site-creator.ts b/src/modules/cli/lib/cli-site-creator.ts index 3212aed334..453db4adf9 100644 --- a/src/modules/cli/lib/cli-site-creator.ts +++ b/src/modules/cli/lib/cli-site-creator.ts @@ -52,7 +52,7 @@ export async function createSiteViaCli( options: CreateSiteOptions ): Promise< C const result: Partial< CreateSiteResult > = {}; let lastErrorMessage: string | null = null; - const [ emitter ] = executeCliCommand( args, { output: 'capture', logPrefix: siteId } ); + const [ emitter ] = executeCliCommand( args, { mode: 'capture-stdio', logPrefix: siteId } ); emitter.on( 'data', ( { data } ) => { const parsed = cliEventSchema.safeParse( data ); diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index f4fd5b35a7..7cab6d2056 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -41,27 +41,28 @@ export interface ExecuteCliCommandOptions { * - 'ignore': ignore stdout/stderr completely * - 'capture': capture stdout/stderr, available in success/failure events */ - output: 'ignore' | 'capture'; - detached?: boolean; + mode: 'ignore-stdio' | 'capture-stdio' | 'detached'; logPrefix?: string; } export function executeCliCommand( args: string[], - options: ExecuteCliCommandOptions = { output: 'ignore', detached: false } + options: ExecuteCliCommandOptions = { mode: 'ignore-stdio' } ): [ CliCommandEventEmitter, ChildProcess ] { const cliPath = getCliPath(); let stdio: StdioOptions | undefined; - if ( options.output === 'capture' ) { + if ( options.mode === 'capture-stdio' ) { stdio = [ 'ignore', 'pipe', 'pipe', 'ipc' ]; - } else if ( options.output === 'ignore' ) { + } else if ( options.mode === 'ignore-stdio' ) { stdio = [ 'ignore', 'ignore', 'ignore', 'ipc' ]; + } else if ( options.mode === 'detached' ) { + stdio = [ 'ignore', 'ignore', 'ignore', 'ignore' ]; } const child = fork( cliPath, [ ...args, '--avoid-telemetry' ], { stdio, - detached: options.detached, + detached: options.mode === 'detached', execPath: getBundledNodeBinaryPath(), } ); const eventEmitter = new CliCommandEventEmitter(); @@ -79,7 +80,7 @@ export function executeCliCommand( let stdout = ''; let stderr = ''; - if ( options.output === 'capture' ) { + if ( options.mode === 'capture-stdio' ) { const logPrefix = options.logPrefix ? `[CLI - pid ${ child.pid } - site ID ${ options.logPrefix }]` : '[CLI]'; @@ -126,7 +127,7 @@ export function executeCliCommand( const exitCode = capturedExitCode ?? code ?? 1; const result: CliCommandResult | undefined = - options.output === 'capture' ? { stdout, stderr, exitCode } : undefined; + options.mode === 'capture-stdio' ? { stdout, stderr, exitCode } : undefined; if ( exitCode === 0 ) { eventEmitter.emit( 'success', { result } ); @@ -135,7 +136,7 @@ export function executeCliCommand( } } ); - if ( options.detached ) { + if ( options.mode === 'detached' ) { child.unref(); } else { app.on( 'will-quit', appQuitHandler ); diff --git a/src/modules/cli/lib/execute-site-watch-command.ts b/src/modules/cli/lib/execute-site-watch-command.ts index 2b12d4ba50..2463fd4b60 100644 --- a/src/modules/cli/lib/execute-site-watch-command.ts +++ b/src/modules/cli/lib/execute-site-watch-command.ts @@ -71,7 +71,7 @@ export async function startSiteWatcher(): Promise< void > { } watcher = executeCliCommand( [ 'site', 'list', '--watch', '--format', 'json' ], { - output: 'ignore', + mode: 'ignore-stdio', } ); const [ eventEmitter ] = watcher; diff --git a/src/site-server.ts b/src/site-server.ts index ab3debb95b..f9483d9ee7 100644 --- a/src/site-server.ts +++ b/src/site-server.ts @@ -40,8 +40,7 @@ export async function stopAllServersOnQuit() { // Use silent mode to avoid terminal errors during quit. return new Promise< void >( ( resolve ) => { const [ emitter, childProcess ] = executeCliCommand( [ 'site', 'stop-all', '--auto-start' ], { - output: 'ignore', - detached: true, + mode: 'detached', } ); console.log( `Spawned stop-all child process with pid ${ childProcess.pid }` ); emitter.on( 'success', () => resolve() ); @@ -350,7 +349,7 @@ export class SiteServer { return new Promise< WpCliResult >( ( resolve ) => { const [ emitter, childProcess ] = executeCliCommand( cliArgs, { - output: 'capture', + mode: 'capture-stdio', logPrefix: this.details.id, } ); diff --git a/src/tests/execute-wp-cli.test.ts b/src/tests/execute-wp-cli.test.ts index b82dc2991f..98e30368e9 100644 --- a/src/tests/execute-wp-cli.test.ts +++ b/src/tests/execute-wp-cli.test.ts @@ -92,7 +92,7 @@ describe( 'SiteServer.executeWpCliCommand', () => { const timeout = isImportExport ? 200 : 100; return new Promise( ( resolve ) => { - const [ emitter ] = executeCliCommand( cliArgs, { output: 'capture' } ); + const [ emitter ] = executeCliCommand( cliArgs, { mode: 'capture-stdio' } ); const timeoutId = setTimeout( () => { resolve( { From 31b5091e9d7d5fa6719c3a0ff8a9a59463c2ec32 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Sun, 11 Jan 2026 15:56:35 +0100 Subject: [PATCH 69/71] Update playwright-core patch to test theory --- patches/playwright-core+1.57.0.patch | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/patches/playwright-core+1.57.0.patch b/patches/playwright-core+1.57.0.patch index 5ee3d900eb..7bead39c29 100644 --- a/patches/playwright-core+1.57.0.patch +++ b/patches/playwright-core+1.57.0.patch @@ -57,7 +57,7 @@ index f7a5ef2..87a2764 100644 if (!this._customCloseHandler) this._didCloseInternal(); diff --git a/node_modules/playwright-core/lib/server/electron/electron.js b/node_modules/playwright-core/lib/server/electron/electron.js -index 3fb4963..54de988 100644 +index 3fb4963..f5890b4 100644 --- a/node_modules/playwright-core/lib/server/electron/electron.js +++ b/node_modules/playwright-core/lib/server/electron/electron.js @@ -75,13 +75,17 @@ class ElectronApplication extends import_instrumentation.SdkObject { @@ -79,6 +79,14 @@ index 3fb4963..54de988 100644 }); } static { +@@ -157,7 +161,6 @@ class Electron extends import_instrumentation.SdkObject { + } + let shell = false; + if (process.platform === "win32") { +- shell = true; + command = `"${command}"`; + electronArguments = electronArguments.map((arg) => `"${arg}"`); + } diff --git a/node_modules/playwright-core/lib/server/utils/processLauncher.js b/node_modules/playwright-core/lib/server/utils/processLauncher.js index 954d5e6..df35158 100644 --- a/node_modules/playwright-core/lib/server/utils/processLauncher.js From a64720d92e27b465a6c0baff8e0ef283b547eb29 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Sun, 11 Jan 2026 16:19:56 +0100 Subject: [PATCH 70/71] Fix patch --- patches/playwright-core+1.57.0.patch | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/patches/playwright-core+1.57.0.patch b/patches/playwright-core+1.57.0.patch index 7bead39c29..a467445a93 100644 --- a/patches/playwright-core+1.57.0.patch +++ b/patches/playwright-core+1.57.0.patch @@ -57,7 +57,7 @@ index f7a5ef2..87a2764 100644 if (!this._customCloseHandler) this._didCloseInternal(); diff --git a/node_modules/playwright-core/lib/server/electron/electron.js b/node_modules/playwright-core/lib/server/electron/electron.js -index 3fb4963..f5890b4 100644 +index 3fb4963..6cd3d49 100644 --- a/node_modules/playwright-core/lib/server/electron/electron.js +++ b/node_modules/playwright-core/lib/server/electron/electron.js @@ -75,13 +75,17 @@ class ElectronApplication extends import_instrumentation.SdkObject { @@ -79,14 +79,18 @@ index 3fb4963..f5890b4 100644 }); } static { -@@ -157,7 +161,6 @@ class Electron extends import_instrumentation.SdkObject { +@@ -156,11 +160,6 @@ class Electron extends import_instrumentation.SdkObject { + electronArguments.unshift("-r", require.resolve("./loader")); } let shell = false; - if (process.platform === "win32") { +- if (process.platform === "win32") { - shell = true; - command = `"${command}"`; - electronArguments = electronArguments.map((arg) => `"${arg}"`); - } +- command = `"${command}"`; +- electronArguments = electronArguments.map((arg) => `"${arg}"`); +- } + delete env.NODE_OPTIONS; + const { launchedProcess, gracefullyClose, kill } = await (0, import_processLauncher.launchProcess)({ + command, diff --git a/node_modules/playwright-core/lib/server/utils/processLauncher.js b/node_modules/playwright-core/lib/server/utils/processLauncher.js index 954d5e6..df35158 100644 --- a/node_modules/playwright-core/lib/server/utils/processLauncher.js From b49fddd752b2d6aa6f97a2d59986a9b15f6af9fd Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Sun, 11 Jan 2026 20:31:13 +0100 Subject: [PATCH 71/71] `spawn` over `fork` --- src/modules/cli/lib/execute-command.ts | 32 +++++++++++++++++--------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 7cab6d2056..02f3c14757 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -1,5 +1,5 @@ import { app } from 'electron'; -import { fork, ChildProcess, StdioOptions } from 'node:child_process'; +import { fork, spawn, ChildProcess, StdioOptions } from 'node:child_process'; import EventEmitter from 'node:events'; import * as Sentry from '@sentry/electron/main'; import { getBundledNodeBinaryPath, getCliPath } from 'src/storage/paths'; @@ -56,15 +56,23 @@ export function executeCliCommand( stdio = [ 'ignore', 'pipe', 'pipe', 'ipc' ]; } else if ( options.mode === 'ignore-stdio' ) { stdio = [ 'ignore', 'ignore', 'ignore', 'ipc' ]; - } else if ( options.mode === 'detached' ) { - stdio = [ 'ignore', 'ignore', 'ignore', 'ignore' ]; } - const child = fork( cliPath, [ ...args, '--avoid-telemetry' ], { - stdio, - detached: options.mode === 'detached', - execPath: getBundledNodeBinaryPath(), - } ); + let child: ChildProcess; + + if ( options.mode === 'detached' ) { + child = spawn( getBundledNodeBinaryPath(), [ cliPath, ...args, '--avoid-telemetry' ], { + detached: options.mode === 'detached', + stdio: 'ignore', + windowsHide: true, + } ); + } else { + child = fork( cliPath, [ ...args, '--avoid-telemetry' ], { + stdio, + execPath: getBundledNodeBinaryPath(), + } ); + } + const eventEmitter = new CliCommandEventEmitter(); child.on( 'spawn', () => { @@ -97,9 +105,11 @@ export function executeCliCommand( } ); } - child.on( 'message', ( message: unknown ) => { - eventEmitter.emit( 'data', { data: message } ); - } ); + if ( options.mode !== 'detached' ) { + child.on( 'message', ( message: unknown ) => { + eventEmitter.emit( 'data', { data: message } ); + } ); + } let capturedExitCode: number | null = null;