77 consumeAPI ,
88 consumeAPISync ,
99 exposeAPI ,
10- sandboxedSpawnHandlerFactory ,
1110} from '@php-wasm/universal' ;
12- import { sprintf } from '@php-wasm/util' ;
11+ import { createSpawnHandler , sprintf } from '@php-wasm/util' ;
1312import { RecommendedPHPVersion } from '@wp-playground/common' ;
1413import {
1514 type WordPressInstallMode ,
@@ -21,6 +20,8 @@ import { jspi } from 'wasm-feature-detect';
2120import { MessageChannel , type MessagePort , parentPort } from 'worker_threads' ;
2221import { mountResources } from '../mounts' ;
2322import { logger } from '@php-wasm/logger' ;
23+ import { spawn } from 'child_process' ;
24+ import { type SpawnedWorker , spawnWorkerThread } from '../run-cli' ;
2425
2526export interface Mount {
2627 hostPath : string ;
@@ -126,23 +127,24 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
126127 }
127128 }
128129
129- async bootAndSetUpInitialWorker ( {
130- siteUrl,
131- mountsBeforeWpInstall,
132- mountsAfterWpInstall,
133- phpVersion : php = RecommendedPHPVersion ,
134- wordpressInstallMode,
135- wordPressZip,
136- sqliteIntegrationPluginZip,
137- firstProcessId,
138- processIdSpaceLength,
139- dataSqlPath,
140- followSymlinks,
141- trace,
142- internalCookieStore,
143- withXdebug,
144- nativeInternalDirPath,
145- } : PrimaryWorkerBootOptions ) {
130+ async bootAndSetUpInitialWorker ( options : PrimaryWorkerBootOptions ) {
131+ const {
132+ siteUrl,
133+ mountsBeforeWpInstall,
134+ mountsAfterWpInstall,
135+ phpVersion : php = RecommendedPHPVersion ,
136+ wordpressInstallMode,
137+ wordPressZip,
138+ sqliteIntegrationPluginZip,
139+ firstProcessId,
140+ processIdSpaceLength,
141+ dataSqlPath,
142+ followSymlinks,
143+ trace,
144+ internalCookieStore,
145+ withXdebug,
146+ nativeInternalDirPath,
147+ } = options ;
146148 if ( this . booted ) {
147149 throw new Error ( 'Playground already booted' ) ;
148150 }
@@ -192,7 +194,7 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
192194 ? new File (
193195 [ sqliteIntegrationPluginZip ] ,
194196 'sqlite-integration-plugin.zip'
195- )
197+ )
196198 : undefined ,
197199 sapiName : 'cli' ,
198200 createFiles : {
@@ -207,7 +209,75 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
207209 } ,
208210 cookieStore : internalCookieStore ? undefined : false ,
209211 dataSqlPath,
210- spawnHandler : sandboxedSpawnHandlerFactory ,
212+ spawnHandler : ( ) =>
213+ createSpawnHandler ( async ( args , processApi , options ) => {
214+ console . log ( 'primary worker' , { args } ) ;
215+ processApi . notifySpawn ( ) ;
216+ if ( args [ 0 ] === 'exec' ) {
217+ args . shift ( ) ;
218+ }
219+
220+ if (
221+ args [ 0 ] . endsWith ( '.php' ) ||
222+ args [ 0 ] . endsWith ( '.phar' )
223+ ) {
224+ args . unshift ( 'php' ) ;
225+ }
226+
227+ const binaryName = args [ 0 ] . split ( '/' ) . pop ( ) ;
228+ if ( binaryName !== 'php' ) {
229+ throw new Error (
230+ `Unsupported binary: ${ binaryName } . Only PHP is supported for now.`
231+ ) ;
232+ }
233+
234+ const newPhpProcess = spawn ( process . argv [ 0 ] , args , {
235+ ...options ,
236+ stdio : [ 'pipe' , 'pipe' , 'pipe' ] ,
237+ } ) ;
238+ const subPhpPort = await new Promise < MessagePort > (
239+ ( resolve , reject ) => {
240+ newPhpProcess . addListener (
241+ 'message' ,
242+ ( message : any ) => {
243+ if (
244+ message . command ===
245+ 'worker-script-initialized'
246+ ) {
247+ resolve ( message . phpPort ) ;
248+ }
249+ }
250+ ) ;
251+ newPhpProcess . once ( 'error' , ( e : Error ) => {
252+ reject (
253+ new Error (
254+ `Worker failed to initialize: ${ e . message } `
255+ )
256+ ) ;
257+ } ) ;
258+ }
259+ ) ;
260+
261+ const handler =
262+ consumeAPI < PlaygroundCliBlueprintV1Worker > (
263+ subPhpPort
264+ ) ;
265+ handler . useFileLockManager ( this . fileLockManager as any ) ;
266+ await handler . bootWorker ( {
267+ phpVersion : php ,
268+ siteUrl,
269+ mountsBeforeWpInstall,
270+ mountsAfterWpInstall,
271+ firstProcessId,
272+ processIdSpaceLength,
273+ followSymlinks,
274+ trace,
275+ nativeInternalDirPath,
276+ } ) ;
277+
278+ handler . cli ( [ 'php' , '-v' ] ) ;
279+ console . log ( await handler . hello ( ) ) ;
280+ } ) ,
211281 async onPHPInstanceCreated ( php ) {
212282 await mountResources ( php , mountsBeforeWpInstall ) ;
213283 if ( wordpressBooted ) {
@@ -234,6 +304,10 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
234304 }
235305 }
236306
307+ async hello ( ) {
308+ return 'hello' ;
309+ }
310+
237311 async bootWorker ( args : WorkerBootOptions ) {
238312 await this . bootRequestHandler ( args ) ;
239313 }
@@ -291,7 +365,89 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
291365 } ,
292366 sapiName : 'cli' ,
293367 cookieStore : false ,
294- spawnHandler : sandboxedSpawnHandlerFactory ,
368+ spawnHandler : ( ) =>
369+ createSpawnHandler ( async ( args , processApi ) => {
370+ console . log ( 'secondary worker' , { args } ) ;
371+ if ( args [ 0 ] === 'exec' ) {
372+ args . shift ( ) ;
373+ }
374+
375+ if (
376+ args [ 0 ] . endsWith ( '.php' ) ||
377+ args [ 0 ] . endsWith ( '.phar' )
378+ ) {
379+ args . unshift ( 'php' ) ;
380+ }
381+
382+ const binaryName = args [ 0 ] . split ( '/' ) . pop ( ) ;
383+ if ( binaryName !== 'php' ) {
384+ throw new Error (
385+ `Unsupported binary: ${ binaryName } . Only PHP is supported for now.`
386+ ) ;
387+ }
388+
389+ let cliCalled = false ;
390+ let spawnedWorker : SpawnedWorker | undefined =
391+ undefined ;
392+ try {
393+ spawnedWorker = await spawnWorkerThread ( 'v1' , {
394+ onExit : ( ) => {
395+ if ( cliCalled ) {
396+ // We're already handling the exit code using
397+ // the cliResponse.exitCode promise.
398+ return ;
399+ }
400+ // The process died before we could call cli().
401+ // Let's exit with an error code.
402+ processApi . exit ( 1 ) ;
403+ } ,
404+ } ) ;
405+ } catch ( e ) {
406+ processApi . exit ( 1 ) ;
407+ throw e ;
408+ }
409+
410+ const handler =
411+ consumeAPI < PlaygroundCliBlueprintV1Worker > (
412+ spawnedWorker . phpPort
413+ ) ;
414+ handler . useFileLockManager ( this . fileLockManager as any ) ;
415+ await handler . bootWorker ( {
416+ phpVersion : phpVersion ,
417+ siteUrl,
418+ mountsBeforeWpInstall,
419+ mountsAfterWpInstall,
420+ firstProcessId,
421+ processIdSpaceLength,
422+ followSymlinks,
423+ trace,
424+ nativeInternalDirPath,
425+ } ) ;
426+
427+ processApi . notifySpawn ( ) ;
428+
429+ const cliResponse = await handler . cli ( args , {
430+ env : process . env as Record < string , string > ,
431+ } ) ;
432+ cliResponse . stdout . pipeTo (
433+ new WritableStream ( {
434+ write ( chunk ) {
435+ processApi . stdout ( chunk ) ;
436+ } ,
437+ } )
438+ ) ;
439+ cliResponse . stderr . pipeTo (
440+ new WritableStream ( {
441+ write ( chunk ) {
442+ processApi . stderr ( chunk ) ;
443+ } ,
444+ } )
445+ ) ;
446+ await cliResponse . exitCode . finally ( async ( ) => {
447+ processApi . exit ( await cliResponse . exitCode ) ;
448+ } ) ;
449+ cliCalled = true ;
450+ } ) ,
295451 } ) ;
296452 this . __internal_setRequestHandler ( requestHandler ) ;
297453
@@ -330,3 +486,5 @@ parentPort?.postMessage(
330486 } ,
331487 [ phpChannel . port2 as any ]
332488) ;
489+
490+ console . log ( 'Worker script initialized!' ) ;
0 commit comments