diff --git a/build.cake b/build.cake
index b2a40a9..5778b0d 100644
--- a/build.cake
+++ b/build.cake
@@ -10,7 +10,8 @@ BuildSettings.Initialize
solutionFile: "net80-pluggable-agent.sln",
unitTests: "**/*.tests.exe",
githubOwner: "NUnit",
- githubRepository: "net80-pluggable-agent"
+ githubRepository: "net80-pluggable-agent",
+ exemptFiles: new[] { "ProcessUtils.cs" }
);
var PackageTests = new PackageTest[] {
@@ -113,7 +114,7 @@ BuildSettings.Packages.Add(new NuGetPackage(
{
HasFiles("LICENSE.txt", "README.md", "nunit_256.png"),
HasDirectory("tools").WithFiles(
- "nunit-agent-launcher-net80.dll", "nunit.engine.api.dll", "nunit.agent.core.dll"),
+ "nunit-agent-launcher-net80.dll", "nunit.engine.api.dll"),
HasDirectory("tools/agent").WithFiles(
"nunit-agent-net80.dll", "nunit.engine.api.dll", "nunit.common.dll",
"nunit.extensibility.api.dll", "nunit.extensibility.dll", "nunit.agent.core.dll",
@@ -131,7 +132,7 @@ BuildSettings.Packages.Add(new ChocolateyPackage(
{
HasDirectory("tools").WithFiles(
"LICENSE.txt", "README.md", "nunit_256.png", "VERIFICATION.txt",
- "nunit-agent-launcher-net80.dll", "nunit.engine.api.dll", "nunit.agent.core.dll"),
+ "nunit-agent-launcher-net80.dll", "nunit.engine.api.dll"),
HasDirectory("tools/agent").WithFiles(
"nunit-agent-net80.dll", "nunit.engine.api.dll", "nunit.common.dll",
"nunit.extensibility.api.dll", "nunit.extensibility.dll", "nunit.agent.core.dll",
diff --git a/choco/net80-pluggable-agent.nuspec b/choco/net80-pluggable-agent.nuspec
index a40585d..edb10ea 100644
--- a/choco/net80-pluggable-agent.nuspec
+++ b/choco/net80-pluggable-agent.nuspec
@@ -29,7 +29,6 @@
-
diff --git a/nuget/Net80PluggableAgent.nuspec b/nuget/Net80PluggableAgent.nuspec
index 86d3560..d11db93 100644
--- a/nuget/Net80PluggableAgent.nuspec
+++ b/nuget/Net80PluggableAgent.nuspec
@@ -25,7 +25,6 @@
-
diff --git a/src/launcher/DotNet.cs b/src/launcher/DotNet.cs
new file mode 100644
index 0000000..99d1f5e
--- /dev/null
+++ b/src/launcher/DotNet.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt
+
+using Microsoft.Win32;
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace NUnit.Common
+{
+ public static class DotNet
+ {
+ public static string? GetInstallDirectory() => Environment.Is64BitProcess
+ ? GetX64InstallDirectory() : GetX86InstallDirectory();
+
+ public static string? GetInstallDirectory(bool x86) => x86
+ ? GetX86InstallDirectory() : GetX64InstallDirectory();
+
+ private static string? _x64InstallDirectory;
+ public static string? GetX64InstallDirectory()
+ {
+ if (_x64InstallDirectory is null)
+ _x64InstallDirectory = Environment.GetEnvironmentVariable("DOTNET_ROOT");
+
+ if (_x64InstallDirectory is null)
+ {
+#if NETFRAMEWORK
+ if (Path.DirectorySeparatorChar == '\\')
+#else
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+#endif
+ {
+ using (RegistryKey? key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\dotnet\SetUp\InstalledVersions\x64\sharedHost\"))
+ _x64InstallDirectory = (string?)key?.GetValue("Path");
+ }
+ else
+ _x64InstallDirectory = "/usr/shared/dotnet/";
+ }
+
+ return _x64InstallDirectory;
+ }
+
+ private static string? _x86InstallDirectory;
+ public static string? GetX86InstallDirectory()
+ {
+ if (_x86InstallDirectory is null)
+ _x86InstallDirectory = Environment.GetEnvironmentVariable("DOTNET_ROOT_X86");
+
+ if (_x86InstallDirectory is null)
+ {
+#if NETFRAMEWORK
+ if (Path.DirectorySeparatorChar == '\\')
+#else
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+#endif
+ {
+ using (RegistryKey? key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\WOW6432Node\dotnet\SetUp\InstalledVersions\x86\"))
+ _x86InstallDirectory = (string?)key?.GetValue("InstallLocation");
+ }
+ else
+ _x86InstallDirectory = "/usr/shared/dotnet/";
+ }
+
+ return _x86InstallDirectory;
+ }
+
+ public static string GetDotNetExe(bool runAsX86)
+ {
+ string? installDirectory = DotNet.GetInstallDirectory(runAsX86);
+ if (installDirectory is not null)
+ {
+ var dotnet_exe = Path.Combine(installDirectory, "dotnet.exe");
+ if (File.Exists(dotnet_exe))
+ return dotnet_exe;
+ }
+
+ var msg = runAsX86
+ ? "The X86 version of dotnet.exe is not installed."
+ : "Unable to locate dotnet.exe.";
+
+ throw new Exception(msg);
+ }
+ }
+}
diff --git a/src/launcher/Net80AgentLauncher.cs b/src/launcher/Net80AgentLauncher.cs
index 5fe39f0..c4468fd 100644
--- a/src/launcher/Net80AgentLauncher.cs
+++ b/src/launcher/Net80AgentLauncher.cs
@@ -2,25 +2,89 @@
#if NETFRAMEWORK
using System;
+using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.Versioning;
-using NUnit.Agents;
+using System.Text;
using NUnit.Common;
+using NUnit.Engine.Extensibility;
using NUnit.Extensibility;
namespace NUnit.Engine.Agents
{
- [Extension]
- public class Net80AgentLauncher : LocalProcessAgentLauncher
+ [Extension(Description = "Pluggable agent running tests under .NET 8.0", EngineVersion = "4.0.0")]
+ public class Net80AgentLauncher : IAgentLauncher
{
- private static readonly string LAUNCHER_DIR = AssemblyHelper.GetDirectoryName(Assembly.GetExecutingAssembly());
+ private static readonly string LAUNCHER_DIR = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
- protected override string AgentName => "Net80Agent";
- protected override TestAgentType AgentType => TestAgentType.LocalProcess;
- protected override FrameworkName AgentRuntime => new FrameworkName(FrameworkIdentifiers.NetCoreApp, new Version(8, 0, 0));
+ private const string RUNTIME_IDENTIFIER = ".NETCoreApp";
+ private static readonly Version RUNTIME_VERSION = new Version(8, 0, 0);
+ private static readonly FrameworkName TARGET_FRAMEWORK = new FrameworkName(RUNTIME_IDENTIFIER, RUNTIME_VERSION);
- protected override string AgentPath => Path.Combine(LAUNCHER_DIR, $"agent/nunit-agent-net80.dll");
+ protected string AgentPath => Path.Combine(LAUNCHER_DIR, $"agent/nunit-agent-net80.dll");
+
+ public TestAgentInfo AgentInfo => new TestAgentInfo(
+ GetType().Name,
+ TestAgentType.LocalProcess,
+ TARGET_FRAMEWORK);
+
+ public bool CanCreateAgent(TestPackage package)
+ {
+ // Get target runtime from package
+ string runtimeSetting = package.Settings.GetValueOrDefault(SettingDefinitions.TargetFrameworkName);
+ var targetRuntime = new FrameworkName(runtimeSetting);
+ bool runAsX86 = package.Settings.GetValueOrDefault(SettingDefinitions.RunAsX86);
+
+ return targetRuntime.Identifier == RUNTIME_IDENTIFIER && targetRuntime.Version.Major <= RUNTIME_VERSION.Major;
+ }
+
+ public Process CreateAgent(Guid agentId, string agencyUrl, TestPackage package)
+ {
+ // Should not be called unless we have previously checked CanCreateAgent
+ if (!CanCreateAgent(package))
+ throw new ArgumentException("Unable to create agent. Check result of CanCreateAgent before calling CreateAgent.", nameof(package));
+
+ var process = new Process()
+ {
+ EnableRaisingEvents = true
+ };
+
+ // Access package settings
+ var settings = package.Settings;
+ bool runAsX86 = settings.GetValueOrDefault(SettingDefinitions.RunAsX86);
+ bool debugTests = settings.GetValueOrDefault(SettingDefinitions.DebugTests);
+ bool debugAgent = settings.GetValueOrDefault(SettingDefinitions.DebugAgent);
+ string traceLevel = settings.GetValueOrDefault(SettingDefinitions.InternalTraceLevel);
+ bool loadUserProfile = settings.GetValueOrDefault(SettingDefinitions.LoadUserProfile);
+ string workDirectory = settings.GetValueOrDefault(SettingDefinitions.WorkDirectory);
+
+ var sb = new StringBuilder($"--agentId={agentId} --agencyUrl={agencyUrl} --pid={Process.GetCurrentProcess().Id}");
+
+ // Set options that need to be in effect before the package
+ // is loaded by using the command line.
+ if (traceLevel != "Off")
+ sb.Append(" --trace=").EscapeProcessArgument(traceLevel);
+ if (debugAgent)
+ sb.Append(" --debug-agent");
+ if (debugTests)
+ sb.Append(" --debug-tests");
+ if (workDirectory != string.Empty)
+ sb.Append(" --work=").EscapeProcessArgument(workDirectory);
+
+ string arguments = sb.ToString();
+
+ var startInfo = process.StartInfo;
+ startInfo.UseShellExecute = false;
+ startInfo.CreateNoWindow = true;
+ startInfo.WorkingDirectory = Environment.CurrentDirectory;
+ startInfo.LoadUserProfile = loadUserProfile;
+
+ startInfo.FileName = DotNet.GetDotNetExe(runAsX86);
+ startInfo.Arguments = $"\"{AgentPath}\" {arguments}";
+
+ return process;
+ }
}
}
#endif
diff --git a/src/launcher/ProcessUtils.cs b/src/launcher/ProcessUtils.cs
new file mode 100644
index 0000000..bbc7cd9
--- /dev/null
+++ b/src/launcher/ProcessUtils.cs
@@ -0,0 +1,96 @@
+// ***********************************************************************
+// Copyright (c) 2016 Joseph N. Musser II
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+// ***********************************************************************
+
+using System.Text;
+
+namespace NUnit.Common
+{
+ public static class ProcessUtils
+ {
+ private static readonly char[] CharsThatRequireQuoting = { ' ', '"' };
+ private static readonly char[] CharsThatRequireEscaping = { '\\', '"' };
+
+ ///
+ /// Escapes arbitrary values so that the process receives the exact string you intend and injection is impossible.
+ /// Spec: https://docs.microsoft.com/en-gb/windows/desktop/api/shellapi/nf-shellapi-commandlinetoargvw
+ ///
+ public static void EscapeProcessArgument(this StringBuilder builder, string literalValue, bool alwaysQuote = false)
+ {
+ if (string.IsNullOrEmpty(literalValue))
+ {
+ builder.Append("\"\"");
+ return;
+ }
+
+ if (literalValue.IndexOfAny(CharsThatRequireQuoting) == -1) // Happy path
+ {
+ if (!alwaysQuote)
+ {
+ builder.Append(literalValue);
+ return;
+ }
+ if (literalValue[literalValue.Length - 1] != '\\')
+ {
+ builder.Append('"').Append(literalValue).Append('"');
+ return;
+ }
+ }
+
+ builder.Append('"');
+
+ var nextPosition = 0;
+ while (true)
+ {
+ var nextEscapeChar = literalValue.IndexOfAny(CharsThatRequireEscaping, nextPosition);
+ if (nextEscapeChar == -1)
+ break;
+
+ builder.Append(literalValue, nextPosition, nextEscapeChar - nextPosition);
+ nextPosition = nextEscapeChar + 1;
+
+ switch (literalValue[nextEscapeChar])
+ {
+ case '"':
+ builder.Append("\\\"");
+ break;
+ case '\\':
+ var numBackslashes = 1;
+ while (nextPosition < literalValue.Length && literalValue[nextPosition] == '\\')
+ {
+ numBackslashes++;
+ nextPosition++;
+ }
+ if (nextPosition == literalValue.Length || literalValue[nextPosition] == '"')
+ numBackslashes <<= 1;
+
+ for (; numBackslashes != 0; numBackslashes--)
+ builder.Append('\\');
+ break;
+ }
+ }
+
+ builder.Append(literalValue, nextPosition, literalValue.Length - nextPosition);
+ builder.Append('"');
+ }
+ }
+}
diff --git a/src/launcher/nunit-agent-launcher-net80.csproj b/src/launcher/nunit-agent-launcher-net80.csproj
index 1bbf57c..72373ba 100644
--- a/src/launcher/nunit-agent-launcher-net80.csproj
+++ b/src/launcher/nunit-agent-launcher-net80.csproj
@@ -9,10 +9,12 @@
true
true
..\nunit.snk
+ Enable
-
+
+
diff --git a/src/tests/Net80AgentLauncherTests.cs b/src/tests/Net80AgentLauncherTests.cs
index 7951279..8615894 100644
--- a/src/tests/Net80AgentLauncherTests.cs
+++ b/src/tests/Net80AgentLauncherTests.cs
@@ -20,8 +20,8 @@ public class Net80AgentLauncherTests
private static string TESTS_DIR = Path.Combine(TestContext.CurrentContext.TestDirectory, "tests");
// Constants used for settings
- private const string NETFX = FrameworkIdentifiers.NetFramework;
- private const string NETCORE = FrameworkIdentifiers.NetCoreApp;
+ private const string NETFX = ".NETFramework";
+ private const string NETCORE = ".NETCoreApp";
private const string NET20 = $"{NETFX},Version=v2.0";
private const string NET30 = $"{NETFX},Version=v3.0";
private const string NET35 = $"{NETFX},Version=v3.5";