Skip to content

Prototype: Spawning PHP sub-processes in Web Workers #1031

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions packages/php-wasm/universal/src/lib/base-php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,11 @@ export abstract class BasePHP implements IsomorphicLocalPHP {
this.#initWebRuntime();
this.#webSapiInitialized = true;
}
if (request.scriptPath && !this.fileExists(request.scriptPath)) {
throw new Error(
`The script path "${request.scriptPath}" does not exist.`
);
}
this.#setScriptPath(request.scriptPath || '');
this.#setRelativeRequestUri(request.relativeUri || '');
this.#setRequestMethod(request.method || 'GET');
Expand Down Expand Up @@ -794,7 +799,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 +835,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 +863,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