-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Add internal junction support to link APIs #57996
Conversation
Tagging subscribers to this area: @dotnet/area-system-io Issue DetailsIn Windows, if a user creates a virtual drive using the By adding junction support to our internal Note: When the user calls
We decided that when a
|
[StructLayout(LayoutKind.Sequential)] | ||
internal unsafe struct REPARSE_DATA_BUFFER | ||
internal unsafe struct SymbolicLinkReparseBuffer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed this to match the names that were provided in the DUMMYUNIONNAME
section of the official Windows docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, the structs are not nested. This is how PowerShell had it already: https://github.com/PowerShell/PowerShell/blob/56d22bc3865fca445145fd685d6d6f6d63194701/src/System.Management.Automation/namespaces/FileSystemProvider.cs#L7957-L7985
src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseJunctions.FileSystem.cs
Outdated
Show resolved
Hide resolved
// Need to reuse the same virtual drive for all the test methods. | ||
// Creating and disposing one virtual drive per class achieves this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If instead of doing this, I splitted the virtual drive tests into multiple different test classes, then each test class would create its own virtual drive. We have so many symlink tests now, that we would end up depleting the available drive letters.
src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs
Outdated
Show resolved
Hide resolved
// Only symbolic links are supported at the moment. | ||
if ((rdb.ReparseTag & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK) == 0) | ||
{ | ||
return null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Source changes look mostly good to me; the tests are a bit hard to comprehend and I think we are missing some scenarios.
src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseJunctions.FileSystem.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseJunctions.FileSystem.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseJunctions.FileSystem.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs
Outdated
Show resolved
Hide resolved
Seems Windows Nano does not have I'll have to add the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall LGTM, but please address the nits that I've found before we merge it. Thank you @carlossanlop !
|
||
/// <summary> | ||
/// Changes the current working directory path to a new temporary directory. | ||
/// Important: Make sure to call this inside a remote executor to avoid changing the cwd for all tests in same process. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a way to ensure that it's called from Remote Executor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if we can add such a check here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would have to be something like this
Debug.Assert(Environment.StackTrace.Contains("RemoteExecutor"));
or it would be reasonable to add a static flag in RemoteExecutor so you could just test RemoteExecutor.IsInRemoteExecutor.
src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs
Outdated
Show resolved
Hide resolved
return (0 == junctionProcess.ExitCode); | ||
} | ||
|
||
public static char CreateVirtualDrive(string targetDir) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am totally OK with a test creating a temporary drive on my machine.
@ViktorHofer could it be an issue for the CI?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dotnet/dnceng @MattGal is creating a temporary virtual drive as part of a test which runs on PRs and rolling builds something that we should avoid?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am totally OK with a test creating a temporary drive on my machine.
We should maybe make sure this is a general consensus with real users who might run your test on their personal machines; the odds of leaking are there.
As for whether it's something to avoid, that depends. TL;DR: Probably OK, but you might need to consider cleanup automation (there's not even a try around this code, if it blows up it just leaves things where it was...) I'll give a few responses since I don't know exactly where this will run.
- If running on an Azure Devops Hosted build machine - no issues, these are unique VMs per build
- If running on a helix-based buildpool.* machine: my (admittedly limited) understanding of
subst
is that the virtual drive mappings don't persist across reboots; we always reboot, no issues as long as this is accurate. - If running on a VM-based Helix test machine: it's probably OK because you're the only user creating random drive mappings, but if there's a failure to cleanup and the same machine runs automation doing this repeatedly, as I don't think you can get more than 26 drive letters.
- If running on an on-premises, physical Helix test machine (every Windows ARM machine) : scariest possible choice; anything that's actually persistent might exist a long time, so if there's a letter-leak this is where I'd expect trouble.
All that said, I think if you had some sort of try {} finally {} here that cleans up the mapping when you're done, you're Ok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MattGal thank you for a very detailed answer!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if you had some sort of try {} finally {} here that cleans up the mapping when you're done, you're Ok.
I added a Dispose
method to the test class, which clears the mapped drive. And by the way, the test class only creates one mapped drive and reuses it for all of its test methods. precisely because I was seeing the problem of using all the available drive letters. Now I only use one per test class.
I'm not sure what I would add to the finally
part of the try
catch
I added to the Dispose
method. Suggestions? Or is it good as it is?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW I have no preference, but note for other "somewhat impactful" tests we made them outerloop. That way we rarely impact our contributors, but still are properly covered. Example: the process.start tests that pop up the browser. It's an option but I have no opinion.
src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise; LGTM. Only suggestions are for tests.
char driveLetter = GetRandomDriveLetter(); | ||
|
||
Process substProcess = new Process(); | ||
substProcess.StartInfo.FileName = "subst"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the subst call ever gets called twice, it would fail with the message Drive already SUBSTed.
Is there something that catches that error? my concern is that a race condition could occur and subst gets unintentionally executed twice and the CI would fail. Or maybe is not a big deal and we can wait for the CI to actually fail.
// Need to reuse the same virtual drive for all the test methods. | ||
// Creating and disposing one virtual drive per class achieves this. | ||
[PlatformSpecific(TestPlatforms.Windows)] | ||
[ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know why this is not supported in Nano?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Windows Nano Server is a stripped down version of Windows. A bunch of components are removed to make it lightweight. The CI error says the subst
executable was not found. It was supposed to be located in C:\Windows\System32
, which is part of either PATH
or some default env variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if subst is not in the PATH for Windows Nano, can't we use the full path to the subst executable?
src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs
Outdated
Show resolved
Hide resolved
// [InlineData(true, true, false, true, false)] // Target is not in virtual drive | ||
[InlineData(true, true, true, false, true)] // Immediate target expected, target is in virtual drive | ||
[InlineData(true, true, true, true, false)] // Final target expected, target is in virtual drive | ||
public void VirtualDrive_SymbolicLinks_WithIndirection(bool isFirstLinkInVirtualDrive, bool isMiddleLinkInVirtualDrive, bool isTargetInVirtualDrive, bool returnFinalTarget, bool isExpectedTargetPathVirtual) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of this method you can add another parameter to the previous test, name it isIndirect
or something alike.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I attempted it but the test became way more complicated than it already is. I would rather keep them separate.
src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs
Outdated
Show resolved
Hide resolved
… refactored/removed in the future. Detect if SUBST is available in Windows machine, to bring back Nano.
@@ -48,20 +46,78 @@ public static bool CreateSymbolicLink(string linkPath, string targetPath, bool i | |||
symLinkProcess.StartInfo.RedirectStandardOutput = true; | |||
symLinkProcess.Start(); | |||
|
|||
if (symLinkProcess != null) | |||
symLinkProcess.WaitForExit(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the code above was not modified, but we should rather refactor it as well:
Depeneding on your personal preferences it could be:
public static bool CreateSymbolicLink(string linkPath, string targetPath, bool isDirectory)
{
ProcessStartInfo startInfo = OperatingSystem.IsWindows()
? CreateStartInfo("cmd", "/c", "mklink", isDirectory ? "/D" : "", linkPath, targetPath)
: CreateStartInfo("/bin/ln", "-s", targetPath, linkPath);
return RunProcess(startInfo);
}
Or
public static bool CreateSymbolicLink(string linkPath, string targetPath, bool isDirectory)
=> RunProcess(OperatingSystem.IsWindows()
? CreateStartInfo("cmd", "/c", "mklink", isDirectory ? "/D" : "", linkPath, targetPath)
: CreateStartInfo("/bin/ln", "-s", targetPath, linkPath));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also refactored it, initially. I used the code you shared the first time. It caused all the symlink enumeration tests to fail. It was taking me too long to figure out why so I decided to revert it and the tests passed again.
I can take a look again in the related PR for AppExecLinks, if you don't mind.
/backport to release/6.0-rc1 |
Started backporting to release/6.0-rc1: https://github.com/dotnet/runtime/actions/runs/1175308538 |
In Windows, if a user creates a virtual drive using the
subst
command, they are actually creating a junction that points to a drive.By adding junction support to our internal
GetImmediateLinkTarget
method, if the user creates symbolic links that point to files or directories inside that virtual drive, we can now ensure that the public APIResolveLinkTarget(returnFinalTarget: false)
returns aFileSystemInfo
in which theFullName
points to a path that uses said virtual drive.Note: When the user calls
ResolveLinkTarget(returnFinalTarget: true)
, we use a different P/Invoke, which returns the absolute normalized path of the symlink's final target. This is expected behavior from Windows. So this code path remains untouched.The root cause was the same as above: Both
LinkTarget
andResolveLinkTarget(returnFinalTarget: false)
call the internal methodGetImmediateLinkTarget
, which returnsnull
if a reparse point that is not a symlink is encountered. And becauseResolveLinkTarget(returnFinalTarget: true)
calls a different P/Invoke, it is able to resolve any kind of reparse point, which explains the discrepancy. By adding support for mount points inGetImmediateLinkTarget
, we can now return the target properly.cc @fitdev