Created
September 1, 2017 01:55
-
-
Save BillyONeal/b4eaa1dbe808a2984eecae516142a685 to your computer and use it in GitHub Desktop.
Because System.Diagnostics.Process is the worst class in the BCL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// /******************************************************** | |
// * * | |
// * Copyright (C) Microsoft. All rights reserved. * | |
// * * | |
// ********************************************************/ | |
namespace Runall.CLI | |
{ | |
using System; | |
using System.Runtime.InteropServices; | |
/// <summary>A captive command line program test process executive.</summary> | |
public static class CommandLineProcess | |
{ | |
/// <summary>Runs a process with the given arguments, captures its output, and echos its | |
/// output to the console.</summary> | |
/// <param name="settings">Settings which control which program is executed.</param> | |
/// <returns>The exit code and console output of the process.</returns> | |
public static CommandLineProcessResult Execute(CommandLineProcessSettings settings) | |
{ | |
StreamOutputCollector output; | |
bool timedOut; | |
DateTime deadline = TimeoutToDeadline(settings.Timeout); | |
UInt32 exitCode; | |
var startInfo = new NativeMethods.STARTUPINFOEX | |
{ | |
StartupInfo = new NativeMethods.STARTUPINFO | |
{ | |
cb = (uint)Marshal.SizeOf(typeof(NativeMethods.STARTUPINFOEX)), | |
dwFlags = NativeMethods.StartupInfoFlags.USESTDHANDLES | NativeMethods.StartupInfoFlags.USESHOWWINDOW, | |
wShowWindow = 0 /* SW_HIDE */ | |
} | |
}; | |
NativeMethods.PROCESS_INFORMATION procInfo = new NativeMethods.PROCESS_INFORMATION(); | |
IntPtr inheritedHandlesArray = IntPtr.Zero; | |
IntPtr procThreadAttributeList = IntPtr.Zero; | |
using (var job = new JobObject()) | |
using (var stdIn = new AnonymousPipe()) | |
using (var stdOut = new AsynchronouslyReadablePipe()) | |
using (var environmentMem = new HGlobalString(settings.GetEnvironmentBlock())) | |
{ | |
job.MakeJobCaptiveChild(); | |
output = new StreamOutputCollector(stdOut.AsynchronousReadStream); | |
try | |
{ | |
stdIn.WritePipe.Close(); | |
startInfo.StartupInfo.hStdInput = stdIn.ReadPipe; | |
startInfo.StartupInfo.hStdOutput = stdOut.SynchronousWritePipe; | |
startInfo.StartupInfo.hStdError = stdOut.SynchronousWritePipe; | |
IntPtr[] handles = new IntPtr[] { stdIn.ReadPipe.DangerousGetHandle(), | |
stdOut.SynchronousWritePipe.DangerousGetHandle() }; | |
int handlesArraySize = IntPtr.Size * 2; | |
inheritedHandlesArray = Marshal.AllocHGlobal(handlesArraySize); | |
Marshal.Copy(handles, 0, inheritedHandlesArray, 2); | |
IntPtr procThreadAttributeListSize = IntPtr.Zero; | |
if (!NativeMethods.InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref procThreadAttributeListSize)) | |
{ | |
int lastError = Marshal.GetHRForLastWin32Error(); | |
if ((uint)lastError != 0x8007007Au) | |
{ | |
Marshal.ThrowExceptionForHR(lastError); | |
} | |
} | |
procThreadAttributeList = Marshal.AllocHGlobal(procThreadAttributeListSize); | |
if (!NativeMethods.InitializeProcThreadAttributeList(procThreadAttributeList, 1, 0, ref procThreadAttributeListSize)) | |
{ | |
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); | |
} | |
if (!NativeMethods.UpdateProcThreadAttribute(procThreadAttributeList, | |
0, | |
NativeMethods.PROC_THREAD_ATTRIBUTE_HANDLE_LIST, | |
procThreadAttributeListSize, | |
(uint)handlesArraySize, | |
IntPtr.Zero, | |
IntPtr.Zero)) | |
{ | |
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); | |
} | |
startInfo.lpAttributeList = procThreadAttributeList; | |
string arguments; | |
if (settings.FileName == null) | |
{ | |
// caller passing executable as part of arguments, don't insert a space as that interferes | |
// with path searching inside CreateProcessW | |
arguments = settings.Arguments; | |
output.AppendLine("### Starting command line: " + arguments); | |
} | |
else | |
{ | |
// caller passed executable explicitly; add an extra space before arguments to ensure called | |
// process doesn't mash the executable name and its first argument together | |
arguments = " " + settings.Arguments; | |
output.AppendLine("### Starting executable: " + settings.FileName); | |
output.AppendLine("### with arguments: " + settings.Arguments); | |
} | |
if (NativeMethods.CreateProcessW( | |
settings.FileName, | |
arguments, | |
IntPtr.Zero, | |
IntPtr.Zero, | |
true, | |
NativeMethods.ProcessCreationFlags.CREATE_NEW_CONSOLE | NativeMethods.ProcessCreationFlags.CREATE_NO_WINDOW | |
| NativeMethods.ProcessCreationFlags.CREATE_UNICODE_ENVIRONMENT | NativeMethods.ProcessCreationFlags.CREATE_SUSPENDED, | |
environmentMem.Value, | |
settings.WorkingDirectory, | |
ref startInfo, | |
out procInfo | |
) == false) | |
{ | |
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); | |
} | |
// close ends of the pipes we aren't using | |
stdIn.ReadPipe.Close(); | |
stdOut.SynchronousWritePipe.Dispose(); | |
// setup a reciever for the process' output | |
// let the process go | |
job.AttachProcessToJobAndRun(ref procInfo); | |
// wait for the process to complete | |
timedOut = AlertableWaitWithTimeout(deadline, procInfo.hProcess); | |
// kill all child processes (in case timeout occurred) | |
job.Terminate(0x454D4954u); // exit code is 'T' 'I' 'M' 'E' as a dword | |
// Get the tested program's exit code. | |
if (NativeMethods.GetExitCodeProcess(procInfo.hProcess, out exitCode) == false) | |
{ | |
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); | |
} | |
} | |
finally | |
{ | |
if (procInfo.hProcess != IntPtr.Zero) | |
{ | |
NativeMethods.CloseHandle(procInfo.hProcess); | |
} | |
if (procInfo.hThread != IntPtr.Zero) | |
{ | |
NativeMethods.CloseHandle(procInfo.hThread); | |
} | |
if (procThreadAttributeList != IntPtr.Zero) | |
{ | |
NativeMethods.DeleteProcThreadAttributeList(procThreadAttributeList); | |
Marshal.FreeHGlobal(procThreadAttributeList); | |
} | |
Marshal.FreeHGlobal(inheritedHandlesArray); | |
} | |
} | |
return new CommandLineProcessResult(output.ToString(), exitCode, timedOut); | |
} | |
private static DateTime TimeoutToDeadline(TimeSpan? timeout) | |
{ | |
DateTime deadline; | |
if (timeout.HasValue) | |
{ | |
deadline = DateTime.UtcNow + timeout.Value; | |
} | |
else | |
{ | |
deadline = DateTime.MaxValue; | |
} | |
return deadline; | |
} | |
private static bool AlertableWaitWithTimeout(DateTime deadline, IntPtr hProcess) | |
{ | |
uint? waitLength; | |
while ((waitLength = CheckTimeout(deadline)).HasValue) | |
{ | |
// Wait for the tested program to exit. | |
switch (NativeMethods.WaitForSingleObjectEx(hProcess, waitLength.Value, true)) | |
{ | |
case NativeMethods.WAIT_OBJECT_0: | |
// child exited normally | |
return false; | |
case NativeMethods.WAIT_TIMEOUT: | |
case NativeMethods.WAIT_IO_COMPLETION: | |
// timeout or I/O, check timeout again | |
continue; | |
case NativeMethods.WAIT_FAILED: | |
// doom | |
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); | |
break; | |
default: | |
// ??? doom | |
throw new Exception("Unexpected return value from WaitForSingleObject"); | |
} | |
} | |
return true; | |
} | |
private static uint? CheckTimeout(DateTime deadline) | |
{ | |
if (deadline == DateTime.MaxValue) | |
{ | |
return uint.MaxValue; | |
} | |
else | |
{ | |
var remainingWait = deadline - DateTime.UtcNow; | |
if (remainingWait.Ticks < 0) | |
{ | |
return default(uint?); | |
} | |
double totalMsLeft = remainingWait.TotalMilliseconds; | |
if (totalMsLeft > uint.MaxValue) | |
{ | |
return uint.MaxValue - 1u; | |
} | |
else | |
{ | |
return (uint)totalMsLeft; | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment