From 799b46a153a9bd350471ef78a03b619594d36042 Mon Sep 17 00:00:00 2001 From: Greg Nagel Date: Wed, 9 Dec 2015 16:26:17 -0800 Subject: [PATCH 1/3] fix struct assignment issue --- JobObject.Net/Job.cs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/JobObject.Net/Job.cs b/JobObject.Net/Job.cs index 5d2a274..1fa07f1 100644 --- a/JobObject.Net/Job.cs +++ b/JobObject.Net/Job.cs @@ -15,12 +15,6 @@ public class Job : IDisposable private const int WS_POPUP = unchecked((int)0x80000000); private const int HWND_MESSAGE = -3; - - - - - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr CreateJobObject(IntPtr a, string lpName); @@ -66,6 +60,8 @@ public void UpdateMemoryLimit(uint? minPrcWorkingSet, uint? maxPrcWorkingSet, ui basicLimits.LimitFlags = (uint)flags; + + extendedInfo.BasicLimitInformation = basicLimits; int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length); @@ -148,7 +144,7 @@ public bool AddProcess(System.Diagnostics.Process process) #region Helper classes [StructLayout(LayoutKind.Sequential)] - struct IO_COUNTERS + internal class IO_COUNTERS { public UInt64 ReadOperationCount; public UInt64 WriteOperationCount; @@ -168,7 +164,7 @@ internal enum JOBOBJECT_BASIC_LIMIT_FLAGS } [StructLayout(LayoutKind.Sequential)] - struct JOBOBJECT_BASIC_LIMIT_INFORMATION + internal class JOBOBJECT_BASIC_LIMIT_INFORMATION { public Int64 PerProcessUserTimeLimit; public Int64 PerJobUserTimeLimit; @@ -182,7 +178,7 @@ struct JOBOBJECT_BASIC_LIMIT_INFORMATION } [StructLayout(LayoutKind.Sequential)] - public struct SECURITY_ATTRIBUTES + internal class SECURITY_ATTRIBUTES { public UInt32 nLength; public IntPtr lpSecurityDescriptor; @@ -190,7 +186,7 @@ public struct SECURITY_ATTRIBUTES } [StructLayout(LayoutKind.Sequential)] - struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION + internal class JOBOBJECT_EXTENDED_LIMIT_INFORMATION { public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; public IO_COUNTERS IoInfo; @@ -200,13 +196,14 @@ struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION public UIntPtr PeakJobMemoryUsed; } - struct JOBOBJECT_CPU_RATE_CONTROL_INFORMATION + [StructLayout(LayoutKind.Sequential)] + internal class JOBOBJECT_CPU_RATE_CONTROL_INFORMATION { public UInt32 ControlFlags; public UInt32 CpuRate; } - public enum JobObjectInfoType + internal enum JobObjectInfoType { AssociateCompletionPortInformation = 7, BasicLimitInformation = 2, From a0b953a66aeb773ebb2065f2c239ef2a150b2d04 Mon Sep 17 00:00:00 2001 From: Greg Nagel Date: Wed, 9 Dec 2015 16:43:40 -0800 Subject: [PATCH 2/3] remove redundancy --- JobObject.Net/Job.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/JobObject.Net/Job.cs b/JobObject.Net/Job.cs index 1fa07f1..97eb83f 100644 --- a/JobObject.Net/Job.cs +++ b/JobObject.Net/Job.cs @@ -60,8 +60,6 @@ public void UpdateMemoryLimit(uint? minPrcWorkingSet, uint? maxPrcWorkingSet, ui basicLimits.LimitFlags = (uint)flags; - - extendedInfo.BasicLimitInformation = basicLimits; int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length); From ec54170ce5065ea5f9be14211ba5f729a9ec2010 Mon Sep 17 00:00:00 2001 From: Greg Nagel Date: Wed, 9 Dec 2015 18:18:57 -0800 Subject: [PATCH 3/3] remove race condition when attaching --- JobObject.Net/Job.cs | 118 ++++++++++++++-------------- JobObject.Net/Process.cs | 162 ++++++++++++++++++++++++--------------- 2 files changed, 161 insertions(+), 119 deletions(-) diff --git a/JobObject.Net/Job.cs b/JobObject.Net/Job.cs index 97eb83f..38c9232 100644 --- a/JobObject.Net/Job.cs +++ b/JobObject.Net/Job.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; @@ -6,19 +7,10 @@ namespace Stormancer.JobManagement { public class Job : IDisposable { - private const int JobObjectExtendedLimitInformation = 9; - private const int CREATE_SUSPENDED = 0x00000004; - - private const int INFINITE = -1; - private const int GWL_STYLE = -16; - private const int WS_CHILD = 0x40000000; - private const int WS_POPUP = unchecked((int)0x80000000); - private const int HWND_MESSAGE = -3; - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern IntPtr CreateJobObject(IntPtr a, string lpName); - [DllImport("kernel32.dll")] + [DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, UInt32 cbJobObjectInfoLength); [DllImport("kernel32.dll", SetLastError = true)] @@ -30,13 +22,39 @@ public class Job : IDisposable public Job() { handle = CreateJobObject(IntPtr.Zero, null); + if (handle == IntPtr.Zero) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Job object already exists."); + } + UpdateMemoryLimit(null, null, null); + } + ~Job() + { + Dispose(false); + } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposed) + return; + + Interop.CloseHandle(handle); + handle = IntPtr.Zero; + disposed = true; } public void UpdateMemoryLimit(uint? minPrcWorkingSet, uint? maxPrcWorkingSet, uint? maxJobVirtualMemory) { + if (disposed) throw new ObjectDisposedException("Job is already disposed."); + var flags = JOBOBJECT_BASIC_LIMIT_FLAGS.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; var basicLimits = new JOBOBJECT_BASIC_LIMIT_INFORMATION(); var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION @@ -52,26 +70,30 @@ public void UpdateMemoryLimit(uint? minPrcWorkingSet, uint? maxPrcWorkingSet, ui } - if(maxJobVirtualMemory.HasValue) + if (maxJobVirtualMemory.HasValue) { flags |= JOBOBJECT_BASIC_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_MEMORY; extendedInfo.JobMemoryLimit = (UIntPtr)maxJobVirtualMemory.Value; } - basicLimits.LimitFlags = (uint)flags; + + extendedInfo.BasicLimitInformation = basicLimits; int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length); - Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); - - if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length)) + try + { + Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); + if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Unable to set extended job information."); + } + } + finally { Marshal.FreeHGlobal(extendedInfoPtr); - throw new Exception(string.Format("Unable to set extended information. Error: {0}", Marshal.GetLastWin32Error())); } - Marshal.FreeHGlobal(extendedInfoPtr); - } /// @@ -80,6 +102,8 @@ public void UpdateMemoryLimit(uint? minPrcWorkingSet, uint? maxPrcWorkingSet, ui /// The limit in total CPU percent. public void UpdateCpuRateLimit(double value) { + if (disposed) throw new ObjectDisposedException("Job is already disposed."); + var cpuRateInfo = new JOBOBJECT_CPU_RATE_CONTROL_INFORMATION { ControlFlags = 1 | 4, @@ -87,56 +111,37 @@ public void UpdateCpuRateLimit(double value) }; var length = Marshal.SizeOf(typeof(JOBOBJECT_CPU_RATE_CONTROL_INFORMATION)); IntPtr cpuRateInfoPtr = Marshal.AllocHGlobal(length); - Marshal.StructureToPtr(cpuRateInfo, cpuRateInfoPtr, false); - if (!SetInformationJobObject(handle, JobObjectInfoType.JobObjectCpuRateControlInformation, cpuRateInfoPtr, (uint)length)) + try + { + Marshal.StructureToPtr(cpuRateInfo, cpuRateInfoPtr, false); + if (!SetInformationJobObject(handle, JobObjectInfoType.JobObjectCpuRateControlInformation, cpuRateInfoPtr, (uint)length)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Unable to set cpu rate job information."); + } + } + finally { Marshal.FreeHGlobal(cpuRateInfoPtr); - throw new Exception(string.Format("Unable to set cpu rate information. Error: {0}", Marshal.GetLastWin32Error())); - } - Marshal.FreeHGlobal(cpuRateInfoPtr); - - } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); } - private void Dispose(bool disposing) + public void AddProcess(IntPtr processHandle) { - if (disposed) - return; - - if (disposing) { } - - Close(); - disposed = true; - } - - public void Close() - { - Interop.CloseHandle(handle); - handle = IntPtr.Zero; - } - - public bool AddProcess(IntPtr processHandle) - { - return AssignProcessToJobObject(handle, processHandle); + if (!AssignProcessToJobObject(handle, processHandle)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Unable to add process to JobObject."); + } } - public bool AddProcess(int processId) + public void AddProcess(int processId) { - return AddProcess(System.Diagnostics.Process.GetProcessById(processId)); + AddProcess(Process.GetProcessById(processId)); } - public bool AddProcess(System.Diagnostics.Process process) + public void AddProcess(Process process) { - return AddProcess(process.Handle); + AddProcess(process.Handle); } - - - } #region Helper classes @@ -201,6 +206,7 @@ internal class JOBOBJECT_CPU_RATE_CONTROL_INFORMATION public UInt32 CpuRate; } + internal enum JobObjectInfoType { AssociateCompletionPortInformation = 7, diff --git a/JobObject.Net/Process.cs b/JobObject.Net/Process.cs index 895a22f..eec5a9c 100644 --- a/JobObject.Net/Process.cs +++ b/JobObject.Net/Process.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -11,103 +13,137 @@ namespace Stormancer.JobManagement /// /// A factory that creates processes with the CREATE_BREAKAWAY_FROM_JOB set, that enables to assign the process to jobs even if the current process is itself running inside a job (That's the case when running the program in Visual Studio for instance) /// + /// Edited with reference to Microsoft corefx library. MIT license: https://github.com/dotnet/corefx/blob/master/LICENSE public class ProcessFactory { private const int CREATE_BREAKAWAY_FROM_JOB = 0x01000000; private const int CREATE_NO_WINDOW = 0x08000000; + private const int CREATE_SUSPENDED = 0x00000004; + private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); - [DllImport("kernel32.dll", SetLastError = true)] + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true, BestFitMapping = false, EntryPoint = "CreateProcessW")] private static extern bool CreateProcess( - string lpApplicationName, - string lpCommandLine, + [MarshalAs(UnmanagedType.LPTStr)] string lpApplicationName, + [MarshalAs(UnmanagedType.LPTStr)] string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, - uint dwCreationFlags, + int dwCreationFlags, IntPtr lpEnvironment, - string lpCurrentDirectory, - ref StartupInfo lpStartupInfo, - out ProcessInfo lpProcessInformation + [MarshalAs(UnmanagedType.LPTStr)] string lpCurrentDirectory, + STARTUPINFO lpStartupInfo, + PROCESS_INFORMATION lpProcessInformation ); + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool TerminateProcess(IntPtr hObject, int uExitCode); - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] - private struct ProcessInfo - { - public IntPtr hProcess; - public IntPtr hThread; - public Int32 ProcessId; - public Int32 ThreadId; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] - private struct SecurityAttributes - { - public int length; - public IntPtr lpSecurityDescriptor; - public bool bInheritHandle; - } + [DllImport("kernel32.dll", SetLastError = true)] + public static extern int ResumeThread(IntPtr hThread); - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] - private struct StartupInfo + public static System.Diagnostics.Process CreateProcess(string path, string args, string workingDirectory) { - public uint cb; - public string lpReserved; - public string lpDesktop; - public string lpTitle; - public uint dwX; - public uint dwY; - public uint dwXSize; - public uint dwYSize; - public uint dwXCountChars; - public uint dwYCountChars; - public uint dwFillAttribute; - public uint dwFlags; - public short wShowWindow; - public short cbReserved2; - public IntPtr lpReserved2; - public IntPtr hStdInput; - public IntPtr hStdOutput; - public IntPtr hStdError; + return CreateProcess(path, args, workingDirectory, null); } - - public static System.Diagnostics.Process CreateProcess(string path, string args, string currentDirectory) + public static System.Diagnostics.Process CreateProcess(string path, string args, string workingDirectory, Job job) { - if (!File.Exists(path)) - { - throw new ArgumentException("File does not exist"); - } - ProcessInfo proc = new ProcessInfo(); + PROCESS_INFORMATION processInfo = new PROCESS_INFORMATION(); try { - var inf = new StartupInfo(); + STARTUPINFO startupInfo = new STARTUPINFO(); + startupInfo.cb = (uint)Marshal.SizeOf(typeof(STARTUPINFO)); + + string command = path ?? ""; + if (!command.StartsWith("\"", StringComparison.Ordinal) || !command.EndsWith("\"", StringComparison.Ordinal)) + { + command = "\"" + command + "\""; + } + if (!string.IsNullOrEmpty(args)) + { + command += " " + args; + } + + int creationFlags = CREATE_BREAKAWAY_FROM_JOB | CREATE_NO_WINDOW; + if (job != null) + { + creationFlags |= CREATE_SUSPENDED; + } - inf.cb = (uint)Marshal.SizeOf(typeof(StartupInfo)); - var cmd = path; - if(!string.IsNullOrWhiteSpace(args)) + if (!CreateProcess(null, command, IntPtr.Zero, IntPtr.Zero, false, creationFlags, IntPtr.Zero, workingDirectory, startupInfo, processInfo)) { - cmd += " " + args; + throw new Win32Exception(); } - if (!CreateProcess(null, cmd, IntPtr.Zero, IntPtr.Zero, false, CREATE_BREAKAWAY_FROM_JOB | CREATE_NO_WINDOW, IntPtr.Zero, currentDirectory, ref inf, out proc)) + + if (job != null) { - throw new InvalidOperationException("Couldn't create process"); + job.AddProcess(processInfo.hProcess); + + if (ResumeThread(processInfo.hThread) == -1) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not start process."); + } } - return System.Diagnostics.Process.GetProcessById(proc.ProcessId); + return Process.GetProcessById(processInfo.ProcessId); + } + catch (Win32Exception e) + { + if (processInfo.hProcess != IntPtr.Zero && processInfo.hProcess != INVALID_HANDLE_VALUE) + { + if (!TerminateProcess(processInfo.hProcess, -1)) + { + throw new Win32Exception("Failed to terminate process with error code " + Marshal.GetLastWin32Error() + ".", e); + } + } + + throw; } finally { - if (proc.hProcess != IntPtr.Zero) + if (processInfo.hProcess != IntPtr.Zero && processInfo.hProcess != INVALID_HANDLE_VALUE) + { + Interop.CloseHandle(processInfo.hProcess); + } + if (processInfo.hThread != IntPtr.Zero && processInfo.hThread != INVALID_HANDLE_VALUE) { - Interop.CloseHandle(proc.hProcess); + Interop.CloseHandle(processInfo.hThread); } } - - } + } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + internal class PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public Int32 ProcessId; + public Int32 ThreadId; + } - + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + internal class STARTUPINFO + { + public uint cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public uint dwX; + public uint dwY; + public uint dwXSize; + public uint dwYSize; + public uint dwXCountChars; + public uint dwYCountChars; + public uint dwFillAttribute; + public uint dwFlags; + public short wShowWindow; + public short cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; } } +