[Issue #125] - Introduce the RunawayProcessKiller extension

pull/133/head
Oleg Nenashev 2016-11-26 22:35:34 +01:00
parent 24a5e93b67
commit 0a59da5f98
12 changed files with 344 additions and 3 deletions

View File

@ -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;" />

View File

@ -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);

View File

@ -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)

View File

@ -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")]

View File

@ -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>

View File

@ -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
{
/// <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 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));
}
/// <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;
}
// Kill the runaway process
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);
}
}
}
}

View File

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

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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