-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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 support for symlink files embedding to binlog #8213
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
f3a9a69
Add support for symlink files embedding to binlog
JanKrivanek 8b6341a
Merge remote-tracking branch 'upstream/main' into proto/bl-embed-syml…
JanKrivanek 296ba00
Make symlink functionality available for FullFW as well
JanKrivanek c1e837b
Add null check
JanKrivanek 884fde8
Simplify the change, add doc
JanKrivanek 202a929
Fix wording
JanKrivanek 95706ab
Merge branch 'main' into proto/bl-embed-symlinks
JanKrivanek 870155c
Fix platform constraint
JanKrivanek 93504d7
Clarify wiki
JanKrivanek b94eadd
Add support for recursive symlinks
JanKrivanek ee5fc55
Switched options in doc
JanKrivanek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
using System.Reflection; | ||
using System.Runtime.InteropServices; | ||
using System.Runtime.Versioning; | ||
using System.Text; | ||
using System.Threading; | ||
|
||
using Microsoft.Build.Shared; | ||
|
@@ -200,9 +201,16 @@ internal enum ProcessorArchitectures | |
Unknown | ||
} | ||
|
||
#endregion | ||
internal enum SymbolicLink | ||
{ | ||
File = 0, | ||
Directory = 1, | ||
AllowUnprivilegedCreate = 2, | ||
} | ||
|
||
#region Structs | ||
#endregion | ||
|
||
#region Structs | ||
|
||
/// <summary> | ||
/// Structure that contain information about the system on which we are running | ||
|
@@ -1035,6 +1043,123 @@ internal static MemoryStatus GetMemoryStatus() | |
return null; | ||
} | ||
|
||
internal static bool ExistAndHasContent(string path) | ||
{ | ||
var fileInfo = new FileInfo(path); | ||
|
||
// File exist and has some content | ||
return fileInfo.Exists && | ||
(fileInfo.Length > 0 || | ||
// Or final destination of the link is nonempty file | ||
( | ||
IsSymLink(fileInfo) && | ||
TryGetFinalLinkTarget(fileInfo, out string finalTarget, out _) && | ||
File.Exists(finalTarget) && | ||
new FileInfo(finalTarget).Length > 0 | ||
) | ||
); | ||
} | ||
|
||
internal static bool IsSymLink(FileInfo fileInfo) | ||
{ | ||
#if NET | ||
return fileInfo.Exists && !string.IsNullOrEmpty(fileInfo.LinkTarget); | ||
#else | ||
if (!IsWindows) | ||
{ | ||
return false; | ||
} | ||
|
||
WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA(); | ||
|
||
return NativeMethods.GetFileAttributesEx(fileInfo.FullName, 0, ref data) && | ||
(data.fileAttributes & NativeMethods.FILE_ATTRIBUTE_DIRECTORY) == 0 && | ||
(data.fileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT; | ||
#endif | ||
} | ||
|
||
internal static bool IsSymLink(string path) | ||
{ | ||
return IsSymLink(new FileInfo(path)); | ||
} | ||
|
||
internal static bool TryGetFinalLinkTarget(FileInfo fileInfo, out string finalTarget, out string errorMessage) | ||
{ | ||
if (!IsWindows) | ||
{ | ||
errorMessage = null; | ||
#if NET | ||
while(!string.IsNullOrEmpty(fileInfo.LinkTarget)) | ||
{ | ||
fileInfo = new FileInfo(fileInfo.LinkTarget); | ||
} | ||
finalTarget = fileInfo.FullName; | ||
return true; | ||
#else | ||
|
||
finalTarget = null; | ||
return false; | ||
#endif | ||
} | ||
|
||
using SafeFileHandle handle = OpenFileThroughSymlinks(fileInfo.FullName); | ||
if (handle.IsInvalid) | ||
{ | ||
// Link is broken. | ||
errorMessage = Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()).Message; | ||
finalTarget = null; | ||
return false; | ||
} | ||
|
||
const int initialBufferSize = 4096; | ||
char[] targetPathBuffer = new char[initialBufferSize]; | ||
uint result = GetFinalPathNameByHandle(handle, targetPathBuffer); | ||
|
||
// Buffer too small | ||
if (result > targetPathBuffer.Length) | ||
{ | ||
targetPathBuffer = new char[(int)result]; | ||
result = GetFinalPathNameByHandle(handle, targetPathBuffer); | ||
} | ||
|
||
// Error | ||
if (result == 0) | ||
{ | ||
errorMessage = Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()).Message; | ||
finalTarget = null; | ||
return false; | ||
} | ||
|
||
// Normalize \\?\ and \??\ syntax. | ||
finalTarget = new string(targetPathBuffer, 0, (int)result).TrimStart(new char[] { '\\', '?' }); | ||
errorMessage = null; | ||
return true; | ||
} | ||
|
||
internal static bool MakeSymbolicLink(string newFileName, string exitingFileName, ref string errorMessage) | ||
{ | ||
bool symbolicLinkCreated; | ||
if (IsWindows) | ||
{ | ||
Version osVersion = Environment.OSVersion.Version; | ||
SymbolicLink flags = SymbolicLink.File; | ||
if (osVersion.Major >= 11 || (osVersion.Major == 10 && osVersion.Build >= 14972)) | ||
{ | ||
flags |= SymbolicLink.AllowUnprivilegedCreate; | ||
} | ||
|
||
symbolicLinkCreated = CreateSymbolicLink(newFileName, exitingFileName, flags); | ||
errorMessage = symbolicLinkCreated ? null : Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()).Message; | ||
} | ||
else | ||
{ | ||
symbolicLinkCreated = symlink(exitingFileName, newFileName) == 0; | ||
errorMessage = symbolicLinkCreated ? null : "The link() library call failed with the following error code: " + Marshal.GetLastWin32Error(); | ||
JanKrivanek marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very valid. Linked this to a PR taking care of localizing the message |
||
} | ||
|
||
return symbolicLinkCreated; | ||
} | ||
|
||
/// <summary> | ||
/// Get the last write time of the fullpath to the file. | ||
/// </summary> | ||
|
@@ -1111,6 +1236,23 @@ DateTime LastWriteFileUtcTime(string path) | |
} | ||
} | ||
|
||
/// <summary> | ||
/// Get the SafeFileHandle for a file, while skipping reparse points (going directly to target file). | ||
/// </summary> | ||
/// <param name="fullPath">Full path to the file in the filesystem</param> | ||
/// <returns>the SafeFileHandle for a file (target file in case of symlinks)</returns> | ||
[SupportedOSPlatform("windows")] | ||
private static SafeFileHandle OpenFileThroughSymlinks(string fullPath) | ||
{ | ||
return CreateFile(fullPath, | ||
GENERIC_READ, | ||
FILE_SHARE_READ, | ||
IntPtr.Zero, | ||
OPEN_EXISTING, | ||
FILE_ATTRIBUTE_NORMAL, /* No FILE_FLAG_OPEN_REPARSE_POINT; read through to content */ | ||
IntPtr.Zero); | ||
} | ||
|
||
/// <summary> | ||
/// Get the last write time of the content pointed to by a file path. | ||
/// </summary> | ||
|
@@ -1125,14 +1267,7 @@ private static DateTime GetContentLastWriteFileUtcTime(string fullPath) | |
{ | ||
DateTime fileModifiedTime = DateTime.MinValue; | ||
|
||
using (SafeFileHandle handle = | ||
CreateFile(fullPath, | ||
GENERIC_READ, | ||
FILE_SHARE_READ, | ||
IntPtr.Zero, | ||
OPEN_EXISTING, | ||
FILE_ATTRIBUTE_NORMAL, /* No FILE_FLAG_OPEN_REPARSE_POINT; read through to content */ | ||
IntPtr.Zero)) | ||
using (SafeFileHandle handle = OpenFileThroughSymlinks(fullPath)) | ||
{ | ||
if (!handle.IsInvalid) | ||
{ | ||
|
@@ -1635,9 +1770,31 @@ out FILETIME lpLastWriteTime | |
[SupportedOSPlatform("windows")] | ||
internal static extern bool SetThreadErrorMode(int newMode, out int oldMode); | ||
|
||
#endregion | ||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] | ||
[return: MarshalAs(UnmanagedType.I1)] | ||
[SupportedOSPlatform("windows")] | ||
internal static extern bool CreateSymbolicLink(string symLinkFileName, string targetFileName, SymbolicLink dwFlags); | ||
|
||
[DllImport("libc", SetLastError = true)] | ||
internal static extern int symlink(string oldpath, string newpath); | ||
|
||
internal const uint FILE_NAME_NORMALIZED = 0x0; | ||
|
||
[SupportedOSPlatform("windows")] | ||
static uint GetFinalPathNameByHandle(SafeFileHandle fileHandle, char[] filePath) => | ||
GetFinalPathNameByHandle(fileHandle, filePath, (uint) filePath.Length, FILE_NAME_NORMALIZED); | ||
|
||
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] | ||
[SupportedOSPlatform("windows")] | ||
static extern uint GetFinalPathNameByHandle( | ||
SafeFileHandle hFile, | ||
[Out] char[] lpszFilePath, | ||
uint cchFilePath, | ||
uint dwFlags); | ||
|
||
#endregion | ||
|
||
#region helper methods | ||
#region helper methods | ||
|
||
internal static bool DirectoryExists(string fullPath) | ||
{ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
This could incorrectly return true for a file that is a reparse point but not a symbolic link.
One could use either GetFileInformationByHandleEx FILE_ATTRIBUTE_TAG_INFO or DeviceIoControl FSCTL_GET_REPARSE_POINT for reading the attributes and the reparse tag, and then compare to IO_REPARSE_TAG_SYMLINK; PowerShell uses FSCTL_GET_REPARSE_POINT. However, these functions would require first opening the file.
To get the file attributes and reparse tag without opening the file, one can use either FindFirstFileW or, starting from Windows 10 version 1709, NtQueryInformationByName FILE_STAT_INFORMATION. I assume that NtQueryInformationByName would be the most efficient of these, because it doesn't require opening and closing a handle to the parent directory.
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.
@KalleOlaviNiemitalo Thank you for pointing to this and for the detailed suggestion!
I'm thinking about keeping the attribute check to filter out 'normal files' scenarios and in case of detecting reparse point querying further (like by obtaining reparse tag via DeviceIoControl - similarly how this is done in runtime: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs#L574-L609).
The
FindFirstFile
seems to be very neat way to get this info in single shot. This feels unnecessary heavy though. I haven't done any measuring though.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 methods in PowerShell and .NET Runtime use FSCTL_GET_REPARSE_POINT which also retrieves the target string of the symbolic link. If you don't need that, I suspect GetFileInformationByHandleEx FILE_ATTRIBUTE_TAG_INFO could be lighter weight as it could avoid the string copy.