Ensure child processes are cleanup up

pull/619/head
NextTurn 2020-08-01 00:00:00 +08:00 committed by Next Turn
parent efc3e34f6d
commit bd61d2986f
6 changed files with 235 additions and 275 deletions

View File

@ -597,7 +597,7 @@ namespace WinSW
/// <summary> /// <summary>
/// Environment variable overrides /// Environment variable overrides
/// </summary> /// </summary>
public override Dictionary<string, string> EnvironmentVariables => new Dictionary<string, string>(this.environmentVariables); public override Dictionary<string, string> EnvironmentVariables => this.environmentVariables;
/// <summary> /// <summary>
/// List of downloads to be performed by the wrapper before starting /// List of downloads to be performed by the wrapper before starting

View File

@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using log4net;
using static WinSW.Native.ProcessApis;
namespace WinSW.Util
{
public static class ProcessExtensions
{
private static readonly ILog Logger = LogManager.GetLogger(typeof(ProcessExtensions));
public static void StopTree(this Process process, TimeSpan stopTimeout)
{
Stop(process, stopTimeout);
foreach (Process child in GetDescendants(process))
{
StopTree(child, stopTimeout);
}
}
internal static void StopDescendants(this Process process, TimeSpan stopTimeout)
{
foreach (Process child in GetDescendants(process))
{
StopTree(child, stopTimeout);
}
}
private static void Stop(Process process, TimeSpan stopTimeout)
{
Logger.Info("Stopping process " + process.Id);
if (process.HasExited)
{
Logger.Info("Process " + process.Id + " is already stopped");
return;
}
// (bool sent, bool exited)
KeyValuePair<bool, bool> result = SignalHelper.SendCtrlCToProcess(process, stopTimeout);
bool exited = result.Value;
if (!exited)
{
bool sent = result.Key;
if (sent)
{
Logger.Info("Process " + process.Id + " did not respond to Ctrl+C signal - Killing as fallback");
}
try
{
process.Kill();
}
catch when (process.HasExited)
{
}
}
// TODO: Propagate error if process kill fails? Currently we use the legacy behavior
}
private static unsafe List<Process> GetDescendants(Process root)
{
DateTime startTime = root.StartTime;
int processId = root.Id;
var children = new List<Process>();
foreach (Process process in Process.GetProcesses())
{
try
{
if (process.StartTime <= startTime)
{
goto Next;
}
IntPtr handle = process.Handle;
if (NtQueryInformationProcess(
handle,
PROCESSINFOCLASS.ProcessBasicInformation,
out PROCESS_BASIC_INFORMATION information,
sizeof(PROCESS_BASIC_INFORMATION)) != 0)
{
goto Next;
}
if ((int)information.InheritedFromUniqueProcessId == processId)
{
Logger.Info($"Found child process '{process.Format()}'.");
children.Add(process);
continue;
}
Next:
process.Dispose();
}
catch (Exception e) when (e is InvalidOperationException || e is Win32Exception)
{
process.Dispose();
}
}
return children;
}
}
}

View File

@ -1,197 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using log4net;
using static WinSW.Native.ProcessApis;
namespace WinSW.Util
{
/// <summary>
/// Provides helper classes for Process Management
/// </summary>
/// <remarks>Since WinSW 2.0</remarks>
public class ProcessHelper
{
private static readonly ILog Logger = LogManager.GetLogger(typeof(ProcessHelper));
/// <summary>
/// Gets all children of the specified process.
/// </summary>
/// <param name="processId">Process PID</param>
/// <returns>List of child process PIDs</returns>
private static unsafe List<Process> GetChildProcesses(DateTime startTime, int processId)
{
var children = new List<Process>();
foreach (Process process in Process.GetProcesses())
{
try
{
if (process.StartTime <= startTime)
{
goto Next;
}
IntPtr handle = process.Handle;
if (NtQueryInformationProcess(
handle,
PROCESSINFOCLASS.ProcessBasicInformation,
out PROCESS_BASIC_INFORMATION information,
sizeof(PROCESS_BASIC_INFORMATION)) != 0)
{
goto Next;
}
if ((int)information.InheritedFromUniqueProcessId == processId)
{
Logger.Info($"Found child process '{process.Format()}'.");
children.Add(process);
continue;
}
Next:
process.Dispose();
}
catch (Exception e) when (e is InvalidOperationException || e is Win32Exception)
{
process.Dispose();
}
}
return children;
}
/// <summary>
/// Stops the process.
/// If the process cannot be stopped within the stop timeout, it gets killed
/// </summary>
public static void StopProcess(Process process, TimeSpan stopTimeout)
{
Logger.Info("Stopping process " + process.Id);
if (process.HasExited)
{
Logger.Info("Process " + process.Id + " is already stopped");
return;
}
// (bool sent, bool exited)
KeyValuePair<bool, bool> result = SignalHelper.SendCtrlCToProcess(process, stopTimeout);
bool exited = result.Value;
if (!exited)
{
bool sent = result.Key;
if (sent)
{
Logger.Warn("Process " + process.Id + " did not respond to Ctrl+C signal - Killing as fallback");
}
try
{
process.Kill();
}
catch when (process.HasExited)
{
}
}
// TODO: Propagate error if process kill fails? Currently we use the legacy behavior
}
/// <summary>
/// Terminate process and its children.
/// By default the child processes get terminated first.
/// </summary>
public static void StopProcessTree(Process process, TimeSpan stopTimeout)
{
StopProcess(process, stopTimeout);
foreach (Process child in GetChildProcesses(process.StartTime, process.Id))
{
StopProcessTree(child, stopTimeout);
}
}
/// <summary>
/// Starts a process and asynchronosly waits for its termination.
/// Once the process exits, the callback will be invoked.
/// </summary>
/// <param name="processToStart">Process object to be used</param>
/// <param name="executable">Executable, which should be invoked</param>
/// <param name="arguments">Arguments to be passed</param>
/// <param name="envVars">Additional environment variables</param>
/// <param name="workingDirectory">Working directory</param>
/// <param name="priority">Priority</param>
/// <param name="onExited">Completion callback. If null, the completion won't be monitored</param>
/// <param name="logHandler">Log handler. If enabled, logs will be redirected to the process and then reported</param>
public static void StartProcessAndCallbackForExit(
Process processToStart,
string? executable = null,
string? arguments = null,
Dictionary<string, string>? envVars = null,
string? workingDirectory = null,
ProcessPriorityClass? priority = null,
Action<Process>? onExited = null,
LogHandler? logHandler = null,
bool hideWindow = false)
{
var ps = processToStart.StartInfo;
ps.FileName = executable ?? ps.FileName;
ps.Arguments = arguments ?? ps.Arguments;
ps.WorkingDirectory = workingDirectory ?? ps.WorkingDirectory;
ps.CreateNoWindow = hideWindow;
ps.UseShellExecute = false;
ps.RedirectStandardOutput = logHandler != null;
ps.RedirectStandardError = logHandler != null;
if (envVars != null)
{
var newEnvironment =
#if NETCOREAPP
ps.Environment;
#else
ps.EnvironmentVariables;
#endif
foreach (KeyValuePair<string, string> pair in envVars)
{
newEnvironment[pair.Key] = pair.Value;
}
}
processToStart.Start();
Logger.Info("Started process " + processToStart.Id);
if (priority != null)
{
processToStart.PriorityClass = priority.Value;
}
// Redirect logs if required
if (logHandler != null)
{
Logger.Debug("Forwarding logs of the process " + processToStart + " to " + logHandler);
logHandler.Log(processToStart.StandardOutput, processToStart.StandardError);
}
// monitor the completion of the process
if (onExited != null)
{
processToStart.Exited += (sender, _) =>
{
try
{
onExited((Process)sender!);
}
catch (Exception e)
{
Logger.Error("Unhandled exception in event handler.", e);
}
};
processToStart.EnableRaisingEvents = true;
}
}
}
}

View File

@ -270,7 +270,7 @@ namespace WinSW.Plugins.RunawayProcessKiller
bldr.Append(proc); bldr.Append(proc);
Logger.Warn(bldr.ToString()); Logger.Warn(bldr.ToString());
ProcessHelper.StopProcessTree(proc, this.StopTimeout); proc.StopTree(this.StopTimeout);
} }
/// <summary> /// <summary>

View File

@ -107,7 +107,7 @@ $@"<service>
if (!proc.HasExited) if (!proc.HasExited)
{ {
Console.Error.WriteLine("Test: Killing runaway process with ID=" + proc.Id); Console.Error.WriteLine("Test: Killing runaway process with ID=" + proc.Id);
ProcessHelper.StopProcessTree(proc, TimeSpan.FromMilliseconds(100)); proc.StopTree(TimeSpan.FromMilliseconds(100));
if (!proc.HasExited) if (!proc.HasExited)
{ {
// The test is failed here anyway, but we add additional diagnostics info // The test is failed here anyway, but we add additional diagnostics info

View File

@ -16,11 +16,9 @@ namespace WinSW
{ {
public sealed class WrapperService : ServiceBase, IEventLogger, IServiceEventLog public sealed class WrapperService : ServiceBase, IEventLogger, IServiceEventLog
{ {
private readonly Process process = new Process(); internal static readonly WrapperServiceEventLogProvider eventLogProvider = new WrapperServiceEventLogProvider();
private readonly XmlServiceConfig config;
private Dictionary<string, string>? envs;
internal WinSWExtensionManager ExtensionManager { get; } private static readonly TimeSpan additionalStopTimeout = new TimeSpan(TimeSpan.TicksPerSecond);
private static readonly ILog Log = LogManager.GetLogger( private static readonly ILog Log = LogManager.GetLogger(
#if NETCOREAPP #if NETCOREAPP
@ -28,13 +26,18 @@ namespace WinSW
#endif #endif
"WinSW"); "WinSW");
internal static readonly WrapperServiceEventLogProvider eventLogProvider = new WrapperServiceEventLogProvider(); private readonly XmlServiceConfig config;
private Process process = null!;
private volatile Process? poststartProcess;
internal WinSWExtensionManager ExtensionManager { get; }
/// <summary> /// <summary>
/// Indicates to the watch dog thread that we are going to terminate the process, /// Indicates to the watch dog thread that we are going to terminate the process,
/// so don't try to kill us when the child exits. /// so don't try to kill us when the child exits.
/// </summary> /// </summary>
private bool orderlyShutdown; private volatile bool orderlyShutdown;
private bool shuttingdown; private bool shuttingdown;
/// <summary> /// <summary>
@ -243,8 +246,6 @@ namespace WinSW
private void DoStart() private void DoStart()
{ {
this.envs = this.config.EnvironmentVariables;
this.HandleFileCopies(); this.HandleFileCopies();
// handle downloads // handle downloads
@ -285,20 +286,21 @@ namespace WinSW
throw new AggregateException(exceptions); throw new AggregateException(exceptions);
} }
try
{
string? prestartExecutable = this.config.PrestartExecutable; string? prestartExecutable = this.config.PrestartExecutable;
if (prestartExecutable != null) if (prestartExecutable != null)
{
try
{ {
using Process process = this.StartProcess(prestartExecutable, this.config.PrestartArguments); using Process process = this.StartProcess(prestartExecutable, this.config.PrestartArguments);
this.WaitForProcessToExit(process); this.WaitForProcessToExit(process);
this.LogInfo($"Pre-start process '{process.Format()}' exited with code {process.ExitCode}."); this.LogInfo($"Pre-start process '{process.Format()}' exited with code {process.ExitCode}.");
} process.StopDescendants(additionalStopTimeout);
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e); Log.Error(e);
} }
}
string startArguments = this.config.StartArguments ?? this.config.Arguments; string startArguments = this.config.StartArguments ?? this.config.Arguments;
@ -309,7 +311,7 @@ namespace WinSW
this.ExtensionManager.FireOnWrapperStarted(); this.ExtensionManager.FireOnWrapperStarted();
LogHandler executableLogHandler = this.CreateExecutableLogHandler(); LogHandler executableLogHandler = this.CreateExecutableLogHandler();
this.StartProcess(this.process, startArguments, this.config.Executable, executableLogHandler); this.process = this.StartProcess(this.config.Executable, startArguments, this.OnMainProcessExited, executableLogHandler);
this.ExtensionManager.FireOnProcessStarted(this.process); this.ExtensionManager.FireOnProcessStarted(this.process);
try try
@ -317,10 +319,19 @@ namespace WinSW
string? poststartExecutable = this.config.PoststartExecutable; string? poststartExecutable = this.config.PoststartExecutable;
if (poststartExecutable != null) if (poststartExecutable != null)
{ {
using Process process = this.StartProcess(poststartExecutable, this.config.PoststartArguments, process => using Process process = StartProcessLocked();
{ this.WaitForProcessToExit(process);
this.LogInfo($"Post-start process '{process.Format()}' exited with code {process.ExitCode}."); this.LogInfo($"Post-start process '{process.Format()}' exited with code {process.ExitCode}.");
}); process.StopDescendants(additionalStopTimeout);
this.poststartProcess = null;
Process StartProcessLocked()
{
lock (this)
{
return this.poststartProcess = this.StartProcess(poststartExecutable, this.config.PoststartArguments);
}
}
} }
} }
catch (Exception e) catch (Exception e)
@ -333,63 +344,75 @@ namespace WinSW
/// Called when we are told by Windows SCM to exit. /// Called when we are told by Windows SCM to exit.
/// </summary> /// </summary>
private void DoStop() private void DoStop()
{
try
{ {
string? prestopExecutable = this.config.PrestopExecutable; string? prestopExecutable = this.config.PrestopExecutable;
if (prestopExecutable != null) if (prestopExecutable != null)
{
try
{ {
using Process process = this.StartProcess(prestopExecutable, this.config.PrestopArguments); using Process process = this.StartProcess(prestopExecutable, this.config.PrestopArguments);
this.WaitForProcessToExit(process); this.WaitForProcessToExit(process);
this.LogInfo($"Pre-stop process '{process.Format()}' exited with code {process.ExitCode}."); this.LogInfo($"Pre-stop process '{process.Format()}' exited with code {process.ExitCode}.");
} process.StopDescendants(additionalStopTimeout);
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e); Log.Error(e);
} }
}
string? stopArguments = this.config.StopArguments; string? stopArguments = this.config.StopArguments;
this.LogInfo("Stopping " + this.config.Id); this.LogInfo("Stopping " + this.config.Id);
this.orderlyShutdown = true; this.orderlyShutdown = true;
this.process.EnableRaisingEvents = false;
if (stopArguments is null) if (stopArguments is null)
{ {
Log.Debug("ProcessKill " + this.process.Id); Log.Debug("ProcessKill " + this.process.Id);
ProcessHelper.StopProcessTree(this.process, this.config.StopTimeout); this.process.StopTree(this.config.StopTimeout);
this.ExtensionManager.FireOnProcessTerminated(this.process); this.ExtensionManager.FireOnProcessTerminated(this.process);
} }
else else
{ {
this.SignalPending(); this.SignalPending();
Process stopProcess = new Process();
string stopExecutable = this.config.StopExecutable ?? this.config.Executable; string stopExecutable = this.config.StopExecutable ?? this.config.Executable;
// TODO: Redirect logging to Log4Net once https://github.com/kohsuke/winsw/pull/213 is integrated
this.StartProcess(stopProcess, stopArguments, stopExecutable, null);
Log.Debug("WaitForProcessToExit " + this.process.Id + "+" + stopProcess.Id);
this.WaitForProcessToExit(this.process);
this.WaitForProcessToExit(stopProcess);
}
try try
{ {
// TODO: Redirect logging to Log4Net once https://github.com/kohsuke/winsw/pull/213 is integrated
Process stopProcess = this.StartProcess(stopExecutable, stopArguments);
Log.Debug("WaitForProcessToExit " + this.process.Id + "+" + stopProcess.Id);
this.WaitForProcessToExit(stopProcess);
stopProcess.StopDescendants(additionalStopTimeout);
this.WaitForProcessToExit(this.process);
this.process.StopDescendants(this.config.StopTimeout);
}
catch
{
this.process.StopTree(this.config.StopTimeout);
throw;
}
}
this.poststartProcess?.StopTree(additionalStopTimeout);
string? poststopExecutable = this.config.PoststopExecutable; string? poststopExecutable = this.config.PoststopExecutable;
if (poststopExecutable != null) if (poststopExecutable != null)
{
try
{ {
using Process process = this.StartProcess(poststopExecutable, this.config.PoststopArguments); using Process process = this.StartProcess(poststopExecutable, this.config.PoststopArguments);
this.WaitForProcessToExit(process); this.WaitForProcessToExit(process);
this.LogInfo($"Post-stop process '{process.Format()}' exited with code {process.ExitCode}."); this.LogInfo($"Post-stop process '{process.Format()}' exited with code {process.ExitCode}.");
} process.StopDescendants(additionalStopTimeout);
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e); Log.Error(e);
} }
}
// Stop extensions // Stop extensions
this.ExtensionManager.FireBeforeWrapperStopped(); this.ExtensionManager.FireBeforeWrapperStopped();
@ -447,10 +470,7 @@ namespace WinSW
sc.SetStatus(this.ServiceHandle, ServiceControllerStatus.Stopped); sc.SetStatus(this.ServiceHandle, ServiceControllerStatus.Stopped);
} }
private void StartProcess(Process processToStart, string arguments, string executable, LogHandler? logHandler) private void OnMainProcessExited(Process process)
{
// Define handler of the completed process
void OnProcessCompleted(Process process)
{ {
string display = process.Format(); string display = process.Format();
@ -462,45 +482,71 @@ namespace WinSW
{ {
Log.Warn($"Child process '{display}' finished with code {process.ExitCode}."); Log.Warn($"Child process '{display}' finished with code {process.ExitCode}.");
process.StopDescendants(this.config.StopTimeout);
lock (this)
{
this.poststartProcess?.StopTree(new TimeSpan(TimeSpan.TicksPerMillisecond));
}
// if we finished orderly, report that to SCM. // if we finished orderly, report that to SCM.
// by not reporting unclean shutdown, we let Windows SCM to decide if it wants to // by not reporting unclean shutdown, we let Windows SCM to decide if it wants to
// restart the service automatically // restart the service automatically
try
{
if (process.ExitCode == 0) if (process.ExitCode == 0)
{
try
{ {
this.SignalStopped(); this.SignalStopped();
} }
} catch (Exception e)
finally
{ {
Log.Error(e);
}
}
Environment.Exit(process.ExitCode); Environment.Exit(process.ExitCode);
} }
} }
}
// Invoke process and exit private Process StartProcess(string executable, string? arguments, Action<Process>? onExited = null, LogHandler? logHandler = null)
ProcessHelper.StartProcessAndCallbackForExit(
processToStart: processToStart,
executable: executable,
arguments: arguments,
envVars: this.envs,
workingDirectory: this.config.WorkingDirectory,
priority: this.config.Priority,
onExited: OnProcessCompleted,
logHandler: logHandler,
hideWindow: this.config.HideWindow);
}
private Process StartProcess(string executable, string? arguments, Action<Process>? onExited = null)
{ {
var info = new ProcessStartInfo(executable, arguments) var startInfo = new ProcessStartInfo(executable, arguments)
{ {
UseShellExecute = false, UseShellExecute = false,
WorkingDirectory = this.config.WorkingDirectory, WorkingDirectory = this.config.WorkingDirectory,
CreateNoWindow = this.config.HideWindow,
RedirectStandardOutput = logHandler != null,
RedirectStandardError = logHandler != null,
}; };
Process process = Process.Start(info); Dictionary<string, string> environment = this.config.EnvironmentVariables;
if (environment.Count > 0)
{
var newEnvironment =
#if NETCOREAPP
startInfo.Environment;
#else
startInfo.EnvironmentVariables;
#endif
foreach (KeyValuePair<string, string> pair in environment)
{
newEnvironment[pair.Key] = pair.Value;
}
}
Process process = Process.Start(startInfo);
Log.Info($"Started process {process.Format()}.");
if (this.config.Priority is ProcessPriorityClass priority)
{
process.PriorityClass = priority;
}
if (logHandler != null)
{
logHandler.Log(process.StandardOutput, process.StandardError);
}
if (onExited != null) if (onExited != null)
{ {