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

Use CanCreateSymbolicLinks instead of IsSymLinkSupported #69592

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 0 additions & 74 deletions src/libraries/Common/tests/System/IO/ReparsePointUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,10 @@
#define DEBUG

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Xunit;

public static partial class MountHelper
{
Expand All @@ -35,77 +31,7 @@ public static partial class MountHelper
// Helper for ConditionalClass attributes
internal static bool IsSubstAvailable => PlatformDetection.IsSubstAvailable;

/// <summary>
/// In some cases (such as when running without elevated privileges),
/// the symbolic link may fail to create. Only run this test if it creates
/// links successfully.
/// </summary>
internal static bool CanCreateSymbolicLinks => s_canCreateSymbolicLinks.Value;

private static readonly Lazy<bool> s_canCreateSymbolicLinks = new Lazy<bool>(() =>
{
bool success = true;

// Verify file symlink creation
string path = Path.GetTempFileName();
string linkPath = path + ".link";
success = CreateSymbolicLink(linkPath: linkPath, targetPath: path, isDirectory: false);
try { File.Delete(path); } catch { }
try { File.Delete(linkPath); } catch { }

// Verify directory symlink creation
path = Path.GetTempFileName();
linkPath = path + ".link";
success = success && CreateSymbolicLink(linkPath: linkPath, targetPath: path, isDirectory: true);
try { Directory.Delete(path); } catch { }
try { Directory.Delete(linkPath); } catch { }

// Reduce the risk we accidentally stop running these altogether
// on Windows, due to a bug in CreateSymbolicLink
if (!success && PlatformDetection.IsWindows)
Assert.True(!PlatformDetection.IsWindowsAndElevated);

return success;
});

/// <summary>Creates a symbolic link using command line tools.</summary>
public static bool CreateSymbolicLink(string linkPath, string targetPath, bool isDirectory)
{
// It's easy to get the parameters backwards.
Assert.EndsWith(".link", linkPath);
if (linkPath != targetPath) // testing loop
Assert.False(targetPath.EndsWith(".link"), $"{targetPath} should not end with .link");

#if NETFRAMEWORK
bool isWindows = true;
#else
if (OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst() || OperatingSystem.IsBrowser()) // OSes that don't support Process.Start()
{
return false;
}

bool isWindows = OperatingSystem.IsWindows();
#endif
Process symLinkProcess = new Process();
if (isWindows)
{
symLinkProcess.StartInfo.FileName = "cmd";
symLinkProcess.StartInfo.Arguments = string.Format("/c mklink{0} \"{1}\" \"{2}\"", isDirectory ? " /D" : "", linkPath, targetPath);
}
else
{
symLinkProcess.StartInfo.FileName = "/bin/ln";
symLinkProcess.StartInfo.Arguments = string.Format("-s \"{0}\" \"{1}\"", targetPath, linkPath);
}
symLinkProcess.StartInfo.UseShellExecute = false;
symLinkProcess.StartInfo.RedirectStandardOutput = true;

symLinkProcess.Start();

symLinkProcess.WaitForExit();

return (symLinkProcess.ExitCode == 0);
}

/// <summary>On Windows, creates a junction using command line tools.</summary>
public static bool CreateJunction(string junctionPath, string targetPath)
Expand Down
83 changes: 83 additions & 0 deletions src/libraries/Common/tests/System/IO/SymbolicLinkHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.Diagnostics;
using System.IO;
using Xunit;

public static class SymbolicLinkHelper
{
/// <summary>
/// In some cases (such as when running without elevated privileges),
/// the symbolic link may fail to create. Only run this test if it creates
/// links successfully.
/// </summary>
internal static bool CanCreateSymbolicLinks => s_canCreateSymbolicLinks.Value;

private static readonly Lazy<bool> s_canCreateSymbolicLinks = new Lazy<bool>(() =>
{
bool success = true;

// Verify file symlink creation
string path = Path.GetTempFileName();
string linkPath = path + ".link";
success = CreateSymbolicLink(linkPath: linkPath, targetPath: path, isDirectory: false);
try { File.Delete(path); } catch { }
try { File.Delete(linkPath); } catch { }

// Verify directory symlink creation
path = Path.GetTempFileName();
linkPath = path + ".link";
success = success && CreateSymbolicLink(linkPath: linkPath, targetPath: path, isDirectory: true);
try { Directory.Delete(path); } catch { }
try { Directory.Delete(linkPath); } catch { }

// Reduce the risk we accidentally stop running these altogether
// on Windows, due to a bug in CreateSymbolicLink
if (!success && PlatformDetection.IsWindows)
Assert.True(!PlatformDetection.IsWindowsAndElevated);

return success;
});

/// <summary>Creates a symbolic link using command line tools.</summary>
public static bool CreateSymbolicLink(string linkPath, string targetPath, bool isDirectory)
{
// It's easy to get the parameters backwards.
Assert.EndsWith(".link", linkPath);
if (linkPath != targetPath) // testing loop
Assert.False(targetPath.EndsWith(".link"), $"{targetPath} should not end with .link");

if (PlatformDetection.IsiOS || PlatformDetection.IstvOS || PlatformDetection.IsMacCatalyst || PlatformDetection.IsBrowser)
{
return false;
}

#if !NETFRAMEWORK
try
{
FileSystemInfo linkInfo = isDirectory ?
Directory.CreateSymbolicLink(linkPath, targetPath) : File.CreateSymbolicLink(linkPath, targetPath);

// Detect silent failures.
Assert.True(linkInfo.Exists);
return true;
}
catch
{
return false;
}
#else
Process symLinkProcess = new Process();

symLinkProcess.StartInfo.FileName = "cmd";
symLinkProcess.StartInfo.Arguments = $"/c mklink{(isDirectory ? " /D" : "")} \"{linkPath}\" \"{targetPath}\"";
symLinkProcess.StartInfo.UseShellExecute = false;
symLinkProcess.StartInfo.RedirectStandardOutput = true;

symLinkProcess.Start();

symLinkProcess.WaitForExit();

return (symLinkProcess.ExitCode == 0);
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ public static partial class PlatformDetection

public static bool IsThreadingSupported => !IsBrowser;
public static bool IsBinaryFormatterSupported => IsNotMobile && !IsNativeAot;
public static bool IsSymLinkSupported => !IsiOS && !IstvOS;

public static bool IsSpeedOptimized => !IsSizeOptimized;
public static bool IsSizeOptimized => IsBrowser || IsAndroid || IsAppleMobile;
Expand Down Expand Up @@ -327,6 +326,8 @@ public static bool IsSubstAvailable
}
}

public static bool CanCreateSymbolicLinks => SymbolicLinkHelper.CanCreateSymbolicLinks;

private static Version GetICUVersion()
{
int version = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- For some reason, xunit.core.props isn't excluded in VS and sets this to true. -->
Expand Down Expand Up @@ -39,6 +39,8 @@

<Compile Include="$(CommonPath)System\Text\ValueStringBuilder.cs"
Link="Common\System\Text\ValueStringBuilder.cs" />
<Compile Include="$(CommonTestPath)System\IO\SymbolicLinkHelper.cs"
Link="Common\System\IO\SymbolicLinkHelper.cs" />
<!--
Interop.Library is not designed to support runtime checks therefore we are picking the Windows
variant from the Common folder and adding the missing members manually.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.FileProviders
{
public partial class PhysicalFileProviderTests : FileCleanupTestBase
{
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsSymLinkSupported))]
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.CanCreateSymbolicLinks))]
[InlineData(false)]
[InlineData(true)]
public async Task UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink(bool useWildcard)
Expand Down Expand Up @@ -45,7 +45,7 @@ public async Task UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink
$"Change event was not raised - current time: {DateTime.UtcNow:O}, file LastWriteTimeUtc: {File.GetLastWriteTimeUtc(filePath):O}.");
}

[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsSymLinkSupported))]
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.CanCreateSymbolicLinks))]
[OuterLoop]
[InlineData(false)]
[InlineData(true)]
Expand All @@ -70,7 +70,7 @@ public async Task UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink
await Assert.ThrowsAsync<TaskCanceledException>(() => tcs.Task);
}

[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsSymLinkSupported))]
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.CanCreateSymbolicLinks))]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
Expand Down Expand Up @@ -119,7 +119,7 @@ public async Task UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink
catch (UnauthorizedAccessException) { }
}

[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsSymLinkSupported))]
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.CanCreateSymbolicLinks))]
[InlineData(false)]
[InlineData(true)]
public async Task UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink_TargetDeleted(bool useWildcard)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public void NonRegularFile_Throws()
}

[PlatformSpecific(TestPlatforms.AnyUnix)]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsSymLinkSupported))]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.CanCreateSymbolicLinks))]
public void Symlink_ValidFile_Succeeds()
{
string filePath = Path.Combine(Directory.GetCurrentDirectory(), TestAssemblyFileName);
Expand Down Expand Up @@ -63,7 +63,7 @@ public void Symlink_ValidFile_Succeeds()
}

[PlatformSpecific(TestPlatforms.AnyUnix)]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsSymLinkSupported))]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.CanCreateSymbolicLinks))]
public void Symlink_InvalidFile_Throws()
{
string sourcePath = Path.Combine(Directory.GetCurrentDirectory(), TestAssemblyFileName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
<Compile Include="TarWriter\TarWriter.WriteEntry.Entry.V7.Tests.cs" />
<Compile Include="WrappedStream.cs" Link="WrappedStream.cs" />
<Compile Include="$(CommonPath)DisableRuntimeMarshalling.cs" Link="Common\DisableRuntimeMarshalling.cs" />
<Compile Include="$(CommonTestPath)System\IO\ReparsePointUtilities.cs" Link="Common\System\IO\ReparsePointUtilities.cs" />
</ItemGroup>
<!-- Windows specific files -->
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'windows'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public void Extract_LinkEntry_TargetOutsideDirectory(TarEntryType entryType)
Assert.Equal(0, Directory.GetFileSystemEntries(root.Path).Count());
}

[ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.CanCreateSymbolicLinks))]
public void Extract_SymbolicLinkEntry_TargetInsideDirectory() => Extract_LinkEntry_TargetInsideDirectory_Internal(TarEntryType.SymbolicLink);

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public void Add_Directory(TarFormat format, bool withContents)
}
}

[ConditionalTheory(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))]
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.CanCreateSymbolicLinks))]
[InlineData(TarFormat.V7, false)]
[InlineData(TarFormat.V7, true)]
[InlineData(TarFormat.Ustar, false)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public void FileSystemWatcher_Directory_Changed_Nested(bool includeSubdirectorie
}
}

[ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.CanCreateSymbolicLinks))]
public void FileSystemWatcher_Directory_Changed_SymLink()
{
string dir = Path.Combine(TestDirectory, "dir");
Expand All @@ -68,7 +68,7 @@ public void FileSystemWatcher_Directory_Changed_SymLink()
// Setup the watcher
watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size;
watcher.IncludeSubdirectories = true;
Assert.True(MountHelper.CreateSymbolicLink(Path.Combine(dir, GetRandomLinkName()), tempDir, true));
Assert.True(SymbolicLinkHelper.CreateSymbolicLink(Path.Combine(dir, GetRandomLinkName()), tempDir, true));

Action action = () => File.AppendAllText(file, "longtext");
Action cleanup = () => File.AppendAllText(file, "short");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public void FileSystemWatcher_Directory_Create_DeepDirectoryStructure()
}
}

[ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.CanCreateSymbolicLinks))]
public void FileSystemWatcher_Directory_Create_SymLink()
{
string dir = CreateTestDirectory(TestDirectory, "dir");
Expand All @@ -90,7 +90,7 @@ public void FileSystemWatcher_Directory_Create_SymLink()
{
// Make the symlink in our path (to the temp folder) and make sure an event is raised
string symLinkPath = Path.Combine(dir, GetRandomLinkName());
Action action = () => Assert.True(MountHelper.CreateSymbolicLink(symLinkPath, temp, true));
Action action = () => Assert.True(SymbolicLinkHelper.CreateSymbolicLink(symLinkPath, temp, true));
Action cleanup = () => Directory.Delete(symLinkPath);

ExpectEvent(watcher, WatcherChangeTypes.Created, action, cleanup, symLinkPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public void FileSystemWatcher_Directory_Delete_DeepDirectoryStructure()
}
}

[ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.CanCreateSymbolicLinks))]
public void FileSystemWatcher_Directory_Delete_SymLink()
{
string dir = CreateTestDirectory(TestDirectory, "dir");
Expand All @@ -73,7 +73,7 @@ public void FileSystemWatcher_Directory_Delete_SymLink()
// Make the symlink in our path (to the temp folder) and make sure an event is raised
string symLinkPath = Path.Combine(dir, GetRandomLinkName());
Action action = () => Directory.Delete(symLinkPath);
Action cleanup = () => Assert.True(MountHelper.CreateSymbolicLink(symLinkPath, tempDir, true));
Action cleanup = () => Assert.True(SymbolicLinkHelper.CreateSymbolicLink(symLinkPath, tempDir, true));
cleanup();

ExpectEvent(watcher, WatcherChangeTypes.Deleted, action, cleanup, expectedPath: symLinkPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@ public void FileSystemWatcher_File_Changed_DataModification()
}
}

[ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.CanCreateSymbolicLinks))]
public void FileSystemWatcher_File_Changed_SymLink()
{
string dir = CreateTestDirectory(TestDirectory, "dir");
string file = CreateTestFile();
using (var watcher = new FileSystemWatcher(dir, "*"))
{
watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size;
Assert.True(MountHelper.CreateSymbolicLink(Path.Combine(dir, GetRandomLinkName()), file, false));
Assert.True(SymbolicLinkHelper.CreateSymbolicLink(Path.Combine(dir, GetRandomLinkName()), file, false));

Action action = () => File.AppendAllText(file, "longtext");
Action cleanup = () => File.AppendAllText(file, "short");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public void FileSystemWatcher_File_Create_DeepDirectoryStructure()
}
}

[ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.CanCreateSymbolicLinks))]
public void FileSystemWatcher_File_Create_SymLink()
{
string dir = CreateTestDirectory(TestDirectory, "dir");
Expand All @@ -129,7 +129,7 @@ public void FileSystemWatcher_File_Create_SymLink()
{
// Make the symlink in our path (to the temp file) and make sure an event is raised
string symLinkPath = Path.Combine(dir, GetRandomLinkName());
Action action = () => Assert.True(MountHelper.CreateSymbolicLink(symLinkPath, temp, false));
Action action = () => Assert.True(SymbolicLinkHelper.CreateSymbolicLink(symLinkPath, temp, false));
Action cleanup = () => File.Delete(symLinkPath);

ExpectEvent(watcher, WatcherChangeTypes.Created, action, cleanup, symLinkPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public void FileSystemWatcher_File_Delete_DeepDirectoryStructure()
}
}

[ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.CanCreateSymbolicLinks))]
public void FileSystemWatcher_File_Delete_SymLink()
{
FileSystemWatcherTest.Execute(() =>
Expand All @@ -96,7 +96,7 @@ public void FileSystemWatcher_File_Delete_SymLink()
// Make the symlink in our path (to the temp file) and make sure an event is raised
string symLinkPath = Path.Combine(dir, GetRandomLinkName());
Action action = () => File.Delete(symLinkPath);
Action cleanup = () => Assert.True(MountHelper.CreateSymbolicLink(symLinkPath, temp, false));
Action cleanup = () => Assert.True(SymbolicLinkHelper.CreateSymbolicLink(symLinkPath, temp, false));
cleanup();

ExpectEvent(watcher, WatcherChangeTypes.Deleted, action, cleanup, symLinkPath);
Expand Down
Loading