Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discover plugins installed using Net tools #5990

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
4ace5f4
Discover Net tools installed plugins
Nigusu-Allehu Aug 24, 2024
e23db95
Read unix executable plugins
Nigusu-Allehu Sep 3, 2024
73dc448
Add tests
Nigusu-Allehu Sep 3, 2024
d5ba119
Linux test
Nigusu-Allehu Sep 3, 2024
3ad5aef
Linux test
Nigusu-Allehu Sep 3, 2024
6d0a92f
cleanup
Nigusu-Allehu Sep 3, 2024
29f9482
Use GetUnixFileMode
Nigusu-Allehu Sep 4, 2024
ec680b4
Fix test
Nigusu-Allehu Sep 5, 2024
eb00875
Add more tests
Nigusu-Allehu Sep 5, 2024
e6f3e9f
white space
Nigusu-Allehu Sep 5, 2024
3ac5084
fix tests
Nigusu-Allehu Sep 5, 2024
de6bd47
Comment
Nigusu-Allehu Sep 5, 2024
b928c1a
use /bin/bash
Nigusu-Allehu Sep 5, 2024
1ec9a79
Fix assert
Nigusu-Allehu Sep 5, 2024
68e86ee
Cleanup testing
Nigusu-Allehu Sep 10, 2024
346b6d6
Add NUGET_PLUGIN_PATHS
Nigusu-Allehu Sep 12, 2024
ce0b9b4
Clean code
Nigusu-Allehu Sep 12, 2024
ae23b52
use empty body constructor
Nigusu-Allehu Sep 12, 2024
0082b0c
NuGetPluginPaths Env variable used by dotnet tools only
Nigusu-Allehu Sep 13, 2024
b0cbeba
Test
Nigusu-Allehu Oct 6, 2024
f68c137
Clean up
Nigusu-Allehu Oct 6, 2024
8329500
fix process
Nigusu-Allehu Oct 6, 2024
4e8effc
Plugins path env
Nigusu-Allehu Oct 8, 2024
c273b7b
fix tests
Nigusu-Allehu Oct 8, 2024
3d1e878
fix linux bash call
Nigusu-Allehu Oct 8, 2024
e3c1b57
fix bash command
Nigusu-Allehu Oct 8, 2024
bceba8e
remove some apis
Nigusu-Allehu Oct 8, 2024
b2a23e1
remove symlink check
Nigusu-Allehu Oct 8, 2024
6cec63b
cleanup
Nigusu-Allehu Oct 9, 2024
daccb03
cleanup
Nigusu-Allehu Oct 9, 2024
97c1b23
cleanup
Nigusu-Allehu Oct 9, 2024
6077448
comments
Nigusu-Allehu Oct 11, 2024
e655a61
factorize and clean up
Nigusu-Allehu Oct 12, 2024
66983c1
Cleanup
Nigusu-Allehu Oct 12, 2024
d0a77e7
Update PluginDiscoverer API
Nigusu-Allehu Oct 15, 2024
704e61d
Cleanup
Nigusu-Allehu Oct 15, 2024
32cab44
cleanup
Nigusu-Allehu Oct 15, 2024
8424237
dispose directories
Nigusu-Allehu Oct 15, 2024
ed12e51
undo
Nigusu-Allehu Oct 15, 2024
7466578
fix netcore test
Nigusu-Allehu Oct 15, 2024
47f4d5d
Fix linux tests
Nigusu-Allehu Oct 15, 2024
3eab1a7
Cleanup
Nigusu-Allehu Oct 15, 2024
2a36928
clean up test conflict
Nigusu-Allehu Oct 15, 2024
3f8b8db
Address comments
Nigusu-Allehu Oct 17, 2024
691c76e
Address comments
Nigusu-Allehu Oct 17, 2024
6abbc3d
Address comments
Nigusu-Allehu Oct 22, 2024
7f017a4
cleanup
Nigusu-Allehu Oct 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 224 additions & 19 deletions src/NuGet.Core/NuGet.Protocol/Plugins/PluginDiscoverer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,17 +18,31 @@ public sealed class PluginDiscoverer : IPluginDiscoverer
{
private bool _isDisposed;
private List<PluginFile> _pluginFiles;
private readonly string _rawPluginPaths;
private readonly string _netCoreOrNetFXPluginPaths;
private readonly string _nuGetPluginPaths;
private IEnumerable<PluginDiscoveryResult> _results;
private readonly SemaphoreSlim _semaphore;
private readonly IEnvironmentVariableReader _environmentVariableReader;

/// <summary>
/// Instantiates a new <see cref="PluginDiscoverer" /> class.
/// </summary>
/// <param name="rawPluginPaths">The raw semicolon-delimited list of supposed plugin file paths.</param>
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);
}

Expand Down Expand Up @@ -75,7 +90,40 @@ public async Task<IEnumerable<PluginDiscoveryResult>> 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<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)
Expand All @@ -97,14 +145,17 @@ public async Task<IEnumerable<PluginDiscoveryResult>> DiscoverAsync(Cancellation
return _results;
}

private List<PluginFile> GetPluginFiles(CancellationToken cancellationToken)
private static List<PluginFile> GetPluginFiles(IEnumerable<string> filePaths, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

var filePaths = GetPluginFilePaths();

var files = new List<PluginFile>();

if (filePaths == null)
{
return files;
}

foreach (var filePath in filePaths)
{
var pluginFile = new PluginFile(filePath, new Lazy<PluginFileState>(() =>
Expand All @@ -124,19 +175,173 @@ private List<PluginFile> GetPluginFiles(CancellationToken cancellationToken)
return files;
}

private IEnumerable<string> GetPluginFilePaths()
/// <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()
{
if (string.IsNullOrEmpty(_rawPluginPaths))
var pluginFiles = new List<PluginFile>();
string[] paths = _nuGetPluginPaths?.Split(Path.PathSeparator) ?? Array.Empty<string>();

foreach (var path in paths)
{
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());
if (PathValidator.IsValidLocalPath(path) || PathValidator.IsValidUncPath(path))
kartheekp-ms marked this conversation as resolved.
Show resolved Hide resolved
{
if (File.Exists(path))
{
FileInfo fileInfo = new FileInfo(path);
kartheekp-ms marked this conversation as resolved.
Show resolved Hide resolved
if (fileInfo.Name.StartsWith("nuget-plugin-", StringComparison.CurrentCultureIgnoreCase))
{
// A DotNet tool plugin
if (IsValidPluginFile(fileInfo))
{
PluginFile pluginFile = new PluginFile(fileInfo.FullName, new Lazy<PluginFileState>(() => PluginFileState.Valid), isDotnetToolsPlugin: true);
Nigusu-Allehu marked this conversation as resolved.
Show resolved Hide resolved
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))
{
pluginFiles.AddRange(GetNetToolsPluginsInDirectory(path) ?? new List<PluginFile>());
}
}
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))
{
pluginFiles.AddRange(GetNetToolsPluginsInDirectory(path) ?? new List<PluginFile>());
}
else
{
pluginFiles.Add(new PluginFile(path, new Lazy<PluginFileState>(() => PluginFileState.InvalidFilePath)));
}
}

return pluginFiles;
}

private static List<PluginFile> GetNetToolsPluginsInDirectory(string directoryPath)
{
var pluginFiles = new List<PluginFile>();

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>(() => PluginFileState.Valid), isDotnetToolsPlugin: true);
pluginFiles.Add(pluginFile);
}
}
}

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 (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
/// <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;
Nigusu-Allehu marked this conversation as resolved.
Show resolved Hide resolved
}

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
}
}
16 changes: 16 additions & 0 deletions src/NuGet.Core/NuGet.Protocol/Plugins/PluginFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ public sealed class PluginFile
/// </summary>
public Lazy<PluginFileState> State { get; }

/// <summary>
/// Is the plugin file, a dotnet tools plugin file?
/// </summary>
internal bool IsDotnetToolsPlugin { get; }

/// <summary>
/// Instantiates a new <see cref="PluginFile" /> class.
/// </summary>
/// <param name="filePath">The plugin's file path.</param>
/// <param name="state">A lazy that evaluates the plugin file state.</param>
/// <param name="isDotnetToolsPlugin">Is the plugin file, a dotnet tools plugin file?</param>
internal PluginFile(string filePath, Lazy<PluginFileState> state, bool isDotnetToolsPlugin) : this(filePath, state)
{
IsDotnetToolsPlugin = isDotnetToolsPlugin;
}

/// <summary>
/// Instantiates a new <see cref="PluginFile" /> class.
/// </summary>
Expand Down
22 changes: 10 additions & 12 deletions src/NuGet.Core/NuGet.Protocol/Plugins/PluginManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public sealed class PluginManager : IPluginManager, IDisposable
private IPluginFactory _pluginFactory;
private ConcurrentDictionary<PluginRequestKey, Lazy<Task<IReadOnlyList<OperationClaim>>>> _pluginOperationClaims;
private ConcurrentDictionary<string, Lazy<IPluginMulticlientUtilities>> _pluginUtilities;
private string _rawPluginPaths;

private static Lazy<int> _currentProcessId = new Lazy<int>(GetCurrentProcessId);
private Lazy<string> _pluginsCacheDirectoryPath;
Expand Down Expand Up @@ -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))
Nigusu-Allehu marked this conversation as resolved.
Show resolved Hide resolved
{
_rawPluginPaths = reader.GetEnvironmentVariable(EnvironmentVariableConstants.PluginPaths);
}

_connectionOptions = ConnectionOptions.CreateDefault(reader);

Expand Down Expand Up @@ -360,12 +350,20 @@ private async Task<IReadOnlyList<OperationClaim>> 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#nullable enable
~NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer(string rawPluginPaths) -> void
NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer() -> void
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#nullable enable
~NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer(string rawPluginPaths) -> void
NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer() -> void
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#nullable enable
~NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer(string rawPluginPaths) -> void
NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer() -> void
Loading
Loading