Browse Source

Remove Runaway Process Killer

NextTurn 5 years ago
parent
commit
d90151b02f

+ 0 - 1
docs/extensions/extensions.md

@@ -6,7 +6,6 @@ These extensions allow to alter the behavior of the Windows service in order to
 ## Available extensions
 
 * [Shared Directory Mapper](shared-directory-mapper.md) - Allows mapping shared drives before starting the executable
-* [Runaway Process Killer](runaway-process-killer.md) - Termination of processes started by the previous runs of WinSW
 
 ## Developer guide
 

+ 0 - 49
docs/extensions/runaway-process-killer.md

@@ -1,49 +0,0 @@
-# Runaway Process Killer extension
-
-In particular cases Windows service wrapper may leak the process after the service completion.
-It happens when WinSW gets terminated without executing the shutdown logic.
-Examples: force kill of the service process, .NET Runtime crash, missing permissions to kill processes or a bug in the logic.
-
-Such runaway processes may conflict with the service process once it restarts.
-This extension allows preventing it by running the runaway process termination on startup before the executable gets started.
-
-Since: WinSW 2.0.
-
-## Usage
-
-The extension can be configured via the [XML configuration file](../xml-config-file.md). Configuration sample:
-
-```xml
-<?xml version="1.0" encoding="utf-8" ?>
-<service>
-  <id>sampleService</id>
-  <name>Sample service</name>
-  <description>This is a stub service.</description>
-  <executable>%BASE%\sleep.bat</executable>
-  <arguments></arguments>
-  <log mode="roll"></log>
-
-  <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.
-           After the timeout WinSW will try to force kill the process.
-      -->
-      <stopTimeout>5000</stopTimeout>
-    </extension>
-  </extensions>
-</service>
-```
-
-## Notes
-
-* The current implementation of the the extension checks only the root process (started executable)
-* If the runaway process is detected the entire, the entire process tree gets terminated
-* WinSW gives the runaway process a chance to the gracefully terminate. 
-If it does not do it within the timeout, the process will be force-killed.
-* If the force kill fails, the WinSW startup continues with a warning.

+ 0 - 19
samples/sample-runaway-process-killer.xml

@@ -1,19 +0,0 @@
-<?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>
-  <log mode="roll"></log>
-
-  <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>
-    </extension>
-  </extensions>
-</service>

+ 67 - 0
src/WinSW.Core/Native/JobApis.cs

@@ -0,0 +1,67 @@
+#pragma warning disable SA1310 // Field names should not contain underscore
+
+using System;
+using System.Runtime.InteropServices;
+
+namespace WinSW.Native
+{
+    internal static class JobApis
+    {
+        internal const uint JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800;
+        internal const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
+
+        [DllImport(Libraries.Kernel32, SetLastError = true)]
+        internal static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
+
+        [DllImport(Libraries.Kernel32, SetLastError = true, CharSet = CharSet.Unicode)]
+        internal static extern IntPtr CreateJobObjectW(IntPtr jobAttributes = default, string? name = null);
+
+        [DllImport(Libraries.Kernel32, SetLastError = true)]
+        internal static extern bool SetInformationJobObject(
+            IntPtr job,
+            JOBOBJECTINFOCLASS jobObjectInformationClass,
+            in JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobObjectInformation,
+            int jobObjectInformationLength);
+
+        internal enum JOBOBJECTINFOCLASS
+        {
+            JobObjectExtendedLimitInformation = 9,
+        }
+
+        [StructLayout(LayoutKind.Sequential)]
+        internal struct JOBOBJECT_BASIC_LIMIT_INFORMATION
+        {
+            public long PerProcessUserTimeLimit;
+            public long PerJobUserTimeLimit;
+            public uint LimitFlags;
+            public IntPtr MinimumWorkingSetSize;
+            public IntPtr MaximumWorkingSetSize;
+            public uint ActiveProcessLimit;
+            public UIntPtr Affinity;
+            public uint PriorityClass;
+            public uint SchedulingClass;
+        }
+
+        [StructLayout(LayoutKind.Sequential)]
+        internal struct IO_COUNTERS
+        {
+            public ulong ReadOperationCount;
+            public ulong WriteOperationCount;
+            public ulong OtherOperationCount;
+            public ulong ReadTransferCount;
+            public ulong WriteTransferCount;
+            public ulong OtherTransferCount;
+        }
+
+        [StructLayout(LayoutKind.Sequential)]
+        internal struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
+        {
+            public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
+            public IO_COUNTERS IoInfo;
+            public IntPtr ProcessMemoryLimit;
+            public IntPtr JobMemoryLimit;
+            public IntPtr PeakProcessMemoryUsed;
+            public IntPtr PeakJobMemoryUsed;
+        }
+    }
+}

+ 0 - 464
src/WinSW.Plugins/RunawayProcessKillerExtension.cs

@@ -1,464 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Xml;
-using log4net;
-using WinSW.Extensions;
-using WinSW.Util;
-using static WinSW.Plugins.RunawayProcessKiller.RunawayProcessKillerExtension.Native;
-
-namespace WinSW.Plugins.RunawayProcessKiller
-{
-    public partial 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 runaway process will be checked for the WinSW environment variable before termination.
-        /// This option is not documented AND not supposed to be used by users.
-        /// </summary>
-        public bool CheckWinSWEnvironmentVariable { get; private set; }
-
-        public override string DisplayName => "Runaway Process Killer";
-
-        private string ServiceId { get; set; }
-
-        private static readonly ILog Logger = LogManager.GetLogger(typeof(RunawayProcessKillerExtension));
-
-#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
-        public RunawayProcessKillerExtension()
-#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
-        {
-            // Default initializer
-        }
-
-#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
-        public RunawayProcessKillerExtension(string pidfile, int stopTimeoutMs = 5000, bool checkWinSWEnvironmentVariable = true)
-#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
-        {
-            this.Pidfile = pidfile;
-            this.StopTimeout = TimeSpan.FromMilliseconds(stopTimeoutMs);
-            this.CheckWinSWEnvironmentVariable = checkWinSWEnvironmentVariable;
-        }
-
-        private static unsafe string? ReadEnvironmentVariable(IntPtr processHandle, string variable)
-        {
-            if (IntPtr.Size == sizeof(long))
-            {
-                return SearchEnvironmentVariable(
-                    processHandle,
-                    variable,
-                    GetEnvironmentAddress64(processHandle).ToInt64(),
-                    (handle, address, buffer, size) => NtReadVirtualMemory(handle, new IntPtr(address), buffer, new IntPtr(size)));
-            }
-
-            if (Is64BitOSWhen32BitProcess(Process.GetCurrentProcess().Handle) && !Is64BitOSWhen32BitProcess(processHandle))
-            {
-                return SearchEnvironmentVariable(
-                    processHandle,
-                    variable,
-                    GetEnvironmentAddressWow64(processHandle),
-                    (handle, address, buffer, size) => NtWow64ReadVirtualMemory64(handle, address, buffer, size));
-            }
-
-            return SearchEnvironmentVariable(
-                processHandle,
-                variable,
-                GetEnvironmentAddress32(processHandle).ToInt64(),
-                (handle, address, buffer, size) => NtReadVirtualMemory(handle, new IntPtr(address), buffer, new IntPtr(size)));
-        }
-
-        private static bool Is64BitOSWhen32BitProcess(IntPtr processHandle) =>
-            IsWow64Process(processHandle, out int isWow64) != 0 && isWow64 != 0;
-
-        private unsafe delegate int ReadMemoryCallback(IntPtr processHandle, long baseAddress, void* buffer, int bufferSize);
-
-        private static unsafe string? SearchEnvironmentVariable(IntPtr processHandle, string variable, long address, ReadMemoryCallback reader)
-        {
-            const int BaseBufferSize = 0x1000;
-            string variableKey = '\0' + variable + '=';
-            string buffer = new string('\0', BaseBufferSize + variableKey.Length);
-            fixed (char* bufferPtr = buffer)
-            {
-                long startAddress = address;
-                for (; ; )
-                {
-                    int status = reader(processHandle, address, bufferPtr, buffer.Length * sizeof(char));
-                    int index = buffer.IndexOf("\0\0");
-                    if (index >= 0)
-                    {
-                        break;
-                    }
-
-                    address += BaseBufferSize * sizeof(char);
-                }
-
-                for (; ; )
-                {
-                    int variableIndex = buffer.IndexOf(variableKey);
-                    if (variableIndex >= 0)
-                    {
-                        int valueStartIndex = variableIndex + variableKey.Length;
-                        int valueEndIndex = buffer.IndexOf('\0', valueStartIndex);
-                        string value = buffer.Substring(valueStartIndex, valueEndIndex - valueStartIndex);
-                        return value;
-                    }
-
-                    address -= BaseBufferSize * sizeof(char);
-                    if (address < startAddress)
-                    {
-                        break;
-                    }
-
-                    int status = reader(processHandle, address, bufferPtr, buffer.Length * sizeof(char));
-                }
-            }
-
-            return null;
-        }
-
-        private static unsafe IntPtr GetEnvironmentAddress32(IntPtr processHandle)
-        {
-            _ = NtQueryInformationProcess(
-                processHandle,
-                PROCESSINFOCLASS.ProcessBasicInformation,
-                out PROCESS_BASIC_INFORMATION32 information,
-                sizeof(PROCESS_BASIC_INFORMATION32));
-
-            PEB32 peb;
-            _ = NtReadVirtualMemory(processHandle, new IntPtr(information.PebBaseAddress), &peb, new IntPtr(sizeof(PEB32)));
-            RTL_USER_PROCESS_PARAMETERS32 parameters;
-            _ = NtReadVirtualMemory(processHandle, new IntPtr(peb.ProcessParameters), &parameters, new IntPtr(sizeof(RTL_USER_PROCESS_PARAMETERS32)));
-            return new IntPtr(parameters.Environment);
-        }
-
-        private static unsafe IntPtr GetEnvironmentAddress64(IntPtr processHandle)
-        {
-            _ = NtQueryInformationProcess(
-                processHandle,
-                PROCESSINFOCLASS.ProcessBasicInformation,
-                out PROCESS_BASIC_INFORMATION64 information,
-                sizeof(PROCESS_BASIC_INFORMATION64));
-
-            PEB64 peb;
-            _ = NtReadVirtualMemory(processHandle, new IntPtr(information.PebBaseAddress), &peb, new IntPtr(sizeof(PEB64)));
-            RTL_USER_PROCESS_PARAMETERS64 parameters;
-            _ = NtReadVirtualMemory(processHandle, new IntPtr(peb.ProcessParameters), &parameters, new IntPtr(sizeof(RTL_USER_PROCESS_PARAMETERS64)));
-            return new IntPtr(parameters.Environment);
-        }
-
-        private static unsafe long GetEnvironmentAddressWow64(IntPtr processHandle)
-        {
-            _ = NtWow64QueryInformationProcess64(
-                processHandle,
-                PROCESSINFOCLASS.ProcessBasicInformation,
-                out PROCESS_BASIC_INFORMATION64 information,
-                sizeof(PROCESS_BASIC_INFORMATION64));
-
-            PEB64 peb;
-            _ = NtWow64ReadVirtualMemory64(processHandle, information.PebBaseAddress, &peb, sizeof(PEB64));
-            RTL_USER_PROCESS_PARAMETERS64 parameters;
-            _ = NtWow64ReadVirtualMemory64(processHandle, peb.ProcessParameters, &parameters, sizeof(RTL_USER_PROCESS_PARAMETERS64));
-            return parameters.Environment;
-        }
-
-        public override void Configure(XmlServiceConfig config, XmlNode node)
-        {
-            // We expect the upper logic to process any errors
-            // TODO: a better parser API for types would be useful
-            this.Pidfile = XmlHelper.SingleElement(node, "pidfile", false)!;
-            this.StopTimeout = TimeSpan.FromMilliseconds(int.Parse(XmlHelper.SingleElement(node, "stopTimeout", false)!));
-            this.ServiceId = config.Id;
-
-            // TODO: Consider making it documented
-            var checkWinSWEnvironmentVariable = XmlHelper.SingleElement(node, "checkWinSWEnvironmentVariable", true);
-            this.CheckWinSWEnvironmentVariable = checkWinSWEnvironmentVariable is null ? true : bool.Parse(checkWinSWEnvironmentVariable);
-        }
-
-        /// <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 OnWrapperStarted()
-        {
-            // Read PID file from the disk
-            int pid;
-            if (File.Exists(this.Pidfile))
-            {
-                string pidstring;
-                try
-                {
-                    pidstring = File.ReadAllText(this.Pidfile);
-                }
-                catch (Exception ex)
-                {
-                    Logger.Error("Cannot read PID file from " + this.Pidfile, ex);
-                    return;
-                }
-
-                try
-                {
-                    pid = int.Parse(pidstring);
-                }
-                catch (FormatException e)
-                {
-                    Logger.Error("Invalid PID file number in '" + this.Pidfile + "'. The runaway process won't be checked", e);
-                    return;
-                }
-            }
-            else
-            {
-                Logger.Warn("The requested PID file '" + this.Pidfile + "' does not exist. The runaway process won't be checked");
-                return;
-            }
-
-            // Now check the process
-            Logger.DebugFormat("Checking the potentially runaway process with PID={0}", pid);
-            Process proc;
-            try
-            {
-                proc = Process.GetProcessById(pid);
-            }
-            catch (ArgumentException)
-            {
-                Logger.Debug("No runaway process with PID=" + pid + ". The process has been already stopped.");
-                return;
-            }
-
-            // Ensure the process references the service
-            string expectedEnvVarName = WinSWSystem.EnvVarNameServiceId;
-            string? affiliatedServiceId = ReadEnvironmentVariable(proc.Handle, expectedEnvVarName);
-            if (affiliatedServiceId is null && this.CheckWinSWEnvironmentVariable)
-            {
-                Logger.Warn("The process " + pid + " has no " + expectedEnvVarName + " environment variable defined. "
-                    + "The process has not been started by WinSW, hence it won't be terminated.");
-
-                return;
-            }
-
-            // Check the service ID value
-            if (this.CheckWinSWEnvironmentVariable && !this.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 '" + this.ServiceId + "'), hence the process won't be terminated.");
-                return;
-            }
-
-            // Kill the runaway process
-            StringBuilder bldr = new StringBuilder("Stopping the runaway process (pid=");
-            bldr.Append(pid);
-            bldr.Append(") and its children. Environment was ");
-            if (!this.CheckWinSWEnvironmentVariable)
-            {
-                bldr.Append("not ");
-            }
-
-            bldr.Append("checked, affiliated service ID: ");
-            bldr.Append(affiliatedServiceId ?? "undefined");
-            bldr.Append(", process to kill: ");
-            bldr.Append(proc);
-
-            Logger.Warn(bldr.ToString());
-            proc.StopTree(this.StopTimeout);
-        }
-
-        /// <summary>
-        /// Records the started process PID for the future use in OnStart() after the restart.
-        /// </summary>
-        public override void OnProcessStarted(Process process)
-        {
-            Logger.Info("Recording PID of the started process:" + process.Id + ". PID file destination is " + this.Pidfile);
-            try
-            {
-                File.WriteAllText(this.Pidfile, process.Id.ToString());
-            }
-            catch (Exception ex)
-            {
-                Logger.Error("Cannot update the PID file " + this.Pidfile, ex);
-            }
-        }
-
-        internal static class Native
-        {
-            private const string Kernel32 = "kernel32.dll";
-            private const string NTDll = "ntdll.dll";
-
-            [DllImport(Kernel32)]
-            internal static extern int IsWow64Process(IntPtr hProcess, out int Wow64Process);
-
-            [DllImport(NTDll)]
-            internal static extern int NtQueryInformationProcess(
-                IntPtr ProcessHandle,
-                PROCESSINFOCLASS ProcessInformationClass,
-                out PROCESS_BASIC_INFORMATION32 ProcessInformation,
-                int ProcessInformationLength,
-                IntPtr ReturnLength = default);
-
-            [DllImport(NTDll)]
-            internal static extern int NtQueryInformationProcess(
-                IntPtr ProcessHandle,
-                PROCESSINFOCLASS ProcessInformationClass,
-                out PROCESS_BASIC_INFORMATION64 ProcessInformation,
-                int ProcessInformationLength,
-                IntPtr ReturnLength = default);
-
-            [DllImport(NTDll)]
-            internal static extern unsafe int NtReadVirtualMemory(
-                IntPtr ProcessHandle,
-                IntPtr BaseAddress,
-                void* Buffer,
-                IntPtr BufferSize,
-                IntPtr NumberOfBytesRead = default);
-
-            [DllImport(NTDll)]
-            internal static extern int NtWow64QueryInformationProcess64(
-                IntPtr ProcessHandle,
-                PROCESSINFOCLASS ProcessInformationClass,
-                out PROCESS_BASIC_INFORMATION64 ProcessInformation,
-                int ProcessInformationLength,
-                IntPtr ReturnLength = default);
-
-            [DllImport(NTDll)]
-            internal static extern unsafe int NtWow64ReadVirtualMemory64(
-                IntPtr ProcessHandle,
-                long BaseAddress,
-                void* Buffer,
-                long BufferSize,
-                long NumberOfBytesRead = default);
-
-            internal enum PROCESSINFOCLASS
-            {
-                ProcessBasicInformation = 0,
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal readonly struct MEMORY_BASIC_INFORMATION
-            {
-                public readonly IntPtr BaseAddress;
-                private readonly IntPtr AllocationBase;
-                private readonly uint AllocationProtect;
-                public readonly IntPtr RegionSize;
-                private readonly uint State;
-                private readonly uint Protect;
-                private readonly uint Type;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal unsafe struct PROCESS_BASIC_INFORMATION32
-            {
-                private readonly int Reserved1;
-                public readonly int PebBaseAddress;
-                private fixed int Reserved2[2];
-                private readonly uint UniqueProcessId;
-                private readonly int Reserved3;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal unsafe struct PROCESS_BASIC_INFORMATION64
-            {
-                private readonly long Reserved1;
-                public readonly long PebBaseAddress;
-                private fixed long Reserved2[2];
-                private readonly ulong UniqueProcessId;
-                private readonly long Reserved3;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal unsafe struct PEB32
-            {
-                private fixed byte Reserved1[2];
-                private readonly byte BeingDebugged;
-                private fixed byte Reserved2[1];
-                private fixed int Reserved3[2];
-                private readonly int Ldr;
-                public readonly int ProcessParameters;
-                private fixed int Reserved4[3];
-                private readonly int AtlThunkSListPtr;
-                private readonly int Reserved5;
-                private readonly uint Reserved6;
-                private readonly int Reserved7;
-                private readonly uint Reserved8;
-                private readonly uint AtlThunkSListPtr32;
-                private fixed int Reserved9[45];
-                private fixed byte Reserved10[96];
-                private readonly int PostProcessInitRoutine;
-                private fixed byte Reserved11[128];
-                private fixed int Reserved12[1];
-                private readonly uint SessionId;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal unsafe struct PEB64
-            {
-                private fixed byte Reserved1[2];
-                private readonly byte BeingDebugged;
-                private fixed byte Reserved2[1];
-                private fixed long Reserved3[2];
-                private readonly long Ldr;
-                public readonly long ProcessParameters;
-                private fixed long Reserved4[3];
-                private readonly long AtlThunkSListPtr;
-                private readonly long Reserved5;
-                private readonly uint Reserved6;
-                private readonly long Reserved7;
-                private readonly uint Reserved8;
-                private readonly uint AtlThunkSListPtr32;
-                private fixed long Reserved9[45];
-                private fixed byte Reserved10[96];
-                private readonly long PostProcessInitRoutine;
-                private fixed byte Reserved11[128];
-                private fixed long Reserved12[1];
-                private readonly uint SessionId;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal unsafe struct RTL_USER_PROCESS_PARAMETERS32
-            {
-                private fixed byte Reserved1[16];
-                private fixed int Reserved2[10];
-                private readonly UNICODE_STRING32 ImagePathName;
-                private readonly UNICODE_STRING32 CommandLine;
-
-                internal readonly int Environment;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal unsafe struct RTL_USER_PROCESS_PARAMETERS64
-            {
-                private fixed byte Reserved1[16];
-                private fixed long Reserved2[10];
-                private readonly UNICODE_STRING64 ImagePathName;
-                private readonly UNICODE_STRING64 CommandLine;
-
-                internal readonly long Environment;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal readonly struct UNICODE_STRING32
-            {
-                private readonly ushort Length;
-                private readonly ushort MaximumLength;
-                private readonly int Buffer;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal readonly struct UNICODE_STRING64
-            {
-                private readonly ushort Length;
-                private readonly ushort MaximumLength;
-                private readonly long Buffer;
-            }
-        }
-    }
-}

+ 0 - 1
src/WinSW.Plugins/WinSW.Plugins.csproj

@@ -4,7 +4,6 @@
     <TargetFrameworks>net461;netcoreapp3.1</TargetFrameworks>
     <LangVersion>latest</LangVersion>
     <Nullable>enable</Nullable>
-    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
   <ItemGroup>

+ 0 - 125
src/WinSW.Tests/Extensions/RunawayProcessKillerTest.cs

@@ -1,125 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using WinSW.Extensions;
-using WinSW.Plugins.RunawayProcessKiller;
-using WinSW.Tests.Util;
-using WinSW.Util;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace WinSW.Tests.Extensions
-{
-    public class RunawayProcessKillerExtensionTest : ExtensionTestBase
-    {
-        private readonly XmlServiceConfig serviceConfig;
-
-        private readonly string testExtension = GetExtensionClassNameWithAssembly(typeof(RunawayProcessKillerExtension));
-
-        private readonly ITestOutputHelper output;
-
-        public RunawayProcessKillerExtensionTest(ITestOutputHelper output)
-        {
-            this.output = output;
-
-            string seedXml =
-$@"<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>
-  <log mode=""roll""></log>
-  <extensions>
-    <extension enabled=""true"" className=""{this.testExtension}"" id=""killRunawayProcess"">
-      <pidfile>foo/bar/pid.txt</pidfile>
-      <stopTimeout>5000</stopTimeout>
-    </extension>
-  </extensions>
-</service>";
-            this.serviceConfig = XmlServiceConfig.FromXml(seedXml);
-        }
-
-        [Fact]
-        public void LoadExtensions()
-        {
-            WinSWExtensionManager manager = new WinSWExtensionManager(this.serviceConfig);
-            manager.LoadExtensions();
-            _ = Assert.Single(manager.Extensions);
-
-            // Check the file is correct
-            var extension = manager.Extensions["killRunawayProcess"] as RunawayProcessKillerExtension;
-            Assert.NotNull(extension);
-            Assert.Equal("foo/bar/pid.txt", extension.Pidfile);
-            Assert.Equal(5000, extension.StopTimeout.TotalMilliseconds);
-        }
-
-        [Fact]
-        public void StartStopExtension()
-        {
-            WinSWExtensionManager manager = new WinSWExtensionManager(this.serviceConfig);
-            manager.LoadExtensions();
-            manager.FireOnWrapperStarted();
-            manager.FireBeforeWrapperStopped();
-        }
-
-        internal void ShouldKillTheSpawnedProcess()
-        {
-            var winswId = "myAppWithRunaway";
-            var extensionId = "runaway-process-killer";
-            var tmpDir = FilesystemTestHelper.CreateTmpDirectory();
-            try
-            {
-                // Spawn the test process
-                Process proc = new Process();
-                var ps = proc.StartInfo;
-                ps.FileName = "cmd.exe";
-                ps.Arguments = "/c pause";
-                ps.UseShellExecute = false;
-                ps.RedirectStandardOutput = true;
-                ps.EnvironmentVariables[WinSWSystem.EnvVarNameServiceId] = winswId;
-                proc.Start();
-
-                try
-                {
-                    // Generate extension and ensure that the roundtrip is correct
-                    var pidfile = Path.Combine(tmpDir, "process.pid");
-                    var config = ConfigXmlBuilder.Create(this.output, id: winswId)
-                        .WithRunawayProcessKiller(new RunawayProcessKillerExtension(pidfile), extensionId)
-                        .ToServiceConfig();
-                    WinSWExtensionManager manager = new WinSWExtensionManager(config);
-                    manager.LoadExtensions();
-                    var extension = manager.Extensions[extensionId] as RunawayProcessKillerExtension;
-                    Assert.NotNull(extension);
-                    Assert.Equal(pidfile, extension.Pidfile);
-
-                    // Inject PID
-                    File.WriteAllText(pidfile, proc.Id.ToString());
-
-                    // Try to terminate
-                    Assert.False(proc.HasExited, "Process " + proc + " has exited before the RunawayProcessKiller extension invocation");
-                    _ = proc.StandardOutput.Read();
-                    extension.OnWrapperStarted();
-                    Assert.True(proc.HasExited, "Process " + proc + " should have been terminated by RunawayProcessKiller");
-                }
-                finally
-                {
-                    if (!proc.HasExited)
-                    {
-                        Console.Error.WriteLine("Test: Killing runaway process with ID=" + proc.Id);
-                        proc.StopTree(TimeSpan.FromMilliseconds(100));
-                        if (!proc.HasExited)
-                        {
-                            // The test is failed here anyway, but we add additional diagnostics info
-                            Console.Error.WriteLine("Test: ProcessHelper failed to properly terminate process with ID=" + proc.Id);
-                        }
-                    }
-                }
-            }
-            finally
-            {
-                Directory.Delete(tmpDir, true);
-            }
-        }
-    }
-}

+ 0 - 15
src/WinSW.Tests/Util/ConfigXmlBuilder.cs

@@ -1,6 +1,5 @@
 using System.Collections.Generic;
 using System.Text;
-using WinSW.Plugins.RunawayProcessKiller;
 using WinSW.Tests.Extensions;
 using Xunit.Abstractions;
 
@@ -120,20 +119,6 @@ namespace WinSW.Tests.Util
             return this.WithRawEntry(string.Format("<{0}>{1}</{0}>", tagName, value));
         }
 
-        public ConfigXmlBuilder WithRunawayProcessKiller(RunawayProcessKillerExtension ext, string extensionId = "killRunawayProcess", bool enabled = true)
-        {
-            var fullyQualifiedExtensionName = ExtensionTestBase.GetExtensionClassNameWithAssembly(typeof(RunawayProcessKillerExtension));
-            StringBuilder str = new StringBuilder();
-            str.AppendFormat("    <extension enabled=\"{0}\" className=\"{1}\" id=\"{2}\">\n", new object[] { enabled, fullyQualifiedExtensionName, extensionId });
-            str.AppendFormat("      <pidfile>{0}</pidfile>\n", ext.Pidfile);
-            str.AppendFormat("      <stopTimeout>{0}</stopTimeout>\n", ext.StopTimeout.TotalMilliseconds);
-            str.AppendFormat("      <checkWinSWEnvironmentVariable>{0}</checkWinSWEnvironmentVariable>\n", ext.CheckWinSWEnvironmentVariable);
-            str.Append("    </extension>\n");
-            this.ExtensionXmls.Add(str.ToString());
-
-            return this;
-        }
-
         public ConfigXmlBuilder WithDownload(Download download)
         {
             StringBuilder xml = new StringBuilder();

+ 28 - 17
src/WinSW/WrapperService.cs

@@ -29,7 +29,8 @@ namespace WinSW
         private readonly XmlServiceConfig config;
 
         private Process process = null!;
-        private volatile Process? poststartProcess;
+        private volatile Process? startingProcess;
+        private volatile Process? stoppingProcess;
 
         internal WinSWExtensionManager ExtensionManager { get; }
 
@@ -314,29 +315,29 @@ namespace WinSW
             this.process = this.StartProcess(this.config.Executable, startArguments, this.OnMainProcessExited, executableLogHandler);
             this.ExtensionManager.FireOnProcessStarted(this.process);
 
-            try
+            string? poststartExecutable = this.config.PoststartExecutable;
+            if (poststartExecutable != null)
             {
-                string? poststartExecutable = this.config.PoststartExecutable;
-                if (poststartExecutable != null)
+                try
                 {
                     using Process process = StartProcessLocked();
                     this.WaitForProcessToExit(process);
                     this.LogInfo($"Post-start process '{process.Format()}' exited with code {process.ExitCode}.");
                     process.StopDescendants(additionalStopTimeout);
-                    this.poststartProcess = null;
+                    this.startingProcess = null;
 
                     Process StartProcessLocked()
                     {
                         lock (this)
                         {
-                            return this.poststartProcess = this.StartProcess(poststartExecutable, this.config.PoststartArguments);
+                            return this.startingProcess = this.StartProcess(poststartExecutable, this.config.PoststartArguments);
                         }
                     }
                 }
-            }
-            catch (Exception e)
-            {
-                Log.Error(e);
+                catch (Exception e)
+                {
+                    Log.Error(e);
+                }
             }
         }
 
@@ -350,10 +351,11 @@ namespace WinSW
             {
                 try
                 {
-                    using Process process = this.StartProcess(prestopExecutable, this.config.PrestopArguments);
+                    using Process process = StartProcessLocked(prestopExecutable, this.config.PrestopArguments);
                     this.WaitForProcessToExit(process);
                     this.LogInfo($"Pre-stop process '{process.Format()}' exited with code {process.ExitCode}.");
                     process.StopDescendants(additionalStopTimeout);
+                    this.stoppingProcess = null;
                 }
                 catch (Exception e)
                 {
@@ -381,11 +383,12 @@ namespace WinSW
                 try
                 {
                     // TODO: Redirect logging to Log4Net once https://github.com/kohsuke/winsw/pull/213 is integrated
-                    Process stopProcess = this.StartProcess(stopExecutable, stopArguments);
+                    using Process stopProcess = StartProcessLocked(stopExecutable, stopArguments);
 
                     Log.Debug("WaitForProcessToExit " + this.process.Id + "+" + stopProcess.Id);
                     this.WaitForProcessToExit(stopProcess);
                     stopProcess.StopDescendants(additionalStopTimeout);
+                    this.stoppingProcess = null;
 
                     this.WaitForProcessToExit(this.process);
                     this.process.StopDescendants(this.config.StopTimeout);
@@ -397,17 +400,16 @@ namespace WinSW
                 }
             }
 
-            this.poststartProcess?.StopTree(additionalStopTimeout);
-
             string? poststopExecutable = this.config.PoststopExecutable;
             if (poststopExecutable != null)
             {
                 try
                 {
-                    using Process process = this.StartProcess(poststopExecutable, this.config.PoststopArguments);
+                    using Process process = StartProcessLocked(poststopExecutable, this.config.PoststopArguments);
                     this.WaitForProcessToExit(process);
                     this.LogInfo($"Post-stop process '{process.Format()}' exited with code {process.ExitCode}.");
                     process.StopDescendants(additionalStopTimeout);
+                    this.stoppingProcess = null;
                 }
                 catch (Exception e)
                 {
@@ -424,6 +426,14 @@ namespace WinSW
             }
 
             Log.Info("Finished " + this.config.Id);
+
+            Process StartProcessLocked(string executable, string? arguments)
+            {
+                lock (this)
+                {
+                    return this.stoppingProcess = this.StartProcess(executable, arguments);
+                }
+            }
         }
 
         private void WaitForProcessToExit(Process process)
@@ -477,7 +487,7 @@ namespace WinSW
 
             if (this.orderlyShutdown)
             {
-                this.LogInfo($"Child process '{display}' terminated with code {process.ExitCode}.");
+                this.LogInfo($"Child process '{display}' terminated.");
             }
             else
             {
@@ -487,7 +497,8 @@ namespace WinSW
 
                 lock (this)
                 {
-                    this.poststartProcess?.StopTree(new TimeSpan(TimeSpan.TicksPerMillisecond));
+                    this.startingProcess?.StopTree(additionalStopTimeout);
+                    this.stoppingProcess?.StopTree(additionalStopTimeout);
                 }
 
                 // if we finished orderly, report that to SCM.