Generate a .NET Framework shim app in dotnet-install-tools on Windows instead of a batch script (#8384)

Implement a simple launcher tool for running new processes on Windows

- This application takes two parameters via the .exe.config configuration file

 - entryPoint: required - the file path to the new process being launched
 - runner: optional - the executable or interpretter used to launch the
entryPoint

- Update dotnet-install-tool to generate an exe instead of a batch script file
This commit is contained in:
Nate McMaster 2018-01-18 14:54:10 -08:00 committed by GitHub
parent 5c35438cfe
commit fa47e95e90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 434 additions and 32 deletions

View file

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2008
VisualStudioVersion = 15.0.27130.2020
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{ED2FE3E2-F7E7-4389-8231-B65123F2076F}"
EndProject
@ -232,6 +232,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.ShellShim.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.ToolPackage.Tests", "test\Microsoft.DotNet.ToolPackage.Tests\Microsoft.DotNet.ToolPackage.Tests.csproj", "{91BFE800-1624-4A58-A1CE-339705A8FFD0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "tool_launcher", "src\tool_launcher\tool_launcher.csproj", "{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -1640,6 +1642,30 @@ Global
{91BFE800-1624-4A58-A1CE-339705A8FFD0}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
{91BFE800-1624-4A58-A1CE-339705A8FFD0}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU
{91BFE800-1624-4A58-A1CE-339705A8FFD0}.RelWithDebInfo|x86.Build.0 = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.Debug|x64.ActiveCfg = Debug|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.Debug|x64.Build.0 = Debug|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.Debug|x86.ActiveCfg = Debug|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.Debug|x86.Build.0 = Debug|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.MinSizeRel|Any CPU.ActiveCfg = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.MinSizeRel|Any CPU.Build.0 = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.MinSizeRel|x64.ActiveCfg = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.MinSizeRel|x64.Build.0 = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.MinSizeRel|x86.ActiveCfg = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.MinSizeRel|x86.Build.0 = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.Release|Any CPU.Build.0 = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.Release|x64.ActiveCfg = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.Release|x64.Build.0 = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.Release|x86.ActiveCfg = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.Release|x86.Build.0 = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5}.RelWithDebInfo|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1713,6 +1739,7 @@ Global
{E7C72EF2-8480-48B4-AAE8-A596F1A6048E} = {17735A9D-BFD9-4585-A7CB-3208CA6EA8A7}
{E84C08C9-70D7-48B0-9507-ADB8B9A2694C} = {17735A9D-BFD9-4585-A7CB-3208CA6EA8A7}
{91BFE800-1624-4A58-A1CE-339705A8FFD0} = {17735A9D-BFD9-4585-A7CB-3208CA6EA8A7}
{EDF19BE6-F20F-4AD0-8E3B-E837030726A5} = {ED2FE3E2-F7E7-4389-8231-B65123F2076F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B526D2CE-EE2D-4AD4-93EF-1867D90FF1F5}

View file

@ -10,6 +10,14 @@ namespace ConsoleApplication
public static void Main(string[] args)
{
Console.WriteLine("Hello World!");
if (args.Length > 0)
{
for (int i = 0; i < args.Length; i++)
{
Console.WriteLine($"{i} = {args[i]}");
}
}
}
}
}

View file

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.DotNet.Cli.Utils
{
@ -22,7 +21,12 @@ namespace Microsoft.DotNet.Cli.Utils
/// <returns></returns>
public static string EscapeAndConcatenateArgArrayForProcessStart(IEnumerable<string> args)
{
return string.Join(" ", EscapeArgArray(args));
var escaped = EscapeArgArray(args);
#if NET35
return string.Join(" ", escaped.ToArray());
#else
return string.Join(" ", escaped);
#endif
}
/// <summary>
@ -36,7 +40,12 @@ namespace Microsoft.DotNet.Cli.Utils
/// <returns></returns>
public static string EscapeAndConcatenateArgArrayForCmdProcessStart(IEnumerable<string> args)
{
return string.Join(" ", EscapeArgArrayForCmd(args));
var escaped = EscapeArgArrayForCmd(args);
#if NET35
return string.Join(" ", escaped.ToArray());
#else
return string.Join(" ", escaped);
#endif
}
/// <summary>

View file

@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using Microsoft.DotNet.PlatformAbstractions;
namespace Microsoft.DotNet
{
class OSVersionUtil
{
public static bool IsWindows8OrNewer()
{
if (RuntimeEnvironment.OperatingSystemPlatform != Platform.Windows)
{
return false;
}
if (!Version.TryParse(RuntimeEnvironment.OperatingSystemVersion, out var winVersion))
{
// All current versions of Windows have a valid System.Version value for OperatingSystemVersion.
// If parsing fails, let's assume Windows is newer than Win 8.
return true;
}
// Windows 7 = "6.1"
// Windows 8 = "6.2"
// Windows 8.1 = "6.3"
if (winVersion.Major > 6)
{
return true;
}
return winVersion.Minor >= 2;
}
}
}

View file

@ -3,8 +3,10 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml.Linq;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Tools;
using Microsoft.Extensions.EnvironmentAbstractions;
@ -13,34 +15,57 @@ namespace Microsoft.DotNet.ShellShim
{
public class ShellShimMaker
{
private const string LauncherExeNet45ResourceName = "Microsoft.DotNet.Tools.Launcher.Executable.Net45";
private const string LauncherExeNet35ResourceName = "Microsoft.DotNet.Tools.Launcher.Executable.Net35";
private const string LauncherConfigNet45ResourceName = "Microsoft.DotNet.Tools.Launcher.Config.Net45";
private const string LauncherConfigNet35ResourceName = "Microsoft.DotNet.Tools.Launcher.Config.Net35";
private readonly string _launcherExeResourceName;
private readonly string _launcherConfigResourceName;
private readonly string _pathToPlaceShim;
public ShellShimMaker(string pathToPlaceShim)
{
_pathToPlaceShim =
pathToPlaceShim ?? throw new ArgumentNullException(nameof(pathToPlaceShim));
if (OSVersionUtil.IsWindows8OrNewer())
{
_launcherExeResourceName = LauncherExeNet45ResourceName;
_launcherConfigResourceName = LauncherConfigNet45ResourceName;
}
else
{
_launcherExeResourceName = LauncherExeNet35ResourceName;
_launcherConfigResourceName = LauncherConfigNet35ResourceName;
}
}
public void CreateShim(string packageExecutablePath, string shellCommandName)
{
var packageExecutable = new FilePath(packageExecutablePath);
FilePath shimPath = GetShimPath(shellCommandName);
var script = new StringBuilder();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
script.AppendLine("@echo off");
script.AppendLine($"dotnet {packageExecutable.ToQuotedString()} %*");
CreateConfigFile(shimPath.Value + ".config", entryPoint: packageExecutablePath, runner: "dotnet");
using (var shim = File.Create(shimPath.Value))
using (var exe = typeof(ShellShimMaker).Assembly.GetManifestResourceStream(_launcherExeResourceName))
{
exe.CopyTo(shim);
}
}
else
{
var packageExecutable = new FilePath(packageExecutablePath);
var script = new StringBuilder();
script.AppendLine("#!/bin/sh");
script.AppendLine($"dotnet {packageExecutable.ToQuotedString()} \"$@\"");
File.WriteAllText(shimPath.Value, script.ToString());
SetUserExecutionPermissionToShimFile(shimPath);
}
FilePath scriptPath = GetScriptPath(shellCommandName);
File.WriteAllText(scriptPath.Value, script.ToString());
SetUserExecutionPermissionToShimFile(scriptPath);
}
public void EnsureCommandNameUniqueness(string shellCommandName)
@ -53,17 +78,31 @@ namespace Microsoft.DotNet.ShellShim
}
}
public void Remove(string shellCommandName)
internal void CreateConfigFile(string outputPath, string entryPoint, string runner)
{
File.Delete(GetScriptPath(shellCommandName).Value);
XDocument config;
using (var resource = typeof(ShellShimMaker).Assembly.GetManifestResourceStream(_launcherConfigResourceName))
{
config = XDocument.Load(resource);
}
var appSettings = config.Descendants("appSettings").First();
appSettings.Add(new XElement("add", new XAttribute("key", "entryPoint"), new XAttribute("value", entryPoint)));
appSettings.Add(new XElement("add", new XAttribute("key", "runner"), new XAttribute("value", runner ?? string.Empty)));
config.Save(outputPath);
}
private FilePath GetScriptPath(string shellCommandName)
public void Remove(string shellCommandName)
{
File.Delete(GetShimPath(shellCommandName).Value);
}
private FilePath GetShimPath(string shellCommandName)
{
var scriptPath = Path.Combine(_pathToPlaceShim, shellCommandName);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
scriptPath += ".cmd";
scriptPath += ".exe";
}
return new FilePath(scriptPath);
@ -71,10 +110,11 @@ namespace Microsoft.DotNet.ShellShim
private static void SetUserExecutionPermissionToShimFile(FilePath scriptPath)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return;
CommandResult result = new CommandFactory()
.Create("chmod", new[] {"u+x", scriptPath.Value})
.Create("chmod", new[] { "u+x", scriptPath.Value })
.CaptureStdOut()
.CaptureStdErr()
.Execute();

View file

@ -76,4 +76,5 @@
<Folder Include="commands\dotnet-migrate\xlf" />
<Folder Include="dotnet-complete\commands\" />
</ItemGroup>
<Import Project="dotnet.win.targets" Condition="'$(OS)' == 'Windows_NT'" />
</Project>

View file

@ -0,0 +1,36 @@
<!-- This file should only be used when building dotnet for windows. -->
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<ItemGroup>
<!-- Only included to ensure this is built first. -->
<ProjectReference Include="..\tool_launcher\tool_launcher.csproj"
ReferenceOutputAssembly="false"
SkipGetTargetFrameworkProperties="true"
SetTargetFramework="TargetFramework=net45"
PrivateAssets="All" />
<ProjectReference Include="..\tool_launcher\tool_launcher.csproj"
ReferenceOutputAssembly="false"
SkipGetTargetFrameworkProperties="true"
SetTargetFramework="TargetFramework=net35"
PrivateAssets="All" />
</ItemGroup>
<Target Name="EmbedDotnetLauncher" BeforeTargets="PrepareForBuild">
<MSBuild Projects="..\tool_launcher\tool_launcher.csproj" Targets="GetTargetPath" Properties="TargetFramework=net45;Configuration=$(Configuration)">
<Output TaskParameter="TargetOutputs" PropertyName="DotnetLauncherNet45FullPath" />
</MSBuild>
<MSBuild Projects="..\tool_launcher\tool_launcher.csproj" Targets="GetTargetPath" Properties="TargetFramework=net35;Configuration=$(Configuration)">
<Output TaskParameter="TargetOutputs" PropertyName="DotnetLauncherNet35FullPath" />
</MSBuild>
<ItemGroup>
<EmbeddedResource Include="$(DotnetLauncherNet45FullPath)" LogicalName="Microsoft.DotNet.Tools.Launcher.Executable.Net45" />
<EmbeddedResource Include="$(DotnetLauncherNet45FullPath).config" LogicalName="Microsoft.DotNet.Tools.Launcher.Config.Net45" />
<EmbeddedResource Include="$(DotnetLauncherNet35FullPath)" LogicalName="Microsoft.DotNet.Tools.Launcher.Executable.Net35" />
<EmbeddedResource Include="$(DotnetLauncherNet35FullPath).config" LogicalName="Microsoft.DotNet.Tools.Launcher.Config.Net35" />
</ItemGroup>
</Target>
</Project>

View file

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Diagnostics;
using Microsoft.DotNet.Cli.Utils;
namespace Microsoft.DotNet.Tools.Launcher
{
/// <summary>
/// The app is simple shim into launching arbitrary command line processes.
/// It is configured via app settings .NET config file. (See app.config).
/// </summary>
/// <remarks>
/// Launching new processes using cmd.exe and .cmd files causes issues for long-running process
/// because CTRL+C always hangs on interrupt with the prompt "Terminate Y/N". This can lead to
/// orphaned processes.
/// </remarks>
class Program
{
private const string TRACE = "DOTNET_LAUNCHER_TRACE";
private const int ERR_FAILED = -1;
private static bool _trace;
public static int Main(string[] argsToForward)
{
bool.TryParse(Environment.GetEnvironmentVariable(TRACE), out _trace);
try
{
var appSettings = ConfigurationManager.AppSettings;
var entryPoint = appSettings["entryPoint"];
if (string.IsNullOrEmpty(entryPoint))
{
LogError("The launcher must specify a non-empty appSetting value for 'entryPoint'.");
return ERR_FAILED;
}
var exePath = entryPoint;
var runner = appSettings["runner"];
var args = new List<string>();
if (!string.IsNullOrEmpty(runner))
{
args.Add(entryPoint);
exePath = runner;
}
args.AddRange(argsToForward);
var argString = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args);
using (var process = new Process
{
StartInfo =
{
FileName = exePath,
Arguments = argString,
CreateNoWindow = false,
UseShellExecute = false,
}
})
{
LogTrace("Starting a new process.");
LogTrace("filename = " + process.StartInfo.FileName);
LogTrace("args = " + process.StartInfo.Arguments);
LogTrace("cwd = " + process.StartInfo.WorkingDirectory);
try
{
process.Start();
}
catch (Win32Exception ex)
{
LogTrace(ex.ToString());
LogError($"Failed to start '{process.StartInfo.FileName}'. " + ex.Message);
return ERR_FAILED;
}
process.WaitForExit();
LogTrace("Exited code " + process.ExitCode);
return process.ExitCode;
}
}
catch (Exception ex)
{
LogError("Unexpected error launching a new process. Run with the environment variable " + TRACE + "='true' for details.");
LogTrace(ex.ToString());
return ERR_FAILED;
}
}
private static void LogError(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.BackgroundColor = ConsoleColor.Black;
Console.Error.WriteLine("ERROR: " + message);
Console.ResetColor();
}
private static void LogTrace(string message)
{
if (!_trace)
return;
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.BackgroundColor = ConsoleColor.Black;
Console.WriteLine("[dotnet-launcher] " + message);
Console.ResetColor();
}
}
}

View file

@ -0,0 +1,18 @@
<!--
Generated by the .NET Core Command Line.
-->
<configuration>
<startup>
<supportedRuntime version="v2.0.50727" />
</startup>
<appSettings>
<!--
To use this launcher, this value must be set. It is a file path or name of the new process being launched.
<add key="entryPoint" value="%entrypoint%" />
This value may also be set. It is an path to another executable used to launch the entry point.
It is treated as single argument.
<add key="runner" value="%runner%" />
-->
</appSettings>
</configuration>

View file

@ -0,0 +1,18 @@
<!--
Generated by the .NET Core Command Line.
-->
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<appSettings>
<!--
To use this launcher, this value must be set. It is a file path or name of the new process being launched.
<add key="entryPoint" value="%entrypoint%" />
This value may also be set. It is an path to another executable used to launch the entry point.
It is treated as single argument.
<add key="runner" value="%runner%" />
-->
</appSettings>
</configuration>

View file

@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Targets .NET Framework 3.5 because .NET Framework 4 is not bundled in Windows 7. -->
<TargetFrameworks>net45;net35</TargetFrameworks>
<PlatformTarget>AnyCPU</PlatformTarget>
<OutputType>exe</OutputType>
<IsPackable>false</IsPackable>
<AssemblyName>Microsoft.DotNet.Tools.Launcher</AssemblyName>
<Title>dotnet-tool-launcher</Title>
<Description>
A simple Windows-only shim for launching new processes.
</Description>
<AppConfig>app.$(TargetFramework).config</AppConfig>
<!-- In other words, a workaround for issues with cmd.exe. -->
</PropertyGroup>
<!-- This project must not have any package references. It must be a standalone .exe file -->
<ItemGroup Condition="'$(TargetFramework)' == 'net45'">
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Linq" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net35'">
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
</ItemGroup>
<!-- Workaround https://github.com/Microsoft/msbuild/issues/1333 -->
<PropertyGroup>
<FrameworkPathOverride Condition="'$(TargetFramework)' == 'net35'">$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v3.5\Profile\Client</FrameworkPathOverride>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Microsoft.DotNet.Cli.Utils\ArgumentEscaper.cs" />
</ItemGroup>
</Project>

View file

@ -6,21 +6,50 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using FluentAssertions;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.TestFramework;
using Microsoft.DotNet.Tools.Test.Utilities;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.DotNet.ShellShim.Tests
{
public class ShellShimMakerTests : TestBase
{
private readonly string _pathToPlaceShim;
private readonly ITestOutputHelper _output;
public ShellShimMakerTests()
public ShellShimMakerTests(ITestOutputHelper output)
{
_pathToPlaceShim = Path.GetTempPath();
_output = output;
}
[Theory]
[InlineData("my_native_app.exe", null)]
[InlineData("./my_native_app.js", "nodejs")]
[InlineData(@"C:\tools\my_native_app.dll", "dotnet")]
public void GivenAnRunnerOrEntryPointItCanCreateConfig(string entryPoint, string runner)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return;
var shellShimMaker = new ShellShimMaker(TempRoot.Root);
var tmpFile = Path.Combine(TempRoot.Root, Path.GetRandomFileName());
shellShimMaker.CreateConfigFile(tmpFile, entryPoint, runner);
new FileInfo(tmpFile).Should().Exist();
var generated = XDocument.Load(tmpFile);
generated.Descendants("appSettings")
.Descendants("add")
.Should()
.Contain(e => e.Attribute("key").Value == "runner" && e.Attribute("value").Value == (runner ?? string.Empty))
.And
.Contain(e => e.Attribute("key").Value == "entryPoint" && e.Attribute("value").Value == entryPoint);
}
[Fact]
@ -28,7 +57,7 @@ namespace Microsoft.DotNet.ShellShim.Tests
{
var outputDll = MakeHelloWorldExecutableDll();
var shellShimMaker = new ShellShimMaker(_pathToPlaceShim);
var shellShimMaker = new ShellShimMaker(TempRoot.Root);
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
shellShimMaker.CreateShim(
@ -39,14 +68,37 @@ namespace Microsoft.DotNet.ShellShim.Tests
stdOut.Should().Contain("Hello World");
}
[Theory]
[InlineData("arg1 arg2", new[] { "arg1", "arg2" })]
[InlineData(" \"arg1 with space\" arg2", new[] { "arg1 with space", "arg2" })]
[InlineData(" \"arg with ' quote\" ", new[] { "arg with ' quote" })]
public void GivenAShimItPassesThroughArguments(string arguments, string[] expectedPassThru)
{
var outputDll = MakeHelloWorldExecutableDll();
var shellShimMaker = new ShellShimMaker(TempRoot.Root);
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
shellShimMaker.CreateShim(
outputDll.FullName,
shellCommandName);
var stdOut = ExecuteInShell(shellCommandName, arguments);
for (int i = 0; i < expectedPassThru.Length; i++)
{
stdOut.Should().Contain($"{i} = {expectedPassThru[i]}");
}
}
[Fact]
public void GivenAnExecutablePathWithExistingSameNameShimItThrows()
{
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
MakeNameConflictingCommand(_pathToPlaceShim, shellCommandName);
MakeNameConflictingCommand(TempRoot.Root, shellCommandName);
var shellShimMaker = new ShellShimMaker(_pathToPlaceShim);
var shellShimMaker = new ShellShimMaker(TempRoot.Root);
Action a = () => shellShimMaker.EnsureCommandNameUniqueness(shellCommandName);
a.ShouldThrow<GracefulException>()
@ -61,7 +113,7 @@ namespace Microsoft.DotNet.ShellShim.Tests
{
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
var shellShimMaker = new ShellShimMaker(_pathToPlaceShim);
var shellShimMaker = new ShellShimMaker(TempRoot.Root);
Action a = () => shellShimMaker.EnsureCommandNameUniqueness(shellCommandName);
a.ShouldNotThrow();
@ -72,17 +124,18 @@ namespace Microsoft.DotNet.ShellShim.Tests
File.WriteAllText(Path.Combine(pathToPlaceShim, shellCommandName), string.Empty);
}
private string ExecuteInShell(string shellCommandName)
private string ExecuteInShell(string shellCommandName, string arguments = "")
{
ProcessStartInfo processStartInfo;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var file = Path.Combine(TempRoot.Root, shellCommandName + ".exe");
processStartInfo = new ProcessStartInfo
{
FileName = "CMD.exe",
Arguments = $"/C {shellCommandName}",
UseShellExecute = false
FileName = file,
UseShellExecute = false,
Arguments = arguments,
};
}
else
@ -90,11 +143,13 @@ namespace Microsoft.DotNet.ShellShim.Tests
processStartInfo = new ProcessStartInfo
{
FileName = "sh",
Arguments = shellCommandName,
Arguments = shellCommandName + " " + arguments,
UseShellExecute = false
};
}
processStartInfo.WorkingDirectory = _pathToPlaceShim;
_output.WriteLine($"Launching '{processStartInfo.FileName} {processStartInfo.Arguments}'");
processStartInfo.WorkingDirectory = TempRoot.Root;
processStartInfo.EnvironmentVariables["PATH"] = Path.GetDirectoryName(new Muxer().MuxerPath);
processStartInfo.ExecuteAndCaptureOutput(out var stdOut, out var stdErr);