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

Execute net tools plugins #6113

1 change: 0 additions & 1 deletion src/NuGet.Core/NuGet.Protocol/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@
[assembly: SuppressMessage("Build", "CA1031:Modify 'FireBeforeClose' to catch a more specific allowed exception type, or rethrow the exception.", Justification = "<Pending>", 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 = "<Pending>", 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 = "<Pending>", 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 = "<Pending>", 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 = "<Pending>", 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 = "<Pending>", 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}}")]
Expand Down
8 changes: 4 additions & 4 deletions src/NuGet.Core/NuGet.Protocol/Plugins/IPluginFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ namespace NuGet.Protocol.Plugins
/// <summary>
/// A plugin factory.
/// </summary>
public interface IPluginFactory : IDisposable
internal interface IPluginFactory : IDisposable
{
/// <summary>
/// Asynchronously gets an existing plugin instance or creates a new instance and connects to it.
/// </summary>
/// <param name="filePath">The file path of the plugin.</param>
/// <param name="pluginFile">A plugin file.</param>
/// <param name="arguments">Command-line arguments to be supplied to the plugin.</param>
/// <param name="requestHandlers">Request handlers.</param>
/// <param name="options">Connection options.</param>
/// <param name="sessionCancellationToken">A cancellation token for the plugin's lifetime.</param>
/// <returns>A task that represents the asynchronous operation.
/// The task result (<see cref="Task{TResult}.Result" />) returns a <see cref="Plugin" />
/// instance.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="filePath" />
/// <exception cref="ArgumentException">Thrown if <paramref name="pluginFile.Path" />
/// is either <see langword="null" /> or empty.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="arguments" />
/// is <see langword="null" />.</exception>
Expand All @@ -37,7 +37,7 @@ public interface IPluginFactory : IDisposable
/// <exception cref="ObjectDisposedException">Thrown if this object is disposed.</exception>
/// <remarks>This is intended to be called by NuGet client tools.</remarks>
Task<IPlugin> GetOrCreateAsync(
string filePath,
PluginFile pluginFile,
Nigusu-Allehu marked this conversation as resolved.
Show resolved Hide resolved
IEnumerable<string> arguments,
IRequestHandlers requestHandlers,
ConnectionOptions options,
Expand Down
23 changes: 17 additions & 6 deletions src/NuGet.Core/NuGet.Protocol/Plugins/PluginDiscoverer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ public sealed class PluginDiscoverer : IPluginDiscoverer
private IEnumerable<PluginDiscoveryResult> _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)
Expand Down Expand Up @@ -168,7 +179,7 @@ private static List<PluginFile> GetPluginFiles(IEnumerable<string> filePaths, Ca
{
return PluginFileState.InvalidFilePath;
}
}));
}), requiresDotnetHost: !IsDesktop);
files.Add(pluginFile);
}

Expand Down Expand Up @@ -197,15 +208,15 @@ internal List<PluginFile> GetPluginsInNuGetPluginPaths()
// A DotNet tool plugin
if (IsValidPluginFile(fileInfo))
{
PluginFile pluginFile = new PluginFile(fileInfo.FullName, new Lazy<PluginFileState>(() => PluginFileState.Valid), isDotnetToolsPlugin: true);
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));
pluginFiles.Add(new PluginFile(fileInfo.FullName, state, requiresDotnetHost: !IsDesktop));
}
}
else if (Directory.Exists(path))
Expand All @@ -215,7 +226,7 @@ internal List<PluginFile> GetPluginsInNuGetPluginPaths()
}
else
{
pluginFiles.Add(new PluginFile(path, new Lazy<PluginFileState>(() => PluginFileState.InvalidFilePath)));
pluginFiles.Add(new PluginFile(path, new Lazy<PluginFileState>(() => PluginFileState.InvalidFilePath), requiresDotnetHost: !IsDesktop));
}
}

Expand All @@ -240,7 +251,7 @@ internal List<PluginFile> GetPluginsInPath()
}
else
{
pluginFiles.Add(new PluginFile(path, new Lazy<PluginFileState>(() => PluginFileState.InvalidFilePath)));
pluginFiles.Add(new PluginFile(path, new Lazy<PluginFileState>(() => PluginFileState.InvalidFilePath), requiresDotnetHost: false));
}
}

Expand All @@ -260,7 +271,7 @@ private static List<PluginFile> GetNetToolsPluginsInDirectory(string directoryPa
{
if (IsValidPluginFile(file))
{
PluginFile pluginFile = new PluginFile(file.FullName, new Lazy<PluginFileState>(() => PluginFileState.Valid), isDotnetToolsPlugin: true);
PluginFile pluginFile = new PluginFile(file.FullName, new Lazy<PluginFileState>(() => PluginFileState.Valid), requiresDotnetHost: false);
pluginFiles.Add(pluginFile);
}
}
Expand Down
84 changes: 47 additions & 37 deletions src/NuGet.Core/NuGet.Protocol/Plugins/PluginFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ namespace NuGet.Protocol.Plugins
/// <summary>
/// A plugin factory.
/// </summary>
public sealed class PluginFactory : IPluginFactory
public class PluginFactory : IPluginFactory
{
private bool _isDisposed;
private readonly IPluginLogger _logger;
private readonly TimeSpan _pluginIdleTimeout;
private readonly ConcurrentDictionary<string, Lazy<Task<IPlugin>>> _plugins;

internal PluginFactory() { }

/// <summary>
/// Instantiates a new <see cref="PluginFactory" /> class.
/// </summary>
Expand All @@ -48,7 +50,7 @@ public PluginFactory(TimeSpan pluginIdleTimeout)
/// <summary>
/// Disposes of this instance.
/// </summary>
public void Dispose()
public virtual void Dispose()
{
if (_isDisposed)
{
Expand Down Expand Up @@ -77,15 +79,15 @@ public void Dispose()
/// <summary>
/// Asynchronously gets an existing plugin instance or creates a new instance and connects to it.
/// </summary>
/// <param name="filePath">The file path of the plugin.</param>
/// <param name="pluginFile">A plugin file.</param>
/// <param name="arguments">Command-line arguments to be supplied to the plugin.</param>
/// <param name="requestHandlers">Request handlers.</param>
/// <param name="options">Connection options.</param>
/// <param name="sessionCancellationToken">A cancellation token for the plugin's lifetime.</param>
/// <returns>A task that represents the asynchronous operation.
/// The task result (<see cref="Task{TResult}.Result" />) returns a <see cref="Plugin" />
/// instance.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="filePath" />
/// <exception cref="ArgumentException">Thrown if <paramref name="pluginFile.Path" />
/// is either <see langword="null" /> or empty.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="arguments" />
/// is <see langword="null" />.</exception>
Expand All @@ -99,8 +101,8 @@ public void Dispose()
/// <exception cref="ProtocolException">Thrown if a plugin protocol error occurs.</exception>
/// <exception cref="PluginException">Thrown for a plugin failure during creation.</exception>
/// <remarks>This is intended to be called by NuGet client tools.</remarks>
public async Task<IPlugin> GetOrCreateAsync(
string filePath,
public virtual async Task<IPlugin> GetOrCreateAsync(
PluginFile pluginFile,
IEnumerable<string> arguments,
IRequestHandlers requestHandlers,
ConnectionOptions options,
Expand All @@ -111,9 +113,9 @@ public async Task<IPlugin> 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)
Expand All @@ -134,9 +136,9 @@ public async Task<IPlugin> GetOrCreateAsync(
sessionCancellationToken.ThrowIfCancellationRequested();

var lazyTask = _plugins.GetOrAdd(
filePath,
pluginFile.Path,
(path) => new Lazy<Task<IPlugin>>(
() => CreatePluginAsync(filePath, arguments, requestHandlers, options, sessionCancellationToken)));
() => CreatePluginAsync(pluginFile, arguments, requestHandlers, options, sessionCancellationToken)));

await lazyTask.Value;

Expand All @@ -145,40 +147,48 @@ public async Task<IPlugin> GetOrCreateAsync(
}

private async Task<IPlugin> CreatePluginAsync(
string filePath,
PluginFile pluginFile,
IEnumerable<string> 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();

Expand Down Expand Up @@ -219,7 +229,7 @@ private async Task<IPlugin> CreatePluginAsync(
connection = new Connection(messageDispatcher, sender, receiver, options, _logger);

var plugin = new Plugin(
filePath,
pluginFile.Path,
connection,
pluginProcess,
isOwnProcess: false,
Expand Down
19 changes: 5 additions & 14 deletions src/NuGet.Core/NuGet.Protocol/Plugins/PluginFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,24 @@ public sealed class PluginFile
public Lazy<PluginFileState> State { get; }

/// <summary>
/// Is the plugin file, a dotnet tools plugin file?
/// Indicates if the plugin file requires a dotnet host.
/// </summary>
internal bool IsDotnetToolsPlugin { get; }
internal bool RequiresDotnetHost { 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>
/// <param name="filePath">The plugin's file path.</param>
/// <param name="state">A lazy that evaluates the plugin file state.</param>
public PluginFile(string filePath, Lazy<PluginFileState> state)
/// <param name="requiresDotnetHost">Indicates if the plugin file requires a dotnet host.</param>
public PluginFile(string filePath, Lazy<PluginFileState> state, bool requiresDotnetHost)
{
if (string.IsNullOrEmpty(filePath))
{
throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(filePath));
}

RequiresDotnetHost = requiresDotnetHost;
Path = filePath;
State = state;
}
Expand Down
16 changes: 9 additions & 7 deletions src/NuGet.Core/NuGet.Protocol/Plugins/PluginManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ private PluginManager()
public PluginManager(
IEnvironmentVariableReader reader,
Lazy<IPluginDiscoverer> pluginDiscoverer,
Func<TimeSpan, IPluginFactory> pluginFactoryCreator,
Func<TimeSpan, PluginFactory> pluginFactoryCreator,
Lazy<string> pluginsCacheDirectoryPath)
{
Initialize(
Expand Down Expand Up @@ -212,12 +212,14 @@ private async Task<Tuple<bool, PluginCreationResult>> 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);

Expand Down
Loading