From fa49dbc380aa96a2bdfa8a9c12d1d96589a799be Mon Sep 17 00:00:00 2001 From: Mateo Torres-Ruiz Date: Thu, 15 Apr 2021 16:50:23 -0700 Subject: [PATCH] Remove unnecessary realpath calls from host probing logic (#50671) * Skip test on windows * Delay file existence check * Check file existence if there are additional probbing paths * PR feedback * Add needs_file_existence_checks * Clean symlinks causing CI problems with stat * Switch back to libc * Ignore symlink deletion in osx --- .../TestProjects/StandaloneApp6x/Program.cs | 15 + .../StandaloneApp6x/StandaloneApp6x.csproj | 8 + .../ResolveComponentDependencies.cs | 48 +-- .../GetNativeSearchDirectories.cs | 2 +- .../AppHostUsedWithSymbolicLinks.cs | 332 ++++++++++++++++++ .../tests/TestUtils/SymbolicLinking.cs | 64 ++++ src/native/corehost/deps_entry.cpp | 77 ++-- src/native/corehost/deps_entry.h | 18 +- .../corehost/hostpolicy/deps_resolver.cpp | 53 +-- .../corehost/hostpolicy/deps_resolver.h | 14 + 10 files changed, 535 insertions(+), 96 deletions(-) create mode 100644 src/installer/tests/Assets/TestProjects/StandaloneApp6x/Program.cs create mode 100644 src/installer/tests/Assets/TestProjects/StandaloneApp6x/StandaloneApp6x.csproj create mode 100644 src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppHostUsedWithSymbolicLinks.cs create mode 100644 src/installer/tests/TestUtils/SymbolicLinking.cs diff --git a/src/installer/tests/Assets/TestProjects/StandaloneApp6x/Program.cs b/src/installer/tests/Assets/TestProjects/StandaloneApp6x/Program.cs new file mode 100644 index 00000000000..2bb065a6e98 --- /dev/null +++ b/src/installer/tests/Assets/TestProjects/StandaloneApp6x/Program.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace StandaloneApp +{ + public static class Program + { + public static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} diff --git a/src/installer/tests/Assets/TestProjects/StandaloneApp6x/StandaloneApp6x.csproj b/src/installer/tests/Assets/TestProjects/StandaloneApp6x/StandaloneApp6x.csproj new file mode 100644 index 00000000000..c2ba2db55f9 --- /dev/null +++ b/src/installer/tests/Assets/TestProjects/StandaloneApp6x/StandaloneApp6x.csproj @@ -0,0 +1,8 @@ + + + StandaloneApp + net6.0 + Exe + $(TestTargetRid) + + diff --git a/src/installer/tests/HostActivation.Tests/DependencyResolution/ResolveComponentDependencies.cs b/src/installer/tests/HostActivation.Tests/DependencyResolution/ResolveComponentDependencies.cs index 63e305a303e..f931594ccd3 100644 --- a/src/installer/tests/HostActivation.Tests/DependencyResolution/ResolveComponentDependencies.cs +++ b/src/installer/tests/HostActivation.Tests/DependencyResolution/ResolveComponentDependencies.cs @@ -59,7 +59,7 @@ public void ComponentWithNoDependenciesCaseChangedOnAsm() // Linux: we fail // Windows and Mac, probing succeeds but // Windows: probing returns the original name - // Mac: probing return the new name including 2 assembly probing with the same new name and the changed deps file + // Mac: probing return the new name including 2 assembly probing with the original and new name, and the changed deps file var component = sharedTestState.ComponentWithNoDependencies.Copy(); @@ -89,7 +89,7 @@ public void ComponentWithNoDependenciesCaseChangedOnAsm() sharedTestState.RunComponentResolutionTest(component) .Should().Pass() .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success") - .And.HaveStdOutContaining($"corehost_resolve_component_dependencies assemblies:[{changeFile}{Path.PathSeparator}{changeFile}{Path.PathSeparator}]") + .And.HaveStdOutContaining($"corehost_resolve_component_dependencies assemblies:[{component.AppDll}{Path.PathSeparator}{changeFile}{Path.PathSeparator}]") .And.HaveStdErrContaining($"app_root='{component.Location}{Path.DirectorySeparatorChar}'") .And.HaveStdErrContaining($"deps='{changeDepsFile}'") .And.HaveStdErrContaining($"mgd_app='{changeFile}'"); @@ -115,7 +115,7 @@ public void ComponentWithNoDependenciesCaseChangedOnDepsAndAsm() // Linux: we fail // Windows and Mac, probing succeeds but // Windows: probing returns the original name - // Mac: probing return the new name including 2 assembly probing with the same new name and the changed deps file + // Mac: probing return the new name including 2 assembly probing with the original and new name, and the changed deps file var component = sharedTestState.ComponentWithNoDependencies.Copy(); @@ -146,7 +146,7 @@ public void ComponentWithNoDependenciesCaseChangedOnDepsAndAsm() sharedTestState.RunComponentResolutionTest(component) .Should().Pass() .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success") - .And.HaveStdOutContaining($"corehost_resolve_component_dependencies assemblies:[{changeFile}{Path.PathSeparator}{changeFile}{Path.PathSeparator}]") + .And.HaveStdOutContaining($"corehost_resolve_component_dependencies assemblies:[{component.AppDll}{Path.PathSeparator}{changeFile}{Path.PathSeparator}]") .And.HaveStdErrContaining($"app_root='{component.Location}{Path.DirectorySeparatorChar}'") .And.HaveStdErrContaining($"deps='{changeDepsFile}'") .And.HaveStdErrContaining($"mgd_app='{changeFile}'"); @@ -242,7 +242,8 @@ public void ComponentWithDependencies() $"{Path.Combine(sharedTestState.ComponentWithDependencies.Location, "Newtonsoft.Json.dll")}{Path.PathSeparator}]") .And.HaveStdOutContaining( $"corehost_resolve_component_dependencies native_search_paths:[" + - $"{ExpectedProbingPaths(Path.Combine(sharedTestState.ComponentWithDependencies.Location, "runtimes", "win10-x86", "native"))}]"); + $"{Path.Combine(sharedTestState.ComponentWithDependencies.Location, "runtimes", "win10-x86", "native")}" + + $"{Path.DirectorySeparatorChar}{Path.PathSeparator}]"); } [Fact] @@ -251,7 +252,6 @@ public void ComponentWithDependenciesAndDependencyRemoved() var component = sharedTestState.ComponentWithDependencies.Copy(); // Remove a dependency - // This will cause the resolution to fail File.Delete(Path.Combine(component.Location, "ComponentDependency.dll")); sharedTestState.RunComponentResolutionTest(component) @@ -259,6 +259,7 @@ public void ComponentWithDependenciesAndDependencyRemoved() .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success") .And.HaveStdOutContaining( $"corehost_resolve_component_dependencies assemblies:[" + + $"{Path.Combine(component.Location, "ComponentDependency.dll")}{Path.PathSeparator}" + $"{component.AppDll}{Path.PathSeparator}" + $"{Path.Combine(component.Location, "Newtonsoft.Json.dll")}{Path.PathSeparator}]"); } @@ -369,36 +370,8 @@ public void ComponentWithResourcesShouldReportResourceSearchPaths() .Should().Pass() .And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success") .And.HaveStdOutContaining($"corehost_resolve_component_dependencies resource_search_paths:[" + - $"{ExpectedProbingPaths(sharedTestState.ComponentWithResources.Location)}]"); - } - - private string ExpectedProbingPaths(params string[] paths) - { - string result = string.Empty; - foreach (string path in paths) - { - string expectedPath = path; - if (expectedPath.EndsWith(Path.DirectorySeparatorChar)) - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - // On non-windows the paths are normalized to not end with a / - expectedPath = expectedPath.Substring(0, expectedPath.Length - 1); - } - } - else - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - // On windows all paths are normalized to end with a \ - expectedPath += Path.DirectorySeparatorChar; - } - } - - result += expectedPath + Path.PathSeparator; - } - - return result; + $"{sharedTestState.ComponentWithResources.Location}" + + $"{Path.DirectorySeparatorChar}{Path.PathSeparator}]"); } [Fact] @@ -425,7 +398,8 @@ public void MultiThreadedComponentDependencyResolutionWhichSucceeeds() .And.HaveStdOutContaining($"ComponentA: corehost_resolve_component_dependencies assemblies:[{sharedTestState.ComponentWithNoDependencies.AppDll}{Path.PathSeparator}]") .And.HaveStdOutContaining($"ComponentB: corehost_resolve_component_dependencies:Success") .And.HaveStdOutContaining($"ComponentB: corehost_resolve_component_dependencies resource_search_paths:[" + - $"{ExpectedProbingPaths(sharedTestState.ComponentWithResources.Location)}]"); + $"{sharedTestState.ComponentWithResources.Location}" + + $"{Path.DirectorySeparatorChar}{Path.PathSeparator}]"); } [Fact] diff --git a/src/installer/tests/HostActivation.Tests/NativeHosting/GetNativeSearchDirectories.cs b/src/installer/tests/HostActivation.Tests/NativeHosting/GetNativeSearchDirectories.cs index 7f09c70943b..234530dcae9 100644 --- a/src/installer/tests/HostActivation.Tests/NativeHosting/GetNativeSearchDirectories.cs +++ b/src/installer/tests/HostActivation.Tests/NativeHosting/GetNativeSearchDirectories.cs @@ -40,7 +40,7 @@ public void BasicApp() CommandResult result = sharedState.CreateNativeHostCommand(args, sharedState.DotNet.BinPath) .Execute(); - string pathSuffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.DirectorySeparatorChar.ToString() : string.Empty; + string pathSuffix = Path.DirectorySeparatorChar.ToString(); string expectedSearchDirectories = Path.GetDirectoryName(sharedState.AppPath) + pathSuffix + Path.PathSeparator + Path.Combine(sharedState.DotNet.BinPath, "shared", "Microsoft.NETCore.App", SharedTestState.NetCoreAppVersion) + pathSuffix + Path.PathSeparator; diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppHostUsedWithSymbolicLinks.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppHostUsedWithSymbolicLinks.cs new file mode 100644 index 00000000000..b36fb431179 --- /dev/null +++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppHostUsedWithSymbolicLinks.cs @@ -0,0 +1,332 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.DotNet.Cli.Build.Framework; +using Microsoft.DotNet.CoreSetup.Test; +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Xunit; + +namespace Microsoft.NET.HostModel.Tests +{ + public class AppHostUsedWithSymbolicLinks : IClassFixture + { + private SharedTestState sharedTestState; + + private void CreateSymbolicLink(string source, string target) + { + if (!SymbolicLinking.MakeSymbolicLink(source, target, out var errorString)) + throw new Exception($"Failed to create symbolic link '{source}' targeting: '{target}': {errorString}"); + } + + public AppHostUsedWithSymbolicLinks(AppHostUsedWithSymbolicLinks.SharedTestState fixture) + { + sharedTestState = fixture; + } + + [Theory] + [InlineData ("a/b/SymlinkToApphost")] + [InlineData ("a/SymlinkToApphost")] + public void Run_apphost_behind_symlink(string symlinkRelativePath) + { + // Creating symbolic links requires administrative privilege on Windows, so skip test. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; + + var fixture = sharedTestState.StandaloneAppFixture_Published + .Copy(); + + var appExe = fixture.TestProject.AppExe; + var testDir = Directory.GetParent(fixture.TestProject.Location).ToString(); + Directory.CreateDirectory(Path.Combine(testDir, Path.GetDirectoryName(symlinkRelativePath))); + var symlinkFullPath = Path.Combine(testDir, symlinkRelativePath); + + CreateSymbolicLink(symlinkFullPath, appExe); + Command.Create(symlinkFullPath) + .CaptureStdErr() + .CaptureStdOut() + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Hello World"); + } + + [Theory] + [InlineData ("a/b/FirstSymlink", "c/d/SecondSymlink")] + [InlineData ("a/b/FirstSymlink", "c/SecondSymlink")] + [InlineData ("a/FirstSymlink", "c/d/SecondSymlink")] + [InlineData ("a/FirstSymlink", "c/SecondSymlink")] + public void Run_apphost_behind_transitive_symlinks(string firstSymlinkRelativePath, string secondSymlinkRelativePath) + { + // Creating symbolic links requires administrative privilege on Windows, so skip test. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; + + var fixture = sharedTestState.StandaloneAppFixture_Published + .Copy(); + + var appExe = fixture.TestProject.AppExe; + var testDir = Directory.GetParent(fixture.TestProject.Location).ToString(); + Directory.CreateDirectory(Path.Combine(testDir, Path.GetDirectoryName(firstSymlinkRelativePath))); + Directory.CreateDirectory(Path.Combine(testDir, Path.GetDirectoryName(secondSymlinkRelativePath))); + + // second symlink -> apphost + string secondSymbolicLink = Path.Combine(testDir, secondSymlinkRelativePath); + CreateSymbolicLink(secondSymbolicLink, appExe); + + // first symlink -> second symlink + string firstSymbolicLink = Path.Combine(testDir, firstSymlinkRelativePath); + CreateSymbolicLink(firstSymbolicLink, secondSymbolicLink); + + Command.Create(firstSymbolicLink) + .CaptureStdErr() + .CaptureStdOut() + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Hello World"); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Directory.Delete(firstSymbolicLink); + Directory.Delete(secondSymbolicLink); + } + } + + //[Theory] + //[InlineData("a/b/SymlinkToFrameworkDependentApp")] + //[InlineData("a/SymlinkToFrameworkDependentApp")] + [Fact(Skip = "Currently failing in OSX with \"No such file or directory\" when running Command.Create. " + + "CI failing to use stat on symbolic links on Linux (permission denied).")] + public void Run_framework_dependent_app_behind_symlink(/* string symlinkRelativePath */) + { + // Creating symbolic links requires administrative privilege on Windows, so skip test. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; + + string symlinkRelativePath = string.Empty; + + var fixture = sharedTestState.FrameworkDependentAppFixture_Published + .Copy(); + + var appExe = fixture.TestProject.AppExe; + var builtDotnet = fixture.BuiltDotnet.BinPath; + var testDir = Directory.GetParent(fixture.TestProject.Location).ToString(); + Directory.CreateDirectory(Path.Combine(testDir, Path.GetDirectoryName(symlinkRelativePath))); + var symlinkFullPath = Path.Combine(testDir, symlinkRelativePath); + + CreateSymbolicLink(symlinkFullPath, appExe); + Command.Create(symlinkFullPath) + .CaptureStdErr() + .CaptureStdOut() + .EnvironmentVariable("DOTNET_ROOT", builtDotnet) + .EnvironmentVariable("DOTNET_ROOT(x86)", builtDotnet) + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Hello World"); + } + + [Fact(Skip = "Currently failing in OSX with \"No such file or directory\" when running Command.Create. " + + "CI failing to use stat on symbolic links on Linux (permission denied).")] + public void Run_framework_dependent_app_with_runtime_behind_symlink() + { + // Creating symbolic links requires administrative privilege on Windows, so skip test. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; + + var fixture = sharedTestState.FrameworkDependentAppFixture_Published + .Copy(); + + var appExe = fixture.TestProject.AppExe; + var testDir = Directory.GetParent(fixture.TestProject.Location).ToString(); + var dotnetSymlink = Path.Combine(testDir, "dotnet"); + var dotnetDir = fixture.BuiltDotnet.BinPath; + + CreateSymbolicLink(dotnetSymlink, dotnetDir); + Command.Create(appExe) + .EnvironmentVariable("DOTNET_ROOT", dotnetSymlink) + .CaptureStdErr() + .CaptureStdOut() + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Hello World"); + } + + [Fact] + public void Put_app_directory_behind_symlink() + { + // Creating symbolic links requires administrative privilege on Windows, so skip test. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; + + var fixture = sharedTestState.StandaloneAppFixture_Published + .Copy(); + + var appExe = fixture.TestProject.AppExe; + var binDir = fixture.TestProject.OutputDirectory; + var binDirNewPath = Path.Combine(Directory.GetParent(fixture.TestProject.Location).ToString(), "PutTheBinDirSomewhereElse"); + Directory.Move(binDir, binDirNewPath); + + CreateSymbolicLink(binDir, binDirNewPath); + Command.Create(appExe) + .CaptureStdErr() + .CaptureStdOut() + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Hello World"); + } + + [Fact] + public void Put_dotnet_behind_symlink() + { + // Creating symbolic links requires administrative privilege on Windows, so skip test. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; + + var fixture = sharedTestState.StandaloneAppFixture_Published + .Copy(); + + var appDll = fixture.TestProject.AppDll; + var dotnetExe = fixture.BuiltDotnet.DotnetExecutablePath; + var testDir = Directory.GetParent(fixture.TestProject.Location).ToString(); + var dotnetSymlink = Path.Combine(testDir, "dotnet"); + + CreateSymbolicLink(dotnetSymlink, dotnetExe); + Command.Create(dotnetSymlink, fixture.TestProject.AppDll) + .CaptureStdErr() + .CaptureStdOut() + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Hello World"); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Directory.Delete(dotnetSymlink); + } + } + + [Fact] + public void Put_app_directory_behind_symlink_and_use_dotnet() + { + // Creating symbolic links requires administrative privilege on Windows, so skip test. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; + + var fixture = sharedTestState.StandaloneAppFixture_Published + .Copy(); + + var dotnet = fixture.BuiltDotnet; + var binDir = fixture.TestProject.OutputDirectory; + var binDirNewPath = Path.Combine(Directory.GetParent(fixture.TestProject.Location).ToString(), "PutTheBinDirSomewhereElse"); + Directory.Move(binDir, binDirNewPath); + + CreateSymbolicLink(binDir, binDirNewPath); + dotnet.Exec(fixture.TestProject.AppDll) + .CaptureStdErr() + .CaptureStdOut() + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Hello World"); + } + + [Fact] + public void Put_app_directory_behind_symlink_and_use_dotnet_run() + { + // Creating symbolic links requires administrative privilege on Windows, so skip test. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; + + var fixture = sharedTestState.StandaloneAppFixture_Published + .Copy(); + + var dotnet = fixture.SdkDotnet; + var binDir = fixture.TestProject.OutputDirectory; + var binDirNewPath = Path.Combine(Directory.GetParent(fixture.TestProject.Location).ToString(), "PutTheBinDirSomewhereElse"); + Directory.Move(binDir, binDirNewPath); + + CreateSymbolicLink(binDir, binDirNewPath); + dotnet.Exec("run") + .WorkingDirectory(fixture.TestProject.Location) + .CaptureStdErr() + .CaptureStdOut() + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Hello World"); + } + + [Fact] + public void Put_satellite_assembly_behind_symlink() + { + // Creating symbolic links requires administrative privilege on Windows, so skip test. + // If enabled, this tests will need to set the console code page to output unicode characters: + // Command.Create("chcp 65001").Execute(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; + + var fixture = sharedTestState.StandaloneAppFixture_Localized + .Copy(); + + var appExe = fixture.TestProject.AppExe; + var binDir = fixture.TestProject.OutputDirectory; + var satellitesDir = Path.Combine(Directory.GetParent(fixture.TestProject.Location).ToString(), "PutSatellitesSomewhereElse"); + Directory.CreateDirectory(satellitesDir); + + var firstSatelliteDir = Directory.GetDirectories(binDir).Single(dir => dir.Contains("kn-IN")); + var firstSatelliteNewDir = Path.Combine(satellitesDir, "kn-IN"); + Directory.Move(firstSatelliteDir, firstSatelliteNewDir); + CreateSymbolicLink(firstSatelliteDir, firstSatelliteNewDir); + + var secondSatelliteDir = Directory.GetDirectories(binDir).Single(dir => dir.Contains("ta-IN")); + var secondSatelliteNewDir = Path.Combine(satellitesDir, "ta-IN"); + Directory.Move(secondSatelliteDir, secondSatelliteNewDir); + CreateSymbolicLink(secondSatelliteDir, secondSatelliteNewDir); + + Command.Create(appExe) + .CaptureStdErr() + .CaptureStdOut() + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("ನಮಸ್ಕಾರ! வணக்கம்! Hello!"); + } + + public class SharedTestState : IDisposable + { + public TestProjectFixture StandaloneAppFixture_Localized { get; } + public TestProjectFixture StandaloneAppFixture_Published { get; } + public TestProjectFixture FrameworkDependentAppFixture_Published { get; } + public RepoDirectoriesProvider RepoDirectories { get; } + + public SharedTestState() + { + RepoDirectories = new RepoDirectoriesProvider(); + + var localizedFixture = new TestProjectFixture("LocalizedApp", RepoDirectories); + localizedFixture + .EnsureRestoredForRid(localizedFixture.CurrentRid) + .PublishProject(runtime: localizedFixture.CurrentRid); + + var publishFixture = new TestProjectFixture("StandaloneApp", RepoDirectories); + publishFixture + .EnsureRestoredForRid(publishFixture.CurrentRid) + .PublishProject(runtime: publishFixture.CurrentRid); + + var fwPublishedFixture = new TestProjectFixture("PortableApp", RepoDirectories); + fwPublishedFixture + .EnsureRestored() + .PublishProject(); + + StandaloneAppFixture_Localized = localizedFixture; + StandaloneAppFixture_Published = publishFixture; + FrameworkDependentAppFixture_Published = fwPublishedFixture; + } + + public void Dispose() + { + StandaloneAppFixture_Localized.Dispose(); + StandaloneAppFixture_Published.Dispose(); + FrameworkDependentAppFixture_Published.Dispose(); + } + } + } +} diff --git a/src/installer/tests/TestUtils/SymbolicLinking.cs b/src/installer/tests/TestUtils/SymbolicLinking.cs new file mode 100644 index 00000000000..564a25f7146 --- /dev/null +++ b/src/installer/tests/TestUtils/SymbolicLinking.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.DotNet.CoreSetup.Test +{ + public static class SymbolicLinking + { + static class Kernel32 + { + [Flags] + internal enum SymbolicLinkFlag + { + IsFile = 0x0, + IsDirectory = 0x1, + AllowUnprivilegedCreate = 0x2 + } + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool CreateSymbolicLink( + string symbolicLinkName, + string targetFileName, + SymbolicLinkFlag flags); + } + + static class libc + { + [DllImport("libc", SetLastError = true)] + internal static extern int symlink( + string targetFileName, + string linkPath); + + [DllImport("libc", CharSet = CharSet.Ansi)] + internal static extern IntPtr strerror(int errnum); + } + + public static bool MakeSymbolicLink(string symbolicLinkName, string targetFileName, out string errorMessage) + { + errorMessage = string.Empty; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (!Kernel32.CreateSymbolicLink(symbolicLinkName, targetFileName, Kernel32.SymbolicLinkFlag.IsFile)) + { + int errno = Marshal.GetLastWin32Error(); + errorMessage = $"CreateSymbolicLink failed with error number {errno}"; + return false; + } + } + else + { + if (libc.symlink(targetFileName, symbolicLinkName) == -1) + { + int errno = Marshal.GetLastWin32Error(); + errorMessage = Marshal.PtrToStringAnsi(libc.strerror(errno)); + return false; + } + } + + return true; + } + } +} diff --git a/src/native/corehost/deps_entry.cpp b/src/native/corehost/deps_entry.cpp index 58c23a3f0d8..e65b1fec590 100644 --- a/src/native/corehost/deps_entry.cpp +++ b/src/native/corehost/deps_entry.cpp @@ -30,17 +30,15 @@ static pal::string_t normalize_dir_separator(const pal::string_t& path) // Parameters: // base - The base directory to look for the relative path of this entry // ietf_dir - If this is a resource asset, the IETF intermediate directory -// look_in_base - Whether to search as a relative path -// look_in_bundle - Whether to look within the single-file bundle -// is_servicing - Whether the base directory is the core-servicing directory // str - (out parameter) If the method returns true, contains the file path for this deps entry +// search_options - Flags to instruct where to look for this deps entry // found_in_bundle - (out parameter) True if the candidate is located within the single-file bundle. // // Returns: // If the file exists in the path relative to the "base" directory within the // single-file or on disk. -bool deps_entry_t::to_path(const pal::string_t& base, const pal::string_t& ietf_dir, bool look_in_base, bool look_in_bundle, bool is_servicing, pal::string_t* str, bool &found_in_bundle) const +bool deps_entry_t::to_path(const pal::string_t& base, const pal::string_t& ietf_dir, pal::string_t* str, uint32_t search_options, bool &found_in_bundle) const { pal::string_t& candidate = *str; @@ -58,6 +56,9 @@ bool deps_entry_t::to_path(const pal::string_t& base, const pal::string_t& ietf_ // Reserve space for the path below candidate.reserve(base.length() + ietf_dir.length() + normalized_path.length() + 3); + bool look_in_base = search_options & deps_entry_t::search_options::look_in_base; + bool look_in_bundle = search_options & deps_entry_t::search_options::look_in_bundle; + bool is_servicing = search_options & deps_entry_t::search_options::is_servicing; pal::string_t file_path = look_in_base ? get_filename(normalized_path) : normalized_path; pal::string_t sub_path = ietf_dir; append_path(&sub_path, file_path.c_str()); @@ -94,36 +95,41 @@ bool deps_entry_t::to_path(const pal::string_t& base, const pal::string_t& ietf_ candidate.assign(base); append_path(&candidate, sub_path.c_str()); - bool exists = pal::file_exists(candidate); const pal::char_t* query_type = look_in_base ? _X("Local") : _X("Relative"); - if (!exists) + if (search_options & deps_entry_t::search_options::file_existence) { - trace::verbose(_X(" %s path query did not exist %s"), query_type, candidate.c_str()); - candidate.clear(); + if (!pal::file_exists(candidate)) + { + trace::verbose(_X(" %s path query did not exist %s"), query_type, candidate.c_str()); + candidate.clear(); + return false; + } + + trace::verbose(_X(" %s path query exists %s"), query_type, candidate.c_str()); } else { - trace::verbose(_X(" %s path query exists %s"), query_type, candidate.c_str()); + trace::verbose(_X(" %s path query %s (skipped file existence check)"), query_type, candidate.c_str()); + } - // If a file is resolved to the servicing directory, mark it as disabled in the bundle. - // This step is necessary because runtime will try to resolve assemblies from the bundle - // before it uses the TPA. So putting the servicing entry into TPA is not enough, since runtime would - // resolve it from the bundle first anyway. Disabling the file's entry in the bundle - // ensures that the servicing entry in the TPA gets priority. - if (is_servicing && bundle::info_t::is_single_file_bundle()) - { - bundle::runner_t* app = bundle::runner_t::mutable_app(); - assert(!app->has_base(base)); - assert(!found_in_bundle); + // If a file is resolved to the servicing directory, mark it as disabled in the bundle. + // This step is necessary because runtime will try to resolve assemblies from the bundle + // before it uses the TPA. So putting the servicing entry into TPA is not enough, since runtime would + // resolve it from the bundle first anyway. Disabling the file's entry in the bundle + // ensures that the servicing entry in the TPA gets priority. + if (is_servicing && bundle::info_t::is_single_file_bundle()) + { + bundle::runner_t* app = bundle::runner_t::mutable_app(); + assert(!app->has_base(base)); + assert(!found_in_bundle); - if (app->disable(sub_path)) - { - trace::verbose(_X(" %s disabled in bundle because of servicing override %s"), sub_path.c_str(), candidate.c_str()); - } + if (app->disable(sub_path)) + { + trace::verbose(_X(" %s disabled in bundle because of servicing override %s"), sub_path.c_str(), candidate.c_str()); } } - return exists; + return true; } // ----------------------------------------------------------------------------- @@ -132,12 +138,13 @@ bool deps_entry_t::to_path(const pal::string_t& base, const pal::string_t& ietf_ // Parameters: // base - The base directory to look for the relative path of this entry // str - If the method returns true, contains the file path for this deps entry +// search_options - Flags to instruct where to look for this deps entry // look_in_bundle - Whether to look within the single-file bundle // // Returns: // If the file exists in the path relative to the "base" directory. // -bool deps_entry_t::to_dir_path(const pal::string_t& base, bool look_in_bundle, pal::string_t* str, bool& found_in_bundle) const +bool deps_entry_t::to_dir_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options, bool& found_in_bundle) const { pal::string_t ietf_dir; @@ -159,7 +166,10 @@ bool deps_entry_t::to_dir_path(const pal::string_t& base, bool look_in_bundle, p base.c_str(), ietf_dir.c_str(), asset.name.c_str()); } - return to_path(base, ietf_dir, true, look_in_bundle, false, str, found_in_bundle); + + search_options |= deps_entry_t::search_options::look_in_base; + search_options &= ~deps_entry_t::search_options::is_servicing; + return to_path(base, ietf_dir, str, search_options, found_in_bundle); } // ----------------------------------------------------------------------------- @@ -169,16 +179,16 @@ bool deps_entry_t::to_dir_path(const pal::string_t& base, bool look_in_bundle, p // Parameters: // base - The base directory to look for the relative path of this entry // str - If the method returns true, contains the file path for this deps entry -// look_in_bundle - Whether to look within the single-file bundle -// is_servicing - Whether the base directory is the core-servicing directory +// search_options - Flags to instruct where to look for this deps entry // // Returns: // If the file exists in the path relative to the "base" directory. // -bool deps_entry_t::to_rel_path(const pal::string_t& base, bool look_in_bundle, bool is_servicing, pal::string_t* str) const +bool deps_entry_t::to_rel_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options) const { bool found_in_bundle; - bool result = to_path(base, _X(""), false, look_in_bundle, is_servicing, str, found_in_bundle); + search_options &= ~deps_entry_t::search_options::look_in_base; + bool result = to_path(base, _X(""), str, search_options, found_in_bundle); assert(!found_in_bundle); return result; } @@ -190,12 +200,12 @@ bool deps_entry_t::to_rel_path(const pal::string_t& base, bool look_in_bundle, b // Parameters: // base - The base directory to look for the relative path of this entry // str - If the method returns true, contains the file path for this deps entry -// is_servicing - Whether the base directory is the core-servicing directory +// search_options - Flags to instruct where to look for this deps entry // // Returns: // If the file exists in the path relative to the "base" directory. // -bool deps_entry_t::to_full_path(const pal::string_t& base, bool is_servicing, pal::string_t* str) const +bool deps_entry_t::to_full_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options) const { str->clear(); @@ -217,5 +227,6 @@ bool deps_entry_t::to_full_path(const pal::string_t& base, bool is_servicing, pa append_path(&new_base, library_path.c_str()); } - return to_rel_path(new_base, false, is_servicing, str); + search_options &= ~deps_entry_t::search_options::look_in_bundle; + return to_rel_path(new_base, str, search_options); } diff --git a/src/native/corehost/deps_entry.h b/src/native/corehost/deps_entry.h index db2a32e27d2..ae769f799f4 100644 --- a/src/native/corehost/deps_entry.h +++ b/src/native/corehost/deps_entry.h @@ -36,6 +36,15 @@ struct deps_entry_t count }; + enum search_options : uint32_t + { + none = 0x0, + look_in_base = 0x1, // Search entry as a relative path + look_in_bundle = 0x2, // Look for entry within the single-file bundle + is_servicing = 0x4, // Whether the base directory is the core-servicing directory + file_existence = 0x8, // Check for entry file existence + }; + static const std::array s_known_asset_types; pal::string_t deps_file; @@ -52,18 +61,19 @@ struct deps_entry_t bool is_rid_specific; // Given a "base" dir, yield the file path within this directory or single-file bundle. - bool to_dir_path(const pal::string_t& base, bool look_in_bundle, pal::string_t* str, bool& found_in_bundle) const; + bool to_dir_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options, bool& found_in_bundle) const; // Given a "base" dir, yield the relative path in the package layout or servicing directory. - bool to_rel_path(const pal::string_t& base, bool look_in_bundle, bool is_servicing, pal::string_t* str) const; + bool to_rel_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options) const; // Given a "base" dir, yield the relative path with package name/version in the package layout or servicing location. - bool to_full_path(const pal::string_t& base, bool is_servicing, pal::string_t* str) const; + bool to_full_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options) const; private: // Given a "base" dir, yield the filepath within this directory or relative to this directory based on "look_in_base" + // flag in "search_options". // Returns a path within the single-file bundle, or a file on disk, - bool to_path(const pal::string_t& base, const pal::string_t& ietf_code, bool look_in_base, bool look_in_bundle, bool is_servicing, pal::string_t* str, bool & found_in_bundle) const; + bool to_path(const pal::string_t& base, const pal::string_t& ietf_code, pal::string_t* str, uint32_t search_options, bool & found_in_bundle) const; }; diff --git a/src/native/corehost/hostpolicy/deps_resolver.cpp b/src/native/corehost/hostpolicy/deps_resolver.cpp index 08b2967446e..5af59fc36ec 100644 --- a/src/native/corehost/hostpolicy/deps_resolver.cpp +++ b/src/native/corehost/hostpolicy/deps_resolver.cpp @@ -43,29 +43,28 @@ void add_unique_path( pal::string_t* non_serviced, const pal::string_t& svc_dir) { - // Resolve sym links. - pal::string_t real = path; - pal::realpath(&real); - - if (existing->count(real)) + // To optimize startup time, we avoid calling realpath here. + // Because of this, there might be duplicates in the output + // whenever path is eiter non-normalized or a symbolic link. + if (existing->count(path)) { return; } - trace::verbose(_X("Adding to %s path: %s"), deps_entry_t::s_known_asset_types[asset_type], real.c_str()); + trace::verbose(_X("Adding to %s path: %s"), deps_entry_t::s_known_asset_types[asset_type], path.c_str()); - if (starts_with(real, svc_dir, false)) + if (starts_with(path, svc_dir, false)) { - serviced->append(real); + serviced->append(path); serviced->push_back(PATH_SEPARATOR); } else { - non_serviced->append(real); + non_serviced->append(path); non_serviced->push_back(PATH_SEPARATOR); } - existing->insert(real); + existing->insert(path); } // Return the filename from deps path; a deps path always uses a '/' for the separator. @@ -185,6 +184,7 @@ void deps_resolver_t::setup_shared_store_probes( { // Shared Store probe: DOTNET_SHARED_STORE environment variable m_probes.push_back(probe_config_t::lookup(shared)); + m_needs_file_existence_checks = true; } } @@ -192,6 +192,7 @@ void deps_resolver_t::setup_shared_store_probes( { // Path relative to the location of "dotnet.exe" if it's being used to run the app m_probes.push_back(probe_config_t::lookup(args.dotnet_shared_store)); + m_needs_file_existence_checks = true; } for (const auto& global_shared : args.global_shared_stores) @@ -200,6 +201,7 @@ void deps_resolver_t::setup_shared_store_probes( { // Global store probe: the global location m_probes.push_back(probe_config_t::lookup(global_shared)); + m_needs_file_existence_checks = true; } } } @@ -236,6 +238,8 @@ void deps_resolver_t::setup_probe_config( pal::string_t ext_pkgs = args.core_servicing; append_path(&ext_pkgs, _X("pkgs")); m_probes.push_back(probe_config_t::svc(ext_pkgs)); + + m_needs_file_existence_checks = true; } // The published deps directory to be probed: either app or FX directory. @@ -253,10 +257,15 @@ void deps_resolver_t::setup_probe_config( setup_shared_store_probes(args); - for (const auto& probe : m_additional_probes) + if (m_additional_probes.size() > 0) { - // Additional paths - m_probes.push_back(probe_config_t::lookup(probe)); + for (const auto& probe : m_additional_probes) + { + // Additional paths + m_probes.push_back(probe_config_t::lookup(probe)); + } + + m_needs_file_existence_checks = true; } if (trace::is_enabled()) @@ -304,6 +313,11 @@ bool deps_resolver_t::probe_deps_entry(const deps_entry_t& entry, const pal::str continue; } pal::string_t probe_dir = config.probe_dir; + uint32_t search_options = deps_entry_t::search_options::none; + if (needs_file_existence_checks()) + { + search_options |= deps_entry_t::search_options::file_existence; + } if (config.is_fx()) { @@ -318,7 +332,7 @@ bool deps_resolver_t::probe_deps_entry(const deps_entry_t& entry, const pal::str // If the deps json has the package name and version, then someone has already done rid selection and // put the right asset in the dir. So checking just package name and version would suffice. // No need to check further for the exact asset relative sub path. - if (config.probe_deps_json->has_package(entry.library_name, entry.library_version) && entry.to_dir_path(probe_dir, false, candidate, found_in_bundle)) + if (config.probe_deps_json->has_package(entry.library_name, entry.library_version) && entry.to_dir_path(probe_dir, candidate, search_options, found_in_bundle)) { assert(!found_in_bundle); trace::verbose(_X(" Probed deps json and matched '%s'"), candidate->c_str()); @@ -337,7 +351,7 @@ bool deps_resolver_t::probe_deps_entry(const deps_entry_t& entry, const pal::str { if (entry.is_rid_specific) { - if (entry.to_rel_path(deps_dir, true, false, candidate)) + if (entry.to_rel_path(deps_dir, candidate, search_options | deps_entry_t::search_options::look_in_bundle)) { trace::verbose(_X(" Probed deps dir and matched '%s'"), candidate->c_str()); return true; @@ -346,7 +360,7 @@ bool deps_resolver_t::probe_deps_entry(const deps_entry_t& entry, const pal::str else { // Non-rid assets, lookup in the published dir. - if (entry.to_dir_path(deps_dir, true, candidate, found_in_bundle)) + if (entry.to_dir_path(deps_dir, candidate, search_options | deps_entry_t::search_options::look_in_bundle, found_in_bundle)) { trace::verbose(_X(" Probed deps dir and matched '%s'"), candidate->c_str()); return true; @@ -356,7 +370,7 @@ bool deps_resolver_t::probe_deps_entry(const deps_entry_t& entry, const pal::str trace::verbose(_X(" Skipping... not found in deps dir '%s'"), deps_dir.c_str()); } - else if (entry.to_full_path(probe_dir, config.only_serviceable_assets, candidate)) + else if (entry.to_full_path(probe_dir, candidate, search_options | (config.only_serviceable_assets ? deps_entry_t::search_options::is_servicing : 0))) { trace::verbose(_X(" Probed package dir and matched '%s'"), candidate->c_str()); return true; @@ -588,10 +602,7 @@ bool deps_resolver_t::resolve_tpa_list( // Convert the paths into a string and return it for (const auto& item : items) { - // Workaround for CoreFX not being able to resolve sym links. - pal::string_t real_asset_path = item.second.resolved_path; - pal::realpath(&real_asset_path); - output->append(real_asset_path); + output->append(item.second.resolved_path); output->push_back(PATH_SEPARATOR); } diff --git a/src/native/corehost/hostpolicy/deps_resolver.h b/src/native/corehost/hostpolicy/deps_resolver.h index 76e1739978c..082cd5cf123 100644 --- a/src/native/corehost/hostpolicy/deps_resolver.h +++ b/src/native/corehost/hostpolicy/deps_resolver.h @@ -52,6 +52,7 @@ public: , m_managed_app(args.managed_application) , m_core_servicing(args.core_servicing) , m_is_framework_dependent(is_framework_dependent) + , m_needs_file_existence_checks(false) { int lowest_framework = static_cast(m_fx_definitions.size()) - 1; int root_framework = -1; @@ -90,6 +91,11 @@ public: setup_additional_probes(args.probe_paths); setup_probe_config(args); + + if (m_additional_deps.size() > 0) + { + m_needs_file_existence_checks = true; + } } bool valid(pal::string_t* errors) @@ -172,6 +178,11 @@ public: return m_is_framework_dependent; } + bool needs_file_existence_checks() const + { + return m_needs_file_existence_checks; + } + void get_app_dir(pal::string_t *app_dir) const { if (m_host_mode == host_mode_t::libhost) @@ -272,6 +283,9 @@ private: // Is the deps file for an app using shared frameworks? const bool m_is_framework_dependent; + + // File existence checks must be performed for probed paths.This will cause symlinks to be resolved. + bool m_needs_file_existence_checks; }; #endif // DEPS_RESOLVER_H -- GitLab