Skip to content
Merged
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
76 changes: 76 additions & 0 deletions packages/php-wasm/node/src/test/php.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import {
getPhpIniEntries,
loadPHPRuntime,
PHP,
PHPProcessManager,
sandboxedSpawnHandlerFactory,
setPhpIniEntries,
type SpawnedPHP,
SupportedPHPVersions,
} from '@php-wasm/universal';
import { createSpawnHandler, joinPaths, phpVar } from '@php-wasm/util';
Expand All @@ -21,6 +24,7 @@ import { vi } from 'vitest';
import { getPHPLoaderModule, loadNodeRuntime } from '..';
import type { PHPLoaderOptions } from '..';
import { createNodeFsMountHandler } from '../lib/node-fs-mount';
import { RecommendedPHPVersion } from '@wp-playground/common';

const testDirPath = '/__test987654321';
const testFilePath = '/__test987654321.txt';
Expand Down Expand Up @@ -2978,3 +2982,75 @@ phpLoaderOptions.forEach((options) => {
});
});
});

describe('sandboxedSpawnHandlerFactory', () => {
const phpVersion = RecommendedPHPVersion;
let php: PHP;
let spawnedPhp: SpawnedPHP;
let processManager: PHPProcessManager;
beforeEach(async () => {
processManager = new PHPProcessManager({
phpFactory: async () => {
const php = new PHP(
await loadNodeRuntime(phpVersion as any, {})
);
php.mkdir('/tmp/shared-test-directory');
php.chdir('/tmp/shared-test-directory');

php.writeFile(
'/tmp/shared-test-directory/README.md',
'Hello, world!'
);
php.mkdir('/tmp/shared-test-directory/code');
php.writeFile(
'/tmp/shared-test-directory/code/index.php',
'Hello, world!'
);
await php.setSpawnHandler(
sandboxedSpawnHandlerFactory(processManager)
);
return php;
},
maxPhpInstances: 5,
});
spawnedPhp = await processManager.acquirePHPInstance();
php = spawnedPhp.php;
});
afterEach(async () => {
await processManager[Symbol.asyncDispose]();
spawnedPhp?.reap();
});
it.each([
// Default cwd
{
command: 'ls',
expected: ['README.md', 'code', ''].join('\n'),
},
// Explicit path
{
command: 'ls /tmp/shared-test-directory',
expected: ['README.md', 'code', ''].join('\n'),
},
// Subdirectory – we expect a different output
{
command: 'ls /tmp/shared-test-directory/code',
expected: ['index.php', ''].join('\n'),
},
// pwd
{
command: 'pwd',
expected: '/tmp/shared-test-directory\n',
},
])('should be able to run "$command"', async ({ command, expected }) => {
const response = await php.run({
code: `<?php
$output = shell_exec(getenv('COMMAND'));
echo $output;
`,
env: {
COMMAND: command,
},
});
expect(response.text).toEqual(expected);
});
});
9 changes: 9 additions & 0 deletions packages/php-wasm/universal/src/lib/php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,15 @@ export class PHP implements Disposable {
this[__private__dont__use].FS.chdir(path);
}

/**
* Gets the current working directory in the PHP filesystem.
*
* @returns The current working directory.
*/
cwd() {
return this[__private__dont__use].FS.cwd();
}

/**
* Changes the permissions of a file or directory.
* @param path - The path to the file or directory.
Expand Down
112 changes: 73 additions & 39 deletions packages/php-wasm/universal/src/lib/sandboxed-spawn-handler-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function sandboxedSpawnHandlerFactory(
processApi.stdout(`140`);
processApi.exit(0);
} else if (binaryName === 'less') {
processApi.on('stdin', (data: Uint8Array) => {
processApi.on('stdin', (data) => {
processApi.stdout(data);
});
// Exit after the stdin stream is exhausted.
Expand All @@ -55,49 +55,83 @@ export function sandboxedSpawnHandlerFactory(
});
processApi.exit(0);
return;
} else if (binaryName === 'php') {
const { php, reap } = await processManager.acquirePHPInstance({
considerPrimary: false,
});
}

php.chdir(options.cwd as string);
try {
// Figure out more about setting env, putenv(), etc.
const result = await php.cli(args, {
env: {
...options.env,
SCRIPT_PATH: args[1],
// Set SHELL_PIPE to 0 to ensure WP-CLI formats
// the output as ASCII tables.
// @see https://github.com/wp-cli/wp-cli/issues/1102
SHELL_PIPE: '0',
},
});
if (!['php', 'ls', 'pwd'].includes(binaryName ?? '')) {
// 127 is the exit code "for command not found".
processApi.exit(127);
return;
}

result.stdout.pipeTo(
new WritableStream({
write(chunk) {
processApi.stdout(chunk);
},
})
);
result.stderr.pipeTo(
new WritableStream({
write(chunk) {
processApi.stderr(chunk);
const { php, reap } = await processManager.acquirePHPInstance({
considerPrimary: false,
});

try {
if (options.cwd) {
php.chdir(options.cwd as string);
}

const cwd = php.cwd();
switch (binaryName) {
case 'php': {
// Figure out more about setting env, putenv(), etc.
const result = await php.cli(args, {
env: {
...options.env,
SCRIPT_PATH: args[1],
// Set SHELL_PIPE to 0 to ensure WP-CLI formats
// the output as ASCII tables.
// @see https://github.com/wp-cli/wp-cli/issues/1102
SHELL_PIPE: '0',
},
})
);
processApi.exit(await result.exitCode);
} catch (e) {
// An exception here means the PHP runtime has crashed.
processApi.exit(1);
throw e;
} finally {
reap();
});

result.stdout.pipeTo(
new WritableStream({
write(chunk) {
processApi.stdout(chunk as any as ArrayBuffer);
},
})
);
result.stderr.pipeTo(
new WritableStream({
write(chunk) {
processApi.stderr(chunk as any as ArrayBuffer);
},
})
);
processApi.exit(await result.exitCode);
break;
}
case 'ls': {
const files = php.listFiles(args[1] ?? cwd);
for (const file of files) {
processApi.stdout(file + '\n');
}
// Technical limitation of subprocesses – we need to
// wait before exiting to give consumer a chance to read
// the output.
await new Promise((resolve) => setTimeout(resolve, 10));
processApi.exit(0);
break;
}
case 'pwd': {
processApi.stdout(cwd + '\n');
// Technical limitation of subprocesses – we need to
// wait before exiting to give consumer a chance to read
// the output.
await new Promise((resolve) => setTimeout(resolve, 10));
processApi.exit(0);
break;
}
}
} else {
} catch (e) {
// An exception here means the PHP runtime has crashed.
processApi.exit(1);
throw e;
} finally {
reap();
}
});
}