@@ -21,6 +21,8 @@ import { jspi } from 'wasm-feature-detect';
2121import { MessageChannel , type MessagePort , parentPort } from 'worker_threads' ;
2222import { mountResources } from '../mounts' ;
2323import { logger } from '@php-wasm/logger' ;
24+ import { spawnWorkerThread } from '../run-cli' ;
25+
2426import type { Mount } from '@php-wasm/cli-util' ;
2527
2628export type WorkerBootOptions = {
@@ -122,31 +124,22 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
122124 }
123125 }
124126
125- async bootAndSetUpInitialWorker ( {
126- siteUrl,
127- mountsBeforeWpInstall,
128- mountsAfterWpInstall,
129- phpVersion : php = RecommendedPHPVersion ,
130- wordpressInstallMode,
131- wordPressZip,
132- sqliteIntegrationPluginZip,
133- firstProcessId,
134- processIdSpaceLength,
135- dataSqlPath,
136- followSymlinks,
137- trace,
138- internalCookieStore,
139- withXdebug,
140- nativeInternalDirPath,
141- } : PrimaryWorkerBootOptions ) {
127+ async bootAndSetUpInitialWorker ( options : PrimaryWorkerBootOptions ) {
128+ const {
129+ siteUrl,
130+ mountsBeforeWpInstall,
131+ mountsAfterWpInstall,
132+ wordpressInstallMode,
133+ wordPressZip,
134+ sqliteIntegrationPluginZip,
135+ dataSqlPath,
136+ internalCookieStore,
137+ } = options ;
142138 if ( this . booted ) {
143139 throw new Error ( 'Playground already booted' ) ;
144140 }
145141 this . booted = true ;
146142
147- let nextProcessId = firstProcessId ;
148- const lastProcessId = firstProcessId + processIdSpaceLength - 1 ;
149-
150143 try {
151144 const constants : Record < string , string | number | boolean | null > =
152145 {
@@ -157,28 +150,10 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
157150 let wordpressBooted = false ;
158151 const requestHandler = await bootWordPressAndRequestHandler ( {
159152 siteUrl,
160- createPhpRuntime : async ( ) => {
161- const processId = nextProcessId ;
162-
163- if ( nextProcessId < lastProcessId ) {
164- nextProcessId ++ ;
165- } else {
166- // We've reached the end of the process ID space. Start over.
167- nextProcessId = firstProcessId ;
168- }
169-
170- return await loadNodeRuntime ( php , {
171- emscriptenOptions : {
172- fileLockManager : this . fileLockManager ! ,
173- processId,
174- trace : trace ? tracePhpWasm : undefined ,
175- phpWasmInitOptions : { nativeInternalDirPath } ,
176- } ,
177- followSymlinks,
178- withXdebug,
179- } ) ;
180- } ,
181- maxPhpInstances : 1 ,
153+ createPhpRuntime : createPhpRuntimeFactory (
154+ options ,
155+ this . fileLockManager !
156+ ) ,
182157 wordpressInstallMode,
183158 wordPressZip :
184159 wordPressZip !== undefined
@@ -204,7 +179,10 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
204179 } ,
205180 cookieStore : internalCookieStore ? undefined : false ,
206181 dataSqlPath,
207- spawnHandler : sandboxedSpawnHandlerFactory ,
182+ spawnHandler : ( ) =>
183+ sandboxedSpawnHandlerFactory ( ( ) =>
184+ createPHPWorker ( options , this . fileLockManager ! )
185+ ) ,
208186 async onPHPInstanceCreated ( php ) {
209187 await mountResources ( php , mountsBeforeWpInstall ) ;
210188 if ( wordpressBooted ) {
@@ -231,65 +209,37 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
231209 }
232210 }
233211
212+ async hello ( ) {
213+ return 'hello' ;
214+ }
215+
234216 async bootWorker ( args : WorkerBootOptions ) {
235217 await this . bootRequestHandler ( args ) ;
236218 }
237219
238- async bootRequestHandler ( {
239- siteUrl,
240- followSymlinks,
241- phpVersion,
242- firstProcessId,
243- processIdSpaceLength,
244- trace,
245- nativeInternalDirPath,
246- mountsBeforeWpInstall,
247- mountsAfterWpInstall,
248- withXdebug,
249- } : WorkerBootRequestHandlerOptions ) {
220+ async bootRequestHandler ( options : WorkerBootRequestHandlerOptions ) {
250221 if ( this . booted ) {
251222 throw new Error ( 'Playground already booted' ) ;
252223 }
253224 this . booted = true ;
254225
255- let nextProcessId = firstProcessId ;
256- const lastProcessId = firstProcessId + processIdSpaceLength - 1 ;
257-
258226 try {
259227 const requestHandler = await bootRequestHandler ( {
260- siteUrl,
261- createPhpRuntime : async ( ) => {
262- const processId = nextProcessId ;
263-
264- if ( nextProcessId < lastProcessId ) {
265- nextProcessId ++ ;
266- } else {
267- // We've reached the end of the process ID space. Start over.
268- nextProcessId = firstProcessId ;
269- }
270-
271- return await loadNodeRuntime ( phpVersion , {
272- emscriptenOptions : {
273- fileLockManager : this . fileLockManager ! ,
274- processId,
275- trace : trace ? tracePhpWasm : undefined ,
276- ENV : {
277- DOCROOT : '/wordpress' ,
278- } ,
279- phpWasmInitOptions : { nativeInternalDirPath } ,
280- } ,
281- followSymlinks,
282- withXdebug,
283- } ) ;
284- } ,
285- maxPhpInstances : 1 ,
228+ siteUrl : options . siteUrl ,
229+ createPhpRuntime : createPhpRuntimeFactory (
230+ options ,
231+ this . fileLockManager !
232+ ) ,
286233 onPHPInstanceCreated : async ( php ) => {
287- await mountResources ( php , mountsBeforeWpInstall ) ;
288- await mountResources ( php , mountsAfterWpInstall ) ;
234+ await mountResources ( php , options . mountsBeforeWpInstall ) ;
235+ await mountResources ( php , options . mountsAfterWpInstall ) ;
289236 } ,
290237 sapiName : 'cli' ,
291238 cookieStore : false ,
292- spawnHandler : sandboxedSpawnHandlerFactory ,
239+ spawnHandler : ( ) =>
240+ sandboxedSpawnHandlerFactory ( ( ) =>
241+ createPHPWorker ( options , this . fileLockManager ! )
242+ ) ,
293243 } ) ;
294244 this . __internal_setRequestHandler ( requestHandler ) ;
295245
@@ -309,6 +259,91 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
309259 }
310260}
311261
262+ /**
263+ * Returns a factory function that starts a new PHP runtime in the currently
264+ * running process. This is used for rotating the PHP runtime periodically.
265+ */
266+ function createPhpRuntimeFactory (
267+ options : WorkerBootRequestHandlerOptions ,
268+ fileLockManager : FileLockManager | RemoteAPI < FileLockManager >
269+ ) {
270+ let nextProcessId = options . firstProcessId ;
271+ const lastProcessId =
272+ options . firstProcessId + options . processIdSpaceLength - 1 ;
273+ return async ( ) => {
274+ const processId = nextProcessId ;
275+
276+ if ( nextProcessId < lastProcessId ) {
277+ nextProcessId ++ ;
278+ } else {
279+ // We've reached the end of the process ID space. Start over.
280+ nextProcessId = options . firstProcessId ;
281+ }
282+
283+ return await loadNodeRuntime (
284+ options . phpVersion || RecommendedPHPVersion ,
285+ {
286+ emscriptenOptions : {
287+ fileLockManager,
288+ processId,
289+ trace : options . trace ? tracePhpWasm : undefined ,
290+ phpWasmInitOptions : {
291+ nativeInternalDirPath : options . nativeInternalDirPath ,
292+ } ,
293+ } ,
294+ followSymlinks : options . followSymlinks ,
295+ withXdebug : options . withXdebug ,
296+ }
297+ ) ;
298+ } ;
299+ }
300+
301+ /**
302+ * Spawns a new PHP process to be used in the PHP spawn handler (in proc_open() etc. calls).
303+ * It boots from this worker-thread-v1.ts file, but is a separate process.
304+ *
305+ * We explicitly avoid using PHPProcessManager.acquirePHPInstance() here.
306+ *
307+ * Why?
308+ *
309+ * Because each PHP instance acquires actual OS-level file locks via fcntl() and LockFileEx()
310+ * syscalls. Running multiple PHP instances from the same OS process would allow them to
311+ * acquire overlapping locks. Running every PHP instance in a separate OS process ensures
312+ * any locks that overlap between PHP instances conflict with each other as expected.
313+ *
314+ * @param options - The options for the worker.
315+ * @param fileLockManager - The file lock manager to use.
316+ * @returns A promise that resolves to the PHP worker.
317+ */
318+ async function createPHPWorker (
319+ options : WorkerBootRequestHandlerOptions ,
320+ fileLockManager : FileLockManager | RemoteAPI < FileLockManager >
321+ ) {
322+ const spawnedWorker = await spawnWorkerThread ( 'v1' ) ;
323+
324+ const handler = consumeAPI < PlaygroundCliBlueprintV1Worker > (
325+ spawnedWorker . phpPort
326+ ) ;
327+ handler . useFileLockManager ( fileLockManager as any ) ;
328+ await handler . bootWorker ( options ) ;
329+
330+ return {
331+ php : handler ,
332+ reap : ( ) => {
333+ try {
334+ handler . dispose ( ) ;
335+ } catch {
336+ /** */
337+ }
338+ try {
339+ spawnedWorker . worker . terminate ( ) ;
340+ } catch {
341+ /** */
342+ }
343+ } ,
344+ } ;
345+ }
346+
312347process . on ( 'unhandledRejection' , ( e : any ) => {
313348 logger . error ( 'Unhandled rejection:' , e ) ;
314349} ) ;
0 commit comments