From 6c1f792de9f95d5a48fd72c0d45a68449c68c6f4 Mon Sep 17 00:00:00 2001 From: Nigusu Solomon Yenework <59111203+Nigusu-Allehu@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:52:49 -0700 Subject: [PATCH 1/2] Discover plugins installed using Net tools (#5990) --- .../Plugins/PluginDiscoverer.cs | 243 ++++++- .../NuGet.Protocol/Plugins/PluginFile.cs | 16 + .../NuGet.Protocol/Plugins/PluginManager.cs | 22 +- .../PublicAPI/net472/PublicAPI.Unshipped.txt | 2 +- .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 2 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 2 +- .../Plugins/PluginDiscovererTests.cs | 594 +++++++++++++++++- 7 files changed, 831 insertions(+), 50 deletions(-) diff --git a/src/NuGet.Core/NuGet.Protocol/Plugins/PluginDiscoverer.cs b/src/NuGet.Core/NuGet.Protocol/Plugins/PluginDiscoverer.cs index 830ba4d4142..d342827a370 100644 --- a/src/NuGet.Core/NuGet.Protocol/Plugins/PluginDiscoverer.cs +++ b/src/NuGet.Core/NuGet.Protocol/Plugins/PluginDiscoverer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using NuGet.Common; @@ -17,17 +18,31 @@ public sealed class PluginDiscoverer : IPluginDiscoverer { private bool _isDisposed; private List _pluginFiles; - private readonly string _rawPluginPaths; + private readonly string _netCoreOrNetFXPluginPaths; + private readonly string _nuGetPluginPaths; private IEnumerable _results; private readonly SemaphoreSlim _semaphore; + private readonly IEnvironmentVariableReader _environmentVariableReader; - /// - /// Instantiates a new class. - /// - /// The raw semicolon-delimited list of supposed plugin file paths. - public PluginDiscoverer(string rawPluginPaths) + public PluginDiscoverer() + : this(EnvironmentVariableWrapper.Instance) { - _rawPluginPaths = rawPluginPaths; + } + + internal PluginDiscoverer(IEnvironmentVariableReader environmentVariableReader) + { + _environmentVariableReader = environmentVariableReader; +#if IS_DESKTOP + _netCoreOrNetFXPluginPaths = environmentVariableReader.GetEnvironmentVariable(EnvironmentVariableConstants.DesktopPluginPaths); +#else + _netCoreOrNetFXPluginPaths = environmentVariableReader.GetEnvironmentVariable(EnvironmentVariableConstants.CorePluginPaths); +#endif + + if (string.IsNullOrEmpty(_netCoreOrNetFXPluginPaths)) + { + _nuGetPluginPaths = _environmentVariableReader.GetEnvironmentVariable(EnvironmentVariableConstants.PluginPaths); + } + _semaphore = new SemaphoreSlim(initialCount: 1, maxCount: 1); } @@ -75,7 +90,40 @@ public async Task> DiscoverAsync(Cancellation return _results; } - _pluginFiles = GetPluginFiles(cancellationToken); + if (!string.IsNullOrEmpty(_netCoreOrNetFXPluginPaths)) + { + // NUGET_NETFX_PLUGIN_PATHS, NUGET_NETCORE_PLUGIN_PATHS have been set. + var filePaths = _netCoreOrNetFXPluginPaths.Split(new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); + _pluginFiles = GetPluginFiles(filePaths, cancellationToken); + } + else if (!string.IsNullOrEmpty(_nuGetPluginPaths)) + { + // NUGET_PLUGIN_PATHS has been set + _pluginFiles = GetPluginsInNuGetPluginPaths(); + } + else + { + // restore to default plugins search. + // Search for plugins in %user%/.nuget/plugins + var directories = new List { PluginDiscoveryUtility.GetNuGetHomePluginsPath() }; +#if IS_DESKTOP + // Internal plugins are only supported for .NET Framework scenarios, namely msbuild.exe + directories.Add(PluginDiscoveryUtility.GetInternalPlugins()); +#endif + var filePaths = PluginDiscoveryUtility.GetConventionBasedPlugins(directories); + _pluginFiles = GetPluginFiles(filePaths, cancellationToken); + + // Search for .Net tools plugins in PATH + if (_pluginFiles != null) + { + _pluginFiles.AddRange(GetPluginsInPath()); + } + else + { + _pluginFiles = GetPluginsInPath(); + } + } + var results = new List(); for (var i = 0; i < _pluginFiles.Count; ++i) @@ -97,14 +145,17 @@ public async Task> DiscoverAsync(Cancellation return _results; } - private List GetPluginFiles(CancellationToken cancellationToken) + private static List GetPluginFiles(IEnumerable filePaths, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var filePaths = GetPluginFilePaths(); - var files = new List(); + if (filePaths == null) + { + return files; + } + foreach (var filePath in filePaths) { var pluginFile = new PluginFile(filePath, new Lazy(() => @@ -124,19 +175,173 @@ private List GetPluginFiles(CancellationToken cancellationToken) return files; } - private IEnumerable GetPluginFilePaths() + /// + /// Retrieves authentication plugins by searching through directories and files specified in the `NuGET_PLUGIN_PATHS` + /// environment variable. The method looks for files prefixed with 'nuget-plugin-' and verifies their validity for .net tools plugins. + /// + /// A list of valid objects representing the discovered plugins. + internal List GetPluginsInNuGetPluginPaths() { - if (string.IsNullOrEmpty(_rawPluginPaths)) + var pluginFiles = new List(); + string[] paths = _nuGetPluginPaths?.Split(Path.PathSeparator) ?? Array.Empty(); + + foreach (var path in paths) { - var directories = new List { PluginDiscoveryUtility.GetNuGetHomePluginsPath() }; -#if IS_DESKTOP - // Internal plugins are only supported for .NET Framework scenarios, namely msbuild.exe - directories.Add(PluginDiscoveryUtility.GetInternalPlugins()); + if (PathValidator.IsValidLocalPath(path) || PathValidator.IsValidUncPath(path)) + { + if (File.Exists(path)) + { + FileInfo fileInfo = new FileInfo(path); + if (fileInfo.Name.StartsWith("nuget-plugin-", StringComparison.CurrentCultureIgnoreCase)) + { + // A DotNet tool plugin + if (IsValidPluginFile(fileInfo)) + { + PluginFile pluginFile = new PluginFile(fileInfo.FullName, new Lazy(() => PluginFileState.Valid), isDotnetToolsPlugin: true); + pluginFiles.Add(pluginFile); + } + } + else + { + // A non DotNet tool plugin file + var state = new Lazy(() => PluginFileState.Valid); + pluginFiles.Add(new PluginFile(fileInfo.FullName, state)); + } + } + else if (Directory.Exists(path)) + { + pluginFiles.AddRange(GetNetToolsPluginsInDirectory(path) ?? new List()); + } + } + else + { + pluginFiles.Add(new PluginFile(path, new Lazy(() => PluginFileState.InvalidFilePath))); + } + } + + return pluginFiles; + } + + /// + /// Retrieves .NET tools authentication plugins by searching through directories specified in `PATH` + /// + /// A list of valid objects representing the discovered plugins. + internal List GetPluginsInPath() + { + var pluginFiles = new List(); + var nugetPluginPaths = _environmentVariableReader.GetEnvironmentVariable("PATH"); + string[] paths = nugetPluginPaths?.Split(Path.PathSeparator) ?? Array.Empty(); + + foreach (var path in paths) + { + if (PathValidator.IsValidLocalPath(path) || PathValidator.IsValidUncPath(path)) + { + pluginFiles.AddRange(GetNetToolsPluginsInDirectory(path) ?? new List()); + } + else + { + pluginFiles.Add(new PluginFile(path, new Lazy(() => PluginFileState.InvalidFilePath))); + } + } + + return pluginFiles; + } + + private static List GetNetToolsPluginsInDirectory(string directoryPath) + { + var pluginFiles = new List(); + + if (Directory.Exists(directoryPath)) + { + var directoryInfo = new DirectoryInfo(directoryPath); + var files = directoryInfo.GetFiles("nuget-plugin-*"); + + foreach (var file in files) + { + if (IsValidPluginFile(file)) + { + PluginFile pluginFile = new PluginFile(file.FullName, new Lazy(() => PluginFileState.Valid), isDotnetToolsPlugin: true); + pluginFiles.Add(pluginFile); + } + } + } + + return pluginFiles; + } + + /// + /// Checks whether a file is a valid plugin file for windows/Unix. + /// Windows: It should be either .bat or .exe + /// Unix: It should be executable + /// + /// + /// + internal static bool IsValidPluginFile(FileInfo fileInfo) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return fileInfo.Extension.Equals(".exe", StringComparison.OrdinalIgnoreCase) || + fileInfo.Extension.Equals(".bat", StringComparison.OrdinalIgnoreCase); + } + else + { +#if NET8_0_OR_GREATER + var fileMode = File.GetUnixFileMode(fileInfo.FullName); + + return fileInfo.Exists && + ((fileMode & UnixFileMode.UserExecute) != 0 || + (fileMode & UnixFileMode.GroupExecute) != 0 || + (fileMode & UnixFileMode.OtherExecute) != 0); +#else + return fileInfo.Exists && IsExecutable(fileInfo); #endif - return PluginDiscoveryUtility.GetConventionBasedPlugins(directories); } + } + +#if !NET8_0_OR_GREATER + /// + /// Checks whether a file is executable or not in Unix. + /// This is done by running bash code: `if [ -x {fileInfo.FullName} ]; then echo yes; else echo no; fi` + /// + /// + /// + internal static bool IsExecutable(FileInfo fileInfo) + { +#pragma warning disable CA1031 // Do not catch general exception types + try + { + string output; + using (var process = new System.Diagnostics.Process()) + { + // Use a shell command to check if the file is executable + process.StartInfo.FileName = "/bin/bash"; + process.StartInfo.Arguments = $" -c \"if [ -x '{fileInfo.FullName}' ]; then echo yes; else echo no; fi\""; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + + process.Start(); + output = process.StandardOutput.ReadToEnd().Trim(); + + if (!process.HasExited && !process.WaitForExit(1000)) + { + process.Kill(); + return false; + } + else if (process.ExitCode != 0) + { + return false; + } - return _rawPluginPaths.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + // Check if the output is "yes" + return output.Equals("yes", StringComparison.OrdinalIgnoreCase); + } + } + catch + { + return false; + } +#pragma warning restore CA1031 // Do not catch general exception types } +#endif } } diff --git a/src/NuGet.Core/NuGet.Protocol/Plugins/PluginFile.cs b/src/NuGet.Core/NuGet.Protocol/Plugins/PluginFile.cs index 9753e0285bf..96c24f2c86f 100644 --- a/src/NuGet.Core/NuGet.Protocol/Plugins/PluginFile.cs +++ b/src/NuGet.Core/NuGet.Protocol/Plugins/PluginFile.cs @@ -20,6 +20,22 @@ public sealed class PluginFile /// public Lazy State { get; } + /// + /// Is the plugin file, a dotnet tools plugin file? + /// + internal bool IsDotnetToolsPlugin { get; } + + /// + /// Instantiates a new class. + /// + /// The plugin's file path. + /// A lazy that evaluates the plugin file state. + /// Is the plugin file, a dotnet tools plugin file? + internal PluginFile(string filePath, Lazy state, bool isDotnetToolsPlugin) : this(filePath, state) + { + IsDotnetToolsPlugin = isDotnetToolsPlugin; + } + /// /// Instantiates a new class. /// diff --git a/src/NuGet.Core/NuGet.Protocol/Plugins/PluginManager.cs b/src/NuGet.Core/NuGet.Protocol/Plugins/PluginManager.cs index 53dfa731769..6ff4407f862 100644 --- a/src/NuGet.Core/NuGet.Protocol/Plugins/PluginManager.cs +++ b/src/NuGet.Core/NuGet.Protocol/Plugins/PluginManager.cs @@ -33,7 +33,6 @@ public sealed class PluginManager : IPluginManager, IDisposable private IPluginFactory _pluginFactory; private ConcurrentDictionary>>> _pluginOperationClaims; private ConcurrentDictionary> _pluginUtilities; - private string _rawPluginPaths; private static Lazy _currentProcessId = new Lazy(GetCurrentProcessId); private Lazy _pluginsCacheDirectoryPath; @@ -312,15 +311,6 @@ private void Initialize(IEnvironmentVariableReader reader, { throw new ArgumentNullException(nameof(pluginFactoryCreator)); } -#if IS_DESKTOP - _rawPluginPaths = reader.GetEnvironmentVariable(EnvironmentVariableConstants.DesktopPluginPaths); -#else - _rawPluginPaths = reader.GetEnvironmentVariable(EnvironmentVariableConstants.CorePluginPaths); -#endif - if (string.IsNullOrEmpty(_rawPluginPaths)) - { - _rawPluginPaths = reader.GetEnvironmentVariable(EnvironmentVariableConstants.PluginPaths); - } _connectionOptions = ConnectionOptions.CreateDefault(reader); @@ -360,12 +350,20 @@ private async Task> GetPluginOperationClaimsAsync( private PluginDiscoverer InitializeDiscoverer() { - return new PluginDiscoverer(_rawPluginPaths); + return new PluginDiscoverer(); } private bool IsPluginPossiblyAvailable() { - return !string.IsNullOrEmpty(_rawPluginPaths); + string pluginEnvVariable; + +#if IS_DESKTOP + pluginEnvVariable = EnvironmentVariableReader.GetEnvironmentVariable(EnvironmentVariableConstants.DesktopPluginPaths); +#else + pluginEnvVariable = EnvironmentVariableReader.GetEnvironmentVariable(EnvironmentVariableConstants.CorePluginPaths); +#endif + pluginEnvVariable ??= EnvironmentVariableReader.GetEnvironmentVariable(EnvironmentVariableConstants.PluginPaths); + return !string.IsNullOrEmpty(pluginEnvVariable); } private void OnPluginClosed(object sender, EventArgs e) diff --git a/src/NuGet.Core/NuGet.Protocol/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/NuGet.Core/NuGet.Protocol/PublicAPI/net472/PublicAPI.Unshipped.txt index 9f62b32fec7..7e98009cd58 100644 --- a/src/NuGet.Core/NuGet.Protocol/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/NuGet.Core/NuGet.Protocol/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -1,2 +1,2 @@ #nullable enable -~NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer(string rawPluginPaths) -> void +NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer() -> void diff --git a/src/NuGet.Core/NuGet.Protocol/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/NuGet.Core/NuGet.Protocol/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 9f62b32fec7..7e98009cd58 100644 --- a/src/NuGet.Core/NuGet.Protocol/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/NuGet.Core/NuGet.Protocol/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1,2 +1,2 @@ #nullable enable -~NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer(string rawPluginPaths) -> void +NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer() -> void diff --git a/src/NuGet.Core/NuGet.Protocol/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/NuGet.Core/NuGet.Protocol/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 9f62b32fec7..7e98009cd58 100644 --- a/src/NuGet.Core/NuGet.Protocol/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/NuGet.Core/NuGet.Protocol/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,2 +1,2 @@ #nullable enable -~NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer(string rawPluginPaths) -> void +NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer() -> void diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginDiscovererTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginDiscovererTests.cs index f48460dee65..f27749db149 100644 --- a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginDiscovererTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginDiscovererTests.cs @@ -3,10 +3,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Moq; +using NuGet.Common; using NuGet.Test.Utility; using Xunit; @@ -20,15 +23,27 @@ public class PluginDiscovererTests [InlineData(" ")] public void Constructor_AcceptsAnyString(string rawPluginPaths) { - using (new PluginDiscoverer(rawPluginPaths)) + var environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.DesktopPluginPaths)).Returns(rawPluginPaths); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.CorePluginPaths)).Returns(rawPluginPaths); + + Exception exception = Record.Exception(() => { - } + using (new PluginDiscoverer()) + { + } + }); + + Assert.Null(exception); } [Fact] public async Task DiscoverAsync_ThrowsIfCancelled() { - using (var discoverer = new PluginDiscoverer(rawPluginPaths: "")) + var environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.DesktopPluginPaths)).Returns(""); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.CorePluginPaths)).Returns(""); + using (var discoverer = new PluginDiscoverer(environmentalVariableReader.Object)) { await Assert.ThrowsAsync( () => discoverer.DiscoverAsync(new CancellationToken(canceled: true))); @@ -38,7 +53,10 @@ await Assert.ThrowsAsync( [Fact] public async Task DiscoverAsync_DoesNotThrowIfNoValidFilePathsAndFallbackEmbeddedSignatureVerifier() { - using (var discoverer = new PluginDiscoverer(rawPluginPaths: ";")) + var environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.DesktopPluginPaths)).Returns(Path.PathSeparator.ToString()); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.CorePluginPaths)).Returns(Path.PathSeparator.ToString()); + using (var discoverer = new PluginDiscoverer(environmentalVariableReader.Object)) { var pluginFiles = await discoverer.DiscoverAsync(CancellationToken.None); @@ -54,13 +72,11 @@ public async Task DiscoverAsync_PerformsDiscoveryOnlyOnce() var pluginPath = Path.Combine(testDirectory.Path, "a"); File.WriteAllText(pluginPath, string.Empty); + var environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.DesktopPluginPaths)).Returns(pluginPath); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.CorePluginPaths)).Returns(pluginPath); - var responses = new Dictionary() - { - { pluginPath, true } - }; - - using (var discoverer = new PluginDiscoverer(pluginPath)) + using (var discoverer = new PluginDiscoverer(environmentalVariableReader.Object)) { var results = (await discoverer.DiscoverAsync(CancellationToken.None)).ToArray(); @@ -90,9 +106,11 @@ public async Task DiscoverAsync_HandlesAllPluginFileStates() File.WriteAllText(pluginPaths[1], string.Empty); string rawPluginPaths = - $"{pluginPaths[0]};{pluginPaths[1]};c"; - - using (var discoverer = new PluginDiscoverer(rawPluginPaths)) + $"{pluginPaths[0]}{Path.PathSeparator}{pluginPaths[1]}{Path.PathSeparator}c"; + var environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.DesktopPluginPaths)).Returns(rawPluginPaths); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.CorePluginPaths)).Returns(rawPluginPaths); + using (var discoverer = new PluginDiscoverer(environmentalVariableReader.Object)) { var results = (await discoverer.DiscoverAsync(CancellationToken.None)).ToArray(); @@ -121,8 +139,10 @@ public async Task DiscoverAsync_HandlesAllPluginFileStates() public async Task DiscoverAsync_DisallowsNonRootedFilePaths(string pluginPath) { var responses = new Dictionary() { { pluginPath, true } }; - - using (var discoverer = new PluginDiscoverer(pluginPath)) + var environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.DesktopPluginPaths)).Returns(pluginPath); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.CorePluginPaths)).Returns(pluginPath); + using (var discoverer = new PluginDiscoverer(environmentalVariableReader.Object)) { var results = (await discoverer.DiscoverAsync(CancellationToken.None)).ToArray(); @@ -140,8 +160,11 @@ public async Task DiscoverAsync_IsIdempotent() var pluginPath = Path.Combine(testDirectory.Path, "a"); File.WriteAllText(pluginPath, string.Empty); + var environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.DesktopPluginPaths)).Returns(pluginPath); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.CorePluginPaths)).Returns(pluginPath); - using (var discoverer = new PluginDiscoverer(pluginPath)) + using (var discoverer = new PluginDiscoverer(environmentalVariableReader.Object)) { var firstResult = await discoverer.DiscoverAsync(CancellationToken.None); var firstState = firstResult.SingleOrDefault().PluginFile.State.Value; @@ -153,5 +176,544 @@ public async Task DiscoverAsync_IsIdempotent() } } } + + [PlatformTheory(Platform.Windows)] + [InlineData("nuget-plugin-myPlugin.exe")] + [InlineData("nuget-plugin-myPlugin.bat")] + public async Task DiscoverAsync_withValidDotNetToolsPluginWindows_FindsThePlugin(string fileName) + { + using (var testDirectory = TestDirectory.Create()) + { + // Arrange + var pluginPath = Path.Combine(testDirectory.Path, "myPlugin"); + Directory.CreateDirectory(pluginPath); + var myPlugin = Path.Combine(pluginPath, fileName); + Mock environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.PluginPaths)).Returns(pluginPath); + + File.WriteAllText(myPlugin, string.Empty); + + using (var discoverer = new PluginDiscoverer(environmentalVariableReader.Object)) + { + // Act + var result = await discoverer.DiscoverAsync(CancellationToken.None); + + // Assert + var discovered = false; + + foreach (PluginDiscoveryResult discoveryResult in result) + { + if (myPlugin == discoveryResult.PluginFile.Path) discovered = true; + } + + Assert.True(discovered); + } + } + } + + [PlatformTheory(Platform.Windows)] + [InlineData("nuget-plugin-myPlugin.exe")] + [InlineData("nuget-plugin-myPlugin.bat")] + public async Task DiscoverAsync_WithPluginPathSpecifiedInNuGetPluginPathsEnvVariableWindows_FindsThePlugin(string fileName) + { + using (var testDirectory = TestDirectory.Create()) + { + // Arrange + var pluginPath = Path.Combine(testDirectory.Path, "myPlugin"); + Directory.CreateDirectory(pluginPath); + var myPlugin = Path.Combine(pluginPath, fileName); + Mock environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable("NUGET_PLUGIN_PATHS")).Returns(pluginPath); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable("PATHS")).Returns(""); + File.WriteAllText(myPlugin, string.Empty); + + using (var discoverer = new PluginDiscoverer(environmentalVariableReader.Object)) + { + // Act + var result = await discoverer.DiscoverAsync(CancellationToken.None); + + // Assert + var discovered = false; + + foreach (PluginDiscoveryResult discoveryResult in result) + { + if (myPlugin == discoveryResult.PluginFile.Path) discovered = true; + } + + Assert.True(discovered); + } + } + } + + [PlatformTheory(Platform.Windows)] + [InlineData("nugetplugin-myPlugin.exe")] + [InlineData("nugetplugin-myPlugin.bat")] + public async Task DiscoverAsync_withInValidDotNetToolsPluginNameWindows_DoesNotFindThePlugin(string fileName) + { + using (var testDirectory = TestDirectory.Create()) + { + // Arrange + var pluginPath = Path.Combine(testDirectory.Path, "myPlugin"); + Directory.CreateDirectory(pluginPath); + var myPlugin = Path.Combine(pluginPath, fileName); + Mock environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(It.IsAny())).Returns(pluginPath); + + File.WriteAllText(myPlugin, string.Empty); + + using (var discoverer = new PluginDiscoverer(environmentalVariableReader.Object)) + { + // Act + var result = await discoverer.DiscoverAsync(CancellationToken.None); + + // Assert + var discovered = false; + + foreach (PluginDiscoveryResult discoveryResult in result) + { + if (myPlugin == discoveryResult.PluginFile.Path) discovered = true; + } + + Assert.False(discovered); + } + } + } + + [PlatformFact(Platform.Linux)] + public async Task DiscoverAsync_withValidDotNetToolsPluginLinux_FindsThePlugin() + { + using (var testDirectory = TestDirectory.Create()) + { + // Arrange + var pluginPath = Path.Combine(testDirectory.Path, "myPlugins"); + Directory.CreateDirectory(pluginPath); + var myPlugin = Path.Combine(pluginPath, "nuget-plugin-MyPlugin"); + File.WriteAllText(myPlugin, string.Empty); + Mock environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.PluginPaths)).Returns(pluginPath); + + using (var process = new Process()) + { + // Use a shell command to make the file executable + process.StartInfo.FileName = "/bin/bash"; + process.StartInfo.Arguments = $"-c \"chmod +x {myPlugin}\""; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + process.WaitForExit(); + + if (process.ExitCode == 0) + { + using (var discoverer = new PluginDiscoverer(environmentalVariableReader.Object)) + { + // Act + var result = await discoverer.DiscoverAsync(CancellationToken.None); + + // Assert + var discovered = false; + + foreach (PluginDiscoveryResult discoveryResult in result) + { + if (myPlugin == discoveryResult.PluginFile.Path) discovered = true; + } + + Assert.True(discovered); + } + } + } + } + } + + [PlatformFact(Platform.Linux)] + public async Task DiscoverAsync_WithPluginPathSpecifiedInNuGetPluginPathsEnvVariableLinux_FindsThePlugin() + { + using (var testDirectory = TestDirectory.Create()) + { + // Arrange + var pluginPath = Path.Combine(testDirectory.Path, "myPlugins"); + Directory.CreateDirectory(pluginPath); + var myPlugin = Path.Combine(pluginPath, "nuget-plugin-MyPlugin"); + File.WriteAllText(myPlugin, string.Empty); + Mock environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.PluginPaths)).Returns(pluginPath); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable("PATHS")).Returns(""); + + using (var process = new Process()) + { + // Use a shell command to make the file executable + process.StartInfo.FileName = "/bin/bash"; + process.StartInfo.Arguments = $"-c \"chmod +x {myPlugin}\""; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + process.WaitForExit(); + + if (process.ExitCode == 0) + { + using (var discoverer = new PluginDiscoverer(environmentalVariableReader.Object)) + { + // Act + var result = await discoverer.DiscoverAsync(CancellationToken.None); + + // Assert + var discovered = false; + + foreach (PluginDiscoveryResult discoveryResult in result) + { + if (myPlugin == discoveryResult.PluginFile.Path) discovered = true; + } + + Assert.True(discovered); + } + } + } + } + } + + [PlatformFact(Platform.Linux)] + public async Task DiscoverAsync_withNoExecutableValidDotNetToolsPluginLinux_DoesNotFindThePlugin() + { + using (var testDirectory = TestDirectory.Create()) + { + // Arrange + var pluginPath = Path.Combine(testDirectory.Path, "myPlugins"); + Directory.CreateDirectory(pluginPath); + var myPlugin = Path.Combine(pluginPath, "nuget-plugin-MyPlugin"); + File.WriteAllText(myPlugin, string.Empty); + Mock environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.PluginPaths)).Returns(pluginPath); + + using (var process = new Process()) + { + // Use a shell command to make the file not executable + process.StartInfo.FileName = "/bin/bash"; + process.StartInfo.Arguments = $"-c \"chmod -x {myPlugin}\""; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + process.WaitForExit(); + + if (process.ExitCode == 0) + { + using (var discoverer = new PluginDiscoverer(environmentalVariableReader.Object)) + { + // Act + var result = await discoverer.DiscoverAsync(CancellationToken.None); + + // Assert + var discovered = false; + + foreach (PluginDiscoveryResult discoveryResult in result) + { + if (myPlugin == discoveryResult.PluginFile.Path) discovered = true; + } + + Assert.False(discovered); + } + } + } + } + } + + [PlatformFact(Platform.Windows)] + public void GetPluginsInNuGetPluginPaths_WithNuGetPluginPathsSet_ReturnsPluginsInNuGetPluginPathOnly() + { + // Arrange + using TestDirectory pluginPathDirectory = TestDirectory.Create(); + using TestDirectory pathDirectory = TestDirectory.Create(); + var pluginInNuGetPluginPathDirectoryFilePath = Path.Combine(pluginPathDirectory.Path, "nuget-plugin-auth.exe"); + var pluginInPathDirectoryFilePath = Path.Combine(pathDirectory.Path, "nuget-plugin-in-path-directory.exe"); + File.Create(pluginInNuGetPluginPathDirectoryFilePath); + File.Create(pluginInPathDirectoryFilePath); + Mock environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.PluginPaths)).Returns(Directory.GetParent(pluginInNuGetPluginPathDirectoryFilePath).FullName); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable("PATH")).Returns(Directory.GetParent(pluginInPathDirectoryFilePath).FullName); + PluginDiscoverer pluginDiscoverer = new PluginDiscoverer(environmentalVariableReader.Object); + + // Act + var plugins = pluginDiscoverer.GetPluginsInNuGetPluginPaths(); + + // Assert + Assert.Single(plugins); + Assert.Equal(pluginInNuGetPluginPathDirectoryFilePath, plugins[0].Path); + Assert.True(plugins[0].IsDotnetToolsPlugin); + } + + [PlatformFact(Platform.Windows)] + public void GetPluginsInNuGetPluginPaths_WithoutNuGetPluginPaths_ReturnsEmpty() + { + // Arrange + using var pathDirectory = TestDirectory.Create(); + var pluginFilePath = Path.Combine(pathDirectory.Path, "nuget-plugin-fallback.exe"); + File.Create(pluginFilePath); + + var environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable("PATH")).Returns(pathDirectory.Path); + + var pluginDiscoverer = new PluginDiscoverer(environmentalVariableReader.Object); + + // Act + var plugins = pluginDiscoverer.GetPluginsInNuGetPluginPaths(); + + // Assert + Assert.Empty(plugins); + } + + [PlatformFact(Platform.Windows)] + public void GetPluginsInPATH_WithPATHSet_ReturnsPlugin() + { + // Arrange + using TestDirectory pluginPathDirectory = TestDirectory.Create(); + using TestDirectory pathDirectory = TestDirectory.Create(); + var pluginInNuGetPluginPathDirectoryFilePath = Path.Combine(pluginPathDirectory.Path, "nuget-plugin-auth.exe"); + var pluginInPathDirectoryFilePath = Path.Combine(pathDirectory.Path, "nuget-plugin-in-path-directory.exe"); + File.Create(pluginInNuGetPluginPathDirectoryFilePath); + File.Create(pluginInPathDirectoryFilePath); + Mock environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.PluginPaths)).Returns(Directory.GetParent(pluginInNuGetPluginPathDirectoryFilePath).FullName); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable("PATH")).Returns(Directory.GetParent(pluginInPathDirectoryFilePath).FullName); + PluginDiscoverer pluginDiscoverer = new PluginDiscoverer(environmentalVariableReader.Object); + + // Act + var plugins = pluginDiscoverer.GetPluginsInPath(); + + // Assert + Assert.Single(plugins); + Assert.Equal(pluginInPathDirectoryFilePath, plugins[0].Path); + Assert.True(plugins[0].IsDotnetToolsPlugin); + } + + [PlatformFact(Platform.Windows)] + public void GetPluginsInNuGetPluginPaths_NuGetPluginPathsPointsToAFile_TreatsAsPlugin() + { + // Arrange + using TestDirectory testDirectory = TestDirectory.Create(); + var pluginFilePath = Path.Combine(testDirectory.Path, "nuget-plugin-auth.exe"); + File.Create(pluginFilePath); + + var environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.PluginPaths)).Returns(pluginFilePath); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable("PATH")).Returns(string.Empty); + + var pluginDiscoverer = new PluginDiscoverer(environmentalVariableReader.Object); + + // Act + var plugins = pluginDiscoverer.GetPluginsInNuGetPluginPaths(); + + // Assert + Assert.Single(plugins); + Assert.Equal(pluginFilePath, plugins[0].Path); + Assert.True(plugins[0].IsDotnetToolsPlugin); + } + + [PlatformFact(Platform.Windows)] + public void GetPluginsInNuGetPluginPaths_NuGetPluginPathsPointsToAFileThatDoesNotStartWithNugetPlugin_ReturnsNonDotnetPlugin() + { + // Arrange + using TestDirectory testDirectory = TestDirectory.Create(); + var pluginFilePath = Path.Combine(testDirectory.Path, "other-plugin.exe"); + File.Create(pluginFilePath); + + var environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable(EnvironmentVariableConstants.PluginPaths)).Returns(pluginFilePath); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable("PATH")).Returns(string.Empty); + + var pluginDiscoverer = new PluginDiscoverer(environmentalVariableReader.Object); + + // Act + var plugins = pluginDiscoverer.GetPluginsInNuGetPluginPaths(); + + // Assert + Assert.Single(plugins); + Assert.False(plugins[0].IsDotnetToolsPlugin); + } + + [PlatformFact(Platform.Windows)] + public void GetPluginsInPATH_PATHPointsToADirectory_ContainsValidPluginFiles() + { + // Arrange + using var pluginPathDirectory = TestDirectory.Create(); + var validPluginFile = Path.Combine(pluginPathDirectory.Path, "nuget-plugin-auth.exe"); + var invalidPluginFile = Path.Combine(pluginPathDirectory.Path, "not-a-nuget-plugin.exe"); + File.Create(validPluginFile); + File.Create(invalidPluginFile); + + var environmentalVariableReader = new Mock(); + environmentalVariableReader.Setup(env => env.GetEnvironmentVariable("PATH")).Returns(pluginPathDirectory.Path); + + var pluginDiscoverer = new PluginDiscoverer(environmentalVariableReader.Object); + + // Act + var plugins = pluginDiscoverer.GetPluginsInPath(); + + // Assert + Assert.Single(plugins); + Assert.Equal(validPluginFile, plugins[0].Path); + Assert.True(plugins[0].IsDotnetToolsPlugin); + } + + [PlatformFact(Platform.Windows)] + public void GetPluginsInNuGetPluginPaths_NoEnvironmentVariables_ReturnsNoPlugins() + { + // Arrange + var environmentalVariableReader = new Mock(); + var pluginDiscoverer = new PluginDiscoverer(environmentalVariableReader.Object); + + // Act + var plugins = pluginDiscoverer.GetPluginsInNuGetPluginPaths(); + + // Assert + Assert.Empty(plugins); + } + + [PlatformFact(Platform.Windows)] + public void IsValidPluginFile_ExeFile_ReturnsTrue() + { + // Arrange + using TestDirectory testDirectory = TestDirectory.Create(); + var workingPath = testDirectory.Path; + var pluginFilePath = Path.Combine(workingPath, "plugin.exe"); + File.Create(pluginFilePath); + var fileInfo = new FileInfo(pluginFilePath); + + // Act + bool result = PluginDiscoverer.IsValidPluginFile(fileInfo); + + // Assert + Assert.True(result); + } + + [PlatformFact(Platform.Windows)] + public void IsValidPluginFile_Windows_NonExecutableFile_ReturnsFalse() + { + // Arrange + using TestDirectory testDirectory = TestDirectory.Create(); + var workingPath = testDirectory.Path; + var nonPluginFilePath = Path.Combine(workingPath, "plugin.txt"); + File.Create(nonPluginFilePath); + var fileInfo = new FileInfo(nonPluginFilePath); + + // Act + bool result = PluginDiscoverer.IsValidPluginFile(fileInfo); + + // Assert + Assert.False(result); + } + + [PlatformFact(Platform.Linux)] + public void IsValidPluginFile_Unix_ExecutableFile_ReturnsTrue() + { + // Arrange + using TestDirectory testDirectory = TestDirectory.Create(); + var workingPath = testDirectory.Path; + var pluginFilePath = Path.Combine(workingPath, "plugin"); + File.Create(pluginFilePath).Dispose(); + +#if NET8_0_OR_GREATER + // Set execute permissions + File.SetUnixFileMode(pluginFilePath, UnixFileMode.UserExecute | UnixFileMode.UserRead); +#else + // Use chmod to set execute permissions + var process = new Process(); + process.StartInfo.FileName = "/bin/bash"; + process.StartInfo.Arguments = $"-c \"chmod +x {pluginFilePath}\""; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + process.WaitForExit(); +#endif + + var fileInfo = new FileInfo(pluginFilePath); + + // Act + bool result = PluginDiscoverer.IsValidPluginFile(fileInfo); + + // Assert + Assert.True(result); + } + +#if !NET8_0_OR_GREATER + [PlatformFact(Platform.Linux)] + public void IsExecutable_FileIsExecutable_ReturnsTrue() + { + // Arrange + using TestDirectory testDirectory = TestDirectory.Create(); + var workingPath = testDirectory.Path; + var pluginFilePath = Path.Combine(workingPath, "plugin"); + File.Create(pluginFilePath); + + // Set execute permissions + var process = new Process(); + process.StartInfo.FileName = "/bin/bash"; + process.StartInfo.Arguments = $"-c \"chmod +x {pluginFilePath}\""; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + process.WaitForExit(); + + var fileInfo = new FileInfo(pluginFilePath); + + // Act + bool result = PluginDiscoverer.IsExecutable(fileInfo); + + // Assert + Assert.True(result); + } + + [PlatformFact(Platform.Linux)] + public void IsExecutable_FileIsNotExecutable_ReturnsFalse() + { + // Arrange + using TestDirectory testDirectory = TestDirectory.Create(); + var workingPath = testDirectory.Path; + var pluginFilePath = Path.Combine(workingPath, "plugin"); + File.Create(pluginFilePath); + + // Remove execute permissions + var process = new Process(); + process.StartInfo.FileName = "/bin/bash"; + process.StartInfo.Arguments = $"-c \"chmod -x {pluginFilePath}\""; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + process.WaitForExit(); + + var fileInfo = new FileInfo(pluginFilePath); + + // Act + bool result = PluginDiscoverer.IsExecutable(fileInfo); + + // Assert + Assert.False(result); + } + + [PlatformFact(Platform.Linux)] + public void IsExecutable_FileWithSpace_ReturnsTrue() + { + // Arrange + using TestDirectory testDirectory = TestDirectory.Create(); + var workingPath = testDirectory.Path; + var pluginFilePath = Path.Combine(workingPath, "plugin with space"); + File.Create(pluginFilePath).Dispose(); + + // Set execute permissions + var process = new Process(); + process.StartInfo.FileName = "/bin/bash"; + process.StartInfo.Arguments = $"-c \"chmod +x '{pluginFilePath}'\""; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + process.WaitForExit(); + + var fileInfo = new FileInfo(pluginFilePath); + + // Act + bool result = PluginDiscoverer.IsExecutable(fileInfo); + + // Assert + Assert.True(result); + } + +#endif } } From d883a09ee735c5b81235b5aa0be78ce34a622492 Mon Sep 17 00:00:00 2001 From: Nigusu Solomon Yenework <59111203+Nigusu-Allehu@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:28:44 -0800 Subject: [PATCH 2/2] Execute net tools plugins (#6113) --- .../NuGet.Protocol/GlobalSuppressions.cs | 1 - .../NuGet.Protocol/Plugins/IPluginFactory.cs | 8 +- .../Plugins/PluginDiscoverer.cs | 23 +++-- .../NuGet.Protocol/Plugins/PluginFactory.cs | 84 +++++++++++-------- .../NuGet.Protocol/Plugins/PluginFile.cs | 19 ++--- .../NuGet.Protocol/Plugins/PluginManager.cs | 16 ++-- .../PublicAPI/net472/PublicAPI.Shipped.txt | 6 -- .../PublicAPI/net472/PublicAPI.Unshipped.txt | 4 + .../PublicAPI/net8.0/PublicAPI.Shipped.txt | 6 -- .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 4 + .../netstandard2.0/PublicAPI.Shipped.txt | 6 -- .../netstandard2.0/PublicAPI.Unshipped.txt | 4 + .../NuGet.Protocol.FuncTest/PluginTests.cs | 22 +++-- .../PluginManagerMock.cs | 20 ++++- ...urePluginCredentialProviderBuilderTests.cs | 17 +++- .../SecurePluginCredentialProviderTests.cs | 34 +++++--- .../Plugins/PluginDiscovererTests.cs | 9 +- .../Plugins/PluginDiscoveryResultTests.cs | 14 +++- .../Plugins/PluginFactoryTests.cs | 54 ++++++++++-- .../Plugins/PluginFileTests.cs | 16 +++- .../Plugins/PluginManagerTests.cs | 44 +++++++--- .../Plugins/PluginResourceProviderTests.cs | 34 ++++++-- 22 files changed, 304 insertions(+), 141 deletions(-) diff --git a/src/NuGet.Core/NuGet.Protocol/GlobalSuppressions.cs b/src/NuGet.Core/NuGet.Protocol/GlobalSuppressions.cs index fbb29505816..c8e0b1cab94 100644 --- a/src/NuGet.Core/NuGet.Protocol/GlobalSuppressions.cs +++ b/src/NuGet.Core/NuGet.Protocol/GlobalSuppressions.cs @@ -133,7 +133,6 @@ [assembly: SuppressMessage("Build", "CA1031:Modify 'FireBeforeClose' to catch a more specific allowed exception type, or rethrow the exception.", Justification = "", Scope = "member", Target = "~M:NuGet.Protocol.Plugins.Plugin.FireBeforeClose")] [assembly: SuppressMessage("Build", "CA1031:Modify 'FireClosed' to catch a more specific allowed exception type, or rethrow the exception.", Justification = "", Scope = "member", Target = "~M:NuGet.Protocol.Plugins.Plugin.FireClosed")] [assembly: SuppressMessage("Build", "CA2000:Call System.IDisposable.Dispose on object created by 'new MonitorNuGetProcessExitRequestHandler(plugin)' before all references to it are out of scope.", Justification = "", Scope = "member", Target = "~M:NuGet.Protocol.Plugins.PluginFactory.CreateFromCurrentProcessAsync(NuGet.Protocol.Plugins.IRequestHandlers,NuGet.Protocol.Plugins.ConnectionOptions,System.Threading.CancellationToken)~System.Threading.Tasks.Task{NuGet.Protocol.Plugins.IPlugin}")] -[assembly: SuppressMessage("Build", "CA2000:Use recommended dispose pattern to ensure that object created by 'new PluginProcess(startInfo)' is disposed on all paths. If possible, wrap the creation within a 'using' statement or a 'using' declaration. Otherwise, use a try-finally pattern, with a dedicated local variable declared before the try region and an unconditional Dispose invocation on non-null value in the 'finally' region, say 'x?.Dispose()'. If the object is explicitly disposed within the try region or the dispose ownership is transfered to another object or method, assign 'null' to the local variable just after such an operation to prevent double dispose in 'finally'.", Justification = "The responsibility to dispose the object is transferred to another object or wrapper that's created in the method and returned to the caller", Scope = "member", Target = "~M:NuGet.Protocol.Plugins.PluginFactory.CreatePluginAsync(System.String,System.Collections.Generic.IEnumerable{System.String},NuGet.Protocol.Plugins.IRequestHandlers,NuGet.Protocol.Plugins.ConnectionOptions,System.Threading.CancellationToken)~System.Threading.Tasks.Task{NuGet.Protocol.Plugins.IPlugin}")] [assembly: SuppressMessage("Build", "CA1031:Modify 'SendCloseRequest' to catch a more specific allowed exception type, or rethrow the exception.", Justification = "", Scope = "member", Target = "~M:NuGet.Protocol.Plugins.PluginFactory.SendCloseRequest(NuGet.Protocol.Plugins.IPlugin)")] [assembly: SuppressMessage("Build", "CA1822:Member GetPluginOperationClaimsAsync does not access instance data and can be marked as static (Shared in VisualBasic)", Justification = "", Scope = "member", Target = "~M:NuGet.Protocol.Plugins.PluginManager.GetPluginOperationClaimsAsync(NuGet.Protocol.Plugins.IPlugin,System.String,Newtonsoft.Json.Linq.JObject,System.Threading.CancellationToken)~System.Threading.Tasks.Task{System.Collections.Generic.IReadOnlyList{NuGet.Protocol.Plugins.OperationClaim}}")] [assembly: SuppressMessage("Build", "CA1031:Modify 'TryCreatePluginAsync' to catch a more specific allowed exception type, or rethrow the exception.", Justification = "", Scope = "member", Target = "~M:NuGet.Protocol.Plugins.PluginManager.TryCreatePluginAsync(NuGet.Protocol.Plugins.PluginDiscoveryResult,NuGet.Protocol.Plugins.OperationClaim,NuGet.Protocol.Plugins.PluginManager.PluginRequestKey,System.String,Newtonsoft.Json.Linq.JObject,System.Threading.CancellationToken)~System.Threading.Tasks.Task{System.Tuple{System.Boolean,NuGet.Protocol.Plugins.PluginCreationResult}}")] diff --git a/src/NuGet.Core/NuGet.Protocol/Plugins/IPluginFactory.cs b/src/NuGet.Core/NuGet.Protocol/Plugins/IPluginFactory.cs index ae187ee4f9f..1c5b0f3c697 100644 --- a/src/NuGet.Core/NuGet.Protocol/Plugins/IPluginFactory.cs +++ b/src/NuGet.Core/NuGet.Protocol/Plugins/IPluginFactory.cs @@ -11,12 +11,12 @@ namespace NuGet.Protocol.Plugins /// /// A plugin factory. /// - public interface IPluginFactory : IDisposable + internal interface IPluginFactory : IDisposable { /// /// Asynchronously gets an existing plugin instance or creates a new instance and connects to it. /// - /// The file path of the plugin. + /// A plugin file. /// Command-line arguments to be supplied to the plugin. /// Request handlers. /// Connection options. @@ -24,7 +24,7 @@ public interface IPluginFactory : IDisposable /// A task that represents the asynchronous operation. /// The task result () returns a /// instance. - /// Thrown if + /// Thrown if /// is either or empty. /// Thrown if /// is . @@ -37,7 +37,7 @@ public interface IPluginFactory : IDisposable /// Thrown if this object is disposed. /// This is intended to be called by NuGet client tools. Task GetOrCreateAsync( - string filePath, + PluginFile pluginFile, IEnumerable arguments, IRequestHandlers requestHandlers, ConnectionOptions options, diff --git a/src/NuGet.Core/NuGet.Protocol/Plugins/PluginDiscoverer.cs b/src/NuGet.Core/NuGet.Protocol/Plugins/PluginDiscoverer.cs index d342827a370..00c5ab489c2 100644 --- a/src/NuGet.Core/NuGet.Protocol/Plugins/PluginDiscoverer.cs +++ b/src/NuGet.Core/NuGet.Protocol/Plugins/PluginDiscoverer.cs @@ -23,6 +23,17 @@ public sealed class PluginDiscoverer : IPluginDiscoverer private IEnumerable _results; private readonly SemaphoreSlim _semaphore; private readonly IEnvironmentVariableReader _environmentVariableReader; + private static bool IsDesktop + { + get + { +#if IS_DESKTOP + return true; +#else + return false; +#endif + } + } public PluginDiscoverer() : this(EnvironmentVariableWrapper.Instance) @@ -168,7 +179,7 @@ private static List GetPluginFiles(IEnumerable filePaths, Ca { return PluginFileState.InvalidFilePath; } - })); + }), requiresDotnetHost: !IsDesktop); files.Add(pluginFile); } @@ -197,7 +208,7 @@ internal List GetPluginsInNuGetPluginPaths() // A DotNet tool plugin if (IsValidPluginFile(fileInfo)) { - PluginFile pluginFile = new PluginFile(fileInfo.FullName, new Lazy(() => PluginFileState.Valid), isDotnetToolsPlugin: true); + PluginFile pluginFile = new PluginFile(fileInfo.FullName, new Lazy(() => PluginFileState.Valid), requiresDotnetHost: false); pluginFiles.Add(pluginFile); } } @@ -205,7 +216,7 @@ internal List GetPluginsInNuGetPluginPaths() { // A non DotNet tool plugin file var state = new Lazy(() => PluginFileState.Valid); - pluginFiles.Add(new PluginFile(fileInfo.FullName, state)); + pluginFiles.Add(new PluginFile(fileInfo.FullName, state, requiresDotnetHost: !IsDesktop)); } } else if (Directory.Exists(path)) @@ -215,7 +226,7 @@ internal List GetPluginsInNuGetPluginPaths() } else { - pluginFiles.Add(new PluginFile(path, new Lazy(() => PluginFileState.InvalidFilePath))); + pluginFiles.Add(new PluginFile(path, new Lazy(() => PluginFileState.InvalidFilePath), requiresDotnetHost: !IsDesktop)); } } @@ -240,7 +251,7 @@ internal List GetPluginsInPath() } else { - pluginFiles.Add(new PluginFile(path, new Lazy(() => PluginFileState.InvalidFilePath))); + pluginFiles.Add(new PluginFile(path, new Lazy(() => PluginFileState.InvalidFilePath), requiresDotnetHost: false)); } } @@ -260,7 +271,7 @@ private static List GetNetToolsPluginsInDirectory(string directoryPa { if (IsValidPluginFile(file)) { - PluginFile pluginFile = new PluginFile(file.FullName, new Lazy(() => PluginFileState.Valid), isDotnetToolsPlugin: true); + PluginFile pluginFile = new PluginFile(file.FullName, new Lazy(() => PluginFileState.Valid), requiresDotnetHost: false); pluginFiles.Add(pluginFile); } } diff --git a/src/NuGet.Core/NuGet.Protocol/Plugins/PluginFactory.cs b/src/NuGet.Core/NuGet.Protocol/Plugins/PluginFactory.cs index 2b2e6b44a01..4034c79086c 100644 --- a/src/NuGet.Core/NuGet.Protocol/Plugins/PluginFactory.cs +++ b/src/NuGet.Core/NuGet.Protocol/Plugins/PluginFactory.cs @@ -17,13 +17,15 @@ namespace NuGet.Protocol.Plugins /// /// A plugin factory. /// - public sealed class PluginFactory : IPluginFactory + public class PluginFactory : IPluginFactory { private bool _isDisposed; private readonly IPluginLogger _logger; private readonly TimeSpan _pluginIdleTimeout; private readonly ConcurrentDictionary>> _plugins; + internal PluginFactory() { } + /// /// Instantiates a new class. /// @@ -48,7 +50,7 @@ public PluginFactory(TimeSpan pluginIdleTimeout) /// /// Disposes of this instance. /// - public void Dispose() + public virtual void Dispose() { if (_isDisposed) { @@ -77,7 +79,7 @@ public void Dispose() /// /// Asynchronously gets an existing plugin instance or creates a new instance and connects to it. /// - /// The file path of the plugin. + /// A plugin file. /// Command-line arguments to be supplied to the plugin. /// Request handlers. /// Connection options. @@ -85,7 +87,7 @@ public void Dispose() /// A task that represents the asynchronous operation. /// The task result () returns a /// instance. - /// Thrown if + /// Thrown if /// is either or empty. /// Thrown if /// is . @@ -99,8 +101,8 @@ public void Dispose() /// Thrown if a plugin protocol error occurs. /// Thrown for a plugin failure during creation. /// This is intended to be called by NuGet client tools. - public async Task GetOrCreateAsync( - string filePath, + public virtual async Task GetOrCreateAsync( + PluginFile pluginFile, IEnumerable arguments, IRequestHandlers requestHandlers, ConnectionOptions options, @@ -111,9 +113,9 @@ public async Task GetOrCreateAsync( throw new ObjectDisposedException(nameof(PluginFactory)); } - if (string.IsNullOrEmpty(filePath)) + if (string.IsNullOrEmpty(pluginFile.Path)) { - throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(filePath)); + throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(pluginFile.Path)); } if (arguments == null) @@ -134,9 +136,9 @@ public async Task GetOrCreateAsync( sessionCancellationToken.ThrowIfCancellationRequested(); var lazyTask = _plugins.GetOrAdd( - filePath, + pluginFile.Path, (path) => new Lazy>( - () => CreatePluginAsync(filePath, arguments, requestHandlers, options, sessionCancellationToken))); + () => CreatePluginAsync(pluginFile, arguments, requestHandlers, options, sessionCancellationToken))); await lazyTask.Value; @@ -145,40 +147,48 @@ public async Task GetOrCreateAsync( } private async Task CreatePluginAsync( - string filePath, + PluginFile pluginFile, IEnumerable arguments, IRequestHandlers requestHandlers, ConnectionOptions options, CancellationToken sessionCancellationToken) { var args = string.Join(" ", arguments); -#if IS_DESKTOP - var startInfo = new ProcessStartInfo(filePath) + + ProcessStartInfo startInfo; + + if (pluginFile.RequiresDotnetHost) { - Arguments = args, - UseShellExecute = false, - RedirectStandardError = false, - RedirectStandardInput = true, - RedirectStandardOutput = true, - StandardOutputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), - WindowStyle = ProcessWindowStyle.Hidden, - }; -#else - var startInfo = new ProcessStartInfo + startInfo = new ProcessStartInfo + { + FileName = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH") ?? + (NuGet.Common.RuntimeEnvironmentHelper.IsWindows ? + "dotnet.exe" : + "dotnet"), + Arguments = $"\"{pluginFile.Path}\" " + args, + UseShellExecute = false, + RedirectStandardError = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + StandardOutputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), + WindowStyle = ProcessWindowStyle.Hidden, + }; + } + else { - FileName = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH") ?? - (NuGet.Common.RuntimeEnvironmentHelper.IsWindows ? - "dotnet.exe" : - "dotnet"), - Arguments = $"\"{filePath}\" " + args, - UseShellExecute = false, - RedirectStandardError = false, - RedirectStandardInput = true, - RedirectStandardOutput = true, - StandardOutputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), - WindowStyle = ProcessWindowStyle.Hidden, - }; -#endif + // Execute file directly. + startInfo = new ProcessStartInfo(pluginFile.Path) + { + Arguments = args, + UseShellExecute = false, + RedirectStandardError = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + StandardOutputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), + WindowStyle = ProcessWindowStyle.Hidden, + }; + } + var pluginProcess = new PluginProcess(startInfo); string pluginId = Plugin.CreateNewId(); @@ -219,7 +229,7 @@ private async Task CreatePluginAsync( connection = new Connection(messageDispatcher, sender, receiver, options, _logger); var plugin = new Plugin( - filePath, + pluginFile.Path, connection, pluginProcess, isOwnProcess: false, diff --git a/src/NuGet.Core/NuGet.Protocol/Plugins/PluginFile.cs b/src/NuGet.Core/NuGet.Protocol/Plugins/PluginFile.cs index 96c24f2c86f..0d4f799d0e0 100644 --- a/src/NuGet.Core/NuGet.Protocol/Plugins/PluginFile.cs +++ b/src/NuGet.Core/NuGet.Protocol/Plugins/PluginFile.cs @@ -21,33 +21,24 @@ public sealed class PluginFile public Lazy State { get; } /// - /// Is the plugin file, a dotnet tools plugin file? + /// Indicates if the plugin file requires a dotnet host. /// - internal bool IsDotnetToolsPlugin { get; } + internal bool RequiresDotnetHost { get; } /// /// Instantiates a new class. /// /// The plugin's file path. /// A lazy that evaluates the plugin file state. - /// Is the plugin file, a dotnet tools plugin file? - internal PluginFile(string filePath, Lazy state, bool isDotnetToolsPlugin) : this(filePath, state) - { - IsDotnetToolsPlugin = isDotnetToolsPlugin; - } - - /// - /// Instantiates a new class. - /// - /// The plugin's file path. - /// A lazy that evaluates the plugin file state. - public PluginFile(string filePath, Lazy state) + /// Indicates if the plugin file requires a dotnet host. + public PluginFile(string filePath, Lazy state, bool requiresDotnetHost) { if (string.IsNullOrEmpty(filePath)) { throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(filePath)); } + RequiresDotnetHost = requiresDotnetHost; Path = filePath; State = state; } diff --git a/src/NuGet.Core/NuGet.Protocol/Plugins/PluginManager.cs b/src/NuGet.Core/NuGet.Protocol/Plugins/PluginManager.cs index 6ff4407f862..11acfc56c71 100644 --- a/src/NuGet.Core/NuGet.Protocol/Plugins/PluginManager.cs +++ b/src/NuGet.Core/NuGet.Protocol/Plugins/PluginManager.cs @@ -55,7 +55,7 @@ private PluginManager() public PluginManager( IEnvironmentVariableReader reader, Lazy pluginDiscoverer, - Func pluginFactoryCreator, + Func pluginFactoryCreator, Lazy pluginsCacheDirectoryPath) { Initialize( @@ -212,12 +212,14 @@ private async Task> TryCreatePluginAsync( { if (result.PluginFile.State.Value == PluginFileState.Valid) { - var plugin = await _pluginFactory.GetOrCreateAsync( - result.PluginFile.Path, - PluginConstants.PluginArguments, - new RequestHandlers(), - _connectionOptions, - cancellationToken); + IPlugin plugin; + + plugin = await _pluginFactory.GetOrCreateAsync( + pluginFile: result.PluginFile, + arguments: PluginConstants.PluginArguments, + requestHandlers: new RequestHandlers(), + options: _connectionOptions, + sessionCancellationToken: cancellationToken); var utilities = await PerformOneTimePluginInitializationAsync(plugin, cancellationToken); diff --git a/src/NuGet.Core/NuGet.Protocol/PublicAPI/net472/PublicAPI.Shipped.txt b/src/NuGet.Core/NuGet.Protocol/PublicAPI/net472/PublicAPI.Shipped.txt index def020375d4..d17e6c22f10 100644 --- a/src/NuGet.Core/NuGet.Protocol/PublicAPI/net472/PublicAPI.Shipped.txt +++ b/src/NuGet.Core/NuGet.Protocol/PublicAPI/net472/PublicAPI.Shipped.txt @@ -1015,8 +1015,6 @@ NuGet.Protocol.Plugins.IPlugin.Closed -> System.EventHandler ~NuGet.Protocol.Plugins.IPlugin.Name.get -> string NuGet.Protocol.Plugins.IPluginDiscoverer ~NuGet.Protocol.Plugins.IPluginDiscoverer.DiscoverAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> -NuGet.Protocol.Plugins.IPluginFactory -~NuGet.Protocol.Plugins.IPluginFactory.GetOrCreateAsync(string filePath, System.Collections.Generic.IEnumerable arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task NuGet.Protocol.Plugins.IPluginManager ~NuGet.Protocol.Plugins.IPluginManager.CreatePluginsAsync(NuGet.Protocol.Core.Types.SourceRepository source, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> ~NuGet.Protocol.Plugins.IPluginManager.FindAvailablePluginsAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> @@ -1211,12 +1209,9 @@ NuGet.Protocol.Plugins.PluginException ~NuGet.Protocol.Plugins.PluginException.PluginException(string message) -> void ~NuGet.Protocol.Plugins.PluginException.PluginException(string message, System.Exception innerException) -> void NuGet.Protocol.Plugins.PluginFactory -NuGet.Protocol.Plugins.PluginFactory.Dispose() -> void -~NuGet.Protocol.Plugins.PluginFactory.GetOrCreateAsync(string filePath, System.Collections.Generic.IEnumerable arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task NuGet.Protocol.Plugins.PluginFactory.PluginFactory(System.TimeSpan pluginIdleTimeout) -> void NuGet.Protocol.Plugins.PluginFile ~NuGet.Protocol.Plugins.PluginFile.Path.get -> string -~NuGet.Protocol.Plugins.PluginFile.PluginFile(string filePath, System.Lazy state) -> void ~NuGet.Protocol.Plugins.PluginFile.State.get -> System.Lazy NuGet.Protocol.Plugins.PluginFileState NuGet.Protocol.Plugins.PluginFileState.InvalidEmbeddedSignature = 3 -> NuGet.Protocol.Plugins.PluginFileState @@ -1228,7 +1223,6 @@ NuGet.Protocol.Plugins.PluginManager NuGet.Protocol.Plugins.PluginManager.Dispose() -> void ~NuGet.Protocol.Plugins.PluginManager.EnvironmentVariableReader.get -> NuGet.Common.IEnvironmentVariableReader ~NuGet.Protocol.Plugins.PluginManager.FindAvailablePluginsAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> -~NuGet.Protocol.Plugins.PluginManager.PluginManager(NuGet.Common.IEnvironmentVariableReader reader, System.Lazy pluginDiscoverer, System.Func pluginFactoryCreator, System.Lazy pluginsCacheDirectoryPath) -> void ~NuGet.Protocol.Plugins.PluginManager.TryGetSourceAgnosticPluginAsync(NuGet.Protocol.Plugins.PluginDiscoveryResult pluginDiscoveryResult, NuGet.Protocol.Plugins.OperationClaim requestedOperationClaim, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> NuGet.Protocol.Plugins.PluginMulticlientUtilities ~NuGet.Protocol.Plugins.PluginMulticlientUtilities.DoOncePerPluginLifetimeAsync(string key, System.Func taskFunc, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task diff --git a/src/NuGet.Core/NuGet.Protocol/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/NuGet.Core/NuGet.Protocol/PublicAPI/net472/PublicAPI.Unshipped.txt index 7e98009cd58..deb1138c126 100644 --- a/src/NuGet.Core/NuGet.Protocol/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/NuGet.Core/NuGet.Protocol/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -1,2 +1,6 @@ #nullable enable NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer() -> void +virtual NuGet.Protocol.Plugins.PluginFactory.Dispose() -> void +~NuGet.Protocol.Plugins.PluginFile.PluginFile(string filePath, System.Lazy state, bool requiresDotnetHost) -> void +~NuGet.Protocol.Plugins.PluginManager.PluginManager(NuGet.Common.IEnvironmentVariableReader reader, System.Lazy pluginDiscoverer, System.Func pluginFactoryCreator, System.Lazy pluginsCacheDirectoryPath) -> void +~virtual NuGet.Protocol.Plugins.PluginFactory.GetOrCreateAsync(NuGet.Protocol.Plugins.PluginFile pluginFile, System.Collections.Generic.IEnumerable arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task diff --git a/src/NuGet.Core/NuGet.Protocol/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/NuGet.Core/NuGet.Protocol/PublicAPI/net8.0/PublicAPI.Shipped.txt index 86a9ec539e4..07c3e1d7c43 100644 --- a/src/NuGet.Core/NuGet.Protocol/PublicAPI/net8.0/PublicAPI.Shipped.txt +++ b/src/NuGet.Core/NuGet.Protocol/PublicAPI/net8.0/PublicAPI.Shipped.txt @@ -1015,8 +1015,6 @@ NuGet.Protocol.Plugins.IPlugin.Closed -> System.EventHandler ~NuGet.Protocol.Plugins.IPlugin.Name.get -> string NuGet.Protocol.Plugins.IPluginDiscoverer ~NuGet.Protocol.Plugins.IPluginDiscoverer.DiscoverAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> -NuGet.Protocol.Plugins.IPluginFactory -~NuGet.Protocol.Plugins.IPluginFactory.GetOrCreateAsync(string filePath, System.Collections.Generic.IEnumerable arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task NuGet.Protocol.Plugins.IPluginManager ~NuGet.Protocol.Plugins.IPluginManager.CreatePluginsAsync(NuGet.Protocol.Core.Types.SourceRepository source, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> ~NuGet.Protocol.Plugins.IPluginManager.FindAvailablePluginsAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> @@ -1211,12 +1209,9 @@ NuGet.Protocol.Plugins.PluginException ~NuGet.Protocol.Plugins.PluginException.PluginException(string message) -> void ~NuGet.Protocol.Plugins.PluginException.PluginException(string message, System.Exception innerException) -> void NuGet.Protocol.Plugins.PluginFactory -NuGet.Protocol.Plugins.PluginFactory.Dispose() -> void -~NuGet.Protocol.Plugins.PluginFactory.GetOrCreateAsync(string filePath, System.Collections.Generic.IEnumerable arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task NuGet.Protocol.Plugins.PluginFactory.PluginFactory(System.TimeSpan pluginIdleTimeout) -> void NuGet.Protocol.Plugins.PluginFile ~NuGet.Protocol.Plugins.PluginFile.Path.get -> string -~NuGet.Protocol.Plugins.PluginFile.PluginFile(string filePath, System.Lazy state) -> void ~NuGet.Protocol.Plugins.PluginFile.State.get -> System.Lazy NuGet.Protocol.Plugins.PluginFileState NuGet.Protocol.Plugins.PluginFileState.InvalidEmbeddedSignature = 3 -> NuGet.Protocol.Plugins.PluginFileState @@ -1228,7 +1223,6 @@ NuGet.Protocol.Plugins.PluginManager NuGet.Protocol.Plugins.PluginManager.Dispose() -> void ~NuGet.Protocol.Plugins.PluginManager.EnvironmentVariableReader.get -> NuGet.Common.IEnvironmentVariableReader ~NuGet.Protocol.Plugins.PluginManager.FindAvailablePluginsAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> -~NuGet.Protocol.Plugins.PluginManager.PluginManager(NuGet.Common.IEnvironmentVariableReader reader, System.Lazy pluginDiscoverer, System.Func pluginFactoryCreator, System.Lazy pluginsCacheDirectoryPath) -> void ~NuGet.Protocol.Plugins.PluginManager.TryGetSourceAgnosticPluginAsync(NuGet.Protocol.Plugins.PluginDiscoveryResult pluginDiscoveryResult, NuGet.Protocol.Plugins.OperationClaim requestedOperationClaim, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> NuGet.Protocol.Plugins.PluginMulticlientUtilities ~NuGet.Protocol.Plugins.PluginMulticlientUtilities.DoOncePerPluginLifetimeAsync(string key, System.Func taskFunc, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task diff --git a/src/NuGet.Core/NuGet.Protocol/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/NuGet.Core/NuGet.Protocol/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 7e98009cd58..deb1138c126 100644 --- a/src/NuGet.Core/NuGet.Protocol/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/NuGet.Core/NuGet.Protocol/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1,2 +1,6 @@ #nullable enable NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer() -> void +virtual NuGet.Protocol.Plugins.PluginFactory.Dispose() -> void +~NuGet.Protocol.Plugins.PluginFile.PluginFile(string filePath, System.Lazy state, bool requiresDotnetHost) -> void +~NuGet.Protocol.Plugins.PluginManager.PluginManager(NuGet.Common.IEnvironmentVariableReader reader, System.Lazy pluginDiscoverer, System.Func pluginFactoryCreator, System.Lazy pluginsCacheDirectoryPath) -> void +~virtual NuGet.Protocol.Plugins.PluginFactory.GetOrCreateAsync(NuGet.Protocol.Plugins.PluginFile pluginFile, System.Collections.Generic.IEnumerable arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task diff --git a/src/NuGet.Core/NuGet.Protocol/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/src/NuGet.Core/NuGet.Protocol/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt index 86a9ec539e4..07c3e1d7c43 100644 --- a/src/NuGet.Core/NuGet.Protocol/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt +++ b/src/NuGet.Core/NuGet.Protocol/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt @@ -1015,8 +1015,6 @@ NuGet.Protocol.Plugins.IPlugin.Closed -> System.EventHandler ~NuGet.Protocol.Plugins.IPlugin.Name.get -> string NuGet.Protocol.Plugins.IPluginDiscoverer ~NuGet.Protocol.Plugins.IPluginDiscoverer.DiscoverAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> -NuGet.Protocol.Plugins.IPluginFactory -~NuGet.Protocol.Plugins.IPluginFactory.GetOrCreateAsync(string filePath, System.Collections.Generic.IEnumerable arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task NuGet.Protocol.Plugins.IPluginManager ~NuGet.Protocol.Plugins.IPluginManager.CreatePluginsAsync(NuGet.Protocol.Core.Types.SourceRepository source, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> ~NuGet.Protocol.Plugins.IPluginManager.FindAvailablePluginsAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> @@ -1211,12 +1209,9 @@ NuGet.Protocol.Plugins.PluginException ~NuGet.Protocol.Plugins.PluginException.PluginException(string message) -> void ~NuGet.Protocol.Plugins.PluginException.PluginException(string message, System.Exception innerException) -> void NuGet.Protocol.Plugins.PluginFactory -NuGet.Protocol.Plugins.PluginFactory.Dispose() -> void -~NuGet.Protocol.Plugins.PluginFactory.GetOrCreateAsync(string filePath, System.Collections.Generic.IEnumerable arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task NuGet.Protocol.Plugins.PluginFactory.PluginFactory(System.TimeSpan pluginIdleTimeout) -> void NuGet.Protocol.Plugins.PluginFile ~NuGet.Protocol.Plugins.PluginFile.Path.get -> string -~NuGet.Protocol.Plugins.PluginFile.PluginFile(string filePath, System.Lazy state) -> void ~NuGet.Protocol.Plugins.PluginFile.State.get -> System.Lazy NuGet.Protocol.Plugins.PluginFileState NuGet.Protocol.Plugins.PluginFileState.InvalidEmbeddedSignature = 3 -> NuGet.Protocol.Plugins.PluginFileState @@ -1228,7 +1223,6 @@ NuGet.Protocol.Plugins.PluginManager NuGet.Protocol.Plugins.PluginManager.Dispose() -> void ~NuGet.Protocol.Plugins.PluginManager.EnvironmentVariableReader.get -> NuGet.Common.IEnvironmentVariableReader ~NuGet.Protocol.Plugins.PluginManager.FindAvailablePluginsAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> -~NuGet.Protocol.Plugins.PluginManager.PluginManager(NuGet.Common.IEnvironmentVariableReader reader, System.Lazy pluginDiscoverer, System.Func pluginFactoryCreator, System.Lazy pluginsCacheDirectoryPath) -> void ~NuGet.Protocol.Plugins.PluginManager.TryGetSourceAgnosticPluginAsync(NuGet.Protocol.Plugins.PluginDiscoveryResult pluginDiscoveryResult, NuGet.Protocol.Plugins.OperationClaim requestedOperationClaim, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> NuGet.Protocol.Plugins.PluginMulticlientUtilities ~NuGet.Protocol.Plugins.PluginMulticlientUtilities.DoOncePerPluginLifetimeAsync(string key, System.Func taskFunc, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task diff --git a/src/NuGet.Core/NuGet.Protocol/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/NuGet.Core/NuGet.Protocol/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 7e98009cd58..deb1138c126 100644 --- a/src/NuGet.Core/NuGet.Protocol/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/NuGet.Core/NuGet.Protocol/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,2 +1,6 @@ #nullable enable NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer() -> void +virtual NuGet.Protocol.Plugins.PluginFactory.Dispose() -> void +~NuGet.Protocol.Plugins.PluginFile.PluginFile(string filePath, System.Lazy state, bool requiresDotnetHost) -> void +~NuGet.Protocol.Plugins.PluginManager.PluginManager(NuGet.Common.IEnvironmentVariableReader reader, System.Lazy pluginDiscoverer, System.Func pluginFactoryCreator, System.Lazy pluginsCacheDirectoryPath) -> void +~virtual NuGet.Protocol.Plugins.PluginFactory.GetOrCreateAsync(NuGet.Protocol.Plugins.PluginFile pluginFile, System.Collections.Generic.IEnumerable arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task diff --git a/test/NuGet.Core.FuncTests/NuGet.Protocol.FuncTest/PluginTests.cs b/test/NuGet.Core.FuncTests/NuGet.Protocol.FuncTest/PluginTests.cs index 157f638d25e..4fc22ae9e0d 100644 --- a/test/NuGet.Core.FuncTests/NuGet.Protocol.FuncTest/PluginTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.Protocol.FuncTest/PluginTests.cs @@ -26,6 +26,18 @@ namespace NuGet.Protocol.FuncTest { public class PluginTests { + public static bool IsDesktop + { + get + { +#if IS_DESKTOP + return true; +#else + return false; +#endif + } + } + private static readonly FileInfo PluginFile; private static readonly ushort PortNumber = 11000; private static readonly IEnumerable PluginArguments = PluginConstants.PluginArguments @@ -60,7 +72,7 @@ public async Task GetOrCreateAsync_WithUnhandledExceptionInPlugin_Throws() using (var pluginFactory = new PluginFactory(PluginConstants.IdleTimeout)) { var exception = await Assert.ThrowsAsync(() => pluginFactory.GetOrCreateAsync( - PluginFile.FullName, + new PluginFile(filePath: PluginFile.FullName, state: new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop), PluginConstants.PluginArguments.Concat(new[] { "-ThrowException Unhandled" }), new RequestHandlers(), ConnectionOptions.CreateDefault(), @@ -81,7 +93,7 @@ public async Task GetOrCreateAsync_WithHandledExceptionAndExitInPlugin_Throws() using (var pluginFactory = new PluginFactory(PluginConstants.IdleTimeout)) { var exception = await Assert.ThrowsAsync(() => pluginFactory.GetOrCreateAsync( - PluginFile.FullName, + new PluginFile(filePath: PluginFile.FullName, state: new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop), PluginConstants.PluginArguments.Concat(new[] { "-ThrowException Handled" }), new RequestHandlers(), ConnectionOptions.CreateDefault(), @@ -102,7 +114,7 @@ public async Task GetOrCreateAsync_WhenPluginFreezes_Throws() using (var pluginFactory = new PluginFactory(PluginConstants.IdleTimeout)) { var exception = await Assert.ThrowsAsync(() => pluginFactory.GetOrCreateAsync( - PluginFile.FullName, + new PluginFile(filePath: PluginFile.FullName, state: new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop), PluginConstants.PluginArguments.Concat(new[] { "-Freeze" }), new RequestHandlers(), ConnectionOptions.CreateDefault(), @@ -123,7 +135,7 @@ public async Task GetOrCreateAsync_WhenPluginCausesProtocolException_Throws() using (var pluginFactory = new PluginFactory(PluginConstants.IdleTimeout)) { var exception = await Assert.ThrowsAsync(() => pluginFactory.GetOrCreateAsync( - PluginFile.FullName, + new PluginFile(filePath: PluginFile.FullName, state: new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop), PluginConstants.PluginArguments.Concat(new[] { "-CauseProtocolException" }), new RequestHandlers(), ConnectionOptions.CreateDefault(), @@ -318,7 +330,7 @@ internal static async Task CreateAsync() var pluginFactory = new PluginFactory(PluginConstants.IdleTimeout); var options = ConnectionOptions.CreateDefault(); var plugin = await pluginFactory.GetOrCreateAsync( - PluginFile.FullName, + new PluginFile(filePath: PluginFile.FullName, state: new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop), PluginArguments, new RequestHandlers(), options, diff --git a/test/NuGet.Core.Tests/NuGet.Credentials.Test/PluginManagerMock.cs b/test/NuGet.Core.Tests/NuGet.Credentials.Test/PluginManagerMock.cs index 7273edc056e..ce13791271e 100644 --- a/test/NuGet.Core.Tests/NuGet.Credentials.Test/PluginManagerMock.cs +++ b/test/NuGet.Core.Tests/NuGet.Credentials.Test/PluginManagerMock.cs @@ -66,9 +66,21 @@ internal TestExpectation( internal sealed class PluginManagerMock : IDisposable { + public static bool IsDesktop + { + get + { +#if IS_DESKTOP + return true; +#else + return false; +#endif + } + } + private readonly Mock _connection; private readonly TestExpectation _expectations; - private readonly Mock _factory; + private readonly Mock _factory; private readonly Mock _plugin; private readonly Mock _pluginDiscoverer; private readonly Mock _reader; @@ -99,7 +111,7 @@ internal PluginManagerMock( _plugin = new Mock(MockBehavior.Strict); EnsurePluginSetupCalls(); - _factory = new Mock(MockBehavior.Strict); + _factory = new Mock(MockBehavior.Strict); EnsureFactorySetupCalls(pluginFilePath); // Setup connection @@ -238,7 +250,7 @@ private void EnsureDiscovererIsCalled(string pluginFilePath, PluginFileState plu _pluginDiscoverer.Setup(x => x.DiscoverAsync(It.IsAny())) .ReturnsAsync(new[] { - new PluginDiscoveryResult(new PluginFile(pluginFilePath, new Lazy(() => pluginFileState))) + new PluginDiscoveryResult(new PluginFile(pluginFilePath, new Lazy(() => pluginFileState), requiresDotnetHost: !IsDesktop)) }); } @@ -281,7 +293,7 @@ private void EnsureFactorySetupCalls(string pluginFilePath) { _factory.Setup(x => x.Dispose()); _factory.Setup(x => x.GetOrCreateAsync( - It.Is(p => p == pluginFilePath), + It.Is(p => p.Path == pluginFilePath), It.IsNotNull>(), It.IsNotNull(), It.IsNotNull(), diff --git a/test/NuGet.Core.Tests/NuGet.Credentials.Test/SecurePluginCredentialProviderBuilderTests.cs b/test/NuGet.Core.Tests/NuGet.Credentials.Test/SecurePluginCredentialProviderBuilderTests.cs index 9acf4a02f70..49d868ff7cd 100644 --- a/test/NuGet.Core.Tests/NuGet.Credentials.Test/SecurePluginCredentialProviderBuilderTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Credentials.Test/SecurePluginCredentialProviderBuilderTests.cs @@ -17,6 +17,17 @@ namespace NuGet.Credentials.Test { public class SecurePluginCredentialProviderBuilderTests : IDisposable { + public static bool IsDesktop + { + get + { +#if IS_DESKTOP + return true; +#else + return false; +#endif + } + } private readonly TestDirectory _testDirectory; public SecurePluginCredentialProviderBuilderTests() @@ -156,7 +167,7 @@ internal PluginManagerBuilderMock(List> pl PluginManager = new PluginManager( reader.Object, new Lazy(() => pluginDiscoverer.Object), - (TimeSpan idleTimeout) => Mock.Of(), + (TimeSpan idleTimeout) => Mock.Of(), new Lazy(() => _testDirectory.Path)); } @@ -170,7 +181,7 @@ private static IEnumerable GetPluginDiscoveryResults(List var results = new List(); foreach (var plugin in plugins) { - var file = new PluginFile(plugin.Key, new Lazy(() => plugin.Value)); + var file = new PluginFile(plugin.Key, new Lazy(() => plugin.Value), requiresDotnetHost: !IsDesktop); results.Add(new PluginDiscoveryResult(file)); } @@ -183,7 +194,7 @@ private PluginManager CreateDefaultPluginManager() return new PluginManager( Mock.Of(), new Lazy(), - (TimeSpan idleTimeout) => Mock.Of(), + (TimeSpan idleTimeout) => Mock.Of(), new Lazy(() => _testDirectory.Path)); } } diff --git a/test/NuGet.Core.Tests/NuGet.Credentials.Test/SecurePluginCredentialProviderTests.cs b/test/NuGet.Core.Tests/NuGet.Credentials.Test/SecurePluginCredentialProviderTests.cs index bbf4d232df5..d17d61c3c81 100644 --- a/test/NuGet.Core.Tests/NuGet.Credentials.Test/SecurePluginCredentialProviderTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Credentials.Test/SecurePluginCredentialProviderTests.cs @@ -17,6 +17,18 @@ namespace NuGet.Credentials.Test { public sealed class SecurePluginCredentialProviderTests : IDisposable { + public bool IsDesktop + { + get + { +#if IS_DESKTOP + return true; +#else + return false; +#endif + } + } + private static readonly Uri _uri = new Uri("https://unit.test"); private const string _username = "username"; private const string _password = "password"; @@ -94,7 +106,7 @@ public async Task GetAsync_WithValidArguments_ReturnsValidCredentials() pluginFileState: PluginFileState.Valid, expectations: expectation)) { - var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid))); + var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop)); var provider = new SecurePluginCredentialProvider(test.PluginManager, discoveryResult, canShowDialog: true, logger: NullLogger.Instance); IWebProxy proxy = null; @@ -129,7 +141,7 @@ public async Task GetAsync_WhenCalledMultipleTimes_DoesNotCreateMultipleInstance pluginFileState: PluginFileState.Valid, expectations: expectation)) { - var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid))); + var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop)); var provider = new SecurePluginCredentialProvider(test.PluginManager, discoveryResult, canShowDialog: true, logger: NullLogger.Instance); IWebProxy proxy = null; @@ -166,7 +178,7 @@ public async Task GetAsync_WhenPluginClaimsMultipleOperations_ReturnsValidCreden pluginFileState: PluginFileState.Valid, expectations: expectation)) { - var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid))); + var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop)); var provider = new SecurePluginCredentialProvider(test.PluginManager, discoveryResult, canShowDialog: true, logger: NullLogger.Instance); IWebProxy proxy = null; @@ -206,7 +218,7 @@ public async Task GetAsync_WhenProxyIsUsed_SetsProxyCredentials() pluginFileState: PluginFileState.Valid, expectations: expectation)) { - var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid))); + var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop)); var provider = new SecurePluginCredentialProvider(test.PluginManager, discoveryResult, canShowDialog: true, logger: NullLogger.Instance); var proxy = new System.Net.WebProxy() { @@ -243,7 +255,7 @@ public async Task GetAsync_WhenCalledMultipleTimes_CachesCapabilities() pluginFileState: PluginFileState.Valid, expectations: expectation)) { - var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid))); + var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop)); var provider = new SecurePluginCredentialProvider(test.PluginManager, discoveryResult, canShowDialog: true, logger: NullLogger.Instance); IWebProxy proxy = null; @@ -273,7 +285,7 @@ public async Task GetAsync_WhenCalledMultipleTimes_CachesCapabilities() pluginFileState: PluginFileState.Valid, expectations: expectations2)) { - var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid))); + var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop)); var provider = new SecurePluginCredentialProvider(test.PluginManager, discoveryResult, canShowDialog: true, logger: NullLogger.Instance); IWebProxy proxy = null; @@ -311,7 +323,7 @@ public async Task GetAsync_SendsCorrectCanShowDialogValue() pluginFileState: PluginFileState.Valid, expectations: expectation)) { - var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid))); + var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop)); var provider = new SecurePluginCredentialProvider(test.PluginManager, discoveryResult, canShowDialog, logger: NullLogger.Instance); IWebProxy proxy = null; @@ -344,7 +356,7 @@ public async Task GetAsync_WhenPluginManagerReturnsException_ExceptionIsPropagat It.IsAny())) .ReturnsAsync(result); - var pluginDiscoveryResult = new PluginDiscoveryResult(new PluginFile("c", new Lazy(() => PluginFileState.Valid))); + var pluginDiscoveryResult = new PluginDiscoveryResult(new PluginFile("c", new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop)); var logger = new Mock(MockBehavior.Strict); logger.Setup(x => x.LogError(It.Is(data => data == expectedMessage))); @@ -379,7 +391,7 @@ public async Task GetAsync_WhenCredentialPluginIsUnableToAcquireCredentials_Retu pluginFileState: PluginFileState.Valid, expectations: expectation)) { - var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid))); + var discoveryResult = new PluginDiscoveryResult(new PluginFile("a", new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop)); var provider = new SecurePluginCredentialProvider(test.PluginManager, discoveryResult, canShowDialog: true, logger: NullLogger.Instance); IWebProxy proxy = null; @@ -397,7 +409,7 @@ public async Task GetAsync_WhenCredentialPluginIsUnableToAcquireCredentials_Retu private PluginDiscoveryResult CreatePluginDiscoveryResult(PluginFileState pluginState = PluginFileState.Valid) { - return new PluginDiscoveryResult(new PluginFile(Path.Combine(_testDirectory.Path, "plugin.exe"), new Lazy(() => pluginState))); + return new PluginDiscoveryResult(new PluginFile(Path.Combine(_testDirectory.Path, "plugin.exe"), new Lazy(() => pluginState), requiresDotnetHost: !IsDesktop)); } private PluginManager CreateDefaultPluginManager() @@ -405,7 +417,7 @@ private PluginManager CreateDefaultPluginManager() return new PluginManager( Mock.Of(), new Lazy(), - (TimeSpan idleTimeout) => Mock.Of(), + (TimeSpan idleTimeout) => Mock.Of(), new Lazy(() => _testDirectory.Path)); } diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginDiscovererTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginDiscovererTests.cs index f27749db149..317dc08d39c 100644 --- a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginDiscovererTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginDiscovererTests.cs @@ -436,7 +436,7 @@ public void GetPluginsInNuGetPluginPaths_WithNuGetPluginPathsSet_ReturnsPluginsI // Assert Assert.Single(plugins); Assert.Equal(pluginInNuGetPluginPathDirectoryFilePath, plugins[0].Path); - Assert.True(plugins[0].IsDotnetToolsPlugin); + Assert.False(plugins[0].RequiresDotnetHost); } [PlatformFact(Platform.Windows)] @@ -480,7 +480,7 @@ public void GetPluginsInPATH_WithPATHSet_ReturnsPlugin() // Assert Assert.Single(plugins); Assert.Equal(pluginInPathDirectoryFilePath, plugins[0].Path); - Assert.True(plugins[0].IsDotnetToolsPlugin); + Assert.False(plugins[0].RequiresDotnetHost); } [PlatformFact(Platform.Windows)] @@ -503,7 +503,7 @@ public void GetPluginsInNuGetPluginPaths_NuGetPluginPathsPointsToAFile_TreatsAsP // Assert Assert.Single(plugins); Assert.Equal(pluginFilePath, plugins[0].Path); - Assert.True(plugins[0].IsDotnetToolsPlugin); + Assert.False(plugins[0].RequiresDotnetHost); } [PlatformFact(Platform.Windows)] @@ -525,7 +525,6 @@ public void GetPluginsInNuGetPluginPaths_NuGetPluginPathsPointsToAFileThatDoesNo // Assert Assert.Single(plugins); - Assert.False(plugins[0].IsDotnetToolsPlugin); } [PlatformFact(Platform.Windows)] @@ -549,7 +548,7 @@ public void GetPluginsInPATH_PATHPointsToADirectory_ContainsValidPluginFiles() // Assert Assert.Single(plugins); Assert.Equal(validPluginFile, plugins[0].Path); - Assert.True(plugins[0].IsDotnetToolsPlugin); + Assert.False(plugins[0].RequiresDotnetHost); } [PlatformFact(Platform.Windows)] diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginDiscoveryResultTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginDiscoveryResultTests.cs index 17493c4913b..2c5b57521e6 100644 --- a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginDiscoveryResultTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginDiscoveryResultTests.cs @@ -8,6 +8,18 @@ namespace NuGet.Protocol.Plugins.Tests { public class PluginDiscoveryResultTests { + public bool IsDesktop + { + get + { +#if IS_DESKTOP + return true; +#else + return false; +#endif + } + } + [Fact] public void Constructor_ThrowsForNullPluginFile() { @@ -20,7 +32,7 @@ public void Constructor_ThrowsForNullPluginFile() [Fact] public void Constructor_InitializesProperties() { - var pluginFile = new PluginFile(filePath: "a", state: new Lazy(() => PluginFileState.InvalidEmbeddedSignature)); + var pluginFile = new PluginFile(filePath: "a", state: new Lazy(() => PluginFileState.InvalidEmbeddedSignature), requiresDotnetHost: !IsDesktop); var result = new PluginDiscoveryResult(pluginFile); diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginFactoryTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginFactoryTests.cs index 4b560bf1acb..bffb021f6f6 100644 --- a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginFactoryTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginFactoryTests.cs @@ -2,14 +2,28 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.IO; using System.Threading; using System.Threading.Tasks; +using NuGet.Test.Utility; using Xunit; namespace NuGet.Protocol.Plugins.Tests { public class PluginFactoryTests { + public bool IsDesktop + { + get + { +#if IS_DESKTOP + return true; +#else + return false; +#endif + } + } + [Fact] public void Constructor_ThrowsForTimeSpanBelowMinimum() { @@ -46,7 +60,7 @@ public async Task GetOrCreateAsync_ThrowsForNullOrEmptyFilePath(string filePath) var exception = await Assert.ThrowsAsync( () => factory.GetOrCreateAsync( - filePath, + new PluginFile(filePath: filePath, state: new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop), PluginConstants.PluginArguments, new RequestHandlers(), ConnectionOptions.CreateDefault(), @@ -62,7 +76,7 @@ public async Task GetOrCreateAsync_ThrowsForNullArguments() var exception = await Assert.ThrowsAsync( () => factory.GetOrCreateAsync( - filePath: "a", + new PluginFile(filePath: "a", state: new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop), arguments: null, requestHandlers: new RequestHandlers(), options: ConnectionOptions.CreateDefault(), @@ -78,7 +92,7 @@ public async Task GetOrCreateAsync_ThrowsForNullRequestHandlers() var exception = await Assert.ThrowsAsync( () => factory.GetOrCreateAsync( - filePath: "a", + new PluginFile(filePath: "a", state: new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop), arguments: PluginConstants.PluginArguments, requestHandlers: null, options: ConnectionOptions.CreateDefault(), @@ -94,7 +108,7 @@ public async Task GetOrCreateAsync_ThrowsForNullConnectionOptions() var exception = await Assert.ThrowsAsync( () => factory.GetOrCreateAsync( - filePath: "a", + new PluginFile(filePath: "a", state: new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop), arguments: PluginConstants.PluginArguments, requestHandlers: new RequestHandlers(), options: null, @@ -110,7 +124,7 @@ public async Task GetOrCreateAsync_ThrowsIfCancelled() await Assert.ThrowsAsync( () => factory.GetOrCreateAsync( - filePath: "a", + new PluginFile(filePath: "a", state: new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop), arguments: PluginConstants.PluginArguments, requestHandlers: new RequestHandlers(), options: ConnectionOptions.CreateDefault(), @@ -126,7 +140,7 @@ public async Task GetOrCreateAsync_ThrowsIfDisposed() var exception = await Assert.ThrowsAsync( () => factory.GetOrCreateAsync( - filePath: "a", + new PluginFile(filePath: "a", state: new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop), arguments: PluginConstants.PluginArguments, requestHandlers: new RequestHandlers(), options: ConnectionOptions.CreateDefault(), @@ -135,6 +149,34 @@ public async Task GetOrCreateAsync_ThrowsIfDisposed() Assert.Equal(nameof(PluginFactory), exception.ObjectName); } + [PlatformFact(Platform.Windows)] + public async Task GetOrCreateNetPluginAsync_UsingBatchFile_CreatesPluginAndExecutes() + { + using TestDirectory testDirectory = TestDirectory.Create(); + string pluginPath = Path.Combine(testDirectory.Path, "nuget-plugin-batFile.bat"); + string outputPath = Path.Combine(testDirectory.Path, "plugin-output.txt"); + + string batFileContent = $@" + @echo off + echo File executed > ""{outputPath}"" + "; + + File.WriteAllText(pluginPath, batFileContent); + + var args = PluginConstants.PluginArguments; + var reqHandler = new RequestHandlers(); + var options = ConnectionOptions.CreateDefault(); + + var pluginFactory = new PluginFactory(Timeout.InfiniteTimeSpan); + + // Act + var plugin = await Assert.ThrowsAnyAsync(() => pluginFactory.GetOrCreateAsync(new PluginFile(filePath: pluginPath, state: new Lazy(() => PluginFileState.Valid), requiresDotnetHost: false), args, reqHandler, options, CancellationToken.None)); + + // Assert + string outputContent = File.ReadAllText(outputPath); + Assert.Contains("File executed", outputContent); + } + [Fact] public async Task CreateFromCurrentProcessAsync_ThrowsForNullRequestHandlers() { diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginFileTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginFileTests.cs index 660f690216f..923c44842ea 100644 --- a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginFileTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginFileTests.cs @@ -8,12 +8,24 @@ namespace NuGet.Protocol.Plugins.Tests { public class PluginFileTests { + public bool IsDesktop + { + get + { +#if IS_DESKTOP + return true; +#else + return false; +#endif + } + } + [Theory] [InlineData(null)] [InlineData("")] public void Constructor_ThrowsForNullOrEmptyFilePath(string filePath) { - var exception = Assert.Throws(() => new PluginFile(filePath, state: new Lazy(() => PluginFileState.NotFound))); + var exception = Assert.Throws(() => new PluginFile(filePath, state: new Lazy(() => PluginFileState.NotFound), requiresDotnetHost: !IsDesktop)); Assert.Equal("filePath", exception.ParamName); } @@ -21,7 +33,7 @@ public void Constructor_ThrowsForNullOrEmptyFilePath(string filePath) [Fact] public void Constructor_InitializesProperties() { - var pluginFile = new PluginFile(filePath: "a", state: new Lazy(() => PluginFileState.Valid)); + var pluginFile = new PluginFile(filePath: "a", state: new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop); Assert.Equal("a", pluginFile.Path); Assert.Equal(PluginFileState.Valid, pluginFile.State.Value); diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginManagerTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginManagerTests.cs index 07e965955e3..230227c289f 100644 --- a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginManagerTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginManagerTests.cs @@ -17,6 +17,17 @@ namespace NuGet.Protocol.Plugins.Tests public class PluginManagerTests { private const string PluginFilePath = "a"; + public bool IsDesktop + { + get + { +#if IS_DESKTOP + return true; +#else + return false; +#endif + } + } [Fact] public async Task TryGetSourceAgnosticPluginAsync_WhenExceptionIsThrownDuringPluginCreation_PropagatesException() @@ -24,11 +35,11 @@ public async Task TryGetSourceAgnosticPluginAsync_WhenExceptionIsThrownDuringPlu const string message = "b"; var reader = Mock.Of(); - var pluginFactory = new Mock(MockBehavior.Strict); + var pluginFactory = new Mock(MockBehavior.Strict); var exception = new Exception(message); pluginFactory.Setup(x => x.GetOrCreateAsync( - It.Is(filePath => string.Equals(filePath, PluginFilePath, StringComparison.Ordinal)), + It.Is(pluginFile => string.Equals(pluginFile.Path, PluginFilePath, StringComparison.Ordinal)), It.Is>(arguments => arguments != null && arguments.Any()), It.IsNotNull(), It.IsNotNull(), @@ -46,7 +57,7 @@ public async Task TryGetSourceAgnosticPluginAsync_WhenExceptionIsThrownDuringPlu var discoveryResult = new PluginDiscoveryResult( new PluginFile( PluginFilePath, - new Lazy(() => PluginFileState.Valid))); + new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop)); Tuple result = await pluginManager.TryGetSourceAgnosticPluginAsync( discoveryResult, @@ -77,7 +88,7 @@ public async Task TryGetSourceAgnosticPluginAsync_WhenSuccessfullyCreated_Operat var discoveryResult = new PluginDiscoveryResult( new PluginFile( PluginFilePath, - new Lazy(() => PluginFileState.Valid))); + new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop)); Tuple result = await test.PluginManager.TryGetSourceAgnosticPluginAsync( discoveryResult, @@ -115,7 +126,7 @@ public async Task TryGetSourceAgnosticPluginAsync_WhenCacheFileIndicatesIndicate var discoveryResult = new PluginDiscoveryResult( new PluginFile( PluginFilePath, - new Lazy(() => PluginFileState.Valid))); + new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop)); Tuple result = await test.PluginManager.TryGetSourceAgnosticPluginAsync( discoveryResult, @@ -164,7 +175,7 @@ public async Task PluginManager_CreatePlugin_PrefersFrameworkSpecificEnvironment var discoveryResult = new PluginDiscoveryResult( new PluginFile( PluginFilePath, - new Lazy(() => PluginFileState.Valid))); + new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop)); Tuple result = await test.PluginManager.TryGetSourceAgnosticPluginAsync( discoveryResult, @@ -214,7 +225,7 @@ public async Task PluginManager_CreatePlugin_EmptyFrameworkSpecificEnvironmentVa var discoveryResult = new PluginDiscoveryResult( new PluginFile( PluginFilePath, - new Lazy(() => PluginFileState.Valid))); + new Lazy(() => PluginFileState.Valid), requiresDotnetHost: !IsDesktop)); Tuple result = await test.PluginManager.TryGetSourceAgnosticPluginAsync( discoveryResult, @@ -236,8 +247,19 @@ public async Task PluginManager_CreatePlugin_EmptyFrameworkSpecificEnvironmentVa private sealed class PluginManagerTest : IDisposable { + public bool IsDesktop + { + get + { +#if IS_DESKTOP + return true; +#else + return false; +#endif + } + } private readonly Mock _connection; - private readonly Mock _factory; + private readonly Mock _factory; private readonly Mock _plugin; private readonly Mock _pluginDiscoverer; private readonly Mock _reader; @@ -289,7 +311,7 @@ internal PluginManagerTest( _pluginDiscoverer.Setup(x => x.DiscoverAsync(It.IsAny())) .ReturnsAsync(new[] { - new PluginDiscoveryResult(new PluginFile(pluginFilePath, new Lazy(() => pluginFileState))) + new PluginDiscoveryResult(new PluginFile(pluginFilePath, new Lazy(() => pluginFileState), requiresDotnetHost : ! IsDesktop)) }); _connection = new Mock(MockBehavior.Strict); @@ -327,11 +349,11 @@ internal PluginManagerTest( _plugin.SetupGet(x => x.Id) .Returns("id"); - _factory = new Mock(MockBehavior.Strict); + _factory = new Mock(MockBehavior.Strict); _factory.Setup(x => x.Dispose()); _factory.Setup(x => x.GetOrCreateAsync( - It.Is(p => p == pluginFilePath), + It.Is(p => p.Path == pluginFilePath), It.IsNotNull>(), It.IsNotNull(), It.IsNotNull(), diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginResourceProviderTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginResourceProviderTests.cs index 787f6b6db73..d4661dc82f4 100644 --- a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginResourceProviderTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/Plugins/PluginResourceProviderTests.cs @@ -226,6 +226,17 @@ private static SourceRepository CreateSourceRepository( private sealed class PluginResourceProviderNegativeTest : IDisposable { + public static bool IsDesktop + { + get + { +#if IS_DESKTOP + return true; +#else + return false; +#endif + } + } private readonly Mock _pluginDiscoverer; private readonly PluginManager _pluginManager; private readonly Mock _environmentVariableReader; @@ -273,7 +284,7 @@ internal PluginResourceProviderNegativeTest(string serviceIndexJson, string sour _pluginManager = new PluginManager( _environmentVariableReader.Object, new Lazy(() => _pluginDiscoverer.Object), - (TimeSpan idleTimeout) => Mock.Of(), + (TimeSpan idleTimeout) => Mock.Of(), new Lazy(() => _testDirectory.Path)); Provider = new PluginResourceProvider(_pluginManager); } @@ -299,7 +310,7 @@ private static IEnumerable GetPluginDiscoveryResults(stri foreach (var path in pluginPaths.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) { var state = path == "a" ? PluginFileState.Valid : PluginFileState.InvalidEmbeddedSignature; - var file = new PluginFile(path, new Lazy(() => state)); + var file = new PluginFile(path, new Lazy(() => state), requiresDotnetHost: !IsDesktop); results.Add(new PluginDiscoveryResult(file)); } @@ -309,9 +320,20 @@ private static IEnumerable GetPluginDiscoveryResults(stri private sealed class PluginResourceProviderPositiveTest : IDisposable { + public static bool IsDesktop + { + get + { +#if IS_DESKTOP + return true; +#else + return false; +#endif + } + } private readonly Mock _connection; private readonly IEnumerable _expectations; - private readonly Mock _factory; + private readonly Mock _factory; private readonly Mock _plugin; private readonly Mock _pluginDiscoverer; private readonly Mock _reader; @@ -355,7 +377,7 @@ internal PluginResourceProviderPositiveTest( _pluginDiscoverer.Setup(x => x.DiscoverAsync(It.IsAny())) .ReturnsAsync(new[] { - new PluginDiscoveryResult(new PluginFile(pluginFilePath, new Lazy(() => pluginFileState))) + new PluginDiscoveryResult(new PluginFile(pluginFilePath, new Lazy(() => pluginFileState), requiresDotnetHost : ! IsDesktop)) }); _connection = new Mock(MockBehavior.Strict); @@ -407,11 +429,11 @@ internal PluginResourceProviderPositiveTest( _plugin.SetupGet(x => x.Id) .Returns("id"); - _factory = new Mock(MockBehavior.Strict); + _factory = new Mock(MockBehavior.Strict); _factory.Setup(x => x.Dispose()); _factory.Setup(x => x.GetOrCreateAsync( - It.Is(p => p == pluginFilePath), + It.Is(p => p.Path == pluginFilePath), It.IsNotNull>(), It.IsNotNull(), It.IsNotNull(),