Tye is a tool that makes developing, testing, and deploying microservices and distributed applications easier. Project Tye includes a local orchestrator to make developing microservices easier and the ability to deploy microservices to Kubernetes with min
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

233 lines
8.5 KiB

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Tye
{
public static class ProcessUtil
{
#region Native Methods
[DllImport("libc", SetLastError = true, EntryPoint = "kill")]
private static extern int sys_kill(int pid, int sig);
#endregion
private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
private const int ProcessExitTimeoutMs = 30 * 1000; // 30 seconds timeout for the process to exit.
public static Task<int> ExecuteAsync(
string command,
string args,
string? workingDir = null,
Action<string>? stdOut = null,
Action<string>? stdErr = null,
params (string key, string value)[] environmentVariables)
{
return System.CommandLine.Invocation.Process.ExecuteAsync(command, args, workingDir, stdOut, stdErr, environmentVariables);
}
public static async Task<ProcessResult> RunAsync(
string filename,
string arguments,
string? workingDirectory = null,
bool throwOnError = true,
IDictionary<string, string>? environmentVariables = null,
Action<string>? outputDataReceived = null,
Action<string>? errorDataReceived = null,
Action<int>? onStart = null,
Action<int>? onStop = null,
CancellationToken cancellationToken = default)
{
using var process = new Process()
{
StartInfo =
{
FileName = filename,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = !IsWindows,
WindowStyle = ProcessWindowStyle.Hidden
},
EnableRaisingEvents = true
};
if (workingDirectory != null)
{
process.StartInfo.WorkingDirectory = workingDirectory;
}
if (environmentVariables != null)
{
foreach (var kvp in environmentVariables)
{
process.StartInfo.Environment.Add(kvp!);
}
}
var outputLock = new SpinLock();
void WithOutputLock(Action action)
{
bool gotLock = false;
try
{
outputLock.Enter(ref gotLock);
action();
}
finally
{
if (gotLock)
{
outputLock.Exit();
}
}
}
var outputBuilder = new StringBuilder();
process.OutputDataReceived += (_, e) =>
{
if (e.Data == null)
{
return;
}
if (outputDataReceived != null)
{
outputDataReceived.Invoke(e.Data);
}
else
{
WithOutputLock(() => outputBuilder.AppendLine(e.Data));
}
};
var errorBuilder = new StringBuilder();
process.ErrorDataReceived += (_, e) =>
{
if (e.Data == null)
{
return;
}
if (errorDataReceived != null)
{
errorDataReceived.Invoke(e.Data);
}
else
{
WithOutputLock(() => errorBuilder.AppendLine(e.Data));
}
};
var processLifetimeTask = new TaskCompletionSource<ProcessResult>();
process.Exited += (_, e) =>
{
lock (process)
{
// Even though the Exited event has been raised, WaitForExit() must still be called to ensure the output buffers
// have been flushed before the process is considered completely done.
// Because of the bug in the dotnet runtime https://github.com/dotnet/runtime/issues/29232, Process.WaitForExit()
// hangs for processes that spawn another long-running processes.
// Since these are expected to be long running processes and we're typically not concerned with capturing all of its output
// i.e. it's probably ok for some output to be lost on shutdown, since Tye is shutting down anyway,
// we call Process.WaitForProcessExit(ProcessExitTimeoutMs).
// Also, since this is a process.Exited event, process.ExitCode is valid even if WaitForExit() times out.
process.WaitForExit(ProcessExitTimeoutMs);
}
// NOTE: If WaitForExit() returns false, more output may be written,
// so we must synchronize access to the output StringBuilders.
WithOutputLock(
() =>
{
if (throwOnError && process.ExitCode != 0)
{
processLifetimeTask.TrySetException(new InvalidOperationException($"Command {filename} {arguments} returned exit code {process.ExitCode}. Standard error: \"{errorBuilder.ToString()}\""));
}
else
{
processLifetimeTask.TrySetResult(new ProcessResult(outputBuilder.ToString(), errorBuilder.ToString(), process.ExitCode));
}
});
};
// lock ensures we're reading output when WaitForExit is called in process.Exited event.
lock (process)
{
process.Start();
onStart?.Invoke(process.Id);
process.BeginOutputReadLine();
process.BeginErrorReadLine();
}
var cancelledTcs = new TaskCompletionSource<object?>();
await using var _ = cancellationToken.Register(() => cancelledTcs.TrySetResult(null));
var result = await Task.WhenAny(processLifetimeTask.Task, cancelledTcs.Task);
if (result == cancelledTcs.Task)
{
if (!IsWindows)
{
sys_kill(process.Id, sig: 2); // SIGINT
}
else
{
if (!process.CloseMainWindow())
{
process.Kill(entireProcessTree: true);
}
}
if (!process.HasExited)
{
var cancel = new CancellationTokenSource();
await Task.WhenAny(processLifetimeTask.Task, Task.Delay(TimeSpan.FromSeconds(5), cancel.Token));
cancel.Cancel();
if (!process.HasExited)
{
process.Kill(entireProcessTree: true);
}
}
}
var processResult = await processLifetimeTask.Task;
onStop?.Invoke(processResult.ExitCode);
return processResult;
}
public static Task<ProcessResult> RunAsync(ProcessSpec processSpec, CancellationToken cancellationToken = default, bool throwOnError = true)
{
return RunAsync(processSpec.Executable!, processSpec.Arguments!, processSpec.WorkingDirectory, throwOnError: throwOnError, processSpec.EnvironmentVariables, processSpec.OutputData, processSpec.ErrorData, processSpec.OnStart, processSpec.OnStop, cancellationToken);
}
public static void KillProcess(int pid)
{
try
{
using var process = Process.GetProcessById(pid);
process?.Kill(entireProcessTree: true);
}
catch (ArgumentException) { }
catch (InvalidOperationException) { }
}
}
}