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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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(); } ); }