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 98e4186 commit 6d38366
Show file tree
Hide file tree
Showing 12 changed files with 270 additions and 97 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
12 changes: 9 additions & 3 deletions packages/php-wasm/util/src/lib/create-spawn-handler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { splitShellCommand } from './split-shell-command';

type Listener = (...args: any[]) => any;

/**
Expand All @@ -15,14 +17,18 @@ type Listener = (...args: any[]) => any;
* @returns
*/
export function createSpawnHandler(
program: (command: string, processApi: ProcessApi) => void
program: (command: string[], processApi: ProcessApi) => void | Promise<void>
): any {
return function (command: string) {
return function (command: string | string[]) {
const childProcess = new ChildProcess();
const processApi = new ProcessApi(childProcess);
// Give PHP a chance to register listeners
setTimeout(async () => {
await program(command, processApi);
const commandArray =
typeof command === 'string'
? splitShellCommand(command)
: command;
await program(commandArray, processApi);
childProcess.emit('spawn', true);
});
return childProcess;
Expand Down
1 change: 1 addition & 0 deletions packages/php-wasm/util/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export { dirname, joinPaths, basename, normalizePath } from './paths';
export { createSpawnHandler } from './create-spawn-handler';
export { randomString } from './random-string';
export { randomFilename } from './random-filename';
export { splitShellCommand } from './split-shell-command';

export * from './php-vars';
27 changes: 27 additions & 0 deletions packages/php-wasm/util/src/lib/split-shell-command.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { splitShellCommand } from './split-shell-command';

describe('splitShellCommand', () => {
it('Should split a shell command into an array', () => {
const command =
'wp post create --post_title="Test post" --post_excerpt="Some content"';
const result = splitShellCommand(command);
expect(result).toEqual([
'wp',
'post',
'create',
'--post_title=Test post',
'--post_excerpt=Some content',
]);
});

it('Should treat multiple spaces as a single space', () => {
const command = 'ls --wordpress --playground --is-great';
const result = splitShellCommand(command);
expect(result).toEqual([
'ls',
'--wordpress',
'--playground',
'--is-great',
]);
});
});
49 changes: 49 additions & 0 deletions packages/php-wasm/util/src/lib/split-shell-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Naive shell command parser.
* Ensures that commands like `wp option set blogname "My blog name"` are split into
* `['wp', 'option', 'set', 'blogname', 'My blog name']` instead of
* `['wp', 'option', 'set', 'blogname', 'My', 'blog', 'name']`.
*
* @param command
* @returns
*/
export function splitShellCommand(command: string) {
const MODE_NORMAL = 0;
const MODE_IN_QUOTE = 1;

let mode = MODE_NORMAL;
let quote = '';

const parts: string[] = [];
let currentPart = '';
for (let i = 0; i < command.length; i++) {
const char = command[i];
if (mode === MODE_NORMAL) {
if (char === '"' || char === "'") {
mode = MODE_IN_QUOTE;
quote = char;
} else if (char.match(/\s/)) {
if (currentPart) {
parts.push(currentPart);
}
currentPart = '';
} else {
currentPart += char;
}
} else if (mode === MODE_IN_QUOTE) {
if (char === '\\') {
i++;
currentPart += command[i];
} else if (char === quote) {
mode = MODE_NORMAL;
quote = '';
} else {
currentPart += char;
}
}
}
if (currentPart) {
parts.push(currentPart);
}
return parts;
}
28 changes: 1 addition & 27 deletions packages/playground/blueprints/src/lib/steps/wp-cli.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NodePHP } from '@php-wasm/node';
import { splitShellCommand, wpCLI } from './wp-cli';
import { wpCLI } from './wp-cli';
import { readFileSync } from 'fs';
import { join } from 'path';
import { unzip } from './unzip';
Expand Down Expand Up @@ -32,29 +32,3 @@ describe('Blueprint step wpCLI', () => {
expect(result.text).toMatch(/Success: Created post/);
});
});

describe('splitShellCommand', () => {
it('Should split a shell command into an array', () => {
const command =
'wp post create --post_title="Test post" --post_excerpt="Some content"';
const result = splitShellCommand(command);
expect(result).toEqual([
'wp',
'post',
'create',
'--post_title=Test post',
'--post_excerpt=Some content',
]);
});

it('Should treat multiple spaces as a single space', () => {
const command = 'ls --wordpress --playground --is-great';
const result = splitShellCommand(command);
expect(result).toEqual([
'ls',
'--wordpress',
'--playground',
'--is-great',
]);
});
});
52 changes: 1 addition & 51 deletions packages/playground/blueprints/src/lib/steps/wp-cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PHPResponse } from '@php-wasm/universal';
import { StepHandler } from '.';
import { phpVar } from '@php-wasm/util';
import { phpVar, splitShellCommand } from '@php-wasm/util';

/**
* @inheritDoc wpCLI
Expand Down Expand Up @@ -86,53 +86,3 @@ export const wpCLI: StepHandler<WPCLIStep, Promise<PHPResponse>> = async (

return result;
};

/**
* Naive shell command parser.
* Ensures that commands like `wp option set blogname "My blog name"` are split into
* `['wp', 'option', 'set', 'blogname', 'My blog name']` instead of
* `['wp', 'option', 'set', 'blogname', 'My', 'blog', 'name']`.
*
* @param command
* @returns
*/
export function splitShellCommand(command: string) {
const MODE_NORMAL = 0;
const MODE_IN_QUOTE = 1;

let mode = MODE_NORMAL;
let quote = '';

const parts: string[] = [];
let currentPart = '';
for (let i = 0; i < command.length; i++) {
const char = command[i];
if (mode === MODE_NORMAL) {
if (char === '"' || char === "'") {
mode = MODE_IN_QUOTE;
quote = char;
} else if (char.match(/\s/)) {
if (currentPart) {
parts.push(currentPart);
}
currentPart = '';
} else {
currentPart += char;
}
} else if (mode === MODE_IN_QUOTE) {
if (char === '\\') {
i++;
currentPart += command[i];
} else if (char === quote) {
mode = MODE_NORMAL;
quote = '';
} else {
currentPart += char;
}
}
}
if (currentPart) {
parts.push(currentPart);
}
return parts;
}
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
Loading

0 comments on commit 6d38366

Please sign in to comment.