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
7 changes: 7 additions & 0 deletions src/NuGet.Core/NuGet.Protocol/Plugins/IPluginFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,12 @@ Task<IPlugin> GetOrCreateAsync(
IRequestHandlers requestHandlers,
ConnectionOptions options,
CancellationToken sessionCancellationToken);

Task<IPlugin> GetOrCreateNetToolsPluginAsync(
string filePath,
IEnumerable<string> arguments,
IRequestHandlers requestHandlers,
ConnectionOptions options,
CancellationToken sessionCancellationToken);
}
}
114 changes: 89 additions & 25 deletions src/NuGet.Core/NuGet.Protocol/Plugins/PluginFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public async Task<IPlugin> GetOrCreateAsync(
var lazyTask = _plugins.GetOrAdd(
filePath,
(path) => new Lazy<Task<IPlugin>>(
() => CreatePluginAsync(filePath, arguments, requestHandlers, options, sessionCancellationToken)));
() => CreatePluginAsync(filePath, arguments, requestHandlers, options, isDotnetTools: false, sessionCancellationToken)));

await lazyTask.Value;

Expand All @@ -149,36 +149,60 @@ private async Task<IPlugin> CreatePluginAsync(
IEnumerable<string> arguments,
IRequestHandlers requestHandlers,
ConnectionOptions options,
bool isDotnetTools,
CancellationToken sessionCancellationToken)
{
var args = string.Join(" ", arguments);
#if IS_DESKTOP
var startInfo = new ProcessStartInfo(filePath)

ProcessStartInfo startInfo;

if (!isDotnetTools)
{
Arguments = args,
UseShellExecute = false,
RedirectStandardError = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
StandardOutputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
WindowStyle = ProcessWindowStyle.Hidden,
};
#if IS_DESKTOP
startInfo = new ProcessStartInfo(filePath)
{
Arguments = args,
UseShellExecute = false,
RedirectStandardError = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
StandardOutputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
WindowStyle = ProcessWindowStyle.Hidden,
};
#else
var startInfo = new ProcessStartInfo
{
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,
};
startInfo = new ProcessStartInfo
{
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
}
else
{
// A dotnet tools plugin. Execute it directly.

Nigusu-Allehu marked this conversation as resolved.
Show resolved Hide resolved
startInfo = new ProcessStartInfo(filePath)
{
FileName = filePath,
Nigusu-Allehu marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -455,5 +479,45 @@ private static void WriteCommonLogMessages(IPluginLogger logger)
logger.Write(new ProcessLogMessage(logger.Now));
logger.Write(new ThreadPoolLogMessage(logger.Now));
}

public async Task<IPlugin> GetOrCreateNetToolsPluginAsync(string filePath, IEnumerable<string> arguments, IRequestHandlers requestHandlers, ConnectionOptions options, CancellationToken sessionCancellationToken)
Nigusu-Allehu marked this conversation as resolved.
Show resolved Hide resolved
{
if (_isDisposed)
{
throw new ObjectDisposedException(nameof(PluginFactory));
}

if (string.IsNullOrEmpty(filePath))
{
throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(filePath));
}

if (arguments == null)
{
throw new ArgumentNullException(nameof(arguments));
}

if (requestHandlers == null)
{
throw new ArgumentNullException(nameof(requestHandlers));
}

if (options == null)
{
throw new ArgumentNullException(nameof(options));
}

sessionCancellationToken.ThrowIfCancellationRequested();

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

await lazyTask.Value;

// Manage plugin lifetime by its idleness. Thus, don't allow callers to prematurely dispose of a plugin.
return new NoOpDisposePlugin(lazyTask.Value.Result);
}
}
}
16 changes: 15 additions & 1 deletion src/NuGet.Core/NuGet.Protocol/Plugins/PluginManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,26 @@ private async Task<Tuple<bool, PluginCreationResult>> TryCreatePluginAsync(
{
if (result.PluginFile.State.Value == PluginFileState.Valid)
{
var plugin = await _pluginFactory.GetOrCreateAsync(
IPlugin plugin;

if (result.PluginFile.IsDotnetToolsPlugin)
{
plugin = await _pluginFactory.GetOrCreateNetToolsPluginAsync(
result.PluginFile.Path,
PluginConstants.PluginArguments,
new RequestHandlers(),
_connectionOptions,
cancellationToken);
Nigusu-Allehu marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
plugin = await _pluginFactory.GetOrCreateAsync(
result.PluginFile.Path,
PluginConstants.PluginArguments,
new RequestHandlers(),
_connectionOptions,
cancellationToken);
}

var utilities = await PerformOneTimePluginInitializationAsync(plugin, cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
#nullable enable
NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer() -> void
~NuGet.Protocol.Plugins.IPluginFactory.GetOrCreateNetToolsPluginAsync(string filePath, System.Collections.Generic.IEnumerable<string> arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task<NuGet.Protocol.Plugins.IPlugin>
~NuGet.Protocol.Plugins.PluginFactory.GetOrCreateNetToolsPluginAsync(string filePath, System.Collections.Generic.IEnumerable<string> arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task<NuGet.Protocol.Plugins.IPlugin>
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
#nullable enable
NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer() -> void
~NuGet.Protocol.Plugins.IPluginFactory.GetOrCreateNetToolsPluginAsync(string filePath, System.Collections.Generic.IEnumerable<string> arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task<NuGet.Protocol.Plugins.IPlugin>
~NuGet.Protocol.Plugins.PluginFactory.GetOrCreateNetToolsPluginAsync(string filePath, System.Collections.Generic.IEnumerable<string> arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task<NuGet.Protocol.Plugins.IPlugin>
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
#nullable enable
NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer() -> void
~NuGet.Protocol.Plugins.IPluginFactory.GetOrCreateNetToolsPluginAsync(string filePath, System.Collections.Generic.IEnumerable<string> arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task<NuGet.Protocol.Plugins.IPlugin>
~NuGet.Protocol.Plugins.PluginFactory.GetOrCreateNetToolsPluginAsync(string filePath, System.Collections.Generic.IEnumerable<string> arguments, NuGet.Protocol.Plugins.IRequestHandlers requestHandlers, NuGet.Protocol.Plugins.ConnectionOptions options, System.Threading.CancellationToken sessionCancellationToken) -> System.Threading.Tasks.Task<NuGet.Protocol.Plugins.IPlugin>
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// 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
Expand Down Expand Up @@ -135,6 +137,119 @@ public async Task GetOrCreateAsync_ThrowsIfDisposed()
Assert.Equal(nameof(PluginFactory), exception.ObjectName);
}

[PlatformFact(Platform.Windows)]
public async Task GetOrCreateNetPluginAsync_CreatesPluginAndSendsHandshakeRequest()
Nigusu-Allehu marked this conversation as resolved.
Show resolved Hide resolved
{
using TestDirectory testDirectory = TestDirectory.Create();
string pluginPath = Path.Combine(testDirectory.Path, "nuget-plugin-batFile.bat");

// Create the .bat file that simulates a plugin
// Simply waits for a json request that looks like`{"RequestId":"ff52f0ae-28c9-4a19-957d-78db5b68f3f2","Type":"Request","Method":"Handshake","Payload":{"ProtocolVersion":"2.0.0","MinimumProtocolVersion":"1.0.0"}} `
// It then creates a response with the same RequestId and Method, but with the Type changed to `Response` and Adds a Payload with a ResponseCode of 0
// It finally sends a handshake request back to the caller. This completes the handshake process.
string batFileContent = $@"
@echo off
setlocal EnableDelayedExpansion

:InputLoop
set /p jsonLine=

set quote=""
set comma=,
set colon=:
set openCurlyBracket={{
set closeCurlyBracket=}}
set key=
set value=
set dot=.
set onKeySearch=true
set onValueSearch=false
set enteredQuotes=false

set pos=0
:NextChar
set index=%pos%
set char=!jsonLine:~%pos%,1!
if ""!char!""=="""" goto endLoop
if %onKeySearch%==true (
if %enteredQuotes%==true (
if !char!==!quote! (
set onKeySearch=false
set onValueSearch=true
set enteredQuotes=false
) else (
set key=!key!!char!
)
) else if !char!==!quote! (
set enteredQuotes=true
)
) else if %onValueSearch%==true (
if %enteredQuotes%==true (
if !char!==!quote! (
set onKeySearch=true
set onValueSearch=true
set enteredQuotes=false
set ""!key!=!value!""
set value=
set /a pos=pos+1
goto ClearKey
) else (
set value=!value!!char!
)
) else if !char!==!quote! (
set enteredQuotes=true
) else if !char!==!openCurlyBracket! (
set onKeySearch=true
set onValueSearch=false
set key=!key!.
)
) else (
echo neither
)
set /a pos=pos+1
if ""!jsonLine:~%pos%,1!"" NEQ """" goto NextChar

:ClearKey
if ""!key!""=="""" (
goto NextChar
)
set lastChar=!key:~-1!
if !lastChar!==!dot! (
goto NextChar
) else (
set key=!key:~0,-1!
goto ClearKey
)
goto NextChar


:endLoop
if ""!RequestId!"" == """" (
goto InputLoop
) else (
set HandshakeReponseJsonString={{""RequestId"":""!RequestId!"",""Type"":""Response"",""Method"":""Handshake"",""Payload"":{{""ProtocolVersion"":""2.0.0"",""ResponseCode"":0}}}}
set HandshakeRequestJsonString={{""RequestId"":""!RequestId!"",""Type"":""Request"",""Method"":""Handshake"",""Payload"":{{""ProtocolVersion"":""2.0.0"",""MinimumProtocolVersion"":""1.0.0""}}}}
echo !HandshakeReponseJsonString!
echo !HandshakeRequestJsonString!
)
goto InputLoop

";
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 pluginFactory.GetOrCreateNetToolsPluginAsync(pluginPath, args, reqHandler, options, CancellationToken.None);

// Assert
Assert.NotNull(plugin.Connection);
}

[Fact]
public async Task CreateFromCurrentProcessAsync_ThrowsForNullRequestHandlers()
{
Expand Down
Loading