Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e9db4d4
Add native PHP worker pool POC
bcotrim May 22, 2026
630d1db
Track the request queue per worker
fredrikekelund May 27, 2026
98bd09b
Potential fix for pull request finding 'CodeQL / Information exposure…
bcotrim May 28, 2026
ce185f3
Merge branch 'trunk' into add-native-php-worker-pool-poc
fredrikekelund May 29, 2026
daa7d8a
Always spawn the php pool the "poor man's" way
fredrikekelund May 29, 2026
96ed96a
Merge branch 'trunk' into add-native-php-worker-pool-poc
fredrikekelund May 29, 2026
8258fbd
Merge branch 'trunk' into add-native-php-worker-pool-poc
fredrikekelund May 29, 2026
7700985
Improve grandchild process cleanup on Windows
fredrikekelund Jun 1, 2026
fd3df18
Unit test
fredrikekelund Jun 1, 2026
f04bfdd
Merge branch 'trunk' into add-native-php-worker-pool-poc
fredrikekelund Jun 1, 2026
ea71db1
Refactor native PHP child helpers
bcotrim Jun 1, 2026
a2fc287
Merge remote PR branch
bcotrim Jun 1, 2026
9cc6be7
Improved E2E cleanup retry behavior
fredrikekelund Jun 1, 2026
71c6125
Improved native PHP child process cleanup
fredrikekelund Jun 1, 2026
4b06c8b
Reap wp, import, export CLI command children
fredrikekelund Jun 1, 2026
f54e74f
Recursively kill php children in happy path
fredrikekelund Jun 1, 2026
23f47bd
Kill grandchildren in stopPhpChild
fredrikekelund Jun 1, 2026
45e903b
Move files around
fredrikekelund Jun 1, 2026
b4f5cd1
Consider that OPcache is included in binary in PHP >=8.5
fredrikekelund Jun 1, 2026
7c0e893
Tweaking
fredrikekelund Jun 1, 2026
625fb8b
Fix Windows e2e cleanup retry for native PHP
bcotrim Jun 1, 2026
6a9bf7f
Pin e2e locale to English
bcotrim Jun 1, 2026
40b2bb5
Collect all process logs when E2E tests fail
fredrikekelund Jun 1, 2026
797dc28
Stabilize Windows native PHP e2e cleanup
bcotrim Jun 1, 2026
cd64c4a
Merge remote-tracking branch 'origin/add-native-php-worker-pool-poc' …
bcotrim Jun 1, 2026
d9bbd0c
Merge remote-tracking branch 'origin/trunk' into add-native-php-worke…
bcotrim Jun 1, 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
21 changes: 19 additions & 2 deletions .buildkite/commands/run-e2e-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,15 @@ if [ "$PLATFORM" = "linux" ]; then
# right-edge content (e.g. the preferences Save button) below the fold
# for the split-pane settings layout. 1920x1080 matches a typical
# desktop and avoids relying on scroll-into-view.
inner_exit=0
xvfb-run -a -s "-screen 0 1920x1080x24" \
npx playwright test --max-failures=1 --output=/tmp/test-results
npx playwright test --max-failures=1 --output=/tmp/test-results || inner_exit=$?
# On failure, collect daemon logs into /tmp/test-results (copied to the
# artifact dir below). $HOME here is the node user that ran the tests.
if [ "$inner_exit" -ne 0 ] && [ -d "$HOME/.studio/daemon/logs" ]; then
cp -r "$HOME/.studio/daemon/logs" /tmp/test-results/daemon-logs || true
fi
exit "$inner_exit"
' || test_exit=$?

if [ -d /tmp/test-results ]; then
Expand All @@ -168,5 +175,15 @@ else
npx playwright install

echo 'Running Playwright tests...'
npx playwright test
# Capture the exit code so a failure doesn't trip `set -e` before we collect
# the daemon logs (~/.studio/daemon/logs) for artifact upload.
test_exit=0
npx playwright test || test_exit=$?

if [ "$test_exit" -ne 0 ] && [ -d "$HOME/.studio/daemon/logs" ]; then
mkdir -p test-results/daemon-logs
cp -r "$HOME/.studio/daemon/logs/." test-results/daemon-logs/ || true
fi

exit "$test_exit"
fi
7 changes: 7 additions & 0 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ e2e_config: &e2e_config
- test-results/**/*.zip
- test-results/**/*.png
- test-results/**/*error-context.md
- test-results/daemon-logs/**/*.log
plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN]
agents:
queue: "{{matrix.platform}}"
Expand Down Expand Up @@ -101,6 +102,10 @@ steps:
STUDIO_RUNTIME: native-php
if: (build.branch == 'trunk' || build.tag =~ /^v[0-9]+/ || !build.pull_request.draft) && build.pull_request.labels includes 'native-php'

# Linux E2E tests run on the shared `default` queue inside a Debian Node
# container — the same pattern the Linux build/unit-test steps use. Kept as
# a separate step (rather than another matrix entry on *e2e_config) because
# Mac and Windows share queue/plugin defaults that Linux doesn't.
# Linux E2E tests run on the shared `default` queue inside a Debian Node
# container — the same pattern the Linux build/unit-test steps use. Kept as
# a separate step (rather than another matrix entry on *e2e_config) because
Expand All @@ -119,6 +124,7 @@ steps:
- test-results/**/*.zip
- test-results/**/*.png
- test-results/**/*error-context.md
- test-results/daemon-logs/**/*.log
env:
DEBUG: "pw:browser"
# TEMP(rsm-2593): if: removed to force E2E on every push while iterating on Linux E2E setup. Revert before merge.
Expand All @@ -140,6 +146,7 @@ steps:
- test-results/**/*.zip
- test-results/**/*.png
- test-results/**/*error-context.md
- test-results/daemon-logs/**/*.log
env:
DEBUG: "pw:browser"
STUDIO_RUNTIME: native-php
Expand Down
31 changes: 21 additions & 10 deletions apps/cli/commands/wp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client';
import { getPhpBinaryPath, getWpCliPharPath } from 'cli/lib/dependency-management/paths';
import { ensurePhpBinaryAvailable } from 'cli/lib/dependency-management/php-binary';
import { getSiteRuntime } from 'cli/lib/feature-flags';
import { getDefaultPhpArgs } from 'cli/lib/native-php';
import { getDefaultPhpArgs } from 'cli/lib/native-php/config';
import { DETACH_FOR_GROUP_KILL, reapPhpTreeOnInterrupt } from 'cli/lib/native-php/php-process';
import { runWpCliCommand, runGlobalWpCliCommand, WpCliResponse } from 'cli/lib/run-wp-cli-command';
import { validatePhpVersion } from 'cli/lib/utils';
import { isServerRunning, sendWpCliCommand } from 'cli/lib/wordpress-server-manager';
Expand Down Expand Up @@ -58,18 +59,28 @@ async function runNativePhpWpCliCommand( site: SiteData, args: string[] ): Promi
{
cwd: site.path,
stdio: 'inherit',
detached: DETACH_FOR_GROUP_KILL,
}
);

const { code, signal } = await new Promise< {
code: number | null;
signal: NodeJS.Signals | null;
} >( ( resolve, reject ) => {
child.once( 'error', reject );
child.once( 'exit', ( exitCode, exitSignal ) =>
resolve( { code: exitCode, signal: exitSignal } )
);
} );
// Reap php.exe and any subprocess it spawned if this command is interrupted before the child exits.
const removeReaper = reapPhpTreeOnInterrupt( child );

let code: number | null;
let signal: NodeJS.Signals | null;
try {
( { code, signal } = await new Promise< {
code: number | null;
signal: NodeJS.Signals | null;
} >( ( resolve, reject ) => {
child.once( 'error', reject );
child.once( 'exit', ( exitCode, exitSignal ) =>
resolve( { code: exitCode, signal: exitSignal } )
);
} ) );
} finally {
removeReaper();
}

if ( signal ) {
process.kill( process.pid, signal );
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/lib/dependency-management/php-binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
type PhpBinaryDownloadInfo,
type NativePhpSupportedVersion,
} from '@studio/common/lib/php-binary-metadata';
import { ensureNativePhpIniFiles } from '../native-php';
import { ensureNativePhpIniFiles } from 'cli/lib/native-php/config';
import { getPhpBinaryPath } from './paths';
import type { SupportedPHPVersion } from '@studio/common/types/php-versions';

Expand Down
89 changes: 89 additions & 0 deletions apps/cli/lib/native-php/blueprints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import fs from 'node:fs';
import path from 'node:path';
import { getBlueprintsPharPath } from 'cli/lib/dependency-management/paths';
import { runPhpCommand } from './php-process';
import type { NativePhpSupportedVersion } from '@studio/common/lib/php-binary-metadata';
import type { ServerConfig } from 'cli/lib/types/wordpress-server-ipc';

export async function runBlueprint(
config: ServerConfig,
blueprint: NonNullable< ServerConfig[ 'blueprint' ] >,
phpVersion: NativePhpSupportedVersion,
signal: AbortSignal
): Promise< void > {
// blueprints.phar accepts local paths only; remote URIs need the Playground runtime.
if ( blueprint.uri.startsWith( 'http://' ) || blueprint.uri.startsWith( 'https://' ) ) {
throw new Error(
`Remote blueprint URIs are not supported by the native PHP runtime: ${ blueprint.uri }`
);
}

const enableDebugLog = config.enableDebugLog ?? false;
const enableDebugDisplay = config.enableDebugDisplay ?? false;
const defaultConstants: Record< string, boolean | string > = {
// The SQLite driver requires a non-empty DB_NAME at runtime.
DB_NAME: 'wordpress',
WP_DEBUG: enableDebugLog || enableDebugDisplay,
WP_DEBUG_LOG: enableDebugLog,
WP_DEBUG_DISPLAY: enableDebugDisplay,
};

blueprint.contents.constants = {
...blueprint.contents.constants,
...defaultConstants,
};
// Native PHP selects PHP and installs WordPress before Blueprint execution.
// Passing preferredVersions makes blueprints.phar validate versions it does not manage here.
delete blueprint.contents.preferredVersions;

const blueprintDir = path.dirname( blueprint.uri );
const tmpPath = path.join( blueprintDir, `studio-blueprint-${ config.siteId }.json` );
await fs.promises.writeFile( tmpPath, JSON.stringify( blueprint.contents ) );

// blueprints.phar detects SQLite under plugins, while Studio installs it under mu-plugins.
const muPluginsSqlite = path.join(
config.sitePath,
'wp-content',
'mu-plugins',
'sqlite-database-integration'
);
const pluginsSqlite = path.join(
config.sitePath,
'wp-content',
'plugins',
'sqlite-database-integration'
);
const needsSymlink = fs.existsSync( muPluginsSqlite ) && ! fs.existsSync( pluginsSqlite );
let symlinkIno: number | undefined;
if ( needsSymlink ) {
fs.symlinkSync( muPluginsSqlite, pluginsSqlite, 'junction' );
// Remove only the entry created here, not unrelated content that replaced it.
symlinkIno = fs.statSync( pluginsSqlite ).ino;
}

try {
await runPhpCommand(
[
getBlueprintsPharPath(),
'exec',
tmpPath,
'--mode=apply-to-existing-site',
`--site-path=${ config.sitePath }`,
`--site-url=${ config.absoluteUrl ?? `http://localhost:${ config.port }` }`,
'--db-engine=sqlite',
],
{ phpVersion, signal }
);
} finally {
await fs.promises.unlink( tmpPath ).catch( () => {} );
if ( needsSymlink ) {
try {
if ( fs.statSync( pluginsSqlite ).ino === symlinkIno ) {
await fs.promises.rm( pluginsSqlite, { recursive: true, force: true } );
}
} catch {
// Best effort - leaving the symlink behind is non-fatal.
}
}
}
}
10 changes: 8 additions & 2 deletions apps/cli/lib/native-php.ts → apps/cli/lib/native-php/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import os from 'os';
import path from 'path';
import { NativePhpSupportedVersion } from '@studio/common/lib/php-binary-metadata';
import { writeFile } from 'atomically';
import { getPhpBinaryPath } from './dependency-management/paths';
import semver from 'semver';
import { getPhpBinaryPath } from '../dependency-management/paths';

// Disabled by default to shrink the attack surface available to PHP code
// running inside a Studio site. Each entry falls into one of:
Expand Down Expand Up @@ -151,9 +152,14 @@ export function getNativePhpIniContents( phpVersion: NativePhpSupportedVersion )
if ( process.platform === 'win32' ) {
directives.push(
`extension_dir="${ toPhpIniPath( getExtensionDir( phpVersion ) ) }"`,
'zend_extension=opcache',
...WINDOWS_PHP_EXTENSIONS.map( ( extension ) => `extension=${ extension }` )
);

const coercedVersion = semver.coerce( phpVersion );
// As of PHP 8.5, the OPcache extension is always bundled with PHP
if ( coercedVersion && semver.lt( coercedVersion, '8.5.0' ) ) {
directives.push( 'zend_extension=opcache' );
}
}

return `${ directives.join( os.EOL ) }${ os.EOL }`;
Expand Down
Loading