Browse Source

Merge pull request #133 from oleg-nenashev/processTerminationOnStartup

[Issue #125] Runaway process termination on startup
Oleg Nenashev 9 years ago
parent
commit
62b495f088

+ 8 - 70
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<int> GetChildPids(int pid)
-        {
-            var searcher = new ManagementObjectSearcher("Select * From Win32_Process Where ParentProcessID=" + pid);
-            var childPids = new List<int>();
-            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);

+ 6 - 1
src/Core/ServiceWrapper/winsw.csproj

@@ -109,6 +109,10 @@
     </BootstrapperPackage>
   </ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="..\..\Plugins\RunawayProcessKiller\RunawayProcessKiller.csproj">
+      <Project>{57284b7a-82a4-407a-b706-ebea6bf8ea13}</Project>
+      <Name>RunawayProcessKiller</Name>
+    </ProjectReference>
     <ProjectReference Include="..\..\Plugins\SharedDirectoryMapper\SharedDirectoryMapper.csproj">
       <Project>{ca5c71db-c5a8-4c27-bf83-8e6daed9d6b5}</Project>
       <Name>SharedDirectoryMapper</Name>
@@ -141,6 +145,7 @@
       <MergeAsm Include="$(OutputPath)$(TargetFileName)" />
       <MergeAsm Include="$(OutputPath)WinSWCore.dll" />
       <MergeAsm Include="$(OutputPath)SharedDirectoryMapper.dll" />
+      <MergeAsm Include="$(OutputPath)RunawayProcessKiller.dll" />
       <MergeAsm Include="$(OutputPath)log4net.dll" />
     </ItemGroup>
     <PropertyGroup>
@@ -171,7 +176,7 @@
       <CertificateTmpPubFile>$(OutputPath)winsw_cert.pub</CertificateTmpPubFile>
     </PropertyGroup>
     <Message Text="Extracting public key from $(AssemblyOriginatorKeyFile)" />
-    <Exec Command="&quot;$(SNPath)&quot; -p &quot;$(AssemblyOriginatorKeyFile)&quot; &quot;$(CertificateTmpPubFile)&quot;" /> 
+    <Exec Command="&quot;$(SNPath)&quot; -p &quot;$(AssemblyOriginatorKeyFile)&quot; &quot;$(CertificateTmpPubFile)&quot;" />
     <Message Text="ILMerge @(MergeAsm) -&gt; $(MergedAssembly)" Importance="high" />
     <ILMerge ToolPath="$(ILMergePath)" InputAssemblies="@(MergeAsm)" OutputFile="$(MergedAssembly)" TargetKind="SameAsPrimaryAssembly" KeyFile="$(CertificateTmpPubFile)" DelaySign="true" />
     <Exec Command="&quot;$(SNPath)&quot; -R &quot;$(MergedAssembly)&quot; &quot;$(AssemblyOriginatorKeyFile)&quot;" />

+ 10 - 0
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
+        }
     }
 }

+ 18 - 1
src/Core/WinSWCore/Extensions/IWinSWExtension.cs

@@ -9,7 +9,8 @@ namespace winsw.Extensions
     /// </summary>
     /// <remarks>
     /// 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.
     /// </remarks>
     public interface IWinSWExtension
     {
@@ -37,6 +38,22 @@ namespace winsw.Extensions
         /// <exception cref="ExtensionException">Any error during execution</exception>
         void OnStart(IEventWriter logger);
 
+        /// <summary>
+        /// Handler, which is being invoked once the child process is started.
+        /// </summary>
+        /// <param name="process">Process</param>
+        /// <param name="logger">Logger</param>
+        /// <exception cref="ExtensionException">Any error during execution</exception>
+        void OnProcessStarted(System.Diagnostics.Process process);
+
+        /// <summary>
+        /// Handler, which is being invoked once the child process is terminated.
+        /// </summary>
+        /// <param name="process">Process</param>
+        /// <param name="logger">Logger</param>
+        /// <exception cref="ExtensionException">Any error during execution</exception>
+        void OnProcessTerminated(System.Diagnostics.Process process);
+
         /// <summary>
         /// Stop handler. Called during stop of the service
         /// </summary>

+ 42 - 0
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<string, IWinSWExtension> 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
             }
         }
 
+        /// <summary>
+        /// Handler, which is being invoked once the child process is started.
+        /// </summary>
+        /// <param name="process">Process</param>
+        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);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Handler, which is being invoked once the child process is terminated.
+        /// </summary>
+        /// <param name="process">Process</param>
+        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);

+ 5 - 1
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);
         }
 
         /// <summary>

+ 109 - 0
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
+{
+    /// <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="pid">Process PID</param>
+        /// <returns>List of child process PIDs</returns>
+        public static List<int> GetChildPids(int pid)
+        {
+            var searcher = new ManagementObjectSearcher("Select * From Win32_Process Where ParentProcessID=" + pid);
+            var childPids = new List<int>();
+            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;
+        }
+
+        /// <summary>
+        /// Stops the process.
+        /// If the process cannot be stopped within the stop timeout, it gets killed
+        /// </summary>
+        /// <param name="pid">PID of the process</param>
+        /// <param name="stopTimeout">Stop timeout</param>
+        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
+        }
+
+        /// <summary>
+        /// Terminate process and its children.
+        /// By default the child processes get terminated first.
+        /// </summary>
+        /// <param name="pid">Process PID</param>
+        /// <param name="stopTimeout">Stop timeout (for each process)</param>
+        /// <param name="stopParentProcessFirst">If enabled, the perent process will be terminated before its children on all levels</param>
+        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
+    }
+}

+ 58 - 0
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);
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        /// <param name="process">The process to attach to and send the SIGINT</param>
+        /// <returns>True if the process shut down successfully to the SIGINT, false if it did not.</returns>
+        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;
+        }
+    }
+}

+ 1 - 1
src/Core/WinSWCore/Util/XmlHelper.cs

@@ -13,7 +13,7 @@ namespace winsw.Util
         /// </summary>
         /// <param name="node">Parent node</param>
         /// <param name="tagName">Element name</param>
-        /// <param name="optional">If otional, don't throw an exception if the elemen is missing</param>
+        /// <param name="optional">If optional, don't throw an exception if the elemen is missing</param>
         /// <returns>String value or null</returns>
         /// <exception cref="InvalidDataException">The required element is missing</exception>
         public static string SingleElement(XmlNode node, string tagName, Boolean optional)

+ 3 - 0
src/Core/WinSWCore/WinSWCore.csproj

@@ -62,8 +62,11 @@
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="ServiceDescriptor.cs" />
     <Compile Include="Util\IEventWriter.cs" />
+    <Compile Include="Util\ProcessHelper.cs" />
+    <Compile Include="Util\SigIntHelper.cs" />
     <Compile Include="Util\XmlHelper.cs" />
     <Compile Include="WinSWException.cs" />
+    <Compile Include="WinSWSystem.cs" />
     <Compile Include="Wmi.cs" />
     <Compile Include="WmiSchema.cs" />
   </ItemGroup>

+ 29 - 0
src/Core/WinSWCore/WinSWSystem.cs

@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace winsw
+{
+    /// <summary>
+    /// Class, which contains generic information about WinSW runtime.
+    /// This information can be used by the service and extensions.
+    /// </summary>
+    public class WinSWSystem
+    {
+        /// <summary>
+        /// Prefix for all environment variables being injected for WinSW
+        /// </summary>
+        public static readonly string SYSTEM_EVNVVAR_PREFIX = "WINSW_";
+
+        /// <summary>
+        /// Variable, which points to the service ID.
+        /// It may be used to determine runaway processes.
+        /// </summary>
+        public static string ENVVAR_NAME_SERVICE_ID { get { return SYSTEM_EVNVVAR_PREFIX + "SERVICE_ID"; } }
+
+        /// <summary>
+        /// Variable, which specifies path to the executable being launched by WinSW.
+        /// </summary>
+        public static string ENVVAR_NAME_EXECUTABLE_PATH { get { return SYSTEM_EVNVVAR_PREFIX + "EXECUTABLE"; } }
+    }
+}

+ 36 - 0
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")]

+ 72 - 0
src/Plugins/RunawayProcessKiller/RunawayProcessKiller.csproj

@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{57284B7A-82A4-407A-B706-EBEA6BF8EA13}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>winsw.Plugins.RunawayProcessKiller</RootNamespace>
+    <AssemblyName>RunawayProcessKiller</AssemblyName>
+    <TargetFrameworkVersion>v2.0</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
+    <RestorePackages>true</RestorePackages>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="log4net">
+      <HintPath>..\..\packages\log4net.2.0.5\lib\net20-full\log4net.dll</HintPath>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="RunawayProcessKillerExtension.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\..\Core\WinSWCore\WinSWCore.csproj">
+      <Project>{9d0c63e2-b6ff-4a85-bd36-b3e5d7f27d06}</Project>
+      <Name>WinSWCore</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config">
+      <SubType>Designer</SubType>
+    </None>
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
+  <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
+    <PropertyGroup>
+      <ErrorText>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}.</ErrorText>
+    </PropertyGroup>
+    <Error Condition="!Exists('$(SolutionDir)\.nuget\NuGet.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\.nuget\NuGet.targets'))" />
+  </Target>
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

+ 155 - 0
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
+    {
+        /// <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; }
+
+        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;
+        }
+
+        /// <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 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);
+        }
+
+        /// <summary>
+        /// Records the started process PID for the future use in OnStart() after the restart.
+        /// </summary>
+        /// <param name="process"></param>
+        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);
+            }
+        }
+    }
+}

+ 4 - 0
src/Plugins/RunawayProcessKiller/packages.config

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="log4net" version="2.0.3" targetFramework="net20" />
+</packages>

+ 21 - 0
src/Plugins/RunawayProcessKiller/sampleConfig.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<service>
+  <id>SERVICE_NAME</id>
+  <name>Jenkins Slave</name>
+  <description>This service runs a slave for Jenkins continuous integration system.</description>
+  <executable>C:\Program Files\Java\jre7\bin\java.exe</executable>
+  <arguments>-Xrs -jar "%BASE%\slave.jar" -jnlpUrl ...</arguments>
+  <logmode>rotate</logmode>
+
+  <extensions>
+	<!-- This is a sample configuration for the RunawayProcessKiller extension. -->
+    <extension enabled="true" className="winsw.Plugins.RunawayProcessKiller.RunawayProcessKillerExtension" id="killOnStartup">
+      <!-- Absolute path to the PID file, which stores ID of the previously launched process. -->
+		  <pidfile>%BASE%\pid.txt</pidfile>
+      <!-- Defines the process termination timeout in milliseconds. This timeout will be applied multiple times for each child process. -->
+      <stopTimeout>5000</stopTimeout>
+      <!-- If true, the parent process will be terminated first if the runaway process gets terminated. -->
+      <stopParentFirst>false</stopParentFirst>
+    </extension>
+  </extensions>
+</service>

+ 63 - 0
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 = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
+                + "<service>                                                                                                        "
+                + "  <id>SERVICE_NAME</id>                                                                                          "
+                + "  <name>Jenkins Slave</name>                                                                                     "
+                + "  <description>This service runs a slave for Jenkins continuous integration system.</description>                "
+                + "  <executable>C:\\Program Files\\Java\\jre7\\bin\\java.exe</executable>                                               "
+                + "  <arguments>-Xrs  -jar \\\"%BASE%\\slave.jar\\\" -jnlpUrl ...</arguments>                                              "
+                + "  <logmode>rotate</logmode>                                                                                      "
+                + "  <extensions>                                                                                                   "
+                + "    <extension enabled=\"true\" className=\"" + testExtension + "\" id=\"mapNetworDirs\"> "
+                + "      <pidfile>foo/bar/pid.txt</pidfile>"
+                + "      <stopTimeout>5000</stopTimeout> "
+                + "      <stopParentFirst>true</stopParentFirst>"
+                + "    </extension>         "
+                + "  </extensions>                                                                                                  "
+                + "</service>";
+            _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);
+        }
+    }
+}

+ 6 - 1
src/Test/winswTests/winswTests.csproj

@@ -37,7 +37,7 @@
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
   </PropertyGroup>
-  <ItemGroup> 
+  <ItemGroup>
     <Reference Include="JetBrains.Annotations, Version=8.0.5.0, Culture=neutral, PublicKeyToken=1010a0d8d6380325, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
       <HintPath>..\..\packages\JetBrains.Annotations.8.0.5.0\lib\net20\JetBrains.Annotations.dll</HintPath>
@@ -52,6 +52,7 @@
     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="Extensions\RunawayProcessKillerTest.cs" />
     <Compile Include="Extensions\WinSWExtensionManagerTest.cs" />
     <Compile Include="MainTest.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
@@ -68,6 +69,10 @@
       <Project>{9d0c63e2-b6ff-4a85-bd36-b3e5d7f27d06}</Project>
       <Name>WinSWCore</Name>
     </ProjectReference>
+    <ProjectReference Include="..\..\Plugins\RunawayProcessKiller\RunawayProcessKiller.csproj">
+      <Project>{57284b7a-82a4-407a-b706-ebea6bf8ea13}</Project>
+      <Name>RunawayProcessKiller</Name>
+    </ProjectReference>
     <ProjectReference Include="..\..\Plugins\SharedDirectoryMapper\SharedDirectoryMapper.csproj">
       <Project>{ca5c71db-c5a8-4c27-bf83-8e6daed9d6b5}</Project>
       <Name>SharedDirectoryMapper</Name>

+ 1 - 0
src/packages/repositories.config

@@ -2,5 +2,6 @@
 <repositories>
   <repository path="..\Core\ServiceWrapper\packages.config" />
   <repository path="..\Core\WinSWCore\packages.config" />
+  <repository path="..\Plugins\RunawayProcessKiller\packages.config" />
   <repository path="..\Test\winswTests\packages.config" />
 </repositories>

+ 13 - 0
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