Skip to content

Conversation

@fredrikekelund
Copy link
Contributor

@fredrikekelund fredrikekelund commented Mar 5, 2025

Related issues

Proposed Changes

First, a quick recap of how SymlinkManager works. Symlinks are technically preserved when mounting the site directory in Playground's virtual file system. However, symlinks are in reality just a special type of file with a target path, and if the target path is outside the document root, it's not accessible in Playground's FS. This is where SymlinkManager comes in – it creates a directory inside Playground's FS at the target path and mounts the target path from the host filesystem there.

This PR specifically fixes an issue that was introduced when we recently upgraded our Playground dependencies (#959). After the upgrade, the php.readlink function started returning the correct value, which is the symlink's exact, unresolved target path (i.e. it suddenly returned relative paths sometimes).

This PR changes the SymlinkManager::addSymlink method to resolve the path returned from php.readlink relative to the directory where the symlink lives (i.e. how symlinks are normally handled).

I've also added a SymlinkManager::isTargetInsideDocumentRoot method that allows us to skip mounting symlink target paths that are already accessible inside the document root. Should make SymlinkManager more efficient.After working on fixing the failing tests, I realized that SymlinkManager::mountTarget already has this performance optimization in that it checks if the file or directory already exists in the Playground FS before attempting to mount it.

Testing Instructions

First, let's test the specific case that was reported to us:

  1. Have a Studio site
  2. Download this plugin and unpack it to wp-content/plugins
  3. Run npm install inside the plugin directory
  4. Start the Studio site and ensure that it doesn't encounter any errors

Secondly, let's test a typical symlink targeting a directory outside the document root:

  1. Have a Studio site where Yoast SEO is not already installed
  2. Download Yoast SEO and extract it to ~/wordpress-seo
  3. In your site's wp-content/plugins directory, run ln -s ~/wordpress-seo
  4. Start the Studio site
  5. Navigate to /wp-admin/plugins.php
  6. Ensure that the Yoast SEO plugin is available and that you can activate it without error

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

@fredrikekelund fredrikekelund requested review from a team and bgrgicak March 5, 2025 11:19
@fredrikekelund fredrikekelund self-assigned this Mar 5, 2025
);
// For some reason, we must access the target directory after mounting it, otherwise the
// mount will not work.
this.php.listFiles( vfsTarget );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bgrgicak @adamziel @ashfame, I'm not sure why this is needed, but the Yoast SEO test case (see PR description) didn't work without it in my testing. Any idea why that's the case..?

Copy link
Contributor

Choose a reason for hiding this comment

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

It continues to work for me when I comment out this line. Maybe it depends on the underlying filesystem and we use different ones?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To quickly double-check: did you restart the entire app after commenting out this line?

Copy link
Member

Choose a reason for hiding this comment

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

I was able to mount and start a site with wp-data-layer-app-pages plugin without listing the files.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, the wp-data-layer-app-pages test case works without this line for me, too. It's only the Yoast test case that was troublesome

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We may find that this line is unnecessary after further testing, but I have a hard time seeing how it would break anything, so I'll land the PR with this included and we can keep the discussion going after merging.


// Get the realpath of the symlink within the PHP runtime.
const vfsTarget = this.php.readlink( vfsPath );
const vfsTarget = path.posix.resolve( path.dirname( vfsPath ), this.php.readlink( vfsPath ) );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

php.realpath does not accomplish the same thing (confirmed in my testing). I believe it just returns false because the target path does not yet exist at this point.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To expand on this: realpath works as a more compact way of expressing this logic if the target path is already within the document root. If this is not the case, realpath returns false because the target does not yet exist (see PHP docs).

The SymlinkManager::isTargetInsideDocumentRoot added in this PR stems from the realization that if the target path is already within the document root, there's no need for SymlinkManager to intervene. This is a performance optimization that should help speed up server starts for sites with many symlinks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Following up about SymlinkManager::isTargetInsideDocumentRoot: I realized that SymlinkManager::mountTarget already has this performance optimization in that it checks if the file or directory already exists in the Playground FS before attempting to mount it. Hence, I removed this method.

@wojtekn
Copy link
Contributor

wojtekn commented Mar 5, 2025

It fixes both cases for me.

Copy link
Member

@sejas sejas left a comment

Choose a reason for hiding this comment

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

Solves the issue and I can start the site without any ../acorn/bin/acorn symlink error. 👍

);
// For some reason, we must access the target directory after mounting it, otherwise the
// mount will not work.
this.php.listFiles( vfsTarget );
Copy link
Member

Choose a reason for hiding this comment

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

I was able to mount and start a site with wp-data-layer-app-pages plugin without listing the files.

@ivan-ottinger ivan-ottinger self-requested a review March 5, 2025 12:44
Copy link
Contributor

@ivan-ottinger ivan-ottinger left a comment

Choose a reason for hiding this comment

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

The changes test well. I haven't been able to reproduce the issue anymore. 👍🏼

Symlinked plugin works well as well.

I did not observe any regressions.

@fredrikekelund fredrikekelund merged commit 85b1ced into trunk Mar 5, 2025
8 checks passed
@fredrikekelund fredrikekelund deleted the f26d/fix-relative-symlink-targets branch March 5, 2025 12:47
@adamziel
Copy link

adamziel commented Mar 5, 2025

Does that solve for Studio problems with symlinks?

@fredrikekelund
Copy link
Contributor Author

@adamziel, not entirely. See https://github.com/Automattic/dotcom-forge/issues/10566 and #548. I'm anticipating that we'll need some assistance with Windows support – the issue description should give you a decent idea of what the problem is currently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

6 participants