Files
winsw/src/WinSW.Core/Util/ProcessExtensions.cs
2021-01-03 23:59:25 +08:00

234 lines
6.4 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using log4net;
using WinSW.Logging;
using WinSW.Native;
using static WinSW.Native.ConsoleApis;
using static WinSW.Native.ProcessApis;
namespace WinSW.Util
{
public static class ProcessExtensions
{
private static readonly ILog Log = LogManager.GetLogger(LoggerNames.Service);
public static void StopTree(this Process process, int millisecondsTimeout)
{
StopPrivate(process, millisecondsTimeout);
foreach (var child in GetChildren(process))
{
using (child.Process)
using (child.Handle)
{
StopTree(child.Process, millisecondsTimeout);
}
}
}
internal static void StopDescendants(this Process process, int millisecondsTimeout)
{
foreach (var child in GetChildren(process))
{
using (child.Process)
using (child.Handle)
{
StopTree(child.Process, millisecondsTimeout);
}
}
}
// The handle is to keep a reference to the process.
internal static unsafe List<(Process Process, Handle Handle)> GetChildren(this Process process)
{
var startTime = process.StartTime;
int processId = process.Id;
var children = new List<(Process Process, Handle Handle)>();
foreach (var other in Process.GetProcesses())
{
var handle = OpenProcess(ProcessAccess.QueryInformation, false, other.Id);
if (handle == IntPtr.Zero)
{
goto Next;
}
try
{
if (other.StartTime <= startTime)
{
goto Next;
}
}
catch (Exception e) when (e is InvalidOperationException || e is Win32Exception)
{
goto Next;
}
if (NtQueryInformationProcess(
handle,
PROCESSINFOCLASS.ProcessBasicInformation,
out var information,
sizeof(PROCESS_BASIC_INFORMATION)) != 0)
{
goto Next;
}
if ((int)information.InheritedFromUniqueProcessId == processId)
{
Log.Debug($"Found child process '{other.Format()}'.");
children.Add((other, handle));
continue;
}
Next:
other.Dispose();
handle.Dispose();
}
return children;
}
// true => canceled
// false => terminated
// null => finished
internal static bool? Stop(this Process process, int millisecondsTimeout)
{
if (process.HasExited)
{
return null;
}
if (SendCtrlC(process) is not bool sent)
{
return null;
}
if (!sent)
{
try
{
sent = process.CloseMainWindow();
}
catch (InvalidOperationException)
{
return null;
}
}
if (sent)
{
if (process.WaitForExit(millisecondsTimeout))
{
return true;
}
}
#if NET
process.Kill();
#else
try
{
process.Kill();
}
catch when (process.HasExited)
{
}
#endif
return false;
}
private static void StopPrivate(Process process, int millisecondsTimeout)
{
Log.Debug($"Stopping process '{process.Format()}'...");
if (process.HasExited)
{
goto Exited;
}
if (SendCtrlC(process) is not bool sent)
{
goto Exited;
}
if (!sent)
{
try
{
sent = process.CloseMainWindow();
}
catch (InvalidOperationException)
{
goto Exited;
}
}
if (sent)
{
if (process.WaitForExit(millisecondsTimeout))
{
Log.Debug($"Process '{process.Format()}' canceled with code {process.ExitCode}.");
return;
}
}
#if NET
process.Kill();
#else
try
{
process.Kill();
}
catch when (process.HasExited)
{
}
#endif
Log.Debug($"Process '{process.Format()}' terminated.");
return;
Exited:
Log.Debug($"Process '{process.Format()}' has already exited.");
}
private static bool? SendCtrlC(Process process)
{
if (!AttachConsole(process.Id))
{
int error = Marshal.GetLastWin32Error();
switch (error)
{
// The process does not have a console.
case Errors.ERROR_INVALID_HANDLE:
return false;
// The process has exited.
case Errors.ERROR_INVALID_PARAMETER:
return null;
// The calling process is already attached to a console.
case Errors.ERROR_ACCESS_DENIED:
default:
Log.Warn("Failed to attach to console. " + new Win32Exception(error).Message);
return false;
}
}
// Don't call GenerateConsoleCtrlEvent immediately after SetConsoleCtrlHandler.
// A delay was observed as of Windows 10, version 2004 and Windows Server 2019.
_ = GenerateConsoleCtrlEvent(CtrlEvents.CTRL_C_EVENT, 0);
bool succeeded = FreeConsole();
Debug.Assert(succeeded);
return true;
}
}
}