Skip to content

Commit 983842a

Browse files
committed
Merge trunk into decouple-request-handler-and-PHPProcessManager
2 parents a31422f + 947ed76 commit 983842a

File tree

7 files changed

+196
-138
lines changed

7 files changed

+196
-138
lines changed

packages/php-wasm/node/src/test/php-part-1.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1850,7 +1850,11 @@ describe('sandboxedSpawnHandlerFactory', () => {
18501850
'Hello, world!'
18511851
);
18521852
await php.setSpawnHandler(
1853-
sandboxedSpawnHandlerFactory(processManager)
1853+
sandboxedSpawnHandlerFactory(() =>
1854+
processManager.acquirePHPInstance({
1855+
considerPrimary: false,
1856+
})
1857+
)
18541858
);
18551859
return php;
18561860
},

packages/php-wasm/node/src/test/php-part-2.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1180,7 +1180,11 @@ describe('sandboxedSpawnHandlerFactory', () => {
11801180
'Hello, world!'
11811181
);
11821182
await php.setSpawnHandler(
1183-
sandboxedSpawnHandlerFactory(processManager)
1183+
sandboxedSpawnHandlerFactory(() =>
1184+
processManager.acquirePHPInstance({
1185+
considerPrimary: false,
1186+
})
1187+
)
11841188
);
11851189
return php;
11861190
},

packages/php-wasm/universal/src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,4 @@ export { sandboxedSpawnHandlerFactory } from './sandboxed-spawn-handler-factory'
9797

9898
export * from './api';
9999
export type { WithAPIState as WithIsReady } from './api';
100+
export type { Remote } from './comlink-sync';

packages/php-wasm/universal/src/lib/sandboxed-spawn-handler-factory.ts

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createSpawnHandler } from '@php-wasm/util';
2-
import type { PHPInstanceManager } from './php-instance-manager';
3-
import { logger } from '@php-wasm/logger';
2+
import type { PHP } from './php';
3+
import type { PHPWorker } from './php-worker';
4+
import type { Remote } from './comlink-sync';
45

56
/**
67
* An isomorphic proc_open() handler that implements typical shell in TypeScript
@@ -13,7 +14,10 @@ import { logger } from '@php-wasm/logger';
1314
* parser.
1415
*/
1516
export function sandboxedSpawnHandlerFactory(
16-
instanceManager?: PHPInstanceManager
17+
getPHPInstance: () => Promise<{
18+
php: PHP | Remote<PHPWorker>;
19+
reap: () => void;
20+
}>
1721
) {
1822
return createSpawnHandler(async function (args, processApi, options) {
1923
processApi.notifySpawn();
@@ -64,25 +68,14 @@ export function sandboxedSpawnHandlerFactory(
6468
return;
6569
}
6670

67-
if (!instanceManager) {
68-
// 127 is the exit code "for command not found".
69-
processApi.exit(127);
70-
logger.warn(
71-
'sandboxedSpawnHandlerFactory tried to spawn a PHP subprocess but was called without an instance manager'
72-
);
73-
return;
74-
}
75-
76-
const { php, reap } = await instanceManager.acquirePHPInstance({
77-
considerPrimary: false,
78-
});
71+
const { php, reap } = await getPHPInstance();
7972

8073
try {
8174
if (options.cwd) {
82-
php.chdir(options.cwd as string);
75+
await php.chdir(options.cwd as string);
8376
}
8477

85-
const cwd = php.cwd();
78+
const cwd = await php.cwd();
8679
switch (binaryName) {
8780
case 'php': {
8881
// Figure out more about setting env, putenv(), etc.
@@ -115,7 +108,7 @@ export function sandboxedSpawnHandlerFactory(
115108
break;
116109
}
117110
case 'ls': {
118-
const files = php.listFiles(args[1] ?? cwd);
111+
const files = await php.listFiles(args[1] ?? cwd);
119112
for (const file of files) {
120113
processApi.stdout(file + '\n');
121114
}

packages/playground/cli/src/blueprints-v1/worker-thread-v1.ts

Lines changed: 122 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { jspi } from 'wasm-feature-detect';
2121
import { MessageChannel, type MessagePort, parentPort } from 'worker_threads';
2222
import { mountResources } from '../mounts';
2323
import { logger } from '@php-wasm/logger';
24+
import { spawnWorkerThread } from '../run-cli';
25+
2426
import type { Mount } from '@php-wasm/cli-util';
2527

2628
export 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+
312347
process.on('unhandledRejection', (e: any) => {
313348
logger.error('Unhandled rejection:', e);
314349
});

0 commit comments

Comments
 (0)