diff --git a/TestAssets/TestProjects/TestAppThatWaits/Program.cs b/TestAssets/TestProjects/TestAppThatWaits/Program.cs index ce6edb7af6..d489210522 100644 --- a/TestAssets/TestProjects/TestAppThatWaits/Program.cs +++ b/TestAssets/TestProjects/TestAppThatWaits/Program.cs @@ -1,5 +1,7 @@ using System; using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; using System.Threading; namespace TestAppThatWaits @@ -9,16 +11,25 @@ class Program static void Main(string[] args) { Console.CancelKeyPress += HandleCancelKeyPress; + AppDomain.CurrentDomain.ProcessExit += HandleProcessExit; + Console.WriteLine(Process.GetCurrentProcess().Id); Console.Out.Flush(); - Console.Read(); - Thread.Sleep(10000); + + Thread.Sleep(Timeout.Infinite); } static void HandleCancelKeyPress(object sender, ConsoleCancelEventArgs e) { Console.WriteLine("Interrupted!"); + AppDomain.CurrentDomain.ProcessExit -= HandleProcessExit; Environment.Exit(42); } + + static void HandleProcessExit(object sender, EventArgs args) + { + Console.WriteLine("Terminating!"); + Environment.ExitCode = 43; + } } } diff --git a/src/Microsoft.DotNet.Cli.Utils/Command.cs b/src/Microsoft.DotNet.Cli.Utils/Command.cs index 160270e95b..437ddb27b7 100644 --- a/src/Microsoft.DotNet.Cli.Utils/Command.cs +++ b/src/Microsoft.DotNet.Cli.Utils/Command.cs @@ -17,7 +17,7 @@ public class Command : ICommand private readonly Process _process; private StreamForwarder _stdOut; - + private StreamForwarder _stdErr; private bool _running = false; @@ -40,8 +40,6 @@ public CommandResult Execute() _process.EnableRaisingEvents = true; - Console.CancelKeyPress += HandleCancelKeyPress; - #if DEBUG var sw = Stopwatch.StartNew(); @@ -51,20 +49,21 @@ public CommandResult Execute() { _process.Start(); - Reporter.Verbose.WriteLine(string.Format( - LocalizableStrings.ProcessId, - _process.Id)); + using (new ProcessReaper(_process)) + { + Reporter.Verbose.WriteLine(string.Format( + LocalizableStrings.ProcessId, + _process.Id)); - var taskOut = _stdOut?.BeginRead(_process.StandardOutput); - var taskErr = _stdErr?.BeginRead(_process.StandardError); - _process.WaitForExit(); + var taskOut = _stdOut?.BeginRead(_process.StandardOutput); + var taskErr = _stdErr?.BeginRead(_process.StandardError); + _process.WaitForExit(); - taskOut?.Wait(); - taskErr?.Wait(); + taskOut?.Wait(); + taskErr?.Wait(); + } } - Console.CancelKeyPress -= HandleCancelKeyPress; - var exitCode = _process.ExitCode; #if DEBUG @@ -215,11 +214,5 @@ private void ThrowIfRunning([CallerMemberName] string memberName = null) memberName)); } } - - private void HandleCancelKeyPress(object sender, ConsoleCancelEventArgs e) - { - // Ignore SIGINT/SIGQUIT so that the child can process the signal - e.Cancel = true; - } } } diff --git a/src/Microsoft.DotNet.Cli.Utils/NativeMethods.cs b/src/Microsoft.DotNet.Cli.Utils/NativeMethods.cs new file mode 100644 index 0000000000..f14b512875 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/NativeMethods.cs @@ -0,0 +1,80 @@ +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.DotNet.Cli.Utils +{ + internal static class NativeMethods + { + internal static class Windows + { + internal enum JobObjectInfoClass : uint + { + JobObjectExtendedLimitInformation = 9, + } + + [Flags] + internal enum JobObjectLimitFlags : uint + { + JobObjectLimitKillOnJobClose = 0x2000, + } + + [StructLayout(LayoutKind.Sequential)] + internal struct JobObjectBasicLimitInformation + { + public Int64 PerProcessUserTimeLimit; + public Int64 PerJobUserTimeLimit; + public JobObjectLimitFlags LimitFlags; + public UIntPtr MinimumWorkingSetSize; + public UIntPtr MaximumWorkingSetSize; + public UInt32 ActiveProcessLimit; + public UIntPtr Affinity; + public UInt32 PriorityClass; + public UInt32 SchedulingClass; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct IoCounters + { + public UInt64 ReadOperationCount; + public UInt64 WriteOperationCount; + public UInt64 OtherOperationCount; + public UInt64 ReadTransferCount; + public UInt64 WriteTransferCount; + public UInt64 OtherTransferCount; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct JobObjectExtendedLimitInformation + { + public JobObjectBasicLimitInformation BasicLimitInformation; + public IoCounters IoInfo; + public UIntPtr ProcessMemoryLimit; + public UIntPtr JobMemoryLimit; + public UIntPtr PeakProcessMemoryUsed; + public UIntPtr PeakJobMemoryUsed; + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern SafeWaitHandle CreateJobObjectW(IntPtr lpJobAttributes, string lpName); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoClass jobObjectInformationClass, IntPtr lpJobObjectInformation, UInt32 cbJobObjectInformationLength); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess); + } + + internal static class Posix + { + [DllImport("libc", SetLastError = true)] + internal static extern int getpgid(int pid); + + [DllImport("libc", SetLastError = true)] + internal static extern int kill(int pid, int sig); + + internal const int SIGINT = 2; + internal const int SIGTERM = 15; + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/ProcessReaper.cs b/src/Microsoft.DotNet.Cli.Utils/ProcessReaper.cs new file mode 100644 index 0000000000..90e1e4bfc3 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/ProcessReaper.cs @@ -0,0 +1,163 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.DotNet.PlatformAbstractions; +using Microsoft.Win32.SafeHandles; + +using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment; + +namespace Microsoft.DotNet.Cli.Utils +{ + /// + /// Responsible for reaping a target process if the current process terminates. + /// + /// + /// On Windows, a job object will be used to ensure the termination of the target + /// process (and its tree) even if the current process is rudely terminated. + /// + /// On POSIX systems, the reaper will handle SIGTERM and attempt to forward the + /// signal to the target process only. + /// + /// The reaper also suppresses SIGINT in the current process to allow the target + /// process to handle the signal. + /// + internal class ProcessReaper : IDisposable + { + /// + /// Creates a new process reaper. + /// + /// The target process to reap if the current process terminates. The process must already be started. + public ProcessReaper(Process process) + { + _process = process; + + if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows) + { + _job = AssignProcessToJobObject(_process.Handle); + } + else + { + AppDomain.CurrentDomain.ProcessExit += HandleProcessExit; + } + + Console.CancelKeyPress += HandleCancelKeyPress; + } + + public void Dispose() + { + if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows) + { + if (_job != null) + { + // Clear the kill on close flag because the child process terminated successfully + // If this fails, then we have no choice but to terminate any remaining processes in the job + SetKillOnJobClose(_job.DangerousGetHandle(), false); + + _job.Dispose(); + _job = null; + } + } + else + { + AppDomain.CurrentDomain.ProcessExit -= HandleProcessExit; + } + + Console.CancelKeyPress -= HandleCancelKeyPress; + } + + private static void HandleCancelKeyPress(object sender, ConsoleCancelEventArgs e) + { + // Ignore SIGINT/SIGQUIT so that the process can handle the signal + e.Cancel = true; + } + + private static SafeWaitHandle AssignProcessToJobObject(IntPtr process) + { + var job = NativeMethods.Windows.CreateJobObjectW(IntPtr.Zero, null); + if (job == null || job.IsInvalid) + { + return null; + } + + if (!SetKillOnJobClose(job.DangerousGetHandle(), true)) + { + job.Dispose(); + return null; + } + + if (!NativeMethods.Windows.AssignProcessToJobObject(job.DangerousGetHandle(), process)) + { + job.Dispose(); + return null; + } + + return job; + } + + private void HandleProcessExit(object sender, EventArgs args) + { + var currentPid = Process.GetCurrentProcess().Id; + bool sendChildSIGTERM = true; + + // First, try to SIGTERM our process group + // If the pgid is not the same as pid, then this process is not the root of the group + if (NativeMethods.Posix.getpgid(currentPid) == currentPid) + { + if (NativeMethods.Posix.kill(-currentPid, NativeMethods.Posix.SIGTERM) == 0) + { + // Successfully sent the signal to the entire group; don't send again to child + sendChildSIGTERM = false; + } + } + + if (sendChildSIGTERM && NativeMethods.Posix.kill(_process.Id, NativeMethods.Posix.SIGTERM) != 0) + { + // Couldn't send the signal, don't wait + return; + } + + // If SIGTERM was ignored by the target, then we'll still wait + _process.WaitForExit(); + + Environment.ExitCode = _process.ExitCode; + } + + private static bool SetKillOnJobClose(IntPtr job, bool value) + { + var information = new NativeMethods.Windows.JobObjectExtendedLimitInformation + { + BasicLimitInformation = new NativeMethods.Windows.JobObjectBasicLimitInformation + { + LimitFlags = (value ? NativeMethods.Windows.JobObjectLimitFlags.JobObjectLimitKillOnJobClose : 0) + } + }; + + var length = Marshal.SizeOf(typeof(NativeMethods.Windows.JobObjectExtendedLimitInformation)); + var informationPtr = Marshal.AllocHGlobal(length); + + try + { + Marshal.StructureToPtr(information, informationPtr, false); + + if (!NativeMethods.Windows.SetInformationJobObject( + job, + NativeMethods.Windows.JobObjectInfoClass.JobObjectExtendedLimitInformation, + informationPtr, + (uint)length)) + { + return false; + } + + return true; + } + finally + { + Marshal.FreeHGlobal(informationPtr); + } + } + + private Process _process; + private SafeWaitHandle _job; + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/ProcessStartInfoExtensions.cs b/src/Microsoft.DotNet.Cli.Utils/ProcessStartInfoExtensions.cs index 0d11313b91..30db24126f 100644 --- a/src/Microsoft.DotNet.Cli.Utils/ProcessStartInfoExtensions.cs +++ b/src/Microsoft.DotNet.Cli.Utils/ProcessStartInfoExtensions.cs @@ -21,7 +21,11 @@ public static int Execute(this ProcessStartInfo startInfo) }; process.Start(); - process.WaitForExit(); + + using (new ProcessReaper(process)) + { + process.WaitForExit(); + } return process.ExitCode; } @@ -43,16 +47,19 @@ public static int ExecuteAndCaptureOutput(this ProcessStartInfo startInfo, out s process.Start(); - var taskOut = outStream.BeginRead(process.StandardOutput); - var taskErr = errStream.BeginRead(process.StandardError); + using (new ProcessReaper(process)) + { + var taskOut = outStream.BeginRead(process.StandardOutput); + var taskErr = errStream.BeginRead(process.StandardError); - process.WaitForExit(); + process.WaitForExit(); - taskOut.Wait(); - taskErr.Wait(); + taskOut.Wait(); + taskErr.Wait(); - stdOut = outStream.CapturedOutput; - stdErr = errStream.CapturedOutput; + stdOut = outStream.CapturedOutput; + stdErr = errStream.CapturedOutput; + } return process.ExitCode; } diff --git a/src/Microsoft.DotNet.Cli.Utils/Properties/AssemblyInfo.cs b/src/Microsoft.DotNet.Cli.Utils/Properties/AssemblyInfo.cs index b2f285d434..e64b2d0cfa 100644 --- a/src/Microsoft.DotNet.Cli.Utils/Properties/AssemblyInfo.cs +++ b/src/Microsoft.DotNet.Cli.Utils/Properties/AssemblyInfo.cs @@ -7,6 +7,7 @@ [assembly: AssemblyMetadataAttribute("Serviceable", "True")] [assembly: InternalsVisibleTo("dotnet, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("dotnet.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("dotnet-run.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.DotNet.Configurer, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.DotNet.Tools.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.DotNet.Cli.Utils.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/dotnet/CommandFactory/DepsJsonCommandFactory.cs b/src/dotnet/CommandFactory/DepsJsonCommandFactory.cs deleted file mode 100644 index 6f9bd24c3c..0000000000 --- a/src/dotnet/CommandFactory/DepsJsonCommandFactory.cs +++ /dev/null @@ -1,48 +0,0 @@ -// 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.Collections.Generic; -using Microsoft.DotNet.Cli.Utils; -using NuGet.Frameworks; - -namespace Microsoft.DotNet.CommandFactory -{ - public class DepsJsonCommandFactory : ICommandFactory - { - private DepsJsonCommandResolver _depsJsonCommandResolver; - private string _temporaryDirectory; - private string _depsJsonFile; - private string _runtimeConfigFile; - - public DepsJsonCommandFactory( - string depsJsonFile, - string runtimeConfigFile, - string nugetPackagesRoot, - string temporaryDirectory) - { - _depsJsonCommandResolver = new DepsJsonCommandResolver(nugetPackagesRoot); - - _temporaryDirectory = temporaryDirectory; - _depsJsonFile = depsJsonFile; - _runtimeConfigFile = runtimeConfigFile; - } - - public ICommand Create( - string commandName, - IEnumerable args, - NuGetFramework framework = null, - string configuration = Constants.DefaultConfiguration) - { - var commandResolverArgs = new CommandResolverArguments() - { - CommandName = commandName, - CommandArguments = args, - DepsJsonFile = _depsJsonFile - }; - - var commandSpec = _depsJsonCommandResolver.Resolve(commandResolverArgs); - - return CommandFactoryUsingResolver.Create(commandSpec); - } - } -} diff --git a/src/dotnet/CommandFactory/ProjectDependenciesCommandFactory.cs b/src/dotnet/CommandFactory/ProjectDependenciesCommandFactory.cs deleted file mode 100644 index 7ab99b6ff8..0000000000 --- a/src/dotnet/CommandFactory/ProjectDependenciesCommandFactory.cs +++ /dev/null @@ -1,122 +0,0 @@ -// 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.Collections.Generic; -using Microsoft.DotNet.Cli.Utils; -using Microsoft.DotNet.PlatformAbstractions; -using NuGet.Frameworks; - -namespace Microsoft.DotNet.CommandFactory -{ - public class ProjectDependenciesCommandFactory : ICommandFactory - { - private readonly NuGetFramework _nugetFramework; - private readonly string _configuration; - private readonly string _outputPath; - private readonly string _buildBasePath; - private readonly string _projectDirectory; - - public ProjectDependenciesCommandFactory( - NuGetFramework nugetFramework, - string configuration, - string outputPath, - string buildBasePath, - string projectDirectory) - { - _nugetFramework = nugetFramework; - _configuration = configuration; - _outputPath = outputPath; - _buildBasePath = buildBasePath; - _projectDirectory = projectDirectory; - - if (_configuration == null) - { - _configuration = Constants.DefaultConfiguration; - } - } - - public ICommand Create( - string commandName, - IEnumerable args, - NuGetFramework framework = null, - string configuration = null) - { - if (string.IsNullOrEmpty(configuration)) - { - configuration = _configuration; - } - - if (framework == null) - { - framework = _nugetFramework; - } - - var commandSpec = FindProjectDependencyCommands( - commandName, - args, - configuration, - framework, - _outputPath, - _buildBasePath, - _projectDirectory); - - return CommandFactoryUsingResolver.Create(commandSpec); - } - - private CommandSpec FindProjectDependencyCommands( - string commandName, - IEnumerable commandArgs, - string configuration, - NuGetFramework framework, - string outputPath, - string buildBasePath, - string projectDirectory) - { - var commandResolverArguments = new CommandResolverArguments - { - CommandName = commandName, - CommandArguments = commandArgs, - Framework = framework, - Configuration = configuration, - OutputPath = outputPath, - BuildBasePath = buildBasePath, - ProjectDirectory = projectDirectory - }; - - var commandResolver = GetProjectDependenciesCommandResolver(framework); - - var commandSpec = commandResolver.Resolve(commandResolverArguments); - if (commandSpec == null) - { - throw new CommandUnknownException(commandName); - } - - return commandSpec; - } - - private ICommandResolver GetProjectDependenciesCommandResolver(NuGetFramework framework) - { - var environment = new EnvironmentProvider(); - - if (framework.IsDesktop()) - { - IPlatformCommandSpecFactory platformCommandSpecFactory = null; - if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows) - { - platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); - } - else - { - platformCommandSpecFactory = new GenericPlatformCommandSpecFactory(); - } - - return new OutputPathCommandResolver(environment, platformCommandSpecFactory); - } - else - { - var packagedCommandSpecFactory = new PackagedCommandSpecFactory(); - return new ProjectDependenciesCommandResolver(environment, packagedCommandSpecFactory); - } - } - } -} diff --git a/src/dotnet/CommandFactory/PublishedPathCommandFactory.cs b/src/dotnet/CommandFactory/PublishedPathCommandFactory.cs deleted file mode 100644 index efe5c9d4e7..0000000000 --- a/src/dotnet/CommandFactory/PublishedPathCommandFactory.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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.Collections.Generic; -using Microsoft.DotNet.Cli.Utils; -using NuGet.Frameworks; - -namespace Microsoft.DotNet.CommandFactory -{ - public class PublishedPathCommandFactory : ICommandFactory - { - private readonly string _publishDirectory; - private readonly string _applicationName; - - public PublishedPathCommandFactory(string publishDirectory, string applicationName) - { - _publishDirectory = publishDirectory; - _applicationName = applicationName; - } - - public ICommand Create( - string commandName, - IEnumerable args, - NuGetFramework framework = null, - string configuration = Constants.DefaultConfiguration) - { - return CommandFactoryUsingResolver.Create(commandName, args, framework, configuration, _publishDirectory, _applicationName); - } - } -} diff --git a/test/Microsoft.DotNet.CommandFactory.Tests/GivenAProjectDependenciesCommandFactory.cs b/test/Microsoft.DotNet.CommandFactory.Tests/GivenAProjectDependenciesCommandFactory.cs deleted file mode 100644 index d579a245d0..0000000000 --- a/test/Microsoft.DotNet.CommandFactory.Tests/GivenAProjectDependenciesCommandFactory.cs +++ /dev/null @@ -1,207 +0,0 @@ -// 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 System.IO; -using FluentAssertions; -using Microsoft.DotNet.TestFramework; -using Microsoft.DotNet.CommandFactory; -using Microsoft.DotNet.Tools.Test.Utilities; -using NuGet.Frameworks; -using Xunit; -using Microsoft.DotNet.Tools.Tests.Utilities; -using Microsoft.DotNet.Cli.Utils; - -namespace Microsoft.DotNet.Tests -{ - public class GivenAProjectDependenciesCommandFactory : TestBase - { - private static readonly NuGetFramework s_desktopTestFramework = FrameworkConstants.CommonFrameworks.Net451; - - private RepoDirectoriesProvider _repoDirectoriesProvider; - - public GivenAProjectDependenciesCommandFactory() - { - _repoDirectoriesProvider = new RepoDirectoriesProvider(); - Environment.SetEnvironmentVariable( - Constants.MSBUILD_EXE_PATH, - Path.Combine(_repoDirectoriesProvider.Stage2Sdk, "MSBuild.dll")); - } - - [WindowsOnlyFact] - public void It_resolves_desktop_apps_defaulting_to_Debug_Configuration() - { - var configuration = "Debug"; - - var testInstance = TestAssets.Get(TestAssetKinds.DesktopTestProjects, "AppWithProjTool2Fx") - .CreateInstance() - .WithSourceFiles() - .WithNuGetConfig(_repoDirectoriesProvider.TestPackages); - - var restoreCommand = new RestoreCommand() - .WithWorkingDirectory(testInstance.Root) - .ExecuteWithCapturedOutput() - .Should().Pass(); - - var buildCommand = new BuildCommand() - .WithWorkingDirectory(testInstance.Root) - .WithConfiguration(configuration) - .WithCapturedOutput() - .Execute() - .Should().Pass(); - - var factory = new ProjectDependenciesCommandFactory( - s_desktopTestFramework, - null, - null, - null, - testInstance.Root.FullName); - - var command = factory.Create("dotnet-desktop-and-portable", null); - - command.CommandName.Should().Contain(testInstance.Root.GetDirectory("bin", configuration).FullName); - - Path.GetFileName(command.CommandName).Should().Be("dotnet-desktop-and-portable.exe"); - } - - [WindowsOnlyFact] - public void It_resolves_desktop_apps_when_configuration_is_Debug() - { - var configuration = "Debug"; - - var testInstance = TestAssets.Get(TestAssetKinds.DesktopTestProjects, "AppWithProjTool2Fx") - .CreateInstance() - .WithSourceFiles() - .WithNuGetConfig(_repoDirectoriesProvider.TestPackages); - - var restoreCommand = new RestoreCommand() - .WithWorkingDirectory(testInstance.Root) - .ExecuteWithCapturedOutput() - .Should().Pass(); - - var buildCommand = new BuildCommand() - .WithWorkingDirectory(testInstance.Root) - .WithConfiguration(configuration) - .Execute() - .Should().Pass(); - - var factory = new ProjectDependenciesCommandFactory( - s_desktopTestFramework, - configuration, - null, - null, - testInstance.Root.FullName); - - var command = factory.Create("dotnet-desktop-and-portable", null); - - command.CommandName.Should().Contain(testInstance.Root.GetDirectory("bin", configuration).FullName); - Path.GetFileName(command.CommandName).Should().Be("dotnet-desktop-and-portable.exe"); - } - - [WindowsOnlyFact] - public void It_resolves_desktop_apps_when_configuration_is_Release() - { - var configuration = "Debug"; - - var testInstance = TestAssets.Get(TestAssetKinds.DesktopTestProjects, "AppWithProjTool2Fx") - .CreateInstance() - .WithSourceFiles() - .WithNuGetConfig(_repoDirectoriesProvider.TestPackages); - - var restoreCommand = new RestoreCommand() - .WithWorkingDirectory(testInstance.Root) - .ExecuteWithCapturedOutput() - .Should().Pass(); - - var buildCommand = new BuildCommand() - .WithWorkingDirectory(testInstance.Root) - .WithConfiguration(configuration) - .WithCapturedOutput() - .Execute() - .Should().Pass(); - - var factory = new ProjectDependenciesCommandFactory( - s_desktopTestFramework, - configuration, - null, - null, - testInstance.Root.FullName); - - var command = factory.Create("dotnet-desktop-and-portable", null); - - command.CommandName.Should().Contain(testInstance.Root.GetDirectory("bin", configuration).FullName); - - Path.GetFileName(command.CommandName).Should().Be("dotnet-desktop-and-portable.exe"); - } - - [WindowsOnlyFact] - public void It_resolves_desktop_apps_using_configuration_passed_to_create() - { - var configuration = "Debug"; - - var testInstance = TestAssets.Get(TestAssetKinds.DesktopTestProjects, "AppWithProjTool2Fx") - .CreateInstance() - .WithSourceFiles() - .WithNuGetConfig(_repoDirectoriesProvider.TestPackages); - - var restoreCommand = new RestoreCommand() - .WithWorkingDirectory(testInstance.Root) - .ExecuteWithCapturedOutput() - .Should().Pass(); - - var buildCommand = new BuildCommand() - .WithWorkingDirectory(testInstance.Root) - .WithConfiguration(configuration) - .WithCapturedOutput() - .Execute() - .Should().Pass(); - - var factory = new ProjectDependenciesCommandFactory( - s_desktopTestFramework, - "Debug", - null, - null, - testInstance.Root.FullName); - - var command = factory.Create("dotnet-desktop-and-portable", null, configuration: configuration); - - command.CommandName.Should().Contain(testInstance.Root.GetDirectory("bin", configuration).FullName); - - Path.GetFileName(command.CommandName).Should().Be("dotnet-desktop-and-portable.exe"); - } - - [Fact] - public void It_resolves_tools_whose_package_name_is_different_than_dll_name() - { - Environment.SetEnvironmentVariable( - Constants.MSBUILD_EXE_PATH, - Path.Combine(new RepoDirectoriesProvider().Stage2Sdk, "MSBuild.dll")); - - var configuration = "Debug"; - - var testInstance = TestAssets.Get("AppWithDirectDepWithOutputName") - .CreateInstance() - .WithSourceFiles() - .WithRestoreFiles(); - - var buildCommand = new BuildCommand() - .WithProjectDirectory(testInstance.Root) - .WithConfiguration(configuration) - .WithCapturedOutput() - .Execute() - .Should().Pass(); - - var factory = new ProjectDependenciesCommandFactory( - NuGetFrameworks.NetCoreApp30, - configuration, - null, - null, - testInstance.Root.FullName); - - var command = factory.Create("dotnet-tool-with-output-name", null); - - command.CommandArgs.Should().Contain( - Path.Combine("toolwithoutputname", "1.0.0", "lib", "netcoreapp3.0", "dotnet-tool-with-output-name.dll")); - } - } -} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/TestCommand.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/TestCommand.cs index 7a47f48d5d..9e997a43a9 100644 --- a/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/TestCommand.cs +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/TestCommand.cs @@ -23,6 +23,8 @@ public class TestCommand public Process CurrentProcess { get; private set; } + public int TimeoutMiliseconds { get; set; } = Timeout.Infinite; + public Dictionary Environment { get; } = new Dictionary(); public event DataReceivedEventHandler ErrorDataReceived; @@ -115,7 +117,10 @@ private async Task ExecuteAsyncInternal(string executable, string await completionTask; - CurrentProcess.WaitForExit(); + if (!CurrentProcess.WaitForExit(TimeoutMiliseconds)) + { + throw new TimeoutException($"The process failed to exit after {TimeoutMiliseconds / 1000.0} seconds."); + } RemoveNullTerminator(stdOut); diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/DotnetUnderTest.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/DotnetUnderTest.cs index 705607f207..7175e9aa45 100644 --- a/test/Microsoft.DotNet.Tools.Tests.Utilities/DotnetUnderTest.cs +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/DotnetUnderTest.cs @@ -26,12 +26,7 @@ public static string FullName { _pathToDotnetUnderTest = Path.Combine( new RepoDirectoriesProvider().DotnetRoot, - "dotnet"); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - _pathToDotnetUnderTest = $"{_pathToDotnetUnderTest}.exe"; - } + $"dotnet{Constants.ExeSuffix}"); } return _pathToDotnetUnderTest; diff --git a/test/dotnet-run.Tests/GivenDotnetRunIsInterrupted.cs b/test/dotnet-run.Tests/GivenDotnetRunIsInterrupted.cs index 2c8f8aeda2..6b31f84ff1 100644 --- a/test/dotnet-run.Tests/GivenDotnetRunIsInterrupted.cs +++ b/test/dotnet-run.Tests/GivenDotnetRunIsInterrupted.cs @@ -5,12 +5,16 @@ using System.Diagnostics; using System.Runtime.InteropServices; using FluentAssertions; +using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Tools.Test.Utilities; +using Xunit.Sdk; namespace Microsoft.DotNet.Cli.Run.Tests { public class GivenDotnetRunIsInterrupted : TestBase { + private const int WaitTimeout = 30000; + // This test is Unix only for the same reason that CoreFX does not test Console.CancelKeyPress on Windows // See https://github.com/dotnet/corefx/blob/a10890f4ffe0fadf090c922578ba0e606ebdd16c/src/System.Console/tests/CancelKeyPress.Unix.cs#L63-L67 [UnixOnlyFact] @@ -36,8 +40,8 @@ public void ItIgnoresSIGINT() // will inherit the current process group from the `dotnet test` process that is running this test. // We would need to fork(), setpgid(), and then execve() to break out of the current group and that is // too complex for a simple unit test. - kill(command.CurrentProcess.Id, SIGINT).Should().Be(0); // dotnet run - kill(Convert.ToInt32(e.Data), SIGINT).Should().Be(0); // TestAppThatWaits + NativeMethods.Posix.kill(command.CurrentProcess.Id, NativeMethods.Posix.SIGINT).Should().Be(0); // dotnet run + NativeMethods.Posix.kill(Convert.ToInt32(e.Data), NativeMethods.Posix.SIGINT).Should().Be(0); // TestAppThatWaits killed = true; }; @@ -52,9 +56,88 @@ public void ItIgnoresSIGINT() killed.Should().BeTrue(); } - [DllImport("libc", SetLastError = true)] - private static extern int kill(int pid, int sig); + [UnixOnlyFact] + public void ItPassesSIGTERMToChild() + { + var asset = TestAssets.Get("TestAppThatWaits") + .CreateInstance() + .WithSourceFiles(); + + var command = new RunCommand() + .WithWorkingDirectory(asset.Root.FullName); + + bool killed = false; + Process child = null; + command.OutputDataReceived += (s, e) => + { + if (killed) + { + return; + } + + child = Process.GetProcessById(Convert.ToInt32(e.Data)); + NativeMethods.Posix.kill(command.CurrentProcess.Id, NativeMethods.Posix.SIGTERM).Should().Be(0); + + killed = true; + }; + + command + .ExecuteWithCapturedOutput() + .Should() + .ExitWith(43) + .And + .HaveStdOutContaining("Terminating!"); + + killed.Should().BeTrue(); + + if (!child.WaitForExit(WaitTimeout)) + { + child.Kill(); + throw new XunitException("child process failed to terminate."); + } + } + + [WindowsOnlyFact] + public void ItTerminatesTheChildWhenKilled() + { + var asset = TestAssets.Get("TestAppThatWaits") + .CreateInstance() + .WithSourceFiles(); + + var command = new RunCommand() + .WithWorkingDirectory(asset.Root.FullName); + + bool killed = false; + Process child = null; + command.OutputDataReceived += (s, e) => + { + if (killed) + { + return; + } + + child = Process.GetProcessById(Convert.ToInt32(e.Data)); + command.CurrentProcess.Kill(); + + killed = true; + }; - private const int SIGINT = 2; + // A timeout is required to prevent the `Process.WaitForExit` call to hang if `dotnet run` failed to terminate the child on Windows. + // This is because `Process.WaitForExit()` hangs waiting for the process launched by `dotnet run` to close the redirected I/O pipes (which won't happen). + command.TimeoutMiliseconds = WaitTimeout; + + command + .ExecuteWithCapturedOutput() + .Should() + .ExitWith(-1); + + killed.Should().BeTrue(); + + if (!child.WaitForExit(WaitTimeout)) + { + child.Kill(); + throw new XunitException("child process failed to terminate."); + } + } } }