using System; using System.ComponentModel; using System.Runtime.InteropServices; using System.Text; using System.Threading; public static class ToolcraftManagedJob { private const uint DUPLICATE_SAME_ACCESS = 0x00000002; private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000; private const uint INFINITE = 0xFFFFFFFF; private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000; private const int JobObjectBasicAccountingInformation = 1; private const int JobObjectExtendedLimitInformation = 9; private static readonly IntPtr PROC_THREAD_ATTRIBUTE_HANDLE_LIST = new IntPtr(0x00020002); private static readonly IntPtr PROC_THREAD_ATTRIBUTE_JOB_LIST = new IntPtr(0x0002000D); private const uint STARTF_USESTDHANDLES = 0x00000100; private const int STD_ERROR_HANDLE = -12; private const int STD_INPUT_HANDLE = -10; private const int STD_OUTPUT_HANDLE = -11; private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); [StructLayout(LayoutKind.Sequential)] private struct IO_COUNTERS { public ulong ReadOperationCount; public ulong WriteOperationCount; public ulong OtherOperationCount; public ulong ReadTransferCount; public ulong WriteTransferCount; public ulong OtherTransferCount; } [StructLayout(LayoutKind.Sequential)] private struct JOBOBJECT_BASIC_LIMIT_INFORMATION { public long PerProcessUserTimeLimit; public long PerJobUserTimeLimit; public uint LimitFlags; public UIntPtr MinimumWorkingSetSize; public UIntPtr MaximumWorkingSetSize; public uint ActiveProcessLimit; public UIntPtr Affinity; public uint PriorityClass; public uint SchedulingClass; } [StructLayout(LayoutKind.Sequential)] private struct JOBOBJECT_BASIC_ACCOUNTING_INFORMATION { public long TotalUserTime; public long TotalKernelTime; public long ThisPeriodTotalUserTime; public long ThisPeriodTotalKernelTime; public uint TotalPageFaultCount; public uint TotalProcesses; public uint ActiveProcesses; public uint TotalTerminatedProcesses; } [StructLayout(LayoutKind.Sequential)] private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION { public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; public IO_COUNTERS IoInfo; public UIntPtr ProcessMemoryLimit; public UIntPtr JobMemoryLimit; public UIntPtr PeakProcessMemoryUsed; public UIntPtr PeakJobMemoryUsed; } [StructLayout(LayoutKind.Sequential)] private struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public uint dwProcessId; public uint dwThreadId; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct STARTUPINFO { public int 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; } [StructLayout(LayoutKind.Sequential)] private struct STARTUPINFOEX { public STARTUPINFO StartupInfo; public IntPtr lpAttributeList; } [DllImport("kernel32.dll")] private static extern bool CloseHandle(IntPtr handle); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern IntPtr CreateJobObject( IntPtr jobAttributes, string name); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool CreateProcess( string applicationName, StringBuilder commandLine, IntPtr processAttributes, IntPtr threadAttributes, bool inheritHandles, uint creationFlags, IntPtr environment, string currentDirectory, ref STARTUPINFOEX startupInfo, out PROCESS_INFORMATION processInformation ); [DllImport("kernel32.dll")] private static extern void DeleteProcThreadAttributeList( IntPtr attributeList); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool DuplicateHandle( IntPtr sourceProcess, IntPtr sourceHandle, IntPtr targetProcess, out IntPtr targetHandle, uint desiredAccess, bool inheritHandle, uint options ); [DllImport("kernel32.dll")] private static extern IntPtr GetCurrentProcess(); [DllImport("kernel32.dll")] private static extern IntPtr GetStdHandle(int standardHandle); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool GetExitCodeProcess( IntPtr process, out uint exitCode); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool InitializeProcThreadAttributeList( IntPtr attributeList, int attributeCount, uint flags, ref IntPtr size ); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool QueryInformationJobObject( IntPtr job, int informationClass, IntPtr information, uint informationLength, IntPtr returnLength ); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetInformationJobObject( IntPtr job, int informationClass, IntPtr information, uint informationLength ); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool TerminateJobObject( IntPtr job, uint exitCode); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool UpdateProcThreadAttribute( IntPtr attributeList, uint flags, IntPtr attribute, IntPtr value, IntPtr size, IntPtr previousValue, IntPtr returnSize ); [DllImport("kernel32.dll", SetLastError = true)] private static extern uint WaitForSingleObject( IntPtr handle, uint milliseconds); private static void CloseOwnedHandle(IntPtr handle) { if (handle != IntPtr.Zero) CloseHandle(handle); } private static IntPtr DuplicateRequiredStdHandle( int standardHandle, string name ) { IntPtr original = GetStdHandle(standardHandle); if (original == IntPtr.Zero || original == INVALID_HANDLE_VALUE) { throw new InvalidOperationException( "Managed Windows job has no valid " + name + " handle." ); } IntPtr duplicate; IntPtr currentProcess = GetCurrentProcess(); if (!DuplicateHandle( currentProcess, original, currentProcess, out duplicate, 0, true, DUPLICATE_SAME_ACCESS )) { CloseOwnedHandle(duplicate); ThrowLastError("DuplicateHandle " + name); } return duplicate; } private static void SetAttribute( IntPtr attributeList, IntPtr attribute, IntPtr value, int size, string name ) { if (!UpdateProcThreadAttribute( attributeList, 0, attribute, value, new IntPtr(size), IntPtr.Zero, IntPtr.Zero )) { ThrowLastError("UpdateProcThreadAttribute " + name); } } private static void ThrowLastError(string operation) { throw new Win32Exception( Marshal.GetLastWin32Error(), operation + " failed." ); } private static string QuoteArgument(string argument) { if ( argument.Length > 0 && argument.IndexOfAny(new char[] { ' ', '\t', '\n', '\v', '"' }) < 0 ) { return argument; } StringBuilder quoted = new StringBuilder(); quoted.Append('"'); int backslashes = 0; foreach (char character in argument) { if (character == '\\') { backslashes += 1; } else if (character == '"') { quoted.Append('\\', backslashes * 2 + 1); quoted.Append('"'); backslashes = 0; } else { quoted.Append('\\', backslashes); quoted.Append(character); backslashes = 0; } } quoted.Append('\\', backslashes * 2); quoted.Append('"'); return quoted.ToString(); } private static void TerminateAndWaitForEmptyJob(IntPtr job) { if (!TerminateJobObject(job, 1)) { ThrowLastError("TerminateJobObject"); } int size = Marshal.SizeOf( typeof(JOBOBJECT_BASIC_ACCOUNTING_INFORMATION) ); IntPtr information = Marshal.AllocHGlobal(size); try { DateTime deadline = DateTime.UtcNow.AddSeconds(10); while (true) { if (!QueryInformationJobObject( job, JobObjectBasicAccountingInformation, information, (uint)size, IntPtr.Zero )) { ThrowLastError("QueryInformationJobObject"); } JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting = (JOBOBJECT_BASIC_ACCOUNTING_INFORMATION) Marshal.PtrToStructure( information, typeof(JOBOBJECT_BASIC_ACCOUNTING_INFORMATION) ); if (accounting.ActiveProcesses == 0) return; if (DateTime.UtcNow >= deadline) { throw new InvalidOperationException( "Managed Windows job did not terminate." ); } Thread.Sleep(10); } } finally { Marshal.FreeHGlobal(information); } } public static int Run( string nodePath, string runnerPath, string requestBase64, string currentDirectory) { IntPtr attributeList = IntPtr.Zero; IntPtr attributeListSize = IntPtr.Zero; bool attributeListInitialized = false; IntPtr handleList = IntPtr.Zero; IntPtr job = IntPtr.Zero; IntPtr jobList = IntPtr.Zero; IntPtr limitPointer = IntPtr.Zero; IntPtr stderrDuplicate = IntPtr.Zero; IntPtr stdinDuplicate = IntPtr.Zero; IntPtr stdoutDuplicate = IntPtr.Zero; PROCESS_INFORMATION process = new PROCESS_INFORMATION(); try { job = CreateJobObject(IntPtr.Zero, null); if (job == IntPtr.Zero) ThrowLastError("CreateJobObject"); JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION(); limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; int limitSize = Marshal.SizeOf(limits); limitPointer = Marshal.AllocHGlobal(limitSize); Marshal.StructureToPtr(limits, limitPointer, false); if (!SetInformationJobObject( job, JobObjectExtendedLimitInformation, limitPointer, (uint)limitSize )) { ThrowLastError("SetInformationJobObject"); } InitializeProcThreadAttributeList( IntPtr.Zero, 2, 0, ref attributeListSize ); if (attributeListSize == IntPtr.Zero) { ThrowLastError("InitializeProcThreadAttributeList size"); } attributeList = Marshal.AllocHGlobal(attributeListSize); if (!InitializeProcThreadAttributeList( attributeList, 2, 0, ref attributeListSize )) { ThrowLastError("InitializeProcThreadAttributeList"); } attributeListInitialized = true; jobList = Marshal.AllocHGlobal(IntPtr.Size); Marshal.WriteIntPtr(jobList, job); SetAttribute( attributeList, PROC_THREAD_ATTRIBUTE_JOB_LIST, jobList, IntPtr.Size, "job list" ); stdinDuplicate = DuplicateRequiredStdHandle( STD_INPUT_HANDLE, "stdin" ); stdoutDuplicate = DuplicateRequiredStdHandle( STD_OUTPUT_HANDLE, "stdout" ); stderrDuplicate = DuplicateRequiredStdHandle( STD_ERROR_HANDLE, "stderr" ); STARTUPINFOEX startup = new STARTUPINFOEX(); startup.StartupInfo.cb = Marshal.SizeOf(startup); startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES; startup.StartupInfo.hStdInput = stdinDuplicate; startup.StartupInfo.hStdOutput = stdoutDuplicate; startup.StartupInfo.hStdError = stderrDuplicate; handleList = Marshal.AllocHGlobal(IntPtr.Size * 3); Marshal.WriteIntPtr(handleList, 0, stdinDuplicate); Marshal.WriteIntPtr(handleList, IntPtr.Size, stdoutDuplicate); Marshal.WriteIntPtr(handleList, IntPtr.Size * 2, stderrDuplicate); SetAttribute( attributeList, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handleList, IntPtr.Size * 3, "handle list" ); startup.lpAttributeList = attributeList; StringBuilder commandLine = new StringBuilder( QuoteArgument(nodePath) + " " + QuoteArgument(runnerPath) + " " + QuoteArgument(requestBase64) ); if (!CreateProcess( nodePath, commandLine, IntPtr.Zero, IntPtr.Zero, true, EXTENDED_STARTUPINFO_PRESENT, IntPtr.Zero, currentDirectory, ref startup, out process )) { ThrowLastError("CreateProcess"); } if (WaitForSingleObject(process.hProcess, INFINITE) == UInt32.MaxValue) { ThrowLastError("WaitForSingleObject"); } uint exitCode; if (!GetExitCodeProcess(process.hProcess, out exitCode)) { ThrowLastError("GetExitCodeProcess"); } return unchecked((int)exitCode); } finally { CloseOwnedHandle(process.hThread); CloseOwnedHandle(process.hProcess); if (attributeListInitialized) DeleteProcThreadAttributeList(attributeList); if (attributeList != IntPtr.Zero) Marshal.FreeHGlobal(attributeList); if (handleList != IntPtr.Zero) Marshal.FreeHGlobal(handleList); if (jobList != IntPtr.Zero) Marshal.FreeHGlobal(jobList); if (limitPointer != IntPtr.Zero) Marshal.FreeHGlobal(limitPointer); CloseOwnedHandle(stderrDuplicate); CloseOwnedHandle(stdoutDuplicate); CloseOwnedHandle(stdinDuplicate); if (job != IntPtr.Zero) { try { TerminateAndWaitForEmptyJob(job); } finally { CloseHandle(job); } } } } }