From 0a59da5f980ad3f5afa47d50517d35ba5d531f99 Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Sat, 26 Nov 2016 22:35:34 +0100 Subject: [PATCH] [Issue #125] - Introduce the RunawayProcessKiller extension --- src/Core/ServiceWrapper/winsw.csproj | 7 +- .../Extensions/WinSWExtensionManager.cs | 1 + src/Core/WinSWCore/Util/XmlHelper.cs | 2 +- .../Properties/AssemblyInfo.cs | 36 ++++++ .../RunawayProcessKiller.csproj | 72 +++++++++++ .../RunawayProcessKillerExtension.cs | 120 ++++++++++++++++++ .../RunawayProcessKiller/packages.config | 4 + .../RunawayProcessKiller/sampleConfig.xml | 21 +++ .../Extensions/RunawayProcessKillerTest.cs | 63 +++++++++ src/Test/winswTests/winswTests.csproj | 7 +- src/packages/repositories.config | 1 + src/winsw.sln | 13 ++ 12 files changed, 344 insertions(+), 3 deletions(-) create mode 100644 src/Plugins/RunawayProcessKiller/Properties/AssemblyInfo.cs create mode 100644 src/Plugins/RunawayProcessKiller/RunawayProcessKiller.csproj create mode 100644 src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs create mode 100644 src/Plugins/RunawayProcessKiller/packages.config create mode 100644 src/Plugins/RunawayProcessKiller/sampleConfig.xml create mode 100644 src/Test/winswTests/Extensions/RunawayProcessKillerTest.cs 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