Skip to content

Commit 74e5631

Browse files
committed
WIP: Share /wordpress and /internal FS between PHP processes
1 parent 5b6d927 commit 74e5631

File tree

9 files changed

+196
-84
lines changed

9 files changed

+196
-84
lines changed

packages/php-wasm/compile/php/php_wasm.c

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ EM_JS(int, wasm_poll_socket, (php_socket_t socketd, int events, int timeout), {
241241
if (stream.stream_ops?.poll) {
242242
mask = stream.stream_ops.poll(stream, -1);
243243
}
244-
244+
245245
mask &= events | POLLERR | POLLHUP;
246246
if (mask) {
247247
return mask;
@@ -418,7 +418,7 @@ EM_JS(__wasi_errno_t, js_fd_read, (__wasi_fd_t fd, const __wasi_iovec_t *iov, si
418418
HEAPU32[pnum >> 2] = num;
419419
return wakeUp(returnCode);
420420
}
421-
421+
422422
// It's a blocking stream and we Blocking stream with no data available yet.
423423
// Let's poll up to a timeout.
424424
await new Promise(resolve => setTimeout(resolve, interval));
@@ -1026,8 +1026,8 @@ int main(int argc, char *argv[]);
10261026
int run_cli()
10271027
{
10281028
// See wasm_sapi_request_init() for details on why we need to redirect stdout and stderr.
1029-
stdout_replacement = redirect_stream_to_file(stdout, "/internal/stdout");
1030-
stderr_replacement = redirect_stream_to_file(stderr, "/internal/stderr");
1029+
stdout_replacement = redirect_stream_to_file(stdout, "/request/stdout");
1030+
stderr_replacement = redirect_stream_to_file(stderr, "/request/stderr");
10311031
if (stdout_replacement == -1 || stderr_replacement == -1)
10321032
{
10331033
return -1;
@@ -1337,7 +1337,7 @@ void wasm_set_request_port(int port)
13371337
*
13381338
* stream: The stream to redirect, e.g. stdout or stderr.
13391339
*
1340-
* path: The path to the file to redirect to, e.g. "/internal/stdout".
1340+
* path: The path to the file to redirect to, e.g. "/request/stdout".
13411341
*
13421342
* returns: The exit code: 0 on success, -1 on failure.
13431343
*/
@@ -1559,11 +1559,11 @@ int wasm_sapi_request_init()
15591559
// Write to files instead of stdout and stderr because Emscripten truncates null
15601560
// bytes from stdout and stderr, and null bytes are a valid output when streaming
15611561
// binary data.
1562-
// We use our custom Emscripten-defined /internal/std* devices and handle the output in JavaScript.
1562+
// We use our custom Emscripten-defined /request/std* devices and handle the output in JavaScript.
15631563
// These /internal devices are not thread-safe and should always stay in per-process MEMFS space.
15641564
// Sharing them between PHP instances may cause intertwined output.
1565-
stdout_replacement = redirect_stream_to_file(stdout, "/internal/stdout");
1566-
stderr_replacement = redirect_stream_to_file(stderr, "/internal/stderr");
1565+
stdout_replacement = redirect_stream_to_file(stdout, "/request/stdout");
1566+
stderr_replacement = redirect_stream_to_file(stderr, "/request/stderr");
15671567
if (stdout_replacement == -1 || stderr_replacement == -1)
15681568
{
15691569
return -1;
@@ -1821,7 +1821,7 @@ FILE *headers_file;
18211821
*/
18221822
static int wasm_sapi_send_headers(sapi_headers_struct *sapi_headers TSRMLS_DC)
18231823
{
1824-
headers_file = fopen("/internal/headers", "w");
1824+
headers_file = fopen("/request/headers", "w");
18251825
if (headers_file == NULL)
18261826
{
18271827
return FAILURE;

packages/php-wasm/compile/php/phpwasm-emscripten-library.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,27 @@ const LibraryExample = {
3838
.filter(Boolean)
3939
.join(':');
4040

41-
// The /internal directory is required by the C module. It's where the
41+
// The /request directory is required by the C module. It's where the
4242
// stdout, stderr, and headers information are written for the JavaScript
43-
// code to read later on.
43+
// code to read later on. This is per-request state that is isolated to a
44+
// single PHP process.
45+
FS.mkdir('/request');
46+
// The /internal directory is shared amongst all PHP processes
47+
// and contains wp-config.php, constants, etc.
4448
FS.mkdir('/internal');
45-
// The files from the shared directory are shared between all th
46-
// PHP processes managed by PHPProcessManager.
47-
FS.mkdir('/internal/shared');
4849

4950
if (phpWasmInitOptions?.nativeInternalDirPath) {
5051
FS.mount(
5152
FS.filesystems.NODEFS,
5253
{ root: phpWasmInitOptions.nativeInternalDirPath },
53-
'/internal/shared'
54+
'/internal'
5455
);
5556
}
5657

58+
// The files from the shared directory are shared between all the
59+
// PHP processes managed by PHPProcessManager.
60+
FS.mkdirTree('/internal/shared');
61+
5762
// The files from the preload directory are preloaded using the
5863
// auto_prepend_file php.ini directive.
5964
FS.mkdirTree('/internal/shared/preload');
@@ -92,7 +97,7 @@ const LibraryExample = {
9297
return length;
9398
},
9499
});
95-
FS.mkdev('/internal/stdout', FS.makedev(64, 0));
100+
FS.mkdev('/request/stdout', FS.makedev(64, 0));
96101

97102
FS.registerDevice(FS.makedev(63, 0), {
98103
open: () => {},
@@ -104,7 +109,7 @@ const LibraryExample = {
104109
return length;
105110
},
106111
});
107-
FS.mkdev('/internal/stderr', FS.makedev(63, 0));
112+
FS.mkdev('/request/stderr', FS.makedev(63, 0));
108113

109114
FS.registerDevice(FS.makedev(62, 0), {
110115
open: () => {},
@@ -116,7 +121,7 @@ const LibraryExample = {
116121
return length;
117122
},
118123
});
119-
FS.mkdev('/internal/headers', FS.makedev(62, 0));
124+
FS.mkdev('/request/headers', FS.makedev(62, 0));
120125

121126
// Handle events.
122127
PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ export {
7979
} from './urls';
8080

8181
export { isExitCode } from './is-exit-code';
82-
export { proxyFileSystem } from './proxy-file-system';
82+
export {
83+
proxyFileSystem,
84+
proxyNonSharedMemoryFileSystems,
85+
} from './proxy-file-system';
8386
export { sandboxedSpawnHandlerFactory } from './sandboxed-spawn-handler-factory';
8487

8588
export * from './api';

packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ export class BlueprintsV1Handler {
136136

137137
await playground.useFileLockManager(fileLockManagerPort);
138138
await playground.bootAsPrimaryWorker({
139-
phpVersion: this.phpVersion,
139+
php: this.phpVersion,
140140
wpVersion: compiledBlueprint.versions.wp,
141-
absoluteUrl: this.siteUrl,
141+
siteUrl: this.siteUrl,
142142
mountsBeforeWpInstall,
143143
mountsAfterWpInstall,
144144
wordPressZip: wordPressZip && (await wordPressZip!.arrayBuffer()),
@@ -187,17 +187,10 @@ export class BlueprintsV1Handler {
187187
await additionalPlayground.isConnected();
188188
await additionalPlayground.useFileLockManager(fileLockManagerPort);
189189
await additionalPlayground.bootAsSecondaryWorker({
190-
phpVersion: this.phpVersion,
191-
absoluteUrl: this.siteUrl,
190+
php: this.phpVersion!,
191+
siteUrl: this.siteUrl,
192192
mountsBeforeWpInstall: this.args['mount-before-install'] || [],
193193
mountsAfterWpInstall: this.args['mount'] || [],
194-
// Skip WordPress zip because we share the /wordpress directory
195-
// populated by the initial worker.
196-
wordPressZip: undefined,
197-
// Skip SQLite integration plugin for now because we
198-
// will copy it from primary's `/internal` directory.
199-
sqliteIntegrationPluginZip: undefined,
200-
dataSqlPath: '/wordpress/wp-content/database/.ht.sqlite',
201194
firstProcessId,
202195
processIdSpaceLength: this.processIdSpaceLength,
203196
followSymlinks: this.args.followSymlinks === true,

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

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '@php-wasm/universal';
1212
import { sprintf } from '@php-wasm/util';
1313
import { RecommendedPHPVersion } from '@wp-playground/common';
14-
import { bootWordPress } from '@wp-playground/wordpress';
14+
import { bootWordPress, bootRequestHandler } from '@wp-playground/wordpress';
1515
import { rootCertificates } from 'tls';
1616
import { jspi } from 'wasm-feature-detect';
1717
import { MessageChannel, type MessagePort, parentPort } from 'worker_threads';
@@ -23,16 +23,12 @@ export interface Mount {
2323
}
2424

2525
export type WorkerBootOptions = {
26-
wpVersion?: string;
27-
phpVersion?: SupportedPHPVersion;
28-
absoluteUrl: string;
26+
php: SupportedPHPVersion;
27+
siteUrl: string;
2928
mountsBeforeWpInstall: Array<Mount>;
3029
mountsAfterWpInstall: Array<Mount>;
31-
wordPressZip?: ArrayBuffer;
32-
sqliteIntegrationPluginZip?: ArrayBuffer;
3330
firstProcessId: number;
3431
processIdSpaceLength: number;
35-
dataSqlPath?: string;
3632
followSymlinks: boolean;
3733
trace: boolean;
3834
/**
@@ -44,9 +40,26 @@ export type WorkerBootOptions = {
4440
*/
4541
internalCookieStore?: boolean;
4642
withXdebug?: boolean;
47-
nativeInternalDirPath?: string;
43+
nativeInternalDirPath: string;
44+
};
45+
46+
export type PrimaryWorkerBootOptions = WorkerBootOptions & {
47+
wpVersion?: string;
48+
wordPressZip?: ArrayBuffer;
49+
sqliteIntegrationPluginZip?: ArrayBuffer;
50+
dataSqlPath?: string;
4851
};
4952

53+
interface WorkerBootRequestHandlerOptions {
54+
siteUrl: string;
55+
allow?: string;
56+
php: SupportedPHPVersion;
57+
firstProcessId: number;
58+
processIdSpaceLength: number;
59+
trace: boolean;
60+
nativeInternalDirPath: string;
61+
}
62+
5063
/**
5164
* Print trace messages from PHP-WASM.
5265
*
@@ -105,10 +118,10 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
105118
}
106119

107120
async bootAsPrimaryWorker({
108-
absoluteUrl,
121+
siteUrl,
109122
mountsBeforeWpInstall,
110123
mountsAfterWpInstall,
111-
phpVersion = RecommendedPHPVersion,
124+
php = RecommendedPHPVersion,
112125
wordPressZip,
113126
sqliteIntegrationPluginZip,
114127
firstProcessId,
@@ -119,7 +132,7 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
119132
internalCookieStore,
120133
withXdebug,
121134
nativeInternalDirPath,
122-
}: WorkerBootOptions) {
135+
}: PrimaryWorkerBootOptions) {
123136
if (this.booted) {
124137
throw new Error('Playground already booted');
125138
}
@@ -137,7 +150,7 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
137150
};
138151

139152
const requestHandler = await bootWordPress({
140-
siteUrl: absoluteUrl,
153+
siteUrl,
141154
createPhpRuntime: async () => {
142155
const processId = nextProcessId;
143156

@@ -148,7 +161,7 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
148161
nextProcessId = firstProcessId;
149162
}
150163

151-
return await loadNodeRuntime(phpVersion, {
164+
return await loadNodeRuntime(php, {
152165
emscriptenOptions: {
153166
fileLockManager: this.fileLockManager!,
154167
processId,
@@ -207,7 +220,72 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
207220
}
208221

209222
async bootAsSecondaryWorker(args: WorkerBootOptions) {
210-
return this.bootAsPrimaryWorker(args);
223+
await this.bootRequestHandler(args);
224+
const primaryPhp = this.__internal_getPHP()!;
225+
// When secondary workers are spawned, WordPress is already installed.
226+
await mountResources(primaryPhp, args.mountsBeforeWpInstall || []);
227+
await mountResources(primaryPhp, args.mountsAfterWpInstall || []);
228+
}
229+
230+
async bootRequestHandler({
231+
siteUrl,
232+
allow,
233+
php,
234+
firstProcessId,
235+
processIdSpaceLength,
236+
trace,
237+
nativeInternalDirPath,
238+
}: WorkerBootRequestHandlerOptions) {
239+
if (this.booted) {
240+
throw new Error('Playground already booted');
241+
}
242+
this.booted = true;
243+
244+
let nextProcessId = firstProcessId;
245+
const lastProcessId = firstProcessId + processIdSpaceLength - 1;
246+
247+
try {
248+
const requestHandler = await bootRequestHandler({
249+
siteUrl,
250+
createPhpRuntime: async () => {
251+
const processId = nextProcessId;
252+
253+
if (nextProcessId < lastProcessId) {
254+
nextProcessId++;
255+
} else {
256+
// We've reached the end of the process ID space. Start over.
257+
nextProcessId = firstProcessId;
258+
}
259+
260+
return await loadNodeRuntime(php!, {
261+
emscriptenOptions: {
262+
fileLockManager: this.fileLockManager!,
263+
processId,
264+
trace: trace ? tracePhpWasm : undefined,
265+
ENV: {
266+
DOCROOT: '/wordpress',
267+
},
268+
phpWasmInitOptions: {
269+
nativeInternalDirPath,
270+
},
271+
},
272+
followSymlinks: allow?.includes('follow-symlinks'),
273+
});
274+
},
275+
sapiName: 'cli',
276+
cookieStore: false,
277+
spawnHandler: sandboxedSpawnHandlerFactory,
278+
});
279+
this.__internal_setRequestHandler(requestHandler);
280+
281+
const primaryPhp = await requestHandler.getPrimaryPhp();
282+
await this.setPrimaryPHP(primaryPhp);
283+
284+
setApiReady();
285+
} catch (e) {
286+
setAPIError(e as Error);
287+
throw e;
288+
}
211289
}
212290

213291
// Provide a named disposal method that can be invoked via comlink.

0 commit comments

Comments
 (0)