Skip to content

Commit

Permalink
Prototype: Spawning PHP sub-processes in Web Workers
Browse files Browse the repository at this point in the history
Related to #1026 and #1027
  • Loading branch information
adamziel committed Feb 11, 2024
1 parent e7b3912 commit 10b61de
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 15 deletions.
26 changes: 17 additions & 9 deletions packages/php-wasm/universal/src/lib/base-php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@ export abstract class BasePHP implements IsomorphicLocalPHP {
// Copy the MEMFS directory structure from the old FS to the new one
if (this.requestHandler) {
const docroot = this.documentRoot;
recreateMemFS(this[__private__dont__use].FS, oldFS, docroot);
copyFS(oldFS, this[__private__dont__use].FS, docroot);
}
}

Expand Down Expand Up @@ -830,14 +830,22 @@ export function normalizeHeaders(

type EmscriptenFS = any;

export function syncFSTo(source: BasePHP, target: BasePHP) {
copyFS(
source[__private__dont__use].FS,
target[__private__dont__use].FS,
source.documentRoot
);
}

/**
* Copies the MEMFS directory structure from one FS in another FS.
* Non-MEMFS nodes are ignored.
*/
function recreateMemFS(newFS: EmscriptenFS, oldFS: EmscriptenFS, path: string) {
function copyFS(source: EmscriptenFS, target: EmscriptenFS, path: string) {
let oldNode;
try {
oldNode = oldFS.lookupPath(path);
oldNode = source.lookupPath(path);
} catch (e) {
return;
}
Expand All @@ -850,23 +858,23 @@ function recreateMemFS(newFS: EmscriptenFS, oldFS: EmscriptenFS, path: string) {
// Let's be extra careful and only proceed if newFs doesn't
// already have a node at the given path.
try {
newFS = newFS.lookupPath(path);
target = target.lookupPath(path);
return;
} catch (e) {
// There's no such node in the new FS. Good,
// we may proceed.
}

if (!oldFS.isDir(oldNode.node.mode)) {
newFS.writeFile(path, oldFS.readFile(path));
if (!source.isDir(oldNode.node.mode)) {
target.writeFile(path, source.readFile(path));
return;
}

newFS.mkdirTree(path);
const filenames = oldFS
target.mkdirTree(path);
const filenames = source
.readdir(path)
.filter((name: string) => name !== '.' && name !== '..');
for (const filename of filenames) {
recreateMemFS(newFS, oldFS, joinPaths(path, filename));
copyFS(source, target, joinPaths(path, filename));
}
}
2 changes: 1 addition & 1 deletion packages/php-wasm/universal/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export type {
SupportedPHPExtension,
SupportedPHPExtensionBundle,
} from './supported-php-extensions';
export { BasePHP, __private__dont__use } from './base-php';
export { BasePHP, syncFSTo, __private__dont__use } from './base-php';
export { loadPHPRuntime } from './load-php-runtime';
export type {
DataModule,
Expand Down
2 changes: 1 addition & 1 deletion packages/playground/remote/src/lib/opfs/bind-opfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { __private__dont__use } from '@php-wasm/universal';
import { Semaphore, joinPaths } from '@php-wasm/util';
import type { WebPHP } from '@php-wasm/web';
import { EmscriptenFS } from './types';
import { journalFSEventsToOpfs } from './journal-memfs-to-opfs';
import { journalFSEventsToOpfs } from './journal-fs-to-opfs';

let unbindOpfs: (() => void) | undefined;
export type SyncProgress = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

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

class OpfsRewriter {
private FS: EmscriptenFS;
private memfsRoot: string;

constructor(
Expand Down
91 changes: 91 additions & 0 deletions packages/playground/remote/src/lib/opfs/journal-fs-to-php.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Synchronize MEMFS changes from a PHP instance into another PHP instance.
*/

/* eslint-disable prefer-rest-params */
import type { WebPHP } from '@php-wasm/web';
import { FilesystemOperation, journalFSEvents } from '@php-wasm/fs-journal';
import { basename, normalizePath } from '@php-wasm/util';

export function journalFSEventsToPhp(
sourcePhp: WebPHP,
targetPhp: WebPHP,
root: string
) {
root = normalizePath(root);

const journal: FilesystemOperation[] = [];
const unbindJournal = journalFSEvents(sourcePhp, root, (entry) => {
journal.push(entry);
});
const rewriter = new MemfsRewriter(sourcePhp, targetPhp, root);

async function flushJournal() {
// @TODO This is way too slow in practice, we need to batch the
// changes into groups of parallelizable operations.
while (journal.length) {
rewriter.processEntry(journal.shift()!);
}
}
sourcePhp.addEventListener('request.end', flushJournal);
return function () {
unbindJournal();
sourcePhp.removeEventListener('request.end', flushJournal);
};
}

type JournalEntry = FilesystemOperation;

class MemfsRewriter {
constructor(
private sourcePhp: WebPHP,
private targetPhp: WebPHP,
private root: string
) {}

public processEntry(entry: JournalEntry) {
if (!entry.path.startsWith(this.root) || entry.path === this.root) {
return;
}
const name = basename(entry.path);
if (!name) {
return;
}

try {
if (entry.operation === 'DELETE') {
try {
if (this.targetPhp.isDir(entry.path)) {
this.targetPhp.rmdir(entry.path);
} else {
this.targetPhp.unlink(entry.path);
}
} catch (e) {
// If the directory already doesn't exist, it's fine
}
} else if (entry.operation === 'CREATE') {
if (entry.nodeType === 'directory') {
this.targetPhp.mkdir(entry.path);
} else {
this.targetPhp.writeFile(entry.path, '');
}
} else if (entry.operation === 'WRITE') {
this.targetPhp.writeFile(
entry.path,
this.sourcePhp.readFileAsBuffer(entry.path)
);
} else if (
entry.operation === 'RENAME' &&
entry.toPath.startsWith(this.root)
) {
this.targetPhp.mv(entry.path, entry.toPath);
}
} catch (e) {
// Useful for debugging – the original error gets lost in the
// Comlink proxy.
console.log({ entry, name });
console.error(e);
throw e;
}
}
}
66 changes: 64 additions & 2 deletions packages/playground/remote/src/lib/worker-thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
SupportedPHPVersion,
SupportedPHPVersionsList,
rotatePHPRuntime,
syncFSTo,
writeFiles,
} from '@php-wasm/universal';
import { createSpawnHandler } from '@php-wasm/util';
Expand All @@ -39,6 +40,7 @@ import transportDummy from './playground-mu-plugin/playground-includes/wp_http_d
/** @ts-ignore */
import playgroundMuPlugin from './playground-mu-plugin/0-playground.php?raw';
import { joinPaths, randomString } from '@php-wasm/util';
import { journalFSEventsToPhp } from './opfs/journal-fs-to-php';

// post message to parent
self.postMessage('worker-script-started');
Expand Down Expand Up @@ -292,11 +294,71 @@ try {
siteUrl: scopedSiteUrl,
});

php.writeFile(
joinPaths(docroot, 'spawn.php'),
`<?php
echo "<plaintext>";
echo "Spawning\n";
$handle = proc_open('php child.php', [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
], $pipes);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
// $status = proc_get_status($handle);
// $exit_code = proc_close($handle);
// var_dump($status);
// var_dump($exit_code);
var_dump($stdout);
var_dump($stderr);
echo "Finished\n";
echo "Created file: " . file_get_contents("/wordpress/new.txt") . "\n";
`
);

php.writeFile(
joinPaths(docroot, 'child.php'),
`<?php
echo "Hi there!";
`
);

// Spawning new processes on the web is not supported,
// let's always fail.
php.setSpawnHandler(
createSpawnHandler(function (_, processApi) {
processApi.exit(1);
createSpawnHandler(async function (command, processApi) {
const runtime = await recreateRuntime();
const childPHP = new WebPHP(runtime, {
documentRoot: DOCROOT,
absoluteUrl: scopedSiteUrl,
});
let unbind = () => {};
try {
console.log('Before syncFS');
syncFSTo(php, childPHP);
unbind = journalFSEventsToPhp(
childPHP,
php,
childPHP.documentRoot
);
const result = await childPHP.run({
throwOnError: true,
code: `<?php
echo "<plaintext>";
echo "Spawned, running\n";
file_put_contents("/wordpress/new.txt", "Hello, world!");
`,
});
processApi.stdout(result.bytes);
processApi.stderr(result.errors);
processApi.exit(result.exitCode);
} finally {
console.log('Exiting childPHP');
unbind();
childPHP.exit();
}
})
);

Expand Down

0 comments on commit 10b61de

Please sign in to comment.