Last active
June 24, 2023 23:37
-
-
Save ycherkes/b216e78ae7ee4418504320f861337ed4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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