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
7 changes: 7 additions & 0 deletions packages/php-wasm/node/src/lib/node-fs-mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
FSHelpers,
type MountHandler,
} from '@php-wasm/universal';
import { isParentOf } from '@php-wasm/util';
import { lstatSync } from 'fs';
import { dirname } from 'path';

Expand Down Expand Up @@ -53,6 +54,12 @@ export function createNodeFsMountHandler(localPath: string): MountHandler {
FS!.unmount(vfsMountPoint);
if (removeVfsNode) {
if (FS.isDir(lookup.node.mode)) {
if (isParentOf(vfsMountPoint, FS.cwd())) {
throw new Error(
`Cannot remove the VFS directory "${vfsMountPoint}" on umount cleanup – it is a parent of the CWD "${FS.cwd()}". Change CWD before ` +
`unmounting or explicitly disable post-unmount node cleanup with createNodeFsMountHandler(path, {cleanupNodesOnUnmount: false}).`
);
}
FS.rmdir(vfsMountPoint);
} else {
FS.unlink(vfsMountPoint);
Expand Down
70 changes: 58 additions & 12 deletions packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ describe.each([true, false])(

const php = new PHP(await recreateRuntime());
php.enableRuntimeRotation({
cwd: '/test-root',
recreateRuntime: recreateRuntimeSpy,
maxRequests: 10,
});
Expand Down Expand Up @@ -88,7 +87,6 @@ describe.each([true, false])(

const php = new PHP(await recreateRuntime());
php.enableRuntimeRotation({
cwd: '/test-root',
recreateRuntime: recreateRuntimeSpy,
maxRequests: 10,
});
Expand Down Expand Up @@ -121,7 +119,6 @@ describe.each([true, false])(

const php = new PHP(await recreateRuntime());
php.enableRuntimeRotation({
cwd: '/wordpress',
recreateRuntime: recreateRuntimeSpy,
maxRequests: 10,
});
Expand Down Expand Up @@ -228,7 +225,6 @@ describe.each([true, false])(
// Rotate the PHP runtime
const php = new PHP(await recreateRuntime());
php.enableRuntimeRotation({
cwd: '/test-root',
recreateRuntime: recreateRuntimeSpy,
maxRequests: 1000,
});
Expand Down Expand Up @@ -257,7 +253,6 @@ describe.each([true, false])(
const recreateRuntimeSpy = vitest.fn(recreateRuntime);
const php = new PHP(await recreateRuntimeSpy());
php.enableRuntimeRotation({
cwd: '/test-root',
recreateRuntime: recreateRuntimeSpy,
maxRequests: 1,
});
Expand All @@ -271,7 +266,6 @@ describe.each([true, false])(
const recreateRuntimeSpy = vitest.fn(recreateRuntime);
const php = new PHP(await recreateRuntimeSpy());
php.enableRuntimeRotation({
cwd: '/test-root',
recreateRuntime: recreateRuntimeSpy,
maxRequests: 1234,
});
Expand All @@ -289,7 +283,6 @@ describe.each([true, false])(
const recreateRuntimeSpy = vitest.fn(recreateRuntime);
const php = new PHP(await recreateRuntimeSpy());
php.enableRuntimeRotation({
cwd: '/test-root',
recreateRuntime: recreateRuntimeSpy,
maxRequests: 1234,
});
Expand Down Expand Up @@ -318,7 +311,6 @@ describe.each([true, false])(
});
const php = new PHP(await recreateRuntimeSpy());
php.enableRuntimeRotation({
cwd: '/test-root',
recreateRuntime: recreateRuntimeSpy,
maxRequests: 1,
});
Expand All @@ -339,7 +331,6 @@ describe.each([true, false])(
it('Should preserve the custom SAPI name', async () => {
const php = new PHP(await recreateRuntime());
php.enableRuntimeRotation({
cwd: '/test-root',
recreateRuntime,
maxRequests: 1,
});
Expand All @@ -356,7 +347,6 @@ describe.each([true, false])(
it('Should preserve the MEMFS files', async () => {
const php = new PHP(await recreateRuntime());
php.enableRuntimeRotation({
cwd: '/test-root',
recreateRuntime,
maxRequests: 1,
});
Expand All @@ -379,7 +369,6 @@ describe.each([true, false])(
it('Should not overwrite the NODEFS files', async () => {
const php = new PHP(await recreateRuntime());
php.enableRuntimeRotation({
cwd: '/test-root',
recreateRuntime,
maxRequests: 1,
});
Expand Down Expand Up @@ -424,7 +413,6 @@ describe.each([true, false])(
it('Should preserve the spawn handler through PHP runtime recreation', async () => {
const php = new PHP(await recreateRuntime());
php.enableRuntimeRotation({
cwd: '/test-root',
recreateRuntime,
maxRequests: 1,
});
Expand Down Expand Up @@ -465,5 +453,63 @@ describe.each([true, false])(
expect(result2.text).toBe('Hello Again');
expect(spawnHandlerCallCount).toBe(2);
}, 30_000);

it('Should preserve NODEFS mount when CWD is the same as mount point', async () => {
const php = new PHP(await recreateRuntime());
php.enableRuntimeRotation({
recreateRuntime,
maxRequests: 1,
});

// Create a temporary directory to mount
const tempDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'nodefs-mount-')
);
const testFile = path.join(tempDir, 'test.txt');
fs.writeFileSync(testFile, 'Hello from NODEFS');

try {
// Mount the temp directory at /wordpress
await php.mount(
'/wordpress',
createNodeFsMountHandler(tempDir)
);

// Set CWD to the mount point
php.chdir('/wordpress');
expect(php.cwd()).toBe('/wordpress');

// Verify the mounted file is accessible
expect(php.fileExists('/wordpress/test.txt')).toBe(true);
expect(php.readFileAsText('/wordpress/test.txt')).toBe(
'Hello from NODEFS'
);

// Trigger runtime rotation by making a request
await php.run({ code: `<?php echo "Triggering rotation";` });
await php.run({ code: `<?php echo "Triggering rotation";` });

// Verify CWD is preserved after rotation
expect(php.cwd()).toBe('/wordpress');

// Verify the mount is still accessible after rotation
expect(php.fileExists('/wordpress/test.txt')).toBe(true);
expect(php.readFileAsText('/wordpress/test.txt')).toBe(
'Hello from NODEFS'
);

// Test that we can still write to the mounted directory
php.writeFile(
'/wordpress/new-file.txt',
'Created after rotation'
);
expect(
fs.readFileSync(path.join(tempDir, 'new-file.txt'), 'utf8')
).toBe('Created after rotation');
} finally {
fs.rmSync(tempDir, { recursive: true });
php.exit();
}
}, 30_000);
}
);
34 changes: 31 additions & 3 deletions packages/php-wasm/universal/src/lib/php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ export class PHP implements Disposable {
needsRotating: boolean;
maxRequests: number;
requestsMade: number;
cwd?: string;
} = {
enabled: false,
recreateRuntime: () => 0,
Expand Down Expand Up @@ -1315,14 +1314,12 @@ export class PHP implements Disposable {
enableRuntimeRotation(options: {
recreateRuntime: () => Promise<number> | number;
maxRequests?: number;
cwd?: string;
}) {
this.#rotationOptions = {
...this.#rotationOptions,
enabled: true,
recreateRuntime: options.recreateRuntime,
maxRequests: options.maxRequests ?? 400,
cwd: options.cwd,
};
}

Expand Down Expand Up @@ -1358,6 +1355,27 @@ export class PHP implements Disposable {
const oldRootLevelPaths = this.listFiles('/').map((file) => `/${file}`);
const oldSpawnProcess = this[__private__dont__use].spawnProcess;

// Temporarily set CWD to / and restore it at the end of this method.
//
// There's a chance cleaning up old mounts via mount.unmount()
// will attempt removing the CWD. Normally, this would throw
// FS.ErrnoError(10) EBUSY and interrupt the PHP runtime rotation,
// leaving us in a broken state.
//
// Even though removing the CWD directory is not allowed by the
// filesystem, we don't care that much here – we're merely freeing
// all the resources allocated by the old filesystem before it's
// garbage collected. We are about to recreate the same filesystem
// structure and mounts in another PHP runtime.
//
// Therefore, let's suspend the strict EBUSY check by setting the CWD
// to / for the cleanup purposes. We'll attempt to restore the original
// CWD on the new runtime once we re-apply all the mounts there. We'll
// only have a real reason to throw an error if the CWD path does not
// exist in the new filesystem after the rotation.
const oldCWD = oldFS.cwd();
oldFS.chdir('/');

// Unmount all the mount handlers
const mountHandlers: { mountHandler: MountHandler; vfsPath: string }[] =
[];
Expand Down Expand Up @@ -1406,6 +1424,16 @@ export class PHP implements Disposable {
this.mkdir(vfsPath);
await this.mount(vfsPath, mountHandler);
}
try {
newFs.chdir(oldCWD);
} catch (e) {
throw new Error(
`Failed to restore CWD to ${oldCWD} after PHP runtime rotation.`,
{
cause: e,
}
);
}
}

/**
Expand Down
2 changes: 0 additions & 2 deletions packages/php-wasm/universal/src/lib/rotate-php-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@ export interface RotateOptions {
*/
export function rotatePHPRuntime({
php,
cwd,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was unused anyway.

recreateRuntime,
maxRequests = 400,
}: RotateOptions) {
return php.enableRuntimeRotation({
recreateRuntime,
maxRequests,
cwd,
});
}
1 change: 0 additions & 1 deletion packages/playground/wordpress/src/boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,6 @@ export async function bootRequestHandler(options: BootRequestHandlerOptions) {
// Rotate the PHP runtime periodically to avoid memory leak-related crashes.
// @see https://github.com/WordPress/wordpress-playground/pull/990 for more context
php.enableRuntimeRotation({
cwd: requestHandler.documentRoot,
recreateRuntime: options.createPhpRuntime,
maxRequests: 400,
});
Expand Down