diff --git a/src/Core/ServiceWrapper/Main.cs b/src/Core/ServiceWrapper/Main.cs index 4fc66c8..977bc31 100644 --- a/src/Core/ServiceWrapper/Main.cs +++ b/src/Core/ServiceWrapper/Main.cs @@ -269,6 +269,7 @@ namespace winsw WriteEvent("Starting " + _descriptor.Executable + ' ' + startarguments); StartProcess(_process, startarguments, _descriptor.Executable); + ExtensionManager.FireOnProcessStarted(_process); // send stdout and stderr to its respective output file. HandleLogfiles(); @@ -320,7 +321,8 @@ namespace winsw try { WriteEvent("ProcessKill " + _process.Id); - StopProcessAndChildren(_process.Id); + ProcessHelper.StopProcessAndChildren(_process.Id, _descriptor.StopTimeout, _descriptor.StopParentProcessFirst); + ExtensionManager.FireOnProcessTerminated(_process); } catch (InvalidOperationException) { @@ -368,75 +370,6 @@ namespace winsw WriteEvent("Finished " + _descriptor.Id); } - private void StopProcessAndChildren(int pid) - { - var childPids = GetChildPids(pid); - - if (_descriptor.StopParentProcessFirst) - { - StopProcess(pid); - foreach (var childPid in childPids) - { - StopProcessAndChildren(childPid); - } - } - else - { - foreach (var childPid in childPids) - { - StopProcessAndChildren(childPid); - } - StopProcess(pid); - } - } - - private List GetChildPids(int pid) - { - var searcher = new ManagementObjectSearcher("Select * From Win32_Process Where ParentProcessID=" + pid); - var childPids = new List(); - foreach (var mo in searcher.Get()) - { - var childProcessId = mo["ProcessID"]; - WriteEvent("Found child process: " + childProcessId + " Name: " + mo["Name"]); - childPids.Add(Convert.ToInt32(childProcessId)); - } - return childPids; - } - - private void StopProcess(int pid) - { - WriteEvent("Stopping process " + pid); - Process proc; - try - { - proc = Process.GetProcessById(pid); - } - catch (ArgumentException) - { - WriteEvent("Process " + pid + " is already stopped"); - return; - } - - WriteEvent("Send SIGINT " + pid); - bool successful = SigIntHelper.SendSIGINTToProcess(proc, _descriptor.StopTimeout); - if (successful) - { - WriteEvent("SIGINT to" + pid + " successful"); - } - else - { - try - { - WriteEvent("SIGINT to " + pid + " failed - Killing as fallback", Level.Warn); - proc.Kill(); - } - catch (ArgumentException) - { - // Process already exited. - } - } - } - private void WaitForProcessToExit(Process processoWait) { SignalShutdownPending(); @@ -516,8 +449,13 @@ namespace winsw ps.RedirectStandardError = true; foreach (string key in _envs.Keys) + { Environment.SetEnvironmentVariable(key, _envs[key]); // ps.EnvironmentVariables[key] = envs[key]; // bugged (lower cases all variable names due to StringDictionary being used, see http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=326163) + } + + // TODO: Make it generic via extension points. The issue mentioned above should be ideally worked around somehow + ps.EnvironmentVariables[WinSWSystem.ENVVAR_NAME_SERVICE_ID.ToLower()] = _descriptor.Id; processToStart.Start(); WriteEvent("Started " + processToStart.Id); diff --git a/src/Core/ServiceWrapper/winsw.csproj b/src/Core/ServiceWrapper/winsw.csproj index c13d484..ad303bd 100644 --- a/src/Core/ServiceWrapper/winsw.csproj +++ b/src/Core/ServiceWrapper/winsw.csproj @@ -109,6 +109,10 @@ + + {57284b7a-82a4-407a-b706-ebea6bf8ea13} + RunawayProcessKiller + {ca5c71db-c5a8-4c27-bf83-8e6daed9d6b5} SharedDirectoryMapper @@ -141,6 +145,7 @@ + @@ -171,7 +176,7 @@ $(OutputPath)winsw_cert.pub - + diff --git a/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs b/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs index acba93c..d1b0c40 100644 --- a/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs +++ b/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs @@ -23,5 +23,15 @@ namespace winsw.Extensions { // Do nothing } + + public virtual void OnProcessStarted(System.Diagnostics.Process process) + { + // Do nothing + } + + public virtual void OnProcessTerminated(System.Diagnostics.Process process) + { + // Do nothing + } } } diff --git a/src/Core/WinSWCore/Extensions/IWinSWExtension.cs b/src/Core/WinSWCore/Extensions/IWinSWExtension.cs index c114381..f3e1ef1 100644 --- a/src/Core/WinSWCore/Extensions/IWinSWExtension.cs +++ b/src/Core/WinSWCore/Extensions/IWinSWExtension.cs @@ -9,7 +9,8 @@ namespace winsw.Extensions /// /// /// All implementations should provide the default empty constructor. - /// The initialization will be performed by Init methods + /// The initialization will be performed by Init methods. + /// Binary comparibility of the class is not guaranteed in WinSW 2. /// public interface IWinSWExtension { @@ -37,6 +38,22 @@ namespace winsw.Extensions /// Any error during execution void OnStart(IEventWriter logger); + /// + /// Handler, which is being invoked once the child process is started. + /// + /// Process + /// Logger + /// Any error during execution + void OnProcessStarted(System.Diagnostics.Process process); + + /// + /// Handler, which is being invoked once the child process is terminated. + /// + /// Process + /// Logger + /// Any error during execution + void OnProcessTerminated(System.Diagnostics.Process process); + /// /// Stop handler. Called during stop of the service /// diff --git a/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs b/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs index c305409..b51f3a0 100644 --- a/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs +++ b/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs @@ -4,6 +4,7 @@ using System.Xml; using System.Reflection; using System.Diagnostics; using winsw.Util; +using log4net; namespace winsw.Extensions { @@ -12,6 +13,8 @@ namespace winsw.Extensions public Dictionary Extensions { private set; get; } public ServiceDescriptor ServiceDescriptor { private set; get; } + private static readonly ILog Log = LogManager.GetLogger(typeof(WinSWExtensionManager)); + public WinSWExtensionManager(ServiceDescriptor serviceDescriptor) { ServiceDescriptor = serviceDescriptor; @@ -42,6 +45,44 @@ namespace winsw.Extensions } } + /// + /// Handler, which is being invoked once the child process is started. + /// + /// Process + public void FireOnProcessStarted(System.Diagnostics.Process process) + { + foreach (var ext in Extensions) + { + try + { + ext.Value.OnProcessStarted(process); + } + catch (ExtensionException ex) + { + Log.Error("onProcessStarted() handler failed for " + ext.Value.DisplayName, ex); + } + } + } + + /// + /// Handler, which is being invoked once the child process is terminated. + /// + /// Process + public void FireOnProcessTerminated(System.Diagnostics.Process process) + { + foreach (var ext in Extensions) + { + try + { + ext.Value.OnProcessTerminated(process); + } + catch (ExtensionException ex) + { + Log.Error("onProcessTerminated() handler failed for " + ext.Value.DisplayName, ex); + } + } + } + //TODO: Implement loading of external extensions. Current version supports internal hack #region Extension load management @@ -79,6 +120,7 @@ namespace winsw.Extensions { IWinSWExtension extension = CreateExtensionInstance(descriptor.Id, descriptor.ClassName); extension.Descriptor = descriptor; + //TODO: Handle exceptions extension.Configure(ServiceDescriptor, configNode, logger); Extensions.Add(id, extension); logger.LogEvent("Extension loaded: "+id, EventLogEntryType.Information); diff --git a/src/Core/WinSWCore/ServiceDescriptor.cs b/src/Core/WinSWCore/ServiceDescriptor.cs index 11da27b..8dc3c40 100755 --- a/src/Core/WinSWCore/ServiceDescriptor.cs +++ b/src/Core/WinSWCore/ServiceDescriptor.cs @@ -72,7 +72,11 @@ namespace winsw Environment.SetEnvironmentVariable("BASE", d.FullName); // ditto for ID Environment.SetEnvironmentVariable("SERVICE_ID", Id); - Environment.SetEnvironmentVariable("WINSW_EXECUTABLE", ExecutablePath); + + // New name + Environment.SetEnvironmentVariable(WinSWSystem.ENVVAR_NAME_EXECUTABLE_PATH, ExecutablePath); + // Also inject system environment variables + Environment.SetEnvironmentVariable(WinSWSystem.ENVVAR_NAME_SERVICE_ID, Id); } /// diff --git a/src/Core/WinSWCore/Util/ProcessHelper.cs b/src/Core/WinSWCore/Util/ProcessHelper.cs new file mode 100644 index 0000000..0decf70 --- /dev/null +++ b/src/Core/WinSWCore/Util/ProcessHelper.cs @@ -0,0 +1,109 @@ +using log4net; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Management; +using System.Text; + +namespace winsw.Util +{ + /// + /// Provides helper classes for Process Management + /// + /// Since WinSW 2.0 + public class ProcessHelper + { + private static readonly ILog Logger = LogManager.GetLogger(typeof(ProcessHelper)); + + /// + /// Gets all children of the specified process. + /// + /// Process PID + /// List of child process PIDs + public static List GetChildPids(int pid) + { + var searcher = new ManagementObjectSearcher("Select * From Win32_Process Where ParentProcessID=" + pid); + var childPids = new List(); + foreach (var mo in searcher.Get()) + { + var childProcessId = mo["ProcessID"]; + Logger.Info("Found child process: " + childProcessId + " Name: " + mo["Name"]); + childPids.Add(Convert.ToInt32(childProcessId)); + } + return childPids; + } + + /// + /// Stops the process. + /// If the process cannot be stopped within the stop timeout, it gets killed + /// + /// PID of the process + /// Stop timeout + public static void StopProcess(int pid, TimeSpan stopTimeout) + { + Logger.Info("Stopping process " + pid); + Process proc; + try + { + proc = Process.GetProcessById(pid); + } + catch (ArgumentException ex) + { + Logger.Info("Process " + pid + " is already stopped", ex); + return; + } + + Logger.Info("Send SIGINT " + pid); + bool successful = SigIntHelper.SendSIGINTToProcess(proc, stopTimeout); + if (successful) + { + Logger.Info("SIGINT to" + pid + " successful"); + } + else + { + try + { + Logger.Warn("SIGINT to " + pid + " failed - Killing as fallback"); + proc.Kill(); + } + catch (ArgumentException) + { + // Process already exited. + } + } + + //TODO: Propagate error if process kill fails? Currently we use the legacy behavior + } + + /// + /// Terminate process and its children. + /// By default the child processes get terminated first. + /// + /// Process PID + /// Stop timeout (for each process) + /// If enabled, the perent process will be terminated before its children on all levels + public static void StopProcessAndChildren(int pid, TimeSpan stopTimeout, bool stopParentProcessFirst) + { + var childPids = GetChildPids(pid); + + if (stopParentProcessFirst) + { + StopProcess(pid, stopTimeout); + foreach (var childPid in childPids) + { + StopProcessAndChildren(childPid, stopTimeout, stopParentProcessFirst); + } + } + else + { + foreach (var childPid in childPids) + { + StopProcessAndChildren(childPid, stopTimeout, stopParentProcessFirst); + } + StopProcess(pid, stopTimeout); + } + } + + // TODO: Also move StartProcess methods once LogEvent()/WriteEvent() mess gets solved + } +} diff --git a/src/Core/WinSWCore/Util/SigIntHelper.cs b/src/Core/WinSWCore/Util/SigIntHelper.cs new file mode 100644 index 0000000..87e19f6 --- /dev/null +++ b/src/Core/WinSWCore/Util/SigIntHelper.cs @@ -0,0 +1,58 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace winsw.Util +{ + public static class SigIntHelper + { + private const string KERNEL32 = "kernel32.dll"; + + [DllImport(KERNEL32, SetLastError = true)] + private static extern bool AttachConsole(uint dwProcessId); + + [DllImport(KERNEL32, SetLastError = true, ExactSpelling = true)] + private static extern bool FreeConsole(); + + [DllImport(KERNEL32)] + private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate HandlerRoutine, bool Add); + // Delegate type to be used as the Handler Routine for SCCH + private delegate Boolean ConsoleCtrlDelegate(CtrlTypes CtrlType); + + // Enumerated type for the control messages sent to the handler routine + private enum CtrlTypes : uint + { + CTRL_C_EVENT = 0, + CTRL_BREAK_EVENT, + CTRL_CLOSE_EVENT, + CTRL_LOGOFF_EVENT = 5, + CTRL_SHUTDOWN_EVENT + } + + [DllImport(KERNEL32)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId); + + /// + /// Uses the native funciton "AttachConsole" to attach the thread to the executing process to try to trigger a CTRL_C event (SIGINT). If the application + /// doesn't honor the event and shut down gracefully, the. wait period will time out after 15 seconds. + /// + /// The process to attach to and send the SIGINT + /// True if the process shut down successfully to the SIGINT, false if it did not. + public static bool SendSIGINTToProcess(Process process, TimeSpan shutdownTimeout) + { + if (AttachConsole((uint)process.Id)) + { + //Disable Ctrl-C handling for our program + SetConsoleCtrlHandler(null, true); + GenerateConsoleCtrlEvent(CtrlTypes.CTRL_C_EVENT, 0); + + process.WaitForExit((int)shutdownTimeout.TotalMilliseconds); + + return process.HasExited; + } + + return false; + } + } +} diff --git a/src/Core/WinSWCore/Util/XmlHelper.cs b/src/Core/WinSWCore/Util/XmlHelper.cs index 687a8d2..7eab4db 100644 --- a/src/Core/WinSWCore/Util/XmlHelper.cs +++ b/src/Core/WinSWCore/Util/XmlHelper.cs @@ -13,7 +13,7 @@ namespace winsw.Util /// /// Parent node /// Element name - /// If otional, don't throw an exception if the elemen is missing + /// If optional, don't throw an exception if the elemen is missing /// String value or null /// The required element is missing public static string SingleElement(XmlNode node, string tagName, Boolean optional) diff --git a/src/Core/WinSWCore/WinSWCore.csproj b/src/Core/WinSWCore/WinSWCore.csproj index 6ea87bb..48f925d 100644 --- a/src/Core/WinSWCore/WinSWCore.csproj +++ b/src/Core/WinSWCore/WinSWCore.csproj @@ -62,8 +62,11 @@ + + + diff --git a/src/Core/WinSWCore/WinSWSystem.cs b/src/Core/WinSWCore/WinSWSystem.cs new file mode 100644 index 0000000..2e853f3 --- /dev/null +++ b/src/Core/WinSWCore/WinSWSystem.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace winsw +{ + /// + /// Class, which contains generic information about WinSW runtime. + /// This information can be used by the service and extensions. + /// + public class WinSWSystem + { + /// + /// Prefix for all environment variables being injected for WinSW + /// + public static readonly string SYSTEM_EVNVVAR_PREFIX = "WINSW_"; + + /// + /// Variable, which points to the service ID. + /// It may be used to determine runaway processes. + /// + public static string ENVVAR_NAME_SERVICE_ID { get { return SYSTEM_EVNVVAR_PREFIX + "SERVICE_ID"; } } + + /// + /// Variable, which specifies path to the executable being launched by WinSW. + /// + public static string ENVVAR_NAME_EXECUTABLE_PATH { get { return SYSTEM_EVNVVAR_PREFIX + "EXECUTABLE"; } } + } +} diff --git a/src/Plugins/RunawayProcessKiller/Properties/AssemblyInfo.cs b/src/Plugins/RunawayProcessKiller/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a0f90d5 --- /dev/null +++ b/src/Plugins/RunawayProcessKiller/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("RunawayProcessKiller")] +[assembly: AssemblyDescription("Kills runaway process on startup")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RunawayProcessKiller")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d962c792-b900-4e60-8ae6-6c8d05b23a61")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Plugins/RunawayProcessKiller/RunawayProcessKiller.csproj b/src/Plugins/RunawayProcessKiller/RunawayProcessKiller.csproj new file mode 100644 index 0000000..e24e615 --- /dev/null +++ b/src/Plugins/RunawayProcessKiller/RunawayProcessKiller.csproj @@ -0,0 +1,72 @@ + + + + + Debug + AnyCPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13} + Library + Properties + winsw.Plugins.RunawayProcessKiller + RunawayProcessKiller + v2.0 + 512 + ..\..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\log4net.2.0.5\lib\net20-full\log4net.dll + + + + + + + + + + + + {9d0c63e2-b6ff-4a85-bd36-b3e5d7f27d06} + WinSWCore + + + + + Designer + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs b/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs new file mode 100644 index 0000000..1e436f3 --- /dev/null +++ b/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using winsw.Extensions; +using winsw.Util; +using log4net; +using System.Collections.Specialized; + +namespace winsw.Plugins.RunawayProcessKiller +{ + public class RunawayProcessKillerExtension : AbstractWinSWExtension + { + /// + /// Absolute path to the PID file, which stores ID of the previously launched process. + /// + public String Pidfile { get; private set; } + + /// + /// Defines the process termination timeout in milliseconds. + /// This timeout will be applied multiple times for each child process. + /// + public TimeSpan StopTimeout { get; private set; } + + /// + /// If true, the parent process will be terminated first if the runaway process gets terminated. + /// + public bool StopParentProcessFirst { get; private set; } + + public override String DisplayName { get { return "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) + { + this.Pidfile = pidfile; + } + + public override void Configure(ServiceDescriptor descriptor, XmlNode node, IEventWriter logger) + { + // 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; + } + + /// + /// This method checks if the PID file is stored on the disk and then terminates runaway processes if they exist. + /// + /// Unused logger + public override void OnStart(IEventWriter logger) + { + // Read PID file from the disk + int pid; + if (System.IO.File.Exists(Pidfile)) { + string pidstring; + try + { + pidstring = System.IO.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 + Process proc; + try + { + proc = Process.GetProcessById(pid); + } + catch (ArgumentException ex) + { + 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.ToLower(); + if (previousProcessEnvVars.ContainsKey(expectedEnvVarName)) + { + affiliatedServiceId = previousProcessEnvVars[expectedEnvVarName]; + } + else + { + Logger.Warn("The process " + pid + " has no " + expectedEnvVarName + " environment variable defined. " + + "The process has not been started by this service, hence it won't be terminated."); + if (Logger.IsDebugEnabled) { + foreach (string key in previousProcessEnvVars.Keys) { + Logger.Debug("Env var of " + pid + ": " + key + "=" + previousProcessEnvVars[key]); + } + } + return; + } + + // Check the service ID value + if (!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 + Logger.Warn("Stopping the runaway process (pid=" + pid + ") and its children."); + ProcessHelper.StopProcessAndChildren(pid, this.StopTimeout, this.StopParentProcessFirst); + } + + /// + /// Records the started process PID for the future use in OnStart() after the restart. + /// + /// + public override void OnProcessStarted(System.Diagnostics.Process process) + { + Logger.Info("Recording PID of the started process:" + process.Id + ". PID file destination is " + Pidfile); + try + { + System.IO.File.WriteAllText(Pidfile, process.Id.ToString()); + } + catch (Exception ex) + { + Logger.Error("Cannot update the PID file " + Pidfile, ex); + } + } + } +} diff --git a/src/Plugins/RunawayProcessKiller/packages.config b/src/Plugins/RunawayProcessKiller/packages.config new file mode 100644 index 0000000..7a2c004 --- /dev/null +++ b/src/Plugins/RunawayProcessKiller/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Plugins/RunawayProcessKiller/sampleConfig.xml b/src/Plugins/RunawayProcessKiller/sampleConfig.xml new file mode 100644 index 0000000..24514bd --- /dev/null +++ b/src/Plugins/RunawayProcessKiller/sampleConfig.xml @@ -0,0 +1,21 @@ + + + SERVICE_NAME + Jenkins Slave + This service runs a slave for Jenkins continuous integration system. + C:\Program Files\Java\jre7\bin\java.exe + -Xrs -jar "%BASE%\slave.jar" -jnlpUrl ... + rotate + + + + + + %BASE%\pid.txt + + 5000 + + false + + + diff --git a/src/Test/winswTests/Extensions/RunawayProcessKillerTest.cs b/src/Test/winswTests/Extensions/RunawayProcessKillerTest.cs new file mode 100644 index 0000000..e979bfa --- /dev/null +++ b/src/Test/winswTests/Extensions/RunawayProcessKillerTest.cs @@ -0,0 +1,63 @@ +using winsw; +using NUnit.Framework; +using winsw.Extensions; +using winsw.Plugins.SharedDirectoryMapper; +using winswTests.util; +using winsw.Plugins.RunawayProcessKiller; + +namespace winswTests.extensions +{ + [TestFixture] + class RunawayProcessKillerExtensionTest + { + ServiceDescriptor _testServiceDescriptor; + readonly TestLogger _logger = new TestLogger(); + + [SetUp] + public void SetUp() + { + string testExtension = typeof (RunawayProcessKillerExtension).ToString(); + string seedXml = "" + + " " + + " SERVICE_NAME " + + " Jenkins Slave " + + " This service runs a slave for Jenkins continuous integration system. " + + " C:\\Program Files\\Java\\jre7\\bin\\java.exe " + + " -Xrs -jar \\\"%BASE%\\slave.jar\\\" -jnlpUrl ... " + + " rotate " + + " " + + " " + + " foo/bar/pid.txt" + + " 5000 " + + " true" + + " " + + " " + + ""; + _testServiceDescriptor = ServiceDescriptor.FromXML(seedXml); + } + + [Test] + public void LoadExtensions() + { + WinSWExtensionManager manager = new WinSWExtensionManager(_testServiceDescriptor); + manager.LoadExtensions(_logger); + Assert.AreEqual(1, manager.Extensions.Count, "One extension should be loaded"); + + // Check the file is correct + var extension = manager.Extensions[typeof(RunawayProcessKillerExtension).ToString()] as RunawayProcessKillerExtension; + Assert.IsNotNull(extension, "RunawayProcessKillerExtension should be loaded"); + Assert.AreEqual("foo/bar/pid.txt", extension.Pidfile, "Loaded PID file path is not equal to the expected one"); + Assert.AreEqual(5000, extension.StopTimeout.TotalMilliseconds, "Loaded Stop Timeout is not equal to the expected one"); + Assert.AreEqual(true, extension.StopParentProcessFirst, "Loaded StopParentFirst is not equal to the expected one"); + } + + [Test] + public void StartStopExtension() + { + WinSWExtensionManager manager = new WinSWExtensionManager(_testServiceDescriptor); + manager.LoadExtensions(_logger); + manager.OnStart(_logger); + manager.OnStop(_logger); + } + } +} diff --git a/src/Test/winswTests/winswTests.csproj b/src/Test/winswTests/winswTests.csproj index 4818a3c..18e0aaf 100644 --- a/src/Test/winswTests/winswTests.csproj +++ b/src/Test/winswTests/winswTests.csproj @@ -37,7 +37,7 @@ prompt 4 - + False ..\..\packages\JetBrains.Annotations.8.0.5.0\lib\net20\JetBrains.Annotations.dll @@ -52,6 +52,7 @@ + @@ -68,6 +69,10 @@ {9d0c63e2-b6ff-4a85-bd36-b3e5d7f27d06} WinSWCore + + {57284b7a-82a4-407a-b706-ebea6bf8ea13} + RunawayProcessKiller + {ca5c71db-c5a8-4c27-bf83-8e6daed9d6b5} SharedDirectoryMapper diff --git a/src/packages/repositories.config b/src/packages/repositories.config index d72f1c5..7125d88 100644 --- a/src/packages/repositories.config +++ b/src/packages/repositories.config @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/src/winsw.sln b/src/winsw.sln index 95098b0..6880679 100644 --- a/src/winsw.sln +++ b/src/winsw.sln @@ -29,6 +29,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".build", ".build", "{D88064 .build\MSBuild.Community.Tasks.targets = .build\MSBuild.Community.Tasks.targets EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RunawayProcessKiller", "Plugins\RunawayProcessKiller\RunawayProcessKiller.csproj", "{57284B7A-82A4-407A-B706-EBEA6BF8EA13}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -87,6 +89,16 @@ Global {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Release|Mixed Platforms.Build.0 = Release|Any CPU {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Release|Win32.ActiveCfg = Release|Any CPU {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Release|Win32.Build.0 = Release|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Debug|Win32.ActiveCfg = Debug|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Release|Any CPU.Build.0 = Release|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Release|Win32.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -96,5 +108,6 @@ Global {93843402-842B-44B4-B303-AEE829BE0B43} = {077C2CEC-B687-4B53-86E9-C1A1BF5554E5} {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5} = {BC4AD891-E87E-4F30-867C-FD8084A29E5D} {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06} = {5297623A-1A95-4F89-9AAE-DA634081EC86} + {57284B7A-82A4-407A-B706-EBEA6BF8EA13} = {BC4AD891-E87E-4F30-867C-FD8084A29E5D} EndGlobalSection EndGlobal