|
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
namespace NuGet.Protocol.Plugins
{
/// <summary>
/// Discovers plugins and their operation claims.
/// </summary>
public sealed class PluginDiscoverer : IPluginDiscoverer
{
private bool _isDisposed;
private List<PluginFile> _pluginFiles;
private readonly string _netCoreOrNetFXPluginPaths;
private readonly string _nuGetPluginPaths;
private IEnumerable<PluginDiscoveryResult> _results;
private readonly SemaphoreSlim _semaphore;
private readonly IEnvironmentVariableReader _environmentVariableReader;
public PluginDiscoverer()
: this(EnvironmentVariableWrapper.Instance)
{
}
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);
}
/// <summary>
/// Disposes of this instance.
/// </summary>
public void Dispose()
{
if (_isDisposed)
{
return;
}
_semaphore.Dispose();
GC.SuppressFinalize(this);
_isDisposed = true;
}
/// <summary>
/// Asynchronously discovers plugins.
/// </summary>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.
/// The task result (<see cref="Task{TResult}.Result" />) returns a
/// <see cref="IEnumerable{PluginDiscoveryResult}" /> from the target.</returns>
/// <exception cref="OperationCanceledException">Thrown if <paramref name="cancellationToken" />
/// is cancelled.</exception>
public async Task<IEnumerable<PluginDiscoveryResult>> DiscoverAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (_results != null)
{
return _results;
}
await _semaphore.WaitAsync(cancellationToken);
try
{
if (_results != null)
{
return _results;
}
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<string> { 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<PluginDiscoveryResult>();
for (var i = 0; i < _pluginFiles.Count; ++i)
{
var pluginFile = _pluginFiles[i];
var result = new PluginDiscoveryResult(pluginFile);
results.Add(result);
}
_results = results;
}
finally
{
_semaphore.Release();
}
return _results;
}
private static List<PluginFile> GetPluginFiles(IEnumerable<string> filePaths, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var files = new List<PluginFile>();
if (filePaths == null)
{
return files;
}
foreach (var filePath in filePaths)
{
var pluginFile = new PluginFile(filePath, new Lazy<PluginFileState>(() =>
{
if (PathValidator.IsValidLocalPath(filePath) || PathValidator.IsValidUncPath(filePath))
{
return File.Exists(filePath) ? PluginFileState.Valid : PluginFileState.NotFound;
}
else
{
return PluginFileState.InvalidFilePath;
}
}));
files.Add(pluginFile);
}
return files;
}
/// <summary>
/// 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.
/// </summary>
/// <returns>A list of valid <see cref="PluginFile"/> objects representing the discovered plugins.</returns>
internal List<PluginFile> GetPluginsInNuGetPluginPaths()
{
var pluginFiles = new List<PluginFile>();
string[] paths = _nuGetPluginPaths?.Split([Path.PathSeparator], StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
foreach (var path in paths)
{
if (PathValidator.IsValidLocalPath(path) || PathValidator.IsValidUncPath(path))
{
if (File.Exists(path))
{
FileInfo fileInfo = new FileInfo(path);
if (IsValidPluginFile(fileInfo))
{
// A DotNet tool plugin
PluginFile pluginFile = new PluginFile(fileInfo.FullName, new Lazy<PluginFileState>(() => PluginFileState.Valid), requiresDotnetHost: false);
pluginFiles.Add(pluginFile);
}
else
{
// A non DotNet tool plugin file
var state = new Lazy<PluginFileState>(() => PluginFileState.Valid);
pluginFiles.Add(new PluginFile(fileInfo.FullName, state));
}
}
else if (Directory.Exists(path))
{
List<PluginFile> plugins = GetNetToolsPluginsInDirectory(path);
if (plugins != null)
{
pluginFiles.AddRange(plugins);
}
}
}
else
{
pluginFiles.Add(new PluginFile(path, new Lazy<PluginFileState>(() => PluginFileState.InvalidFilePath)));
}
}
return pluginFiles;
}
/// <summary>
/// Retrieves .NET tools authentication plugins by searching through directories specified in `PATH`
/// </summary>
/// <returns>A list of valid <see cref="PluginFile"/> objects representing the discovered plugins.</returns>
internal List<PluginFile> GetPluginsInPath()
{
var pluginFiles = new List<PluginFile>();
var nugetPluginPaths = _environmentVariableReader.GetEnvironmentVariable("PATH");
string[] paths = nugetPluginPaths?.Split(Path.PathSeparator) ?? Array.Empty<string>();
foreach (var path in paths)
{
if (PathValidator.IsValidLocalPath(path) || PathValidator.IsValidUncPath(path))
{
List<PluginFile> plugins = GetNetToolsPluginsInDirectory(path);
if (plugins != null)
{
pluginFiles.AddRange(plugins);
}
}
}
return pluginFiles;
}
private static List<PluginFile> GetNetToolsPluginsInDirectory(string directoryPath)
{
List<PluginFile> pluginFiles = null;
if (!Directory.Exists(directoryPath))
{
return pluginFiles;
}
try
{
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>(() => PluginFileState.Valid), requiresDotnetHost: false);
pluginFiles ??= [];
pluginFiles.Add(pluginFile);
}
}
}
catch (UnauthorizedAccessException) { }
catch (SecurityException) { }
catch (PathTooLongException) { }
catch (DirectoryNotFoundException) { }
catch (DriveNotFoundException) { }
return pluginFiles;
}
/// <summary>
/// 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
/// </summary>
/// <param name="fileInfo"></param>
/// <returns></returns>
internal static bool IsValidPluginFile(FileInfo fileInfo)
{
if (!fileInfo.Name.StartsWith("nuget-plugin-", StringComparison.Ordinal))
{
return false;
}
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
}
}
#if !NET8_0_OR_GREATER
/// <summary>
/// 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`
/// </summary>
/// <param name="fileInfo"></param>
/// <returns></returns>
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;
}
// 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
}
}
|