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

Add internal AppExecLink support to link APIs #58233

Closed
wants to merge 7 commits into from

Conversation

carlossanlop
Copy link
Member

@carlossanlop carlossanlop commented Aug 27, 2021

This is a continuation of #57996

AppExecLinks, also known as Application Execution Aliases, are a Windows-specific kind of symbolic link for Windows Apps. It's a scenario we should support when resolving links with our FileSystemInfo APIs or the static File/Directory APIs.

AppExecLinks are an explicitly supported scenario by PowerShell, along with mount points. Our APIs should handle these types of links as well, to facilitate the task of the PowerShell team to port their code to use these new link APIs.

@carlossanlop carlossanlop added this to the 6.0.0 milestone Aug 27, 2021
@carlossanlop carlossanlop self-assigned this Aug 27, 2021
@ghost
Copy link

ghost commented Aug 27, 2021

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

Issue Details

DRAFT: This is a continuation of #57996 , which is still open, so this PR will contain those changes as well, until it gets merged.

AppExecLinks, also known as Application Execution Aliases, are a Windows-specific kind of symbolic link for Windows Apps. It's a scenario we should support when resolving links with our FileSystemInfo APIs or the static File/Directory APIs.

AppExecLinks are an explicitly supported scenario by PowerShell, along with mount points. Our APIs should handle these types of links as well, to facilitate the task of the PowerShell team to port their code to use these new link APIs.

Author: carlossanlop
Assignees: carlossanlop
Labels:

area-System.IO

Milestone: 6.0.0

string windowsAppsDir = Path.Join(localAppData, "Microsoft", "WindowsApps");
if (Directory.Exists(windowsAppsDir))
{
return Directory.GetFiles(windowsAppsDir, "*.exe", new EnumerationOptions() { RecurseSubdirectories = true, MaxRecursionDepth = 3 }).Length > 0;
Copy link
Member

Choose a reason for hiding this comment

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

It is granted that %LOCALAPPDATA%/Microsoft/WindowsApps will always contain an AppExecLink?
For extra safety we could add a filter and ensure that at least one of the files is a ReparsePoint.

Suggested change
return Directory.GetFiles(windowsAppsDir, "*.exe", new EnumerationOptions() { RecurseSubdirectories = true, MaxRecursionDepth = 3 }).Length > 0;
new FileSystemEnumerable<string?>(
windowsAppsDir,
(ref FileSystemEntry entry) => null,
new EnumerationOptions { RecurseSubdirectories = true })
{
ShouldIncludePredicate = (ref FileSystemEntry entry) =>
FileSystemName.MatchesWin32Expression("*.exe", entry.FileName) &&
(entry.Attributes & FileAttributes.ReparsePoint) != 0
}.Any();

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's not guaranteed, hence why I added this platform detection check.
The extra ReparsePoint check makes sense. I'll add it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Let's hope this PlatformDetection change does not cause build failure if the used APIs are not available in netstandard.

@@ -557,7 +594,8 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i
if ((data.dwFileAttributes & (uint)FileAttributes.ReparsePoint) == 0 ||
// Only symbolic links and mount points are supported at the moment.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Only symbolic links and mount points are supported at the moment.
// Only symbolic links, mount points and windows apps are supported at the moment.

Debug.Assert(success);

// The target file is at index 2
if (rbAppExecLink.StringCount >= 3)
Copy link
Member

Choose a reason for hiding this comment

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

This SO answer says that this field is actually named Version https://stackoverflow.com/a/65583702/4231460. It may be a good idea to ask the Windows team what's the exact definition of this struct and validate if we are using it correctly.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

From our conversation with the Windows team, this code is doing the correct job.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe show an example in a comment here, to explain the mysterious magic number.
We want to only use documented Windows API - but I see this is documented https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/c3a420cb-8a72-4adf-87e8-eee95379d78f

Copy link
Member

Choose a reason for hiding this comment

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

or just "see documentation for REPARSE_DATA_BUFFER"

Copy link
Contributor

Choose a reason for hiding this comment

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

PowerShell team found the const in Windows SDK so it is public and the fact opened a way to use the const.

Copy link
Member Author

Choose a reason for hiding this comment

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

@iSazonov can you share a link to the public Windows SDK location where you found it?

I'm not sure such a link would qualify as "public documentation" though.

Copy link
Contributor

Choose a reason for hiding this comment

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

@carlossanlop If I remember right @SteveL-MSFT found this in Windows SDK files (I guess in *.cpp).

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't remember where exactly I got the original buffer struct from, but given the name, I think most likely from the SO post. Since I can't find any public docs on the struct, it should probably not be relied upon as it could change. I'll start an internal thread with the AppX team to see about getting it documented publicly.

Copy link
Contributor

@iSazonov iSazonov Sep 1, 2021

Choose a reason for hiding this comment

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

@carlossanlop carlossanlop marked this pull request as ready for review August 27, 2021 21:33
@@ -539,13 +539,59 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i
Debug.Assert(!PathInternal.IsPartiallyQualified(targetPath));
return targetPath.ToString();
}
else if (rbSymlink.ReparseTag == Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_APPEXECLINK)
Copy link
Member

Choose a reason for hiding this comment

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

there's a long list of IO_REPARSE_TAG_* .. how is this one special?
https://docs.microsoft.com/en-us/windows/win32/fileio/reparse-point-tags

Copy link
Member

@jozkee jozkee Aug 27, 2021

Choose a reason for hiding this comment

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

It is one of the reparse tags proven to point to another file/folder and it is also one of the three tags covered by PowerShell: https://github.com/PowerShell/PowerShell/blob/008f4b057fdc1482d3200de0a61d23570b4d30e4/src/System.Management.Automation/namespaces/FileSystemProvider.cs#L8217-L8230

I am uncertain if all the reparse tags in the link you posted indicate that the reparse point contains a reference to another file/folder, but having these 3 tags (Symlinks, MountPoints and AppExecLinks) seems to me like a good start.

Copy link
Member

Choose a reason for hiding this comment

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

Yup fair enough, if Powershell has had this support for a while, that's good evidence those are the ones that matter.

Copy link
Contributor

Choose a reason for hiding this comment

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

how is this one special?

It is unnormal reparse point :-) so we had to add it explicitly in PowerShell.

In common, reparse point list is "open" and if new reparse points will be added in future perhaps we will have to add its explicitly too.

I'd remind about OneDrive. .Net API should be tested for OneDrive paths/links too (at list manually).

Copy link
Member Author

@carlossanlop carlossanlop Aug 31, 2021

Choose a reason for hiding this comment

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

I manually tested enumeration of the OneDrive directory (ensuring there were cloud files) and the behavior was the same in 3.1, 5.0 and 6.0 (including the changes in this PR): I can read the file name and the file attributes, and the files are not getting unexpectedly downloaded - their cloud icon stays next to the files when viewing them in the File Explorer.

@iSazonov by any chance, do you know any specific scenarios in which the OneDrive files get unexpectedly downloaded when manipulating them with .NET? I am not sure if you've mentioned this strange behavior in the past, or if it was someone else. It would be a good chance for me to verify it.

Copy link
Contributor

Choose a reason for hiding this comment

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

@carlossanlop These links are related OneDrive and could be interesting for you:
PowerShell/PowerShell#9895
PowerShell/PowerShell#9509
PowerShell/PowerShell#8745

Also for removing PowerShell/PowerShell#15571

Feel free to ask me if you need additional comments.

@danmoseley
Copy link
Member

This is presumably a Windows 8 or Windows 10 feature. Will there be any problems on Windows 7? I assume not, it just won't hit any.

@carlossanlop
Copy link
Member Author

Had a conversation with Dan about this issue and it doesn't meet the bar for a 6.0 backport.

@carlossanlop carlossanlop modified the milestones: 6.0.0, 7.0.0 Aug 30, 2021
Add CANT_ACCESS_FILE inside IsPathUnreachableError. Add comment to that method to avoid catching ACCESS_DENIED, SHARING_VIOLATION, SEM_TIMEOUT.
@carlossanlop
Copy link
Member Author

This is presumably a Windows 8 or Windows 10 feature. Will there be any problems on Windows 7? I assume not, it just won't hit any.

Execution Aliases for UWP applications were introduced in Windows 10 Fall Creators Update (1709/RS3, build 16299). Windows versions before that should not have appexeclink files in their system. If they do, the scenario would be unsupported on the Windows side, so either OpenSafeFileHandle or DeviceIoControl would fail and we would handle the error.

Comment on lines +42 to +49
[StructLayout(LayoutKind.Sequential)]
internal struct AppExecLinkReparseBuffer
{
public uint ReparseTag;
public ushort ReparseDataLength;
public ushort Reserved;
public uint StringCount;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

It is not related the PR but I wonder why do such structs returned from P/Invokes would not be readonly structs?

@@ -132,6 +134,7 @@ internal static bool IsPathUnreachableError(int errorCode)
case Interop.Errors.ERROR_NETWORK_ACCESS_DENIED:
case Interop.Errors.ERROR_INVALID_HANDLE: // eg from \\.\CON
case Interop.Errors.ERROR_FILENAME_EXCED_RANGE: // Path is too long
case Interop.Errors.ERROR_CANT_ACCESS_FILE: // Broken AppExecLink files may cause this
Copy link
Contributor

@iSazonov iSazonov Aug 31, 2021

Choose a reason for hiding this comment

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

The error looks very generic. No false positives could be for the error in the context?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure I understand the question. Would you mind rephrasing that?

Copy link
Contributor

Choose a reason for hiding this comment

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

Broken AppExecLink files may cause this

My concern is about this comment. Could there be other files that cause the same error?

[PlatformSpecific(TestPlatforms.Windows)]
public class AppExecLinks : BaseSymbolicLinks
{
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.HasUsableAppExecLinksDirectory))]
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry if I skipped this - will this throw if HasUsableAppExecLinksDirectory return false or the test could be skipped always silently in CIs?

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 should return false and cause the test to be skipped due to the absence of a WindowsApps dir.

The property this attribute is pointing to, should not throw. If it throws, it's a bug and needs to be fixed.

Copy link
Contributor

Choose a reason for hiding this comment

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

My concern is that in the case the tests can be never really evaluated on CIs.

@adamsitnik
Copy link
Member

Had a conversation with Dan about this issue and it doesn't meet the bar for a 6.0 backport.

The bar for approving changes for .NET 6 is:

  1. Broken or significantly incomplete 6.0 scenario

I would not say that it's significantly incomplete, but since it blocks one of our 1st party customers from switching to the new APIs (PowerShell/PowerShell#15960) we should rather include it in 6.0. Just my two cents.

@danmoseley
Copy link
Member

I would not say that it's significantly incomplete, but since it blocks one of our 1st party customers from switching to the new APIs (PowerShell/PowerShell#15960) we should rather include it in 6.0. Just my two cents.

It does, but I figured this customer already has code that works fine. So it would really just be code cleanup for them. If there were other customers that want to adopt the API and are missing support for "AppExecLink" that would be a little different.

That being said, @iSazonov if we took this change, would it be the last piece allowing significant code cleanup from Powershell? Would Powershell be able to adopt it promptly? Is that a signficant win for you guys?

@iSazonov
Copy link
Contributor

iSazonov commented Sep 1, 2021

That being said, @iSazonov if we took this change, would it be the last piece allowing significant code cleanup from Powershell? Would Powershell be able to adopt it promptly? Is that a signficant win for you guys?

For me, it is not critical to have the API today since we are near RC too (and it is LTS version too!) and I guess PowerShell team doesn't want to catch a regression or bug. It is better to ask @SteveL-MSFT directly.

Really there are some API proposal PowerShell can benefit from (specially path enumeration API) and I hope to see them implemented in next milestone early so that we can migrate and test early too.

@carlossanlop
Copy link
Member Author

carlossanlop commented Sep 2, 2021

We had a conversation with the Windows team that owns AppExecLinks. They explained that we are not supposed to care about the target path. AppExecLinks are meant to work seamlessly as any other executable. Hence why they never exposed the APIs that we were adding in this PR, and which PowerShell is also calling.

Having learned that, we decided won't be treating AppExecLinks like symlinks and junctions. We will still return true for Attributes.HasFlag(FileAttributes.ReparsePoint), but the new APIs LinkTarget and ResolveLinkTarget will return null (this scenario already works, thanks to @jozkee 's PR).

We also had a conversation with @SteveL-MSFT , @rjmholt, @daxian-dbw from the PowerShell team to let them know what the Windows people told us, so they make adjustments to their code if needed.

With that said, since we are already providing support to symlinks, junctions and appexeclinks as expected, the PowerShell scenario will be complete in 6.0 after we merge this backport to RC2 (@danmoseley).

@iSazonov

@carlossanlop carlossanlop deleted the appexeclink branch September 2, 2021 19:02
@ghost ghost locked as resolved and limited conversation to collaborators Oct 2, 2021
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.

6 participants