Skip to content

Commit 10b61de

Browse files
committed
Prototype: Spawning PHP sub-processes in Web Workers
Related to #1026 and #1027
1 parent e7b3912 commit 10b61de

File tree

6 files changed

+174
-15
lines changed

6 files changed

+174
-15
lines changed

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,7 @@ export abstract class BasePHP implements IsomorphicLocalPHP {
794794
// Copy the MEMFS directory structure from the old FS to the new one
795795
if (this.requestHandler) {
796796
const docroot = this.documentRoot;
797-
recreateMemFS(this[__private__dont__use].FS, oldFS, docroot);
797+
copyFS(oldFS, this[__private__dont__use].FS, docroot);
798798
}
799799
}
800800

@@ -830,14 +830,22 @@ export function normalizeHeaders(
830830

831831
type EmscriptenFS = any;
832832

833+
export function syncFSTo(source: BasePHP, target: BasePHP) {
834+
copyFS(
835+
source[__private__dont__use].FS,
836+
target[__private__dont__use].FS,
837+
source.documentRoot
838+
);
839+
}
840+
833841
/**
834842
* Copies the MEMFS directory structure from one FS in another FS.
835843
* Non-MEMFS nodes are ignored.
836844
*/
837-
function recreateMemFS(newFS: EmscriptenFS, oldFS: EmscriptenFS, path: string) {
845+
function copyFS(source: EmscriptenFS, target: EmscriptenFS, path: string) {
838846
let oldNode;
839847
try {
840-
oldNode = oldFS.lookupPath(path);
848+
oldNode = source.lookupPath(path);
841849
} catch (e) {
842850
return;
843851
}
@@ -850,23 +858,23 @@ function recreateMemFS(newFS: EmscriptenFS, oldFS: EmscriptenFS, path: string) {
850858
// Let's be extra careful and only proceed if newFs doesn't
851859
// already have a node at the given path.
852860
try {
853-
newFS = newFS.lookupPath(path);
861+
target = target.lookupPath(path);
854862
return;
855863
} catch (e) {
856864
// There's no such node in the new FS. Good,
857865
// we may proceed.
858866
}
859867

860-
if (!oldFS.isDir(oldNode.node.mode)) {
861-
newFS.writeFile(path, oldFS.readFile(path));
868+
if (!source.isDir(oldNode.node.mode)) {
869+
target.writeFile(path, source.readFile(path));
862870
return;
863871
}
864872

865-
newFS.mkdirTree(path);
866-
const filenames = oldFS
873+
target.mkdirTree(path);
874+
const filenames = source
867875
.readdir(path)
868876
.filter((name: string) => name !== '.' && name !== '..');
869877
for (const filename of filenames) {
870-
recreateMemFS(newFS, oldFS, joinPaths(path, filename));
878+
copyFS(source, target, joinPaths(path, filename));
871879
}
872880
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export type {
3939
SupportedPHPExtension,
4040
SupportedPHPExtensionBundle,
4141
} from './supported-php-extensions';
42-
export { BasePHP, __private__dont__use } from './base-php';
42+
export { BasePHP, syncFSTo, __private__dont__use } from './base-php';
4343
export { loadPHPRuntime } from './load-php-runtime';
4444
export type {
4545
DataModule,

packages/playground/remote/src/lib/opfs/bind-opfs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { __private__dont__use } from '@php-wasm/universal';
1313
import { Semaphore, joinPaths } from '@php-wasm/util';
1414
import type { WebPHP } from '@php-wasm/web';
1515
import { EmscriptenFS } from './types';
16-
import { journalFSEventsToOpfs } from './journal-memfs-to-opfs';
16+
import { journalFSEventsToOpfs } from './journal-fs-to-opfs';
1717

1818
let unbindOpfs: (() => void) | undefined;
1919
export type SyncProgress = {

packages/playground/remote/src/lib/opfs/journal-memfs-to-opfs.ts renamed to packages/playground/remote/src/lib/opfs/journal-fs-to-opfs.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
/* eslint-disable prefer-rest-params */
1212
import type { WebPHP } from '@php-wasm/web';
13-
import type { EmscriptenFS } from './types';
1413
import { FilesystemOperation, journalFSEvents } from '@php-wasm/fs-journal';
1514
import { __private__dont__use } from '@php-wasm/universal';
1615
import { copyMemfsToOpfs, overwriteOpfsFile } from './bind-opfs';
@@ -48,7 +47,6 @@ export function journalFSEventsToOpfs(
4847
type JournalEntry = FilesystemOperation;
4948

5049
class OpfsRewriter {
51-
private FS: EmscriptenFS;
5250
private memfsRoot: string;
5351

5452
constructor(
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* Synchronize MEMFS changes from a PHP instance into another PHP instance.
3+
*/
4+
5+
/* eslint-disable prefer-rest-params */
6+
import type { WebPHP } from '@php-wasm/web';
7+
import { FilesystemOperation, journalFSEvents } from '@php-wasm/fs-journal';
8+
import { basename, normalizePath } from '@php-wasm/util';
9+
10+
export function journalFSEventsToPhp(
11+
sourcePhp: WebPHP,
12+
targetPhp: WebPHP,
13+
root: string
14+
) {
15+
root = normalizePath(root);
16+
17+
const journal: FilesystemOperation[] = [];
18+
const unbindJournal = journalFSEvents(sourcePhp, root, (entry) => {
19+
journal.push(entry);
20+
});
21+
const rewriter = new MemfsRewriter(sourcePhp, targetPhp, root);
22+
23+
async function flushJournal() {
24+
// @TODO This is way too slow in practice, we need to batch the
25+
// changes into groups of parallelizable operations.
26+
while (journal.length) {
27+
rewriter.processEntry(journal.shift()!);
28+
}
29+
}
30+
sourcePhp.addEventListener('request.end', flushJournal);
31+
return function () {
32+
unbindJournal();
33+
sourcePhp.removeEventListener('request.end', flushJournal);
34+
};
35+
}
36+
37+
type JournalEntry = FilesystemOperation;
38+
39+
class MemfsRewriter {
40+
constructor(
41+
private sourcePhp: WebPHP,
42+
private targetPhp: WebPHP,
43+
private root: string
44+
) {}
45+
46+
public processEntry(entry: JournalEntry) {
47+
if (!entry.path.startsWith(this.root) || entry.path === this.root) {
48+
return;
49+
}
50+
const name = basename(entry.path);
51+
if (!name) {
52+
return;
53+
}
54+
55+
try {
56+
if (entry.operation === 'DELETE') {
57+
try {
58+
if (this.targetPhp.isDir(entry.path)) {
59+
this.targetPhp.rmdir(entry.path);
60+
} else {
61+
this.targetPhp.unlink(entry.path);
62+
}
63+
} catch (e) {
64+
// If the directory already doesn't exist, it's fine
65+
}
66+
} else if (entry.operation === 'CREATE') {
67+
if (entry.nodeType === 'directory') {
68+
this.targetPhp.mkdir(entry.path);
69+
} else {
70+
this.targetPhp.writeFile(entry.path, '');
71+
}
72+
} else if (entry.operation === 'WRITE') {
73+
this.targetPhp.writeFile(
74+
entry.path,
75+
this.sourcePhp.readFileAsBuffer(entry.path)
76+
);
77+
} else if (
78+
entry.operation === 'RENAME' &&
79+
entry.toPath.startsWith(this.root)
80+
) {
81+
this.targetPhp.mv(entry.path, entry.toPath);
82+
}
83+
} catch (e) {
84+
// Useful for debugging – the original error gets lost in the
85+
// Comlink proxy.
86+
console.log({ entry, name });
87+
console.error(e);
88+
throw e;
89+
}
90+
}
91+
}

packages/playground/remote/src/lib/worker-thread.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
SupportedPHPVersion,
1414
SupportedPHPVersionsList,
1515
rotatePHPRuntime,
16+
syncFSTo,
1617
writeFiles,
1718
} from '@php-wasm/universal';
1819
import { createSpawnHandler } from '@php-wasm/util';
@@ -39,6 +40,7 @@ import transportDummy from './playground-mu-plugin/playground-includes/wp_http_d
3940
/** @ts-ignore */
4041
import playgroundMuPlugin from './playground-mu-plugin/0-playground.php?raw';
4142
import { joinPaths, randomString } from '@php-wasm/util';
43+
import { journalFSEventsToPhp } from './opfs/journal-fs-to-php';
4244

4345
// post message to parent
4446
self.postMessage('worker-script-started');
@@ -292,11 +294,71 @@ try {
292294
siteUrl: scopedSiteUrl,
293295
});
294296

297+
php.writeFile(
298+
joinPaths(docroot, 'spawn.php'),
299+
`<?php
300+
echo "<plaintext>";
301+
echo "Spawning\n";
302+
$handle = proc_open('php child.php', [
303+
0 => ['pipe', 'r'],
304+
1 => ['pipe', 'w'],
305+
2 => ['pipe', 'w'],
306+
], $pipes);
307+
$stdout = stream_get_contents($pipes[1]);
308+
$stderr = stream_get_contents($pipes[2]);
309+
310+
// $status = proc_get_status($handle);
311+
// $exit_code = proc_close($handle);
312+
// var_dump($status);
313+
// var_dump($exit_code);
314+
var_dump($stdout);
315+
var_dump($stderr);
316+
echo "Finished\n";
317+
echo "Created file: " . file_get_contents("/wordpress/new.txt") . "\n";
318+
`
319+
);
320+
321+
php.writeFile(
322+
joinPaths(docroot, 'child.php'),
323+
`<?php
324+
echo "Hi there!";
325+
`
326+
);
327+
295328
// Spawning new processes on the web is not supported,
296329
// let's always fail.
297330
php.setSpawnHandler(
298-
createSpawnHandler(function (_, processApi) {
299-
processApi.exit(1);
331+
createSpawnHandler(async function (command, processApi) {
332+
const runtime = await recreateRuntime();
333+
const childPHP = new WebPHP(runtime, {
334+
documentRoot: DOCROOT,
335+
absoluteUrl: scopedSiteUrl,
336+
});
337+
let unbind = () => {};
338+
try {
339+
console.log('Before syncFS');
340+
syncFSTo(php, childPHP);
341+
unbind = journalFSEventsToPhp(
342+
childPHP,
343+
php,
344+
childPHP.documentRoot
345+
);
346+
const result = await childPHP.run({
347+
throwOnError: true,
348+
code: `<?php
349+
echo "<plaintext>";
350+
echo "Spawned, running\n";
351+
file_put_contents("/wordpress/new.txt", "Hello, world!");
352+
`,
353+
});
354+
processApi.stdout(result.bytes);
355+
processApi.stderr(result.errors);
356+
processApi.exit(result.exitCode);
357+
} finally {
358+
console.log('Exiting childPHP');
359+
unbind();
360+
childPHP.exit();
361+
}
300362
})
301363
);
302364

0 commit comments

Comments
 (0)