winsw/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtensi...

194 lines
7.9 KiB
C#

using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Xml;
using log4net;
using winsw.Extensions;
using winsw.Util;
namespace winsw.Plugins.RunawayProcessKiller
{
public class RunawayProcessKillerExtension : AbstractWinSWExtension
{
/// <summary>
/// Absolute path to the PID file, which stores ID of the previously launched process.
/// </summary>
public String Pidfile { get; private set; }
/// <summary>
/// Defines the process termination timeout in milliseconds.
/// This timeout will be applied multiple times for each child process.
/// </summary>
public TimeSpan StopTimeout { get; private set; }
/// <summary>
/// If true, the parent process will be terminated first if the runaway process gets terminated.
/// </summary>
public bool StopParentProcessFirst { get; private set; }
/// <summary>
/// If true, the runaway process will be checked for the WinSW environment variable before termination.
/// This option is not documented AND not supposed to be used by users.
/// </summary>
public bool CheckWinSWEnvironmentVariable { get; private set; }
public override String DisplayName => "Runaway Process Killer";
private String ServiceId { get; set; }
private static readonly ILog Logger = LogManager.GetLogger(typeof(RunawayProcessKillerExtension));
public RunawayProcessKillerExtension()
{
// Default initializer
}
public RunawayProcessKillerExtension(String pidfile, int stopTimeoutMs = 5000, bool stopParentFirst = false, bool checkWinSWEnvironmentVariable = true)
{
this.Pidfile = pidfile;
this.StopTimeout = TimeSpan.FromMilliseconds(stopTimeoutMs);
this.StopParentProcessFirst = stopParentFirst;
this.CheckWinSWEnvironmentVariable = checkWinSWEnvironmentVariable;
}
public override void Configure(ServiceDescriptor descriptor, XmlNode node)
{
// We expect the upper logic to process any errors
// TODO: a better parser API for types would be useful
Pidfile = XmlHelper.SingleElement(node, "pidfile", false);
StopTimeout = TimeSpan.FromMilliseconds(Int32.Parse(XmlHelper.SingleElement(node, "stopTimeout", false)));
StopParentProcessFirst = Boolean.Parse(XmlHelper.SingleElement(node, "stopParentFirst", false));
ServiceId = descriptor.Id;
// TODO: Consider making it documented
var checkWinSWEnvironmentVariable = XmlHelper.SingleElement(node, "checkWinSWEnvironmentVariable", true);
CheckWinSWEnvironmentVariable = checkWinSWEnvironmentVariable != null ? Boolean.Parse(checkWinSWEnvironmentVariable) : true;
}
/// <summary>
/// This method checks if the PID file is stored on the disk and then terminates runaway processes if they exist.
/// </summary>
/// <param name="logger">Unused logger</param>
public override void OnWrapperStarted()
{
// Read PID file from the disk
int pid;
if (File.Exists(Pidfile))
{
string pidstring;
try
{
pidstring = File.ReadAllText(Pidfile);
}
catch (Exception ex)
{
Logger.Error("Cannot read PID file from " + Pidfile, ex);
return;
}
try
{
pid = Int32.Parse(pidstring);
}
catch (FormatException e)
{
Logger.Error("Invalid PID file number in '" + Pidfile + "'. The runaway process won't be checked", e);
return;
}
}
else
{
Logger.Warn("The requested PID file '" + Pidfile + "' does not exist. The runaway process won't be checked");
return;
}
// Now check the process
Logger.DebugFormat("Checking the potentially runaway process with PID={0}", pid);
Process proc;
try
{
proc = Process.GetProcessById(pid);
}
catch (ArgumentException)
{
Logger.Debug("No runaway process with PID=" + pid + ". The process has been already stopped.");
return;
}
// Ensure the process references the service
String affiliatedServiceId;
// TODO: This method is not ideal since it works only for vars explicitly mentioned in the start info
// No Windows 10- compatible solution for EnvVars retrieval, see https://blog.gapotchenko.com/eazfuscator.net/reading-environment-variables
StringDictionary previousProcessEnvVars = proc.StartInfo.EnvironmentVariables;
String expectedEnvVarName = WinSWSystem.ENVVAR_NAME_SERVICE_ID;
if (previousProcessEnvVars.ContainsKey(expectedEnvVarName))
{
// StringDictionary is case-insensitive, hence it will fetch variable definitions in any case
affiliatedServiceId = previousProcessEnvVars[expectedEnvVarName];
}
else if (CheckWinSWEnvironmentVariable)
{
Logger.Warn("The process " + pid + " has no " + expectedEnvVarName + " environment variable defined. "
+ "The process has not been started by WinSW, hence it won't be terminated.");
if (Logger.IsDebugEnabled)
{
// TODO replace by String.Join() in .NET 4
String[] keys = new String[previousProcessEnvVars.Count];
previousProcessEnvVars.Keys.CopyTo(keys, 0);
Logger.DebugFormat("Env vars of the process with PID={0}: {1}", new Object[] { pid, String.Join(",", keys) });
}
return;
}
else
{
// We just skip this check
affiliatedServiceId = null;
}
// Check the service ID value
if (CheckWinSWEnvironmentVariable && !ServiceId.Equals(affiliatedServiceId))
{
Logger.Warn("The process " + pid + " has been started by Windows service with ID='" + affiliatedServiceId + "'. "
+ "It is another service (current service id is '" + ServiceId + "'), hence the process won't be terminated.");
return;
}
// Kill the runaway process
StringBuilder bldr = new StringBuilder("Stopping the runaway process (pid=");
bldr.Append(pid);
bldr.Append(") and its children. Environment was ");
if (!CheckWinSWEnvironmentVariable)
{
bldr.Append("not ");
}
bldr.Append("checked, affiliated service ID: ");
bldr.Append(affiliatedServiceId ?? "undefined");
bldr.Append(", process to kill: ");
bldr.Append(proc);
Logger.Warn(bldr.ToString());
ProcessHelper.StopProcessAndChildren(pid, this.StopTimeout, this.StopParentProcessFirst);
}
/// <summary>
/// Records the started process PID for the future use in OnStart() after the restart.
/// </summary>
/// <param name="process"></param>
public override void OnProcessStarted(Process process)
{
Logger.Info("Recording PID of the started process:" + process.Id + ". PID file destination is " + Pidfile);
try
{
File.WriteAllText(Pidfile, process.Id.ToString());
}
catch (Exception ex)
{
Logger.Error("Cannot update the PID file " + Pidfile, ex);
}
}
}
}