Support sending `WM_CLOSE` message

pull/626/head
NextTurn 2020-08-04 00:00:00 +08:00 committed by Next Turn
parent 6cefa93c69
commit b0423e3156
4 changed files with 107 additions and 77 deletions

View File

@ -708,6 +708,8 @@ namespace WinSW
/// </summary> /// </summary>
public override TimeSpan StopTimeout => this.SingleTimeSpanElement(this.dom, "stoptimeout", base.StopTimeout); public override TimeSpan StopTimeout => this.SingleTimeSpanElement(this.dom, "stoptimeout", base.StopTimeout);
public int StopTimeoutInMs => (int)this.StopTimeout.TotalMilliseconds;
/// <summary> /// <summary>
/// Desired process priority or null if not specified. /// Desired process priority or null if not specified.
/// </summary> /// </summary>

View File

@ -2,35 +2,38 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices;
using log4net; using log4net;
using WinSW.Native;
using static WinSW.Native.ConsoleApis;
using static WinSW.Native.ProcessApis; using static WinSW.Native.ProcessApis;
namespace WinSW.Util namespace WinSW.Util
{ {
public static class ProcessExtensions public static class ProcessExtensions
{ {
private static readonly ILog Logger = LogManager.GetLogger(typeof(ProcessExtensions)); private static readonly ILog Log = LogManager.GetLogger(typeof(ProcessExtensions));
public static void StopTree(this Process process, TimeSpan stopTimeout) public static void StopTree(this Process process, int millisecondsTimeout)
{ {
StopPrivate(process, stopTimeout); StopPrivate(process, millisecondsTimeout);
foreach (Process child in GetChildren(process)) foreach (Process child in GetChildren(process))
{ {
using (child) using (child)
{ {
StopTree(child, stopTimeout); StopTree(child, millisecondsTimeout);
} }
} }
} }
internal static void StopDescendants(this Process process, TimeSpan stopTimeout) internal static void StopDescendants(this Process process, int millisecondsTimeout)
{ {
foreach (Process child in GetChildren(process)) foreach (Process child in GetChildren(process))
{ {
using (child) using (child)
{ {
StopTree(child, stopTimeout); StopTree(child, millisecondsTimeout);
} }
} }
} }
@ -64,7 +67,7 @@ namespace WinSW.Util
if ((int)information.InheritedFromUniqueProcessId == processId) if ((int)information.InheritedFromUniqueProcessId == processId)
{ {
Logger.Info($"Found child process '{other.Format()}'."); Log.Debug($"Found child process '{other.Format()}'.");
children.Add(other); children.Add(other);
continue; continue;
} }
@ -84,22 +87,41 @@ namespace WinSW.Util
// true => canceled // true => canceled
// false => terminated // false => terminated
// null => finished // null => finished
internal static bool? Stop(this Process process, TimeSpan stopTimeout) internal static bool? Stop(this Process process, int millisecondsTimeout)
{ {
if (process.HasExited) if (process.HasExited)
{ {
return null; return null;
} }
// (bool sent, bool exited) if (!(SendCtrlC(process) is bool sent))
KeyValuePair<bool, bool> result = SignalHelper.SendCtrlCToProcess(process, stopTimeout);
bool exited = result.Value;
if (exited)
{ {
bool sent = result.Key; return null;
return sent ? true : (bool?)null;
} }
if (!sent)
{
try
{
sent = process.CloseMainWindow();
}
catch (InvalidOperationException)
{
return null;
}
}
if (sent)
{
if (process.WaitForExit(millisecondsTimeout))
{
return true;
}
}
#if NETCOREAPP
process.Kill();
#else
try try
{ {
process.Kill(); process.Kill();
@ -107,41 +129,88 @@ namespace WinSW.Util
catch when (process.HasExited) catch when (process.HasExited)
{ {
} }
#endif
return false; return false;
} }
private static void StopPrivate(Process process, TimeSpan stopTimeout) private static void StopPrivate(Process process, int millisecondsTimeout)
{ {
Logger.Info("Stopping process " + process.Id); Log.Debug($"Stopping process '{process.Format()}'...");
if (process.HasExited) if (process.HasExited)
{ {
Logger.Info("Process " + process.Id + " is already stopped"); goto Exited;
return;
} }
// (bool sent, bool exited) if (!(SendCtrlC(process) is bool sent))
KeyValuePair<bool, bool> result = SignalHelper.SendCtrlCToProcess(process, stopTimeout);
bool exited = result.Value;
if (!exited)
{ {
bool sent = result.Key; goto Exited;
if (sent) }
{
Logger.Info("Process " + process.Id + " did not respond to Ctrl+C signal - Killing as fallback");
}
if (!sent)
{
try try
{ {
process.Kill(); sent = process.CloseMainWindow();
} }
catch when (process.HasExited) catch (InvalidOperationException)
{ {
goto Exited;
} }
} }
// TODO: Propagate error if process kill fails? Currently we use the legacy behavior if (sent)
{
if (process.WaitForExit(millisecondsTimeout))
{
Log.Debug($"Process '{process.Format()}' canceled with code {process.ExitCode}.");
return;
}
}
#if NETCOREAPP
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();
Log.Debug("Failed to attach to console. " + error switch
{
Errors.ERROR_ACCESS_DENIED => "WinSW is already attached to a console.", // TODO: test mode
Errors.ERROR_INVALID_HANDLE => "The process does not have a console.",
Errors.ERROR_INVALID_PARAMETER => "The process has exited.",
_ => new Win32Exception(error).Message // unreachable
});
return error == Errors.ERROR_INVALID_PARAMETER ? (bool?)null : false;
}
_ = SetConsoleCtrlHandler(null, true);
_ = GenerateConsoleCtrlEvent(CtrlEvents.CTRL_C_EVENT, 0);
_ = SetConsoleCtrlHandler(null, false);
bool succeeded = FreeConsole();
Debug.Assert(succeeded);
return true;
} }
} }
} }

View File

@ -1,41 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using log4net;
using WinSW.Native;
namespace WinSW.Util
{
internal static class SignalHelper
{
private static readonly ILog Logger = LogManager.GetLogger(typeof(SignalHelper));
// (bool sent, bool exited)
internal static KeyValuePair<bool, bool> SendCtrlCToProcess(Process process, TimeSpan shutdownTimeout)
{
if (!ConsoleApis.AttachConsole(process.Id))
{
int error = Marshal.GetLastWin32Error();
Logger.Info("Failed to attach to console. " + error switch
{
Errors.ERROR_ACCESS_DENIED => "WinSW is already attached to a console.", // TODO: test mode
Errors.ERROR_INVALID_HANDLE => "The process does not have a console.",
Errors.ERROR_INVALID_PARAMETER => "The process has exited.",
_ => new Win32Exception(error).Message // unreachable
});
return new KeyValuePair<bool, bool>(false, error == Errors.ERROR_INVALID_PARAMETER);
}
_ = ConsoleApis.SetConsoleCtrlHandler(null, true);
_ = ConsoleApis.GenerateConsoleCtrlEvent(ConsoleApis.CtrlEvents.CTRL_C_EVENT, 0);
_ = ConsoleApis.SetConsoleCtrlHandler(null, false);
bool succeeded = ConsoleApis.FreeConsole();
Debug.Assert(succeeded);
return new KeyValuePair<bool, bool>(true, process.WaitForExit((int)shutdownTimeout.TotalMilliseconds));
}
}
}

View File

@ -18,7 +18,7 @@ namespace WinSW
{ {
internal static readonly WrapperServiceEventLogProvider eventLogProvider = new WrapperServiceEventLogProvider(); internal static readonly WrapperServiceEventLogProvider eventLogProvider = new WrapperServiceEventLogProvider();
private static readonly TimeSpan additionalStopTimeout = new TimeSpan(TimeSpan.TicksPerSecond); private static readonly int additionalStopTimeout = 1_000;
private static readonly ILog Log = LogManager.GetLogger( private static readonly ILog Log = LogManager.GetLogger(
#if NETCOREAPP #if NETCOREAPP
@ -380,14 +380,14 @@ namespace WinSW
{ {
Process process = this.process; Process process = this.process;
Log.Debug("ProcessKill " + process.Id); Log.Debug("ProcessKill " + process.Id);
bool? result = process.Stop(this.config.StopTimeout); bool? result = process.Stop(this.config.StopTimeoutInMs);
this.LogMinimal($"Child process '{process.Format()}' " + result switch this.LogMinimal($"Child process '{process.Format()}' " + result switch
{ {
true => $"canceled with code {process.ExitCode}.", true => $"canceled with code {process.ExitCode}.",
false => "terminated.", false => "terminated.",
null => $"finished with code '{process.ExitCode}'." null => $"finished with code '{process.ExitCode}'."
}); });
this.process.StopDescendants(this.config.StopTimeout); this.process.StopDescendants(this.config.StopTimeoutInMs);
this.ExtensionManager.FireOnProcessTerminated(process); this.ExtensionManager.FireOnProcessTerminated(process);
} }
else else
@ -407,11 +407,11 @@ namespace WinSW
this.stoppingProcess = null; this.stoppingProcess = null;
this.WaitForProcessToExit(this.process); this.WaitForProcessToExit(this.process);
this.process.StopDescendants(this.config.StopTimeout); this.process.StopDescendants(this.config.StopTimeoutInMs);
} }
catch catch
{ {
this.process.StopTree(this.config.StopTimeout); this.process.StopTree(this.config.StopTimeoutInMs);
throw; throw;
} }
} }
@ -505,7 +505,7 @@ namespace WinSW
{ {
Log.Warn($"Child process '{process.Format()}' finished with code {process.ExitCode}."); Log.Warn($"Child process '{process.Format()}' finished with code {process.ExitCode}.");
process.StopDescendants(this.config.StopTimeout); process.StopDescendants(this.config.StopTimeoutInMs);
this.startingProcess?.StopTree(additionalStopTimeout); this.startingProcess?.StopTree(additionalStopTimeout);
this.stoppingProcess?.StopTree(additionalStopTimeout); this.stoppingProcess?.StopTree(additionalStopTimeout);