Skip to content

Commit 9964612

Browse files
committed
OS-based spawn handler in CLI worker
1 parent 10277ed commit 9964612

File tree

2 files changed

+210
-50
lines changed

2 files changed

+210
-50
lines changed

packages/playground/cli/src/blueprints-v1/worker-thread-v1.ts

Lines changed: 180 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ import {
77
consumeAPI,
88
consumeAPISync,
99
exposeAPI,
10-
sandboxedSpawnHandlerFactory,
1110
} from '@php-wasm/universal';
12-
import { sprintf } from '@php-wasm/util';
11+
import { createSpawnHandler, sprintf } from '@php-wasm/util';
1312
import { RecommendedPHPVersion } from '@wp-playground/common';
1413
import {
1514
type WordPressInstallMode,
@@ -21,6 +20,8 @@ import { jspi } from 'wasm-feature-detect';
2120
import { MessageChannel, type MessagePort, parentPort } from 'worker_threads';
2221
import { mountResources } from '../mounts';
2322
import { logger } from '@php-wasm/logger';
23+
import { spawn } from 'child_process';
24+
import { type SpawnedWorker, spawnWorkerThread } from '../run-cli';
2425

2526
export interface Mount {
2627
hostPath: string;
@@ -126,23 +127,24 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
126127
}
127128
}
128129

129-
async bootAndSetUpInitialWorker({
130-
siteUrl,
131-
mountsBeforeWpInstall,
132-
mountsAfterWpInstall,
133-
phpVersion: php = RecommendedPHPVersion,
134-
wordpressInstallMode,
135-
wordPressZip,
136-
sqliteIntegrationPluginZip,
137-
firstProcessId,
138-
processIdSpaceLength,
139-
dataSqlPath,
140-
followSymlinks,
141-
trace,
142-
internalCookieStore,
143-
withXdebug,
144-
nativeInternalDirPath,
145-
}: PrimaryWorkerBootOptions) {
130+
async bootAndSetUpInitialWorker(options: PrimaryWorkerBootOptions) {
131+
const {
132+
siteUrl,
133+
mountsBeforeWpInstall,
134+
mountsAfterWpInstall,
135+
phpVersion: php = RecommendedPHPVersion,
136+
wordpressInstallMode,
137+
wordPressZip,
138+
sqliteIntegrationPluginZip,
139+
firstProcessId,
140+
processIdSpaceLength,
141+
dataSqlPath,
142+
followSymlinks,
143+
trace,
144+
internalCookieStore,
145+
withXdebug,
146+
nativeInternalDirPath,
147+
} = options;
146148
if (this.booted) {
147149
throw new Error('Playground already booted');
148150
}
@@ -192,7 +194,7 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
192194
? new File(
193195
[sqliteIntegrationPluginZip],
194196
'sqlite-integration-plugin.zip'
195-
)
197+
)
196198
: undefined,
197199
sapiName: 'cli',
198200
createFiles: {
@@ -207,7 +209,75 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
207209
},
208210
cookieStore: internalCookieStore ? undefined : false,
209211
dataSqlPath,
210-
spawnHandler: sandboxedSpawnHandlerFactory,
212+
spawnHandler: () =>
213+
createSpawnHandler(async (args, processApi, options) => {
214+
console.log('primary worker', { args });
215+
processApi.notifySpawn();
216+
if (args[0] === 'exec') {
217+
args.shift();
218+
}
219+
220+
if (
221+
args[0].endsWith('.php') ||
222+
args[0].endsWith('.phar')
223+
) {
224+
args.unshift('php');
225+
}
226+
227+
const binaryName = args[0].split('/').pop();
228+
if (binaryName !== 'php') {
229+
throw new Error(
230+
`Unsupported binary: ${binaryName}. Only PHP is supported for now.`
231+
);
232+
}
233+
234+
const newPhpProcess = spawn(process.argv[0], args, {
235+
...options,
236+
stdio: ['pipe', 'pipe', 'pipe'],
237+
});
238+
const subPhpPort = await new Promise<MessagePort>(
239+
(resolve, reject) => {
240+
newPhpProcess.addListener(
241+
'message',
242+
(message: any) => {
243+
if (
244+
message.command ===
245+
'worker-script-initialized'
246+
) {
247+
resolve(message.phpPort);
248+
}
249+
}
250+
);
251+
newPhpProcess.once('error', (e: Error) => {
252+
reject(
253+
new Error(
254+
`Worker failed to initialize: ${e.message}`
255+
)
256+
);
257+
});
258+
}
259+
);
260+
261+
const handler =
262+
consumeAPI<PlaygroundCliBlueprintV1Worker>(
263+
subPhpPort
264+
);
265+
handler.useFileLockManager(this.fileLockManager as any);
266+
await handler.bootWorker({
267+
phpVersion: php,
268+
siteUrl,
269+
mountsBeforeWpInstall,
270+
mountsAfterWpInstall,
271+
firstProcessId,
272+
processIdSpaceLength,
273+
followSymlinks,
274+
trace,
275+
nativeInternalDirPath,
276+
});
277+
278+
handler.cli(['php', '-v']);
279+
console.log(await handler.hello());
280+
}),
211281
async onPHPInstanceCreated(php) {
212282
await mountResources(php, mountsBeforeWpInstall);
213283
if (wordpressBooted) {
@@ -234,6 +304,10 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
234304
}
235305
}
236306

307+
async hello() {
308+
return 'hello';
309+
}
310+
237311
async bootWorker(args: WorkerBootOptions) {
238312
await this.bootRequestHandler(args);
239313
}
@@ -291,7 +365,89 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
291365
},
292366
sapiName: 'cli',
293367
cookieStore: false,
294-
spawnHandler: sandboxedSpawnHandlerFactory,
368+
spawnHandler: () =>
369+
createSpawnHandler(async (args, processApi) => {
370+
console.log('secondary worker', { args });
371+
if (args[0] === 'exec') {
372+
args.shift();
373+
}
374+
375+
if (
376+
args[0].endsWith('.php') ||
377+
args[0].endsWith('.phar')
378+
) {
379+
args.unshift('php');
380+
}
381+
382+
const binaryName = args[0].split('/').pop();
383+
if (binaryName !== 'php') {
384+
throw new Error(
385+
`Unsupported binary: ${binaryName}. Only PHP is supported for now.`
386+
);
387+
}
388+
389+
let cliCalled = false;
390+
let spawnedWorker: SpawnedWorker | undefined =
391+
undefined;
392+
try {
393+
spawnedWorker = await spawnWorkerThread('v1', {
394+
onExit: () => {
395+
if (cliCalled) {
396+
// We're already handling the exit code using
397+
// the cliResponse.exitCode promise.
398+
return;
399+
}
400+
// The process died before we could call cli().
401+
// Let's exit with an error code.
402+
processApi.exit(1);
403+
},
404+
});
405+
} catch (e) {
406+
processApi.exit(1);
407+
throw e;
408+
}
409+
410+
const handler =
411+
consumeAPI<PlaygroundCliBlueprintV1Worker>(
412+
spawnedWorker.phpPort
413+
);
414+
handler.useFileLockManager(this.fileLockManager as any);
415+
await handler.bootWorker({
416+
phpVersion: phpVersion,
417+
siteUrl,
418+
mountsBeforeWpInstall,
419+
mountsAfterWpInstall,
420+
firstProcessId,
421+
processIdSpaceLength,
422+
followSymlinks,
423+
trace,
424+
nativeInternalDirPath,
425+
});
426+
427+
processApi.notifySpawn();
428+
429+
const cliResponse = await handler.cli(args, {
430+
env: process.env as Record<string, string>,
431+
});
432+
cliResponse.stdout.pipeTo(
433+
new WritableStream({
434+
write(chunk) {
435+
processApi.stdout(chunk);
436+
},
437+
})
438+
);
439+
cliResponse.stderr.pipeTo(
440+
new WritableStream({
441+
write(chunk) {
442+
processApi.stderr(chunk);
443+
},
444+
})
445+
);
446+
await cliResponse.exitCode.finally(async () => {
447+
processApi.exit(await cliResponse.exitCode);
448+
});
449+
cliCalled = true;
450+
}),
295451
});
296452
this.__internal_setRequestHandler(requestHandler);
297453

@@ -330,3 +486,5 @@ parentPort?.postMessage(
330486
},
331487
[phpChannel.port2 as any]
332488
);
489+
490+
console.log('Worker script initialized!');

packages/playground/cli/src/run-cli.ts

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,37 +1134,14 @@ async function spawnWorkerThreads(
11341134
): Promise<SpawnedWorker[]> {
11351135
const promises = [];
11361136
for (let i = 0; i < count; i++) {
1137-
const worker = await spawnWorkerThread(workerType);
11381137
const onExit: (code: number) => void = (code: number) => {
11391138
onWorkerExit({
11401139
exitCode: code,
11411140
workerIndex: i,
11421141
});
11431142
};
1144-
promises.push(
1145-
new Promise<{ worker: Worker; phpPort: NodeMessagePort }>(
1146-
(resolve, reject) => {
1147-
worker.once('message', function (message: any) {
1148-
// Let the worker confirm it has initialized.
1149-
// We could use the 'online' event to detect start of JS execution,
1150-
// but that would miss initialization errors.
1151-
if (message.command === 'worker-script-initialized') {
1152-
resolve({ worker, phpPort: message.phpPort });
1153-
}
1154-
});
1155-
worker.once('error', function (e: Error) {
1156-
console.error(e);
1157-
const error = new Error(
1158-
`Worker failed to load worker. ${
1159-
e.message ? `Original error: ${e.message}` : ''
1160-
}`
1161-
);
1162-
reject(error);
1163-
});
1164-
worker.once('exit', onExit);
1165-
}
1166-
)
1167-
);
1143+
const worker = spawnWorkerThread(workerType, { onExit });
1144+
promises.push(worker);
11681145
}
11691146
return Promise.all(promises);
11701147
}
@@ -1180,7 +1157,10 @@ async function spawnWorkerThreads(
11801157
* @param workerType
11811158
* @returns
11821159
*/
1183-
async function spawnWorkerThread(workerType: 'v1' | 'v2') {
1160+
export function spawnWorkerThread(
1161+
workerType: 'v1' | 'v2',
1162+
{ onExit }: { onExit: (code: number) => void }
1163+
) {
11841164
/**
11851165
* When running the CLI from source via `node cli.ts`, the Vite-provided
11861166
* __WORKER_V1_URL__ and __WORKER_V2_URL__ are undefined. Let's set them to
@@ -1194,11 +1174,33 @@ async function spawnWorkerThread(workerType: 'v1' | 'v2') {
11941174
// @ts-expect-error
11951175
globalThis['__WORKER_V2_URL__'] = './blueprints-v2/worker-thread-v2.ts';
11961176
}
1177+
let worker: Worker;
11971178
if (workerType === 'v1') {
1198-
return new Worker(new URL(__WORKER_V1_URL__, import.meta.url));
1179+
worker = new Worker(new URL(__WORKER_V1_URL__, import.meta.url));
11991180
} else {
1200-
return new Worker(new URL(__WORKER_V2_URL__, import.meta.url));
1181+
worker = new Worker(new URL(__WORKER_V2_URL__, import.meta.url));
12011182
}
1183+
1184+
return new Promise<SpawnedWorker>((resolve, reject) => {
1185+
worker.once('message', function (message: any) {
1186+
// Let the worker confirm it has initialized.
1187+
// We could use the 'online' event to detect start of JS execution,
1188+
// but that would miss initialization errors.
1189+
if (message.command === 'worker-script-initialized') {
1190+
resolve({ worker, phpPort: message.phpPort });
1191+
}
1192+
});
1193+
worker.once('error', function (e: Error) {
1194+
console.error(e);
1195+
const error = new Error(
1196+
`Worker failed to load worker. ${
1197+
e.message ? `Original error: ${e.message}` : ''
1198+
}`
1199+
);
1200+
reject(error);
1201+
});
1202+
worker.once('exit', onExit);
1203+
});
12021204
}
12031205

12041206
/**

0 commit comments

Comments
 (0)