Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make polling use the symbolic link target's LastWriteTime #55664

Merged
merged 18 commits into from
Jul 22, 2021

Conversation

jozkee
Copy link
Member

@jozkee jozkee commented Jul 14, 2021

Fixes #36091

Builds on top of #55668

Adding NetCoreAppCurrent to Microsoft.Extensions.FileProviders.Physical and to all the projects it depends on, in order to use the Symbolic Link APIs added in .NET 6.
cc @ericstj @jeffhandley

The fix consists on avoid using the link's LastWriteTimeUtc with the one of its target.

@jozkee jozkee added this to the 6.0.0 milestone Jul 14, 2021
@jozkee jozkee self-assigned this Jul 14, 2021
@ghost
Copy link

ghost commented Jul 14, 2021

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #36091

Adding NetCoreAppCurrent to Microsoft.Extensions.FileProviders.Physical and to all the projects it depends on, in order to use the Symbolic Link APIs added in .NET 6.
cc @ericstj @jeffhandley

Author: Jozkee
Assignees: Jozkee
Labels:

area-System.IO

Milestone: 6.0.0

@@ -38,7 +38,7 @@ public class PollingFileChangeToken : IPollingChangeToken
/// <param name="fileInfo">The <see cref="System.IO.FileInfo"/> to poll</param>
public PollingFileChangeToken(FileInfo fileInfo)
{
_fileInfo = fileInfo;
_fileInfo = FileSystemInfoHelper.ResolveFileLinkTarget(fileInfo) ?? fileInfo;
Copy link
Member

Choose a reason for hiding this comment

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

Will this raise new exceptions if the target of the file is not reachable? Perhaps we can add a test case for that: watching a normal file, watching a non-existent file, watching a symlinked file, watching a symlink with non-existent target.

Copy link
Member Author

Choose a reason for hiding this comment

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

It shouldn't, Exists and LinkTarget != null should guard for that.

if (fileInfo.Exists && fileInfo.LinkTarget != null)
{
FileSystemInfo targetInfo = fileInfo.ResolveLinkTarget(returnFinalTarget: true);
if (targetInfo.Exists)
{
return (FileInfo)targetInfo;
}
}

However since these APIs are not atomic, it could be that the link gets deleted between LinkTarget and ResolveLinkTarget. I don't know if there's an atomic way to do this.

Copy link
Member

Choose a reason for hiding this comment

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

I think it's worse than just not being atomic, since you capture the FileInfo for the lifetime of the token, which might be long.

Here's another scenario. Suppose that the symlink initially points at one target, but then is changed to point at a new target and that new target has a new modified time? I think that's actually the scenario in this issue. If you captured the FileInfo from the initial target you'd miss that change since you'd be checking the original target.

Copy link
Member

Choose a reason for hiding this comment

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

What happens if the symlinks chain goes through an area that you can't read?

touch bar
mkdir foo
cd foo
ln -s ../bar .
cd ..
ln -s baz foo/bar
sha256sum bar baz
chmod a-x foo
sha256sum bar baz

In our world, baz.LinkTarget should resolve (to foo/bar). Does foo/bar then report Exists is false since it can't be read (the directory is no longer allowed to be traversed into)?

Copy link
Member Author

@jozkee jozkee Jul 15, 2021

Choose a reason for hiding this comment

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

In our world, baz.LinkTarget should resolve (to foo/bar). Does foo/bar then report Exists is false since it can't be read (the directory is no longer allowed to be traversed into)?

@bartonjs Yes, that's what I would expect. Therefore this should print false, unless that the FileInfo .ctor does some validation on inaccessible paths:

var targetInfo = File.ResolveLinkTarget("baz", returnFinaltarget: true);
Console.Write(targetInfo.Exists);

Copy link
Member

Choose a reason for hiding this comment

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

Thank you for addressing the link target feedback. Please consider adding test cases for the deleted link target cases, as they seem unique to this addition as it has to handle non-existent link targets, and link targets that get deleted / added.

Copy link
Member Author

@jozkee jozkee Jul 19, 2021

Choose a reason for hiding this comment

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

If the link target does not exists, we are returning the LastWriteTimeUtc from the link itself but I think that doesn't tell us anything, I will change it to return DateTime.MinValue instead (same thing we do when the file does not exists).
Let me know if you think that could be problematic.

I also added the requested tests: 98b737a.

@@ -2,5 +2,6 @@
<Import Project="..\Directory.Build.props" />
<PropertyGroup>
<IsAspNetCoreApp>true</IsAspNetCoreApp>
<UnsupportedOSPlatforms>browser</UnsupportedOSPlatforms>
Copy link
Member

Choose a reason for hiding this comment

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

How is this unsupported on browser? Is the whole assembly not supported on browser?

Copy link
Member Author

Choose a reason for hiding this comment

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

this project references System.IO.FileSystem.Watcher and that is not supported in browser:

<UnsupportedOSPlatforms>browser</UnsupportedOSPlatforms>

Copy link
Member

Choose a reason for hiding this comment

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

I see, so then should we add a runtime assembly for Browser that throws PNSE?

cc: @marek-safar @steveisok

Copy link
Member

Choose a reason for hiding this comment

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

/cc @lewing

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, so then should we add a runtime assembly for Browser that throws PNSE?

Yes which is what this setting does

Copy link
Member

@safern safern Jul 16, 2021

Choose a reason for hiding this comment

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

This setting doesn't generate a PNSE automatically, this setting is just to add an assembly attribute for the platform analyzer. Unless something changed since last time I did it, in order to generate PNSE we need to add a -Browser tfm and then set the flag to generate a pnse when TargetsBrowser == true. So then it seems like we should do that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for catching this, I've addressed @safern's comment in 75fcf96, please let me know if something else (related to mark the project as unsupported in Browser) needs to be done.

// Act 2 - Change link target to file 2.
token = provider.Watch(filter); // Once HasChanged is true, the value will always be true. Get a new change token.
Assert.False(token.HasChanged);
File.Delete(linkPath);
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to reset the target without deleting the link?

Copy link
Member Author

@jozkee jozkee Jul 16, 2021

Choose a reason for hiding this comment

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

Through File.CreateSymbolicLink, it is not. It may be possible to modify the target of an existing link by using FSCTL_SET_REPARSE_POINT, see https://docs.microsoft.com/en-us/windows/win32/fileio/reparse-point-operations.

For Unix, I know that you can use the -f switch in ln -sf bar foo to force the link to be created even if it already exists, but I guess that's just deleting the old link.

Copy link
Member

Choose a reason for hiding this comment

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

https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ln.html says that -f in ln is functionally equivalent to "call unlink", which we call File.Delete.

@@ -38,7 +38,7 @@ public class PollingFileChangeToken : IPollingChangeToken
/// <param name="fileInfo">The <see cref="System.IO.FileInfo"/> to poll</param>
public PollingFileChangeToken(FileInfo fileInfo)
{
_fileInfo = fileInfo;
_fileInfo = FileSystemInfoHelper.ResolveFileLinkTarget(fileInfo) ?? fileInfo;
Copy link
Member

Choose a reason for hiding this comment

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

Thank you for addressing the link target feedback. Please consider adding test cases for the deleted link target cases, as they seem unique to this addition as it has to handle non-existent link targets, and link targets that get deleted / added.

@jozkee jozkee requested a review from ericstj July 20, 2021 00:36
@jozkee jozkee requested a review from safern July 20, 2021 00:36
…o debug CI failures and increase delay between writes
Debug.Assert(fileInfo.Exists);
if (fileInfo.LinkTarget != null)
{
FileSystemInfo targetInfo = fileInfo.ResolveLinkTarget(returnFinalTarget: true);
Copy link
Member

Choose a reason for hiding this comment

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

@jozkee have you considered keeping all these libs .NET Standard 2.0 and referencing the code that defines ResolveLinkTarget (and other things if needed)? We are already doing something like this for Microsoft.IO.Redist which targets net471. I am just wondering how much effort it would require to keep it NS2.0.

@jeffhandley @ericstj how long do we plan to keep targeting netstandard2.0 and net461 for the Microsoft.Extensions* libs?

Copy link
Member

Choose a reason for hiding this comment

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

@jeffhandley Jeff Handley FTE @ericstj Eric St. John FTE how long do we plan to keep targeting netstandard2.0 and net461 for the Microsoft.Extensions* libs?

At the moment we have no plans for dropping netstandard2.0 (and therefor net461) from the extensions. Perhaps it's something that could be considered in the future if this limitation hinders development considerably. They were intentionally omitted from aspnet/Announcements#324. cc @davidfowl @DamianEdwards @maryamariyan

Copy link
Member

Choose a reason for hiding this comment

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

Yes, we're keeping ns2.0 and net461 until further notice.

Copy link
Member Author

Choose a reason for hiding this comment

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

We can do a different (and more efficient) approach to get the target's Last write time, see @ericstj's suggestion #55664 (comment).
But (not) saving a couple of allocations is not a big issue considering that the performance penalty is relatively low compared to other pieces of the code that allocate much more.

Copy link
Member

Choose a reason for hiding this comment

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

We can do a different (and more efficient) approach to get the target's Last write time

@jozkee I wanted to know whether it would be possible to make this fix work for all TFMs (6 + NS2 + net461), because currently, it's going to work only for .NET 6.

Copy link
Member Author

@jozkee jozkee Jul 21, 2021

Choose a reason for hiding this comment

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

It is, but we can defer fixing this on other TFMs if no one is asking for it.
Do you think we can take a dependency on MS.IO.Redist here to utilize the same set of Symbolic Link APIs in order to fix this in ns2.0?
cc @jeffhandley.

Copy link
Member

Choose a reason for hiding this comment

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

I think to fix on older TFMs it needs more than just MS.IO.Redist. I believe the implementation of the IO API you are using here (or would add) would depend on the system-native PALs which we don't consider part of the public API. Having a package depend on those is fragile at the least, and might not even work if the versions of those don't have the exports you need on older frameworks. Adding private copies of the system-native PALs (similar to what System.IO.Ports does) is expensive too. Something to consider around OOB'ing our IO API.

Copy link
Member

Choose a reason for hiding this comment

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

Ahh, good info @ericstj. So far, we don't have any requests to have this fix apply to previous versions; I'm OK with this being net6.0+ until such requests come in.

@jozkee jozkee requested a review from adamsitnik July 21, 2021 00:49
@jozkee
Copy link
Member Author

jozkee commented Jul 21, 2021

Judging by the Helix log, it appears to be that LastWriteTimeUtc in OSX doesn't report the milliseconds. I have now increased the delay between writes to one second. All tests should pass now.
We can track the milliseconds issue in #55951.

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

LGTM, thank you for the fix @jozkee!

@jozkee jozkee merged commit 101e68a into dotnet:main Jul 22, 2021
@jozkee jozkee deleted the symlinks_polling branch July 22, 2021 18:11
@tmds
Copy link
Member

tmds commented Jul 22, 2021

fwiw, I think we failed to acknowledge the root cause of #36091 is FileInfo does not return LastWriteTimeUtc for the target (but for the link). Instead of trying to improve that, specific APIs are now patched to handle symbolic links to get to the target LastWriteTimeUtc.
#52908 is a better solution imo (and is more efficient on Linux).

@jozkee
Copy link
Member Author

jozkee commented Jul 22, 2021

#52908 is a better solution imo (and is more efficient on Linux).

After working on fixing this I agree on that, we should definitely give it a try.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ReloadOnChange of AddJsonFile is invalid in kubernetes.