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/WinSWExtensionManager.cs b/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs
index a82213a..b51f3a0 100644
--- a/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs
+++ b/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs
@@ -120,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/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/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..b6e7133
--- /dev/null
+++ b/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.Xml;
+using System.Diagnostics;
+using winsw.Extensions;
+using winsw.Util;
+using log4net;
+
+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 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));
+ }
+
+ ///
+ /// 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;
+ }
+
+ // Kill the runaway process
+ 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