Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
2743dc8
Abort async operations on SIGTERM/SIGINT
fredrikekelund Dec 18, 2025
d34ee1e
Also send abort message to child server
fredrikekelund Dec 18, 2025
9a31e58
Only create AbortController when topic is not `abort`
fredrikekelund Dec 18, 2025
61f3256
Fix
fredrikekelund Dec 18, 2025
f81ba0c
More fixes
fredrikekelund Dec 18, 2025
bd6b4d9
Tweaks
fredrikekelund Dec 18, 2025
eb32b3b
Merge branch 'dev/studio-cli-i2' into f26d/cli-abort-async-operations
fredrikekelund Dec 18, 2025
a6a7e7a
Fix unit tests
fredrikekelund Dec 18, 2025
27bd5c3
Merge branch 'dev/studio-cli-i2' into f26d/cli-abort-async-operations
fredrikekelund Dec 19, 2025
569058f
Fix types
fredrikekelund Dec 19, 2025
8786163
Remove `this.sessionPath` files individually
fredrikekelund Dec 19, 2025
abbcfc8
Stop running servers in a detached process
fredrikekelund Dec 19, 2025
fa0537e
Revert "Remove `this.sessionPath` files individually"
fredrikekelund Dec 19, 2025
c984234
Retry
fredrikekelund Dec 19, 2025
87e62ec
Increase timeouts
fredrikekelund Dec 19, 2025
d42d25e
Fix deprecated blueprint syntax
fredrikekelund Dec 19, 2025
3f9ef30
Merge branch 'dev/studio-cli-i2' into f26d/fix-e2e-tests-windows
fredrikekelund Dec 19, 2025
10955cf
Increase timeout
fredrikekelund Dec 19, 2025
fc4fa56
Try adding a small delay
fredrikekelund Dec 19, 2025
814d4ae
Kill `site list --watch` on SIGINT
fredrikekelund Dec 19, 2025
96e03aa
Create main window after creating site watcher
fredrikekelund Dec 19, 2025
ef932ca
Try using async fs method for cleanup
fredrikekelund Dec 22, 2025
0354512
Try rimraf (which has advanced retry strategies)
fredrikekelund Dec 22, 2025
1511151
Merge branch 'dev/studio-cli-i2' into f26d/fix-e2e-tests-windows
fredrikekelund Jan 2, 2026
71c5d1f
Merge branch 'dev/studio-cli-i2' into f26d/fix-e2e-tests-windows
fredrikekelund Jan 7, 2026
07fc679
New approach: don't remove `sessionPath` dir
fredrikekelund Jan 7, 2026
055d14c
Unused import
fredrikekelund Jan 7, 2026
0d0dc26
Force close app
fredrikekelund Jan 7, 2026
bc83d93
disconnect from pm2 in response to SIGTERM
fredrikekelund Jan 8, 2026
8264bbe
Revert user data watcher changes from #2313
fredrikekelund Jan 8, 2026
1c0cfda
Wait for running button
fredrikekelund Jan 8, 2026
e12e4ad
Use Electron's will-quit event in `execute-command.ts`
fredrikekelund Jan 8, 2026
21b2e77
SIGINT and SIGTERM listeners in `wp` command
fredrikekelund Jan 8, 2026
6355215
More SIGINT and SIGTERM listeners in `wp` command
fredrikekelund Jan 8, 2026
22bdd58
Merge branch 'dev/studio-cli-i2' into f26d/fix-e2e-tests-windows
fredrikekelund Jan 8, 2026
0d16ae5
Temporarily skip blueprints test
fredrikekelund Jan 8, 2026
d64e35a
Revert "Temporarily skip blueprints test"
fredrikekelund Jan 8, 2026
07eaa56
Try with force kill again
fredrikekelund Jan 8, 2026
1534b2c
Logging
fredrikekelund Jan 8, 2026
f75e007
New approach to waiting for app close
fredrikekelund Jan 8, 2026
bc41e38
Logging again
fredrikekelund Jan 8, 2026
f99f4b0
Try to make all child processes detached
fredrikekelund Jan 8, 2026
3596bac
Experiment
fredrikekelund Jan 8, 2026
4b7364a
Revert "Experiment"
fredrikekelund Jan 8, 2026
c0ab642
Revert "Try to make all child processes detached"
fredrikekelund Jan 8, 2026
1682341
Try a 5s timeout for closing the app
fredrikekelund Jan 8, 2026
e2faab8
Experiment with removing stopAllServersOnQuit
fredrikekelund Jan 8, 2026
9d56d0c
shutdown message
bcotrim Jan 8, 2026
ed7991a
Revert "shutdown message"
fredrikekelund Jan 9, 2026
8c2e2ba
Revert E2ESession::closeApp implementation
fredrikekelund Jan 9, 2026
8307750
Temporarily skip app.test.ts
fredrikekelund Jan 9, 2026
9d78492
Temporarily skip blueprints.test.ts
fredrikekelund Jan 9, 2026
57878b9
Revert "Temporarily skip app.test.ts"
fredrikekelund Jan 9, 2026
0727a04
Revert "Temporarily skip blueprints.test.ts"
fredrikekelund Jan 9, 2026
64e90d8
Add logging to Playwright source code
fredrikekelund Jan 9, 2026
438fcc8
pidtree
fredrikekelund Jan 9, 2026
6b8f3d4
More playwright logging
fredrikekelund Jan 9, 2026
453fa90
More logging and await pidtree
fredrikekelund Jan 9, 2026
2a77010
pidtree after close
fredrikekelund Jan 9, 2026
368eec0
Remove stdio listeners
fredrikekelund Jan 9, 2026
1ad9224
Fix pidtree logging after close
fredrikekelund Jan 9, 2026
6a21d36
destroy stdio streams on exit
fredrikekelund Jan 9, 2026
37fe3ee
Disconnect IPC
fredrikekelund Jan 9, 2026
42099ce
Try teardown workaround
fredrikekelund Jan 9, 2026
96f4816
Log pid and result in will-quit handler
fredrikekelund Jan 9, 2026
5fd1f89
Unregister will-quit handlers
fredrikekelund Jan 9, 2026
8c5b80e
Bring back logging
fredrikekelund Jan 9, 2026
22ca274
Stop all servers on quit
fredrikekelund Jan 9, 2026
cd06620
Undo all Windows hacks in E2ESession
fredrikekelund Jan 9, 2026
a6084b5
Bring back tree-kill
fredrikekelund Jan 9, 2026
601f39e
Log stop-all pid and pidtree
fredrikekelund Jan 9, 2026
c10e414
Catch errors from tree-kill
fredrikekelund Jan 9, 2026
d437c1d
Remove pidtree
fredrikekelund Jan 9, 2026
60906ff
No IPC channel in stopAllServersOnQuit
fredrikekelund Jan 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cli/commands/site/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ export async function runCommand( format: 'table' | 'json', watch: boolean ): Pr
},
{ debounceMs: 500 }
);

process.on( 'SIGINT', disconnect );
Comment on lines +129 to +130
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The studio site list --watch command would previously not interrupt in response to Ctrl+C

process.on( 'SIGTERM', disconnect );
}
} finally {
if ( ! watch ) {
Expand Down
6 changes: 6 additions & 0 deletions cli/commands/wp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -40,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,
Expand Down
7 changes: 2 additions & 5 deletions cli/lib/wordpress-server-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }`;
Expand Down
14 changes: 7 additions & 7 deletions e2e/blueprints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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' );
Expand Down Expand Up @@ -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' );
Expand Down Expand Up @@ -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' );
Expand Down Expand Up @@ -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' );
Expand Down Expand Up @@ -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' );
Expand Down Expand Up @@ -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' );
Expand Down
73 changes: 42 additions & 31 deletions e2e/e2e-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { randomUUID } from 'crypto';
import { tmpdir } from 'os';
import { tmpdir, platform } from 'os';
import path from 'path';
import { promisify } from 'util';
import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers';
import fs from 'fs-extra';
import { _electron as electron, Page, ElectronApplication } from 'playwright';
import treeKill from 'tree-kill';

const treeKillAsync = promisify( treeKill );

export class E2ESession {
electronApp: ElectronApplication;
Expand Down Expand Up @@ -39,40 +43,30 @@ 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' );
}
async restart() {
await this.closeApp();
await this.launchFirstWindow();
}

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,
} );
this.mainWindow = await this.electronApp.firstWindow( { timeout: 60_000 } );
async cleanup() {
await this.closeApp();

// Removing the `sessionPath` directory has proven to be difficult, especially on Windows. Since
// session paths are unique, the WordPress installations are relatively small, and the E2E tests
// primarily run in ephemeral CI workers, we've decided to fix this issue by simply not removing
// the `sessionPath` directory.
}

// 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' );
}

Expand All @@ -81,6 +75,7 @@ export class E2ESession {
executablePath,
env: {
...process.env,
...testEnv,
E2E: 'true',
E2E_APP_DATA_PATH: this.appDataPath,
E2E_HOME_PATH: this.homePath,
Expand All @@ -90,9 +85,25 @@ export class E2ESession {
this.mainWindow = await this.electronApp.firstWindow( { timeout: 60_000 } );
}

async cleanup() {
await this.electronApp?.close();
// Clean up temporary folder to hold application data
fs.rmSync( this.sessionPath, { recursive: true, force: true } );
private async closeApp() {
const pid = this.electronApp.process().pid;

// In Windows CI environments, Playwright's electronApp.close() can hang indefinitely because the
// 'close' event on the spawned process never fires. The 'exit' event (which differs from 'close'
// in that it fires before the stdio stream have closed) fires correctly, which leads us to
// believe that there's an issue with stdio streams being inherited by child processes.
//
// The workaround is to manually trigger `app.quit()` and kill the process tree instead of using
// `electronApp.close()`.
if ( platform() === 'win32' && pid ) {
await this.electronApp.evaluate( ( { app } ) => app.quit() ).catch( () => {} );
try {
await treeKillAsync( pid );
} catch ( error ) {
console.error( 'Failed to kill process tree:', error );
}
} else {
await this.electronApp.close();
}
}
}
4 changes: 2 additions & 2 deletions e2e/fixtures/blueprints/activate-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"steps": [
{
"step": "installPlugin",
"pluginZipFile": {
"pluginData": {
"resource": "wordpress.org/plugins",
"slug": "hello-dolly"
}
Expand All @@ -14,4 +14,4 @@
"pluginPath": "hello-dolly/hello.php"
}
]
}
}
4 changes: 2 additions & 2 deletions e2e/fixtures/blueprints/activate-theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"steps": [
{
"step": "installTheme",
"themeZipFile": {
"themeData": {
"resource": "wordpress.org/themes",
"slug": "twentytwentyone"
}
Expand All @@ -14,4 +14,4 @@
"themeFolderName": "twentytwentyone"
}
]
}
}
4 changes: 2 additions & 2 deletions e2e/fixtures/blueprints/install-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
"steps": [
{
"step": "installPlugin",
"pluginZipFile": {
"pluginData": {
"resource": "wordpress.org/plugins",
"slug": "akismet"
}
}
]
}
}
4 changes: 2 additions & 2 deletions e2e/fixtures/blueprints/install-theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
"steps": [
{
"step": "installTheme",
"themeZipFile": {
"themeData": {
"resource": "wordpress.org/themes",
"slug": "twentytwentytwo"
}
}
]
}
}
2 changes: 1 addition & 1 deletion e2e/localization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion e2e/site-navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
Expand Down
Loading