Skip to content

Commit 848a3be

Browse files
committed
Replace no-op spawn handler with native child_process.spawn bridge
The createNoopSpawnHandler unconditionally replaced PHP process spawning with a handler that immediately exits with code 1, silently breaking proc_open, exec, shell_exec, popen, and system calls. This replaces it with a handler that bridges to Node.js child_process.spawn, which php-wasm's setSpawnHandler already supports directly. Studio is a local development tool where PHP already has host filesystem access via createNodeFsMountHandler mounts, so process spawning is consistent with the existing trust model. Fixes: #3044 See also: #2519
1 parent c9ed8c1 commit 848a3be

1 file changed

Lines changed: 6 additions & 19 deletions

File tree

apps/cli/lib/run-wp-cli-command.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { spawn } from 'node:child_process';
12
import { rootCertificates } from 'node:tls';
23
import { loadNodeRuntime, createNodeFsMountHandler } from '@php-wasm/node';
34
import {
@@ -6,8 +7,8 @@ import {
67
PHP,
78
setPhpIniEntries,
89
ProcessIdAllocator,
10+
type SpawnHandler,
911
} from '@php-wasm/universal';
10-
import { createSpawnHandler } from '@php-wasm/util';
1112
import { IS_JSPI_AVAILABLE } from '@studio/common/lib/jspi';
1213
import { cleanupLegacyMuPlugins, getMuPlugins } from '@studio/common/lib/mu-plugins';
1314
import { LatestSupportedPHPVersion } from '@studio/common/types/php-versions';
@@ -18,22 +19,8 @@ import { getSqliteCommandPath, getWpCliPharPath } from 'cli/lib/server-files';
1819
const processIdAllocator = new ProcessIdAllocator();
1920
const PLAYGROUND_INTERNAL_SHARED_FOLDER = '/internal/shared';
2021

21-
/**
22-
* Creates a no-op spawn handler that immediately exits with code 1.
23-
* This allows process spawning functions (proc_open, exec, etc.) to be called
24-
* without crashing, but they will fail gracefully. WP-CLI detects these failures
25-
* and falls back to single-threaded mode.
26-
*
27-
* The timeout before exit is required by the createSpawnHandler API — PHP needs
28-
* an event loop tick to set up its stream listeners after proc_open() returns.
29-
* Without it, the process exits before PHP registers its handlers and
30-
* createSpawnHandler throws a "exited synchronously" error.
31-
*/
32-
function createNoopSpawnHandler() {
33-
return createSpawnHandler( async ( args, processApi ) => {
34-
await new Promise( ( resolve ) => setTimeout( resolve, 1 ) );
35-
processApi.exit( 1 );
36-
} );
22+
function createNativeSpawnHandler(): SpawnHandler {
23+
return spawn as unknown as SpawnHandler;
3724
}
3825

3926
export interface RunWpCliCommandOptions {
@@ -77,7 +64,7 @@ export async function runWpCliCommand(
7764
allow_url_fopen: 1,
7865
} );
7966

80-
await php.setSpawnHandler( createNoopSpawnHandler() );
67+
await php.setSpawnHandler( createNativeSpawnHandler() );
8168

8269
await cleanupLegacyMuPlugins( siteFolder );
8370

@@ -137,7 +124,7 @@ export async function runGlobalWpCliCommand( args: string[] ): Promise< Disposab
137124
allow_url_fopen: 1,
138125
} );
139126

140-
await php.setSpawnHandler( createNoopSpawnHandler() );
127+
await php.setSpawnHandler( createNativeSpawnHandler() );
141128

142129
await php.mount( '/tmp/wp-cli.phar', createNodeFsMountHandler( getWpCliPharPath() ) );
143130

0 commit comments

Comments
 (0)