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
53 changes: 53 additions & 0 deletions packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,59 @@ describe.each([true, false])(
).toBe('plugin-3');
});

it('Preserves a nested NODEFS mount through PHP runtime recreation', async () => {
// Rotate the PHP runtime
const recreateRuntimeSpy = vitest.fn(recreateRuntime);

const php = new PHP(await recreateRuntime());
php.enableRuntimeRotation({
recreateRuntime: recreateRuntimeSpy,
maxRequests: 1,
});

// Create a temporary directory and a file in it
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'temp-'));
const dirToMountAsParent = path.join(tempDir, 'parent');
fs.mkdirSync(dirToMountAsParent);
const dirToMountAsNested = path.join(tempDir, 'nested');
fs.mkdirSync(dirToMountAsNested);

// Mount the parent and nested child directories
await php.mount(
'/parent',
createNodeFsMountHandler(dirToMountAsParent)
);
await php.mount(
'/parent/nested',
createNodeFsMountHandler(dirToMountAsNested)
);

const childFile = path.join(dirToMountAsNested, 'nested.php');
const nestedPhpCode = `<?php echo "nested file";`;
fs.writeFileSync(childFile, nestedPhpCode);
const parentFileThatReferencesChildFile = path.join(
dirToMountAsParent,
'test.php'
);
const parentPhpCode = `<?php require_once __DIR__ . '/nested/nested.php';`;
fs.writeFileSync(parentFileThatReferencesChildFile, parentPhpCode);

// Confirm output based on nested NODEFS mount is the same before and after rotation.
const scriptPath = '/parent/test.php';
const firstResult = await php.run({ scriptPath });
expect(firstResult.text).toBe('nested file');
const secondResult = await php.run({ scriptPath });
expect(secondResult.text).toBe('nested file');

expect(recreateRuntimeSpy).toHaveBeenCalledTimes(1);

// Infer that the nested NODEFS mounts are not lost
expect(php.readFileAsText('/parent/nested/nested.php')).toBe(
nestedPhpCode
);
expect(php.readFileAsText(scriptPath)).toBe(parentPhpCode);
});

it('Free up the available PHP memory', async () => {
const freeMemory = (php: PHP) =>
php[__private__dont__use].HEAPU32.reduce(
Expand Down
23 changes: 16 additions & 7 deletions packages/php-wasm/universal/src/lib/php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1398,11 +1398,20 @@ export class PHP implements Disposable {
const oldCWD = oldFS.cwd();
oldFS.chdir('/');

// Unmount all the mount handlers
const mountHandlers: { mountHandler: MountHandler; vfsPath: string }[] =
[];
for (const [vfsPath, mount] of Object.entries(this.#mounts)) {
mountHandlers.push({ mountHandler: mount.mountHandler, vfsPath });
// Remember mounts to apply to new runtime
const mountHandlersToReapplyInOrder = Object.entries(this.#mounts).map(
([vfsPath, mount]) => ({
mountHandler: mount.mountHandler,
vfsPath,
})
);

// Unmount all the mount handlers in reverse order because each nested
// mount depends upon the parent mount which preceded it.
const mountsToUnmountInReverseOrder = Object.values(
this.#mounts
).reverse();
for (const mount of mountsToUnmountInReverseOrder) {
await mount.unmount();
}

Expand Down Expand Up @@ -1441,8 +1450,8 @@ export class PHP implements Disposable {
}
}

// Re-mount all the mount handlers
for (const { mountHandler, vfsPath } of mountHandlers) {
// Re-mount all the mount handlers in order
for (const { mountHandler, vfsPath } of mountHandlersToReapplyInOrder) {
this.mkdir(vfsPath);
await this.mount(vfsPath, mountHandler);
}
Expand Down