Skip to content

Commit 6d38366

Browse files
committed
Prototype: Spawning PHP sub-processes in Web Workers
Related to #1026 and #1027
1 parent 98e4186 commit 6d38366

File tree

12 files changed

+270
-97
lines changed

12 files changed

+270
-97
lines changed

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

+17-9
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

+1-1
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/php-wasm/util/src/lib/create-spawn-handler.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { splitShellCommand } from './split-shell-command';
2+
13
type Listener = (...args: any[]) => any;
24

35
/**
@@ -15,14 +17,18 @@ type Listener = (...args: any[]) => any;
1517
* @returns
1618
*/
1719
export function createSpawnHandler(
18-
program: (command: string, processApi: ProcessApi) => void
20+
program: (command: string[], processApi: ProcessApi) => void | Promise<void>
1921
): any {
20-
return function (command: string) {
22+
return function (command: string | string[]) {
2123
const childProcess = new ChildProcess();
2224
const processApi = new ProcessApi(childProcess);
2325
// Give PHP a chance to register listeners
2426
setTimeout(async () => {
25-
await program(command, processApi);
27+
const commandArray =
28+
typeof command === 'string'
29+
? splitShellCommand(command)
30+
: command;
31+
await program(commandArray, processApi);
2632
childProcess.emit('spawn', true);
2733
});
2834
return childProcess;

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

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ export { dirname, joinPaths, basename, normalizePath } from './paths';
55
export { createSpawnHandler } from './create-spawn-handler';
66
export { randomString } from './random-string';
77
export { randomFilename } from './random-filename';
8+
export { splitShellCommand } from './split-shell-command';
89

910
export * from './php-vars';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { splitShellCommand } from './split-shell-command';
2+
3+
describe('splitShellCommand', () => {
4+
it('Should split a shell command into an array', () => {
5+
const command =
6+
'wp post create --post_title="Test post" --post_excerpt="Some content"';
7+
const result = splitShellCommand(command);
8+
expect(result).toEqual([
9+
'wp',
10+
'post',
11+
'create',
12+
'--post_title=Test post',
13+
'--post_excerpt=Some content',
14+
]);
15+
});
16+
17+
it('Should treat multiple spaces as a single space', () => {
18+
const command = 'ls --wordpress --playground --is-great';
19+
const result = splitShellCommand(command);
20+
expect(result).toEqual([
21+
'ls',
22+
'--wordpress',
23+
'--playground',
24+
'--is-great',
25+
]);
26+
});
27+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Naive shell command parser.
3+
* Ensures that commands like `wp option set blogname "My blog name"` are split into
4+
* `['wp', 'option', 'set', 'blogname', 'My blog name']` instead of
5+
* `['wp', 'option', 'set', 'blogname', 'My', 'blog', 'name']`.
6+
*
7+
* @param command
8+
* @returns
9+
*/
10+
export function splitShellCommand(command: string) {
11+
const MODE_NORMAL = 0;
12+
const MODE_IN_QUOTE = 1;
13+
14+
let mode = MODE_NORMAL;
15+
let quote = '';
16+
17+
const parts: string[] = [];
18+
let currentPart = '';
19+
for (let i = 0; i < command.length; i++) {
20+
const char = command[i];
21+
if (mode === MODE_NORMAL) {
22+
if (char === '"' || char === "'") {
23+
mode = MODE_IN_QUOTE;
24+
quote = char;
25+
} else if (char.match(/\s/)) {
26+
if (currentPart) {
27+
parts.push(currentPart);
28+
}
29+
currentPart = '';
30+
} else {
31+
currentPart += char;
32+
}
33+
} else if (mode === MODE_IN_QUOTE) {
34+
if (char === '\\') {
35+
i++;
36+
currentPart += command[i];
37+
} else if (char === quote) {
38+
mode = MODE_NORMAL;
39+
quote = '';
40+
} else {
41+
currentPart += char;
42+
}
43+
}
44+
}
45+
if (currentPart) {
46+
parts.push(currentPart);
47+
}
48+
return parts;
49+
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NodePHP } from '@php-wasm/node';
2-
import { splitShellCommand, wpCLI } from './wp-cli';
2+
import { wpCLI } from './wp-cli';
33
import { readFileSync } from 'fs';
44
import { join } from 'path';
55
import { unzip } from './unzip';
@@ -32,29 +32,3 @@ describe('Blueprint step wpCLI', () => {
3232
expect(result.text).toMatch(/Success: Created post/);
3333
});
3434
});
35-
36-
describe('splitShellCommand', () => {
37-
it('Should split a shell command into an array', () => {
38-
const command =
39-
'wp post create --post_title="Test post" --post_excerpt="Some content"';
40-
const result = splitShellCommand(command);
41-
expect(result).toEqual([
42-
'wp',
43-
'post',
44-
'create',
45-
'--post_title=Test post',
46-
'--post_excerpt=Some content',
47-
]);
48-
});
49-
50-
it('Should treat multiple spaces as a single space', () => {
51-
const command = 'ls --wordpress --playground --is-great';
52-
const result = splitShellCommand(command);
53-
expect(result).toEqual([
54-
'ls',
55-
'--wordpress',
56-
'--playground',
57-
'--is-great',
58-
]);
59-
});
60-
});

packages/playground/blueprints/src/lib/steps/wp-cli.ts

+1-51
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { PHPResponse } from '@php-wasm/universal';
22
import { StepHandler } from '.';
3-
import { phpVar } from '@php-wasm/util';
3+
import { phpVar, splitShellCommand } from '@php-wasm/util';
44

55
/**
66
* @inheritDoc wpCLI
@@ -86,53 +86,3 @@ export const wpCLI: StepHandler<WPCLIStep, Promise<PHPResponse>> = async (
8686

8787
return result;
8888
};
89-
90-
/**
91-
* Naive shell command parser.
92-
* Ensures that commands like `wp option set blogname "My blog name"` are split into
93-
* `['wp', 'option', 'set', 'blogname', 'My blog name']` instead of
94-
* `['wp', 'option', 'set', 'blogname', 'My', 'blog', 'name']`.
95-
*
96-
* @param command
97-
* @returns
98-
*/
99-
export function splitShellCommand(command: string) {
100-
const MODE_NORMAL = 0;
101-
const MODE_IN_QUOTE = 1;
102-
103-
let mode = MODE_NORMAL;
104-
let quote = '';
105-
106-
const parts: string[] = [];
107-
let currentPart = '';
108-
for (let i = 0; i < command.length; i++) {
109-
const char = command[i];
110-
if (mode === MODE_NORMAL) {
111-
if (char === '"' || char === "'") {
112-
mode = MODE_IN_QUOTE;
113-
quote = char;
114-
} else if (char.match(/\s/)) {
115-
if (currentPart) {
116-
parts.push(currentPart);
117-
}
118-
currentPart = '';
119-
} else {
120-
currentPart += char;
121-
}
122-
} else if (mode === MODE_IN_QUOTE) {
123-
if (char === '\\') {
124-
i++;
125-
currentPart += command[i];
126-
} else if (char === quote) {
127-
mode = MODE_NORMAL;
128-
quote = '';
129-
} else {
130-
currentPart += char;
131-
}
132-
}
133-
}
134-
if (currentPart) {
135-
parts.push(currentPart);
136-
}
137-
return parts;
138-
}

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

+1-1
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

-2
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(

0 commit comments

Comments
 (0)