Skip to content

Instantly share code, notes, and snippets.

@BillyONeal
Created September 1, 2017 01:55
Show Gist options
  • Save BillyONeal/b4eaa1dbe808a2984eecae516142a685 to your computer and use it in GitHub Desktop.
Save BillyONeal/b4eaa1dbe808a2984eecae516142a685 to your computer and use it in GitHub Desktop.
Because System.Diagnostics.Process is the worst class in the BCL
// /********************************************************
// * *
// * 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