Add kestrel tests.

Tests will 'build', 'run', 'publish' and 'execute' a Kestrel Hello World server as a PortableFatApp and as a Standalone app.
This commit is contained in:
Sridhar Periyasamy 2016-04-04 17:51:36 -07:00
parent f665c28173
commit 8f00b95783
17 changed files with 766 additions and 25 deletions

View file

@ -0,0 +1,29 @@
{
"version": "1.0.0-*",
"dependencies": {
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-20113",
"Microsoft.AspNet.Hosting": "1.0.0-rc2-16253",
"Microsoft.Extensions.Logging.Console": "1.0.0-rc2-20254"
},
"compilationOptions": {
"emitEntryPoint": true
},
"compile": [
"../src/*.cs"
],
"frameworks": {
"netstandard1.5": {
"dependencies": {
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.0.0-rc2-23931"
}
},
"imports": [
"dnxcore50",
"portable-net45+win8"
]
}
}
}

View file

@ -0,0 +1,26 @@
{
"version": "1.0.0-*",
"dependencies": {
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-20113",
"Microsoft.AspNet.Hosting": "1.0.0-rc2-16253",
"Microsoft.Extensions.Logging.Console": "1.0.0-rc2-20254"
},
"compilationOptions": {
"emitEntryPoint": true
},
"compile": [
"../src/*.cs"
],
"frameworks": {
"netstandardapp1.5": {
"dependencies": {
"Microsoft.NETCore.App": "1.0.0-rc2-23931"
},
"imports": [
"dnxcore50",
"portable-net45+win8"
]
}
}
}

View file

@ -0,0 +1,88 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Filter;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.PlatformAbstractions;
namespace SampleApp
{
public class Startup
{
private static string Args { get; set; }
private static CancellationTokenSource ServerCancellationTokenSource { get; set; }
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory, IApplicationEnvironment env)
{
var ksi = app.ServerFeatures.Get<IKestrelServerInformation>();
ksi.NoDelay = true;
loggerFactory.AddConsole(LogLevel.Error);
app.UseKestrelConnectionLogging();
app.Run(async context =>
{
Console.WriteLine("{0} {1}{2}{3}",
context.Request.Method,
context.Request.PathBase,
context.Request.Path,
context.Request.QueryString);
Console.WriteLine($"Method: {context.Request.Method}");
Console.WriteLine($"PathBase: {context.Request.PathBase}");
Console.WriteLine($"Path: {context.Request.Path}");
Console.WriteLine($"QueryString: {context.Request.QueryString}");
var connectionFeature = context.Connection;
Console.WriteLine($"Peer: {connectionFeature.RemoteIpAddress?.ToString()} {connectionFeature.RemotePort}");
Console.WriteLine($"Sock: {connectionFeature.LocalIpAddress?.ToString()} {connectionFeature.LocalPort}");
var content = $"Hello world!{Environment.NewLine}Received '{Args}' from command line.";
context.Response.ContentLength = content.Length;
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync(content);
});
}
public static int Main(string[] args)
{
if (args.Length == 0)
{
Console.WriteLine("KestrelHelloWorld <url to host>");
return 1;
}
var url = new Uri(args[0]);
Args = string.Join(" ", args);
var host = new WebHostBuilder()
.UseServer("Microsoft.AspNetCore.Server.Kestrel")
.UseUrls(url.ToString())
.UseStartup<Startup>()
.Build();
ServerCancellationTokenSource = new CancellationTokenSource();
// shutdown server after 20s.
var shutdownTask = Task.Run(async () =>
{
await Task.Delay(20000);
ServerCancellationTokenSource.Cancel();
});
host.Run(ServerCancellationTokenSource.Token);
shutdownTask.Wait();
return 0;
}
}
}

View file

@ -34,7 +34,8 @@ namespace Microsoft.DotNet.Cli.Build
"Microsoft.Extensions.DependencyModel.Tests",
"ArgumentForwardingTests",
"dotnet-test.UnitTests",
"dotnet-test.Tests"
"dotnet-test.Tests",
"Kestrel.Tests"
};
public static readonly dynamic[] ConditionalTestAssets = new[]

View file

@ -0,0 +1,48 @@
using System.IO;
using Microsoft.DotNet.Tools.Test.Utilities;
using Xunit;
using Microsoft.DotNet.TestFramework;
namespace Microsoft.DotNet.Kestrel.Tests
{
public class DotnetBuildTest : TestBase
{
public static string KestrelPortableApp { get; } = "KestrelPortable";
[Fact]
public void BuildingKestrelPortableFatAppProducesExpectedArtifacts()
{
var testInstance = TestAssetsManager.CreateTestInstance("KestrelSample")
.WithLockFiles();
BuildAndTest(Path.Combine(testInstance.TestRoot, KestrelPortableApp));
}
private static void BuildAndTest(string testRoot)
{
string appName = Path.GetFileName(testRoot);
var result = new BuildCommand(
projectPath: testRoot)
.ExecuteWithCapturedOutput();
result.Should().Pass();
var outputBase = new DirectoryInfo(Path.Combine(testRoot, "bin", "Debug"));
var netstandardappOutput = outputBase.Sub("netstandard1.5");
netstandardappOutput.Should()
.Exist().And
.OnlyHaveFiles(new[]
{
$"{appName}.deps.json",
$"{appName}.dll",
$"{appName}.pdb",
$"{appName}.runtimeconfig.json",
$"{appName}.runtimeconfig.dev.json"
});
}
}
}

View file

@ -0,0 +1,65 @@
// 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 Microsoft.DotNet.TestFramework;
using Microsoft.DotNet.Tools.Test.Utilities;
using Xunit;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
namespace Microsoft.DotNet.Kestrel.Tests
{
public class DotnetRunTest : TestBase
{
private const string KestrelSampleBase = "KestrelSample";
private const string KestrelPortable = "KestrelPortable";
private const string KestrelStandalone = "KestrelStandalone";
[Fact]
public void ItRunsKestrelPortableApp()
{
TestInstance instance = TestAssetsManager.CreateTestInstance(KestrelSampleBase)
.WithLockFiles();
var url = NetworkHelper.GetLocalhostUrlWithFreePort();
var args = $"{url} {Guid.NewGuid().ToString()}";
var runCommand = new RunCommand(Path.Combine(instance.TestRoot, KestrelPortable));
try
{
runCommand.ExecuteAsync(args);
NetworkHelper.IsServerUp(url).Should().BeTrue($"Unable to connect to kestrel server - {KestrelPortable} @ {url}");
NetworkHelper.TestGetRequest(url, args);
}
finally
{
runCommand.KillTree();
}
}
[Fact]
public void ItRunsKestrelStandaloneApp()
{
TestInstance instance = TestAssetsManager.CreateTestInstance(KestrelSampleBase)
.WithLockFiles();
var url = NetworkHelper.GetLocalhostUrlWithFreePort();
var args = $"{url} {Guid.NewGuid().ToString()}";
var runCommand = new RunCommand(Path.Combine(instance.TestRoot, KestrelStandalone));
try
{
runCommand.ExecuteAsync(args);
NetworkHelper.IsServerUp(url).Should().BeTrue($"Unable to connect to kestrel server - {KestrelStandalone} @ {url}");
NetworkHelper.TestGetRequest(url, args);
}
finally
{
runCommand.KillTree();
}
}
}
}

View file

@ -0,0 +1,142 @@
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.DotNet.ProjectModel;
using Microsoft.DotNet.TestFramework;
using Microsoft.DotNet.Tools.Test.Utilities;
using Xunit;
using System.Threading.Tasks;
using FluentAssertions;
namespace Microsoft.DotNet.Kestrel.Tests
{
public class DotnetTest : TestBase
{
private const string KestrelSampleBase = "KestrelSample";
private const string KestrelPortable = "KestrelPortable";
private const string KestrelStandalone = "KestrelStandalone";
[Fact]
public void ItRunsKestrelPortableAfterBuild()
{
TestInstance instance = TestAssetsManager.CreateTestInstance(KestrelSampleBase)
.WithLockFiles();
var url = NetworkHelper.GetLocalhostUrlWithFreePort();
var args = $"{url} {Guid.NewGuid().ToString()}";
var dotnetCommand = new DotnetCommand();
var output = Build(Path.Combine(instance.TestRoot, KestrelPortable));
try
{
dotnetCommand.ExecuteAsync($"{output} {args}");
NetworkHelper.IsServerUp(url).Should().BeTrue($"Unable to connect to kestrel server - {KestrelPortable} @ {url}");
NetworkHelper.TestGetRequest(url, args);
}
finally
{
dotnetCommand.KillTree();
}
}
[Fact]
public void ItRunsKestrelStandaloneAfterBuild()
{
TestInstance instance = TestAssetsManager.CreateTestInstance(KestrelSampleBase)
.WithLockFiles();
var url = NetworkHelper.GetLocalhostUrlWithFreePort();
var args = $"{url} {Guid.NewGuid().ToString()}";
var dotnetCommand = new DotnetCommand();
var output = Build(Path.Combine(instance.TestRoot, KestrelStandalone));
try
{
dotnetCommand.ExecuteAsync($"{output} {args}");
NetworkHelper.IsServerUp(url).Should().BeTrue($"Unable to connect to kestrel server - {KestrelStandalone} @ {url}");
NetworkHelper.TestGetRequest(url, args);
}
finally
{
dotnetCommand.KillTree();
}
}
[Fact]
public void ItRunsKestrelPortableAfterPublish()
{
TestInstance instance = TestAssetsManager.CreateTestInstance(KestrelSampleBase)
.WithLockFiles();
var url = NetworkHelper.GetLocalhostUrlWithFreePort();
var args = $"{url} {Guid.NewGuid().ToString()}";
var dotnetCommand = new DotnetCommand();
var output = Publish(Path.Combine(instance.TestRoot, KestrelPortable), true);
try
{
dotnetCommand.ExecuteAsync($"{output} {args}");
NetworkHelper.IsServerUp(url).Should().BeTrue($"Unable to connect to kestrel server - {KestrelPortable} @ {url}");
NetworkHelper.TestGetRequest(url, args);
}
finally
{
dotnetCommand.KillTree();
}
}
[Fact]
public void ItRunsKestrelStandaloneAfterPublish()
{
TestInstance instance = TestAssetsManager.CreateTestInstance(KestrelSampleBase)
.WithLockFiles();
var url = NetworkHelper.GetLocalhostUrlWithFreePort();
var args = $"{url} {Guid.NewGuid().ToString()}";
var output = Publish(Path.Combine(instance.TestRoot, KestrelStandalone), false);
var command = new TestCommand(output);
try
{
command.ExecuteAsync($"{args}");
NetworkHelper.IsServerUp(url).Should().BeTrue($"Unable to connect to kestrel server - {KestrelStandalone} @ {url}");
NetworkHelper.TestGetRequest(url, args);
}
finally
{
command.KillTree();
}
}
private static string Build(string testRoot)
{
string appName = Path.GetFileName(testRoot);
var result = new BuildCommand(
projectPath: testRoot)
.ExecuteWithCapturedOutput();
result.Should().Pass();
// the correct build assembly is next to its deps.json file
var depsJsonFile = Directory.EnumerateFiles(testRoot, appName + FileNameSuffixes.DepsJson, SearchOption.AllDirectories).First();
return Path.Combine(Path.GetDirectoryName(depsJsonFile), appName + ".dll");
}
private static string Publish(string testRoot, bool isPortable)
{
string appName = Path.GetFileName(testRoot);
var publishCmd = new PublishCommand(projectPath: testRoot, output: Path.Combine(testRoot, "bin"));
var result = publishCmd.ExecuteWithCapturedOutput();
result.Should().Pass();
var publishDir = publishCmd.GetOutputDirectory(portable: isPortable).FullName;
return Path.Combine(publishDir, appName + (isPortable ? ".dll" : FileNameSuffixes.CurrentPlatform.Exe));
}
}
}

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0.24720" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.24720</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>605AA1EE-82A4-477B-A711-5944BD7B04E0</ProjectGuid>
<RootNamespace>Kestrel.Tests</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View file

@ -0,0 +1,24 @@
{
"version": "1.0.0-*",
"dependencies": {
"Microsoft.NETCore.App": "1.0.0-rc2-23931",
"System.Runtime.Serialization.Primitives": "4.1.1-rc2-23931",
"Microsoft.DotNet.Tools.Tests.Utilities": {
"target": "project"
},
"Microsoft.DotNet.Cli.Utils": {
"target": "project"
},
"xunit": "2.1.0",
"dotnet-test-xunit": "1.0.0-dev-128011-22"
},
"frameworks": {
"netstandardapp1.5": {
"imports": [
"dnxcore50",
"portable-net45+win8"
]
}
},
"testRunner": "xunit"
}

View file

@ -64,5 +64,21 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
return new AndConstraint<DirectoryInfoAssertions>(new DirectoryInfoAssertions(dir));
}
public AndConstraint<DirectoryInfoAssertions> OnlyHaveFiles(IEnumerable<string> expectedFiles)
{
var actualFiles = _dirInfo.EnumerateFiles("*", SearchOption.TopDirectoryOnly).Select(f => f.Name);
var missingFiles = Enumerable.Except(expectedFiles, actualFiles);
var extraFiles = Enumerable.Except(actualFiles, expectedFiles);
var nl = Environment.NewLine;
Execute.Assertion.ForCondition(!missingFiles.Any())
.FailWith($"Following files cannot be found inside directory {_dirInfo.FullName} {nl} {string.Join(nl, missingFiles)}");
Execute.Assertion.ForCondition(!extraFiles.Any())
.FailWith($"Following extra files are found inside directory {_dirInfo.FullName} {nl} {string.Join(nl, extraFiles)}");
return new AndConstraint<DirectoryInfoAssertions>(this);
}
}
}

View file

@ -0,0 +1,17 @@
// 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.Cli.Utils;
namespace Microsoft.DotNet.Tools.Test.Utilities
{
public sealed class DotnetCommand : TestCommand
{
public DotnetCommand()
: base("dotnet")
{
}
}
}

View file

@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using Microsoft.DotNet.Cli.Utils;
using System.Threading.Tasks;
namespace Microsoft.DotNet.Tools.Test.Utilities
{
@ -60,10 +61,10 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
public RunCommand(
string projectPath,
string framework="",
string configuration="",
bool preserveTemporary=false,
string appArgs="")
string framework = "",
string configuration = "",
bool preserveTemporary = false,
string appArgs = "")
: base("dotnet")
{
_projectPath = projectPath;
@ -78,12 +79,19 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
args = $"run {BuildArgs()} {args}";
return base.Execute(args);
}
public override CommandResult ExecuteWithCapturedOutput(string args = "")
{
args = $"run {BuildArgs()} {args}";
return base.ExecuteWithCapturedOutput(args);
}
public override Task<CommandResult> ExecuteAsync(string args = "")
{
args = $"run {BuildArgs()} {args}";
return base.ExecuteAsync(args);
}
private string BuildArgs()
{
return $"{ProjectPathOption} {FrameworkOption} {ConfigurationOption} {PreserveTemporaryOption} {AppArgsArgument}";

View file

@ -6,6 +6,8 @@ using System;
using System.Diagnostics;
using System.IO;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace Microsoft.DotNet.Tools.Test.Utilities
{
@ -15,6 +17,8 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
public string WorkingDirectory { get; set; }
public Process CurrentProcess { get; set; }
public Dictionary<string, string> Environment { get; } = new Dictionary<string, string>();
public TestCommand(string command)
@ -38,6 +42,22 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
return RunProcess(commandPath, args, stdOut, stdErr);
}
public virtual Task<CommandResult> ExecuteAsync(string args = "")
{
var commandPath = _command;
ResolveCommand(ref commandPath, ref args);
Console.WriteLine($"Executing - {commandPath} {args}");
var stdOut = new StreamForwarder();
var stdErr = new StreamForwarder();
stdOut.ForwardTo(writeLine: Reporter.Output.WriteLine);
stdErr.ForwardTo(writeLine: Reporter.Output.WriteLine);
return RunProcessAsync(commandPath, args, stdOut, stdErr);
}
public virtual CommandResult ExecuteWithCapturedOutput(string args = "")
{
var command = _command;
@ -56,6 +76,16 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
return RunProcess(commandPath, args, stdOut, stdErr);
}
public void KillTree()
{
if (CurrentProcess == null)
{
throw new InvalidOperationException("No process is available to be killed");
}
CurrentProcess.KillTree();
}
private void ResolveCommand(ref string executable, ref string args)
{
if (executable.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
@ -77,13 +107,55 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
}
private CommandResult RunProcess(string executable, string args, StreamForwarder stdOut, StreamForwarder stdErr)
{
CurrentProcess = StartProcess(executable, args);
var threadOut = stdOut.BeginRead(CurrentProcess.StandardOutput);
var threadErr = stdErr.BeginRead(CurrentProcess.StandardError);
CurrentProcess.WaitForExit();
threadOut.Join();
threadErr.Join();
var result = new CommandResult(
CurrentProcess.StartInfo,
CurrentProcess.ExitCode,
stdOut.CapturedOutput,
stdErr.CapturedOutput);
return result;
}
private Task<CommandResult> RunProcessAsync(string executable, string args, StreamForwarder stdOut, StreamForwarder stdErr)
{
CurrentProcess = StartProcess(executable, args);
var threadOut = stdOut.BeginRead(CurrentProcess.StandardOutput);
var threadErr = stdErr.BeginRead(CurrentProcess.StandardError);
var tcs = new TaskCompletionSource<CommandResult>();
CurrentProcess.Exited += (sender, arg) =>
{
threadOut.Join();
threadErr.Join();
var result = new CommandResult(
CurrentProcess.StartInfo,
CurrentProcess.ExitCode,
stdOut.CapturedOutput,
stdErr.CapturedOutput);
tcs.SetResult(result);
};
return tcs.Task;
}
private Process StartProcess(string executable, string args)
{
var psi = new ProcessStartInfo
{
FileName = executable,
Arguments = args,
RedirectStandardError = true,
RedirectStandardOutput = true
RedirectStandardOutput = true,
RedirectStandardInput = true
};
foreach (var item in Environment)
@ -103,21 +175,7 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
process.EnableRaisingEvents = true;
process.Start();
var threadOut = stdOut.BeginRead(process.StandardOutput);
var threadErr = stdErr.BeginRead(process.StandardError);
process.WaitForExit();
threadOut.Join();
threadErr.Join();
var result = new CommandResult(
process.StartInfo,
process.ExitCode,
stdOut.CapturedOutput,
stdErr.CapturedOutput);
return result;
return process;
}
}
}

View file

@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.ProjectModel;
using FluentAssertions;
namespace Microsoft.DotNet.Tools.Test.Utilities
{
public class NetworkHelper
{
// in milliseconds
private const int Timeout = 20000;
private static Queue<TcpListener> s_PortPool = new Queue<TcpListener>();
public static string Localhost { get; } = "http://localhost";
public static bool IsServerUp(string url)
{
return SpinWait.SpinUntil(() =>
{
using (var client = new HttpClient())
{
try
{
client.BaseAddress = new Uri(url);
HttpResponseMessage response = client.GetAsync("").Result;
return response.IsSuccessStatusCode;
}
catch (Exception)
{
return false;
}
}
}, Timeout);
}
public static void TestGetRequest(string url, string expectedResponse)
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(url);
HttpResponseMessage response = client.GetAsync("").Result;
if (response.IsSuccessStatusCode)
{
var responseString = response.Content.ReadAsStringAsync().Result;
responseString.Should().Contain(expectedResponse);
}
}
}
public static int GetFreePort()
{
lock (s_PortPool)
{
if (s_PortPool.Count == 0)
{
for (int i = 0; i < 20; i++)
{
var tcpl = new TcpListener(IPAddress.Loopback, 0);
tcpl.Start();
s_PortPool.Enqueue(tcpl);
}
Console.WriteLine($"Ports Count >>>>>>>>>>>>>>>>>>> {s_PortPool.Count}");
}
var currentTcpl = s_PortPool.Dequeue();
var port = ((IPEndPoint)currentTcpl.LocalEndpoint).Port;
currentTcpl.Stop();
currentTcpl = null;
GC.Collect();
GC.WaitForPendingFinalizers();
return port;
}
}
public static string GetLocalhostUrlWithFreePort()
{
return $"{Localhost}:{GetFreePort()}/";
}
}
}

View file

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
namespace Microsoft.DotNet.Tools.Test.Utilities
{
internal static class ProcessExtensions
{
private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30);
public static void KillTree(this Process process)
{
process.KillTree(_defaultTimeout);
}
public static void KillTree(this Process process, TimeSpan timeout)
{
string stdout;
if (_isWindows)
{
RunProcessAndWaitForExit(
"taskkill",
$"/T /F /PID {process.Id}",
timeout,
out stdout);
}
else
{
var children = new HashSet<int>();
GetAllChildIdsUnix(process.Id, children, timeout);
foreach (var childId in children)
{
KillProcessUnix(childId, timeout);
}
KillProcessUnix(process.Id, timeout);
}
}
private static void GetAllChildIdsUnix(int parentId, ISet<int> children, TimeSpan timeout)
{
string stdout;
var exitCode = RunProcessAndWaitForExit(
"pgrep",
$"-P {parentId}",
timeout,
out stdout);
if (exitCode == 0 && !string.IsNullOrEmpty(stdout))
{
using (var reader = new StringReader(stdout))
{
while (true)
{
var text = reader.ReadLine();
if (text == null)
{
return;
}
int id;
if (int.TryParse(text, out id))
{
children.Add(id);
// Recursively get the children
GetAllChildIdsUnix(id, children, timeout);
}
}
}
}
}
private static void KillProcessUnix(int processId, TimeSpan timeout)
{
string stdout;
RunProcessAndWaitForExit(
"kill",
$"-TERM {processId}",
timeout,
out stdout);
}
private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout)
{
var startInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
RedirectStandardOutput = true,
UseShellExecute = false
};
var process = Process.Start(startInfo);
stdout = null;
if (process.WaitForExit((int)timeout.TotalMilliseconds))
{
stdout = process.StandardOutput.ReadToEnd();
}
else
{
process.Kill();
}
return process.ExitCode;
}
}
}