Skip to content

Instantly share code, notes, and snippets.

@ycherkes
Last active June 24, 2023 23:37
Show Gist options
  • Save ycherkes/b216e78ae7ee4418504320f861337ed4 to your computer and use it in GitHub Desktop.
Save ycherkes/b216e78ae7ee4418504320f861337ed4 to your computer and use it in GitHub Desktop.
using System.Diagnostics;
// !!! This is not working example !!!
namespace GracefulTermination
{
internal static class Program
{
private static async Task Main()
{
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "ping",
Arguments = "-t 142.251.208.174", // google.com
UseShellExecute = false,
RedirectStandardOutput = true,
}
};
process.OutputDataReceived += (_, e) => Console.WriteLine("child: " + e.Data);
process.Start();
process.BeginOutputReadLine();
await Task.Delay(6000);
var cts = new CancellationTokenSource(5000);
try
{
Console.WriteLine($"parent: sending CTRL_C to child process {process.Id}");
var success = await WindowsProcessTerminator.StopProcessGracefully(process, cts.Token);
var logResult = success ? "stopped" : "failed to stop";
Console.WriteLine($"parent: child process is gracefully {logResult}");
if (!success)
{
process.Kill();
await process.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false);
Console.WriteLine("parent: child process is forcibly terminated");
}
}
catch (TaskCanceledException)
{
process.Kill();
await process.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false);
Console.WriteLine("parent: child process is forcibly terminated");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
}
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace GracefulTermination
{
public static class WindowsProcessTerminator
{
private delegate bool ConsoleCtrlDelegate(CtrlTypes dwCtrlEvent);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint GetConsoleProcessList(uint[] lpdwProcessList, uint dwProcessCount);
[DllImport("kernel32.dll")]
private static extern IntPtr GetConsoleWindow();
[DllImport("kernel32.dll")]
private static extern bool FreeConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AttachConsole(uint dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate? handlerRoutine, bool add);
private static readonly SemaphoreSlim ObjectLocker = new(1, 1);
public static async Task<bool> StopProcessGracefully(Process process, CancellationToken token)
{
if (process.MainWindowHandle != IntPtr.Zero)
{
return CloseGuiProcess(process);
}
return await CloseConsoleProcess(process.Id, token).ConfigureAwait(false);
}
private static async Task<bool> CloseConsoleProcess(int processId, CancellationToken token)
{
await ObjectLocker.WaitAsync(token).ConfigureAwait(false);
var hasNoConsole = HasNoConsole();
try
{
if (hasNoConsole)
{
FreeConsole();
if (!AttachConsole(checked((uint)processId)))
return false;
}
else if (!HasSameConsole(processId))
{
// if the target process does not share the console with signaling process, an external executable is required - unsupported now
return false;
}
return await CloseSameConsoleProcess(processId, token).ConfigureAwait(false);
}
finally
{
if (hasNoConsole)
{
FreeConsole();
}
ObjectLocker.Release();
}
}
private static async Task<bool> CloseSameConsoleProcess(int processId, CancellationToken token)
{
using var waitForSignalSemaphore = new SemaphoreSlim(initialCount: 0, maxCount: 1);
bool CancelKeyPress(CtrlTypes dwCtrlEvent)
{
waitForSignalSemaphore.Release();
return dwCtrlEvent == CtrlTypes.CTRL_C_EVENT && processId != Environment.ProcessId;
}
try
{
if (!SetConsoleCtrlHandler(CancelKeyPress, true))
{
return false;
}
if (!GenerateConsoleCtrlEvent(CtrlTypes.CTRL_C_EVENT, 0))
{
return false;
}
await waitForSignalSemaphore.WaitAsync(token).ConfigureAwait(false);
return true;
}
finally
{
SetConsoleCtrlHandler(CancelKeyPress, false);
}
}
private static bool CloseGuiProcess(Process process)
{
return process.CloseMainWindow();
}
private static bool HasNoConsole()
{
return GetConsoleWindow() == IntPtr.Zero;
}
private static bool HasSameConsole(int processId)
{
// see https://docs.microsoft.com/en-us/windows/console/getconsoleprocesslist
// for instructions on calling this method
uint processListCount = 1;
uint[] processIdListBuffer;
do
{
processIdListBuffer = new uint[processListCount];
processListCount = GetConsoleProcessList(processIdListBuffer, processListCount);
}
while (processListCount > processIdListBuffer.Length);
checked
{
return processIdListBuffer.Take((int)processListCount).Contains(checked((uint)processId));
}
}
// Enumerated type for the control messages sent to the handler routine
enum CtrlTypes
{
CTRL_C_EVENT = 0,
//CTRL_BREAK_EVENT,
//CTRL_CLOSE_EVENT,
//CTRL_LOGOFF_EVENT = 5,
//CTRL_SHUTDOWN_EVENT
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment