mirror of https://github.com/winsw/winsw
Remove Runaway Process Killer
parent
35ebd4a0a1
commit
d90151b02f
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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), ¶meters, 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), ¶meters, 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, ¶meters, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@
|
|||
<TargetFrameworks>net461;netcoreapp3.1</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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,23 +315,22 @@ 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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -339,6 +339,7 @@ namespace WinSW
|
|||
Log.Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when we are told by Windows SCM to exit.
|
||||
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue