AwaitDriver.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
using System.Diagnostics;
using System.Runtime.InteropServices;
 
namespace AwaitDriver
{
    public class AwaitDriver
    {
        /// <summary>
        /// Creates a new instance of the AwaitDriver class, which lets you perform
        /// expect-like functionality on Console applications.
        /// </summary>
        public AwaitDriver()
        {
            // Launch a new instance of PowerShell and steal its console input / output handles
            ProcessStartInfo startInfo = new ProcessStartInfo("C:\\windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "-NoProfile");
            startInfo.WindowStyle = ProcessWindowStyle.Hidden;
            driverHost = Process.Start(startInfo);
 
            System.Threading.Thread.Sleep(100);
 
            NativeMethods.FreeConsole();
            bool result = NativeMethods.AttachConsole(driverHost.Id);
 
            const UInt32 STD_INPUT_HANDLE = 0xFFFFFFF6;
            const UInt32 STD_OUTPUT_HANDLE = 0xFFFFFFF5;
            hInput = NativeMethods.GetStdHandle(STD_INPUT_HANDLE);
            hOutput = NativeMethods.GetStdHandle(STD_OUTPUT_HANDLE);
        }
 
        // Handles from the driver process
        Process driverHost = null;
        IntPtr hInput = IntPtr.Zero;
        IntPtr hOutput = IntPtr.Zero;
 
        // State to remember information between Read() calls so
        // that we don't have to scan the full buffer each time.
        int lastReadPosition = 0;
        List<string> lastRawContent = new List<string>();
 
        /// <summary>
        ///
        /// Sends the specified input to the driver process. Does not
        /// include a newline at the end of the input.
        ///
        /// Input can be basic characters, or can use the same set of metacharacters
        /// that the SendKeys() API supports, such as {ESC}.
        ///
        /// http://msdn.microsoft.com/en-us/library/system.windows.forms.sendkeys.send(v=vs.110).aspx
        ///
        /// While the syntax is the same as the SendKeys() API, the approach is not
        /// based on SendKeys() at all - to ensure that applications can be automated while
        /// you use your keyboard and mouse for other things.
        ///
        /// </summary>
        /// <param name="text">The text to send</param>
        public void Send(string text)
        {
            List<NativeMethods.INPUT_RECORD> inputs = new List<NativeMethods.INPUT_RECORD>();
 
            foreach (string inputElement in SendKeysParser.Parse(text))
            {
                foreach (NativeMethods.INPUT_RECORD mappedInput in SendKeysParser.MapInput(inputElement))
                {
                    inputs.Add(mappedInput);
                }
            }
 
            uint eventsWritten = 0;
            NativeMethods.WriteConsoleInput(hInput, inputs.ToArray(), (uint)inputs.Count, out eventsWritten);
        }
 
        /// <summary>
        /// Sends the specified input to the driver process, including a trailing newline.
        /// </summary>
        /// <param name="text">The text to send</param>
        public void SendLine(string input)
        {
            Send(input + "{ENTER}");
        }
 
        /// <summary>
        /// Waits for the given string to be available as a command response, and
        /// returns the result of that reponse.
        ///
        /// This method is stateful. Once output has been returned from an AwaitOutput()
        /// call, it will not be output again. Further AwaitOutput() calls will operate
        /// against output produced after preceeding AwaitOutput() calls complete.
        ///
        /// </summary>
        /// <param name="expected">The string to search for in the command response.</param>
        public string AwaitOutput(string expected)
        {
            return AwaitOutput(expected, false);
        }
 
        /// <summary>
        /// Waits for the given string to be available as a command response, and
        /// returns the result of that reponse.
        ///
        /// </summary>
        /// <param name="expected">The string to search for in the command response.</param>
        /// <param name="all">
        /// If 'All' is specified, this method acts against the entire content of the
        /// console buffer, and the output of this method is the entire content of the
        /// console buffer.
        /// </param>
        public string AwaitOutput(string expected, bool all)
        {
            // Save the internal buffer position state so that we can restore
            // it if the content doesn't match.
            int savedStartPosition = lastReadPosition;
            List<string> savedLastRawContent = new List<string>(lastRawContent);
            string output = null;
 
            // Should be able to replace this with a WinEventHook for console changes:
            // http://msdn.microsoft.com/en-us/library/windows/desktop/ms682102(v=vs.85).aspx
            while (true)
            {
                output = ReadOutput(all);
 
                if (output.Contains(expected))
                {
                    break;
                }
                else
                {
                    lastReadPosition = savedStartPosition;
                    lastRawContent = new List<string>(savedLastRawContent);
                }
 
                System.Threading.Thread.Sleep(10);
            }
 
            return output;
        }
 
        /// <summary>
        /// Closes the AwaitDriver class, which lets you perform
        /// expect-like functionality on Console applications.
        /// </summary>
        public void Close()
        {
            driverHost.Kill();
        }
 
        public string ReadOutput()
        {
            return ReadOutput(false);
        }
 
        // Can scrape 650 lines of output at 11ms per scrape.
        public string ReadOutput(bool all)
        {
            // Check the current console screen buffer dimensions, cursor coordinates / etc.
            NativeMethods.CONSOLE_SCREEN_BUFFER_INFO csbi;
            NativeMethods.GetConsoleScreenBufferInfo(hOutput, out csbi);
 
            StringBuilder output = new StringBuilder();
 
            // If the cursor has gone before where we last scanned, then the screen
            // has been cleared and we should reset our state.
            if (lastReadPosition > csbi.dwCursorPosition.Y + 1)
            {
                ResetLastScanInfo();
            }
 
            // If the cursor is at the end of the buffer, then we no longer have a
            // quick way to determine if new content has been written. To address this,
            // every ReadOutput() call retains the last 10 lines of output that it read.
            //
            // As we scan through the buffer, we fill the 'currentHeuristicContent' list
            // with the output lines we see. Once we have 10 matching lines of output, we
            // know we're at the end of the previous scan.
            //
            // Additionally, we only do this heuristic scan if they haven't specified the
            // 'All' flag.
            bool inHeuristicContentScan = false;
            List<string> currentHeuristicContent = new List<string>();
 
            if ((!all) && (lastReadPosition == csbi.dwSize.Y))
            {
                inHeuristicContentScan = true;
                lastReadPosition = 0;
            }
 
            // Figure out where to start and stop scanning
            int startReadPosition = lastReadPosition;
            int endReadPosition = csbi.dwCursorPosition.Y;
 
            if (all)
            {
                startReadPosition = 0;
                endReadPosition = csbi.dwSize.Y - 1;
            }
 
            // Go through each line in the buffer
            for (int row = startReadPosition; row <= endReadPosition; row++)
            {
                int width = csbi.dwMaximumWindowSize.X;
                StringBuilder lpCharacter = new StringBuilder(width - 1);
 
                // Read the current line from the buffer
                NativeMethods.COORD dwReadCoord;
                dwReadCoord.X = 0;
                dwReadCoord.Y = (short)row;
 
                uint lpNumberOfCharsRead = 0;
                NativeMethods.ReadConsoleOutputCharacter(hOutput, lpCharacter, (uint)width, dwReadCoord, out lpNumberOfCharsRead);
 
                // If we're in a heuristic scan, and the current line to the heuristic scan buffer
                if (inHeuristicContentScan)
                {
                    currentHeuristicContent.Add(lpCharacter.ToString());
 
                    // Ensure the two lists remain the same size
                    if (currentHeuristicContent.Count > lastRawContent.Count)
                    {
                        currentHeuristicContent.RemoveAt(0);
                    }
 
                    // If the heuristic scan buffer is the same size as the trailing buffer from the last ReadOutput()
                    // call, we can see if they match.
                    if (currentHeuristicContent.Count == lastRawContent.Count)
                    {
                        bool foundContentMatch = true;
 
                        // Only scan to the second-last line of the saved content buffer, as the last line
                        // is frequently different. For example, a prompt in the last scan, and now a prompt
                        // that includes the user's command.
                        for (int contentIndex = 0; contentIndex < currentHeuristicContent.Count - 1; contentIndex++)
                        {
                            if (lastRawContent[contentIndex] != currentHeuristicContent[contentIndex])
                            {
                                foundContentMatch = false;
                                break;
                            }
                        }
 
                        // All 10 lines matched
                        if (foundContentMatch)
                        {
                            inHeuristicContentScan = false;
                            continue;
                        }
                    }
                }
 
                // If we're not just scanning through the buffer heuristically, then capture the content.
                if (!inHeuristicContentScan)
                {
                    lastRawContent.Add(lpCharacter.ToString());
 
                    if (lastRawContent.Count > 10)
                    {
                        lastRawContent.RemoveAt(0);
                    }
 
                    output.AppendLine(lpCharacter.ToString().Substring(0, width).TrimEnd());
                }
            }
 
            // Update our state to remember where to start scanning the buffer next.
            lastReadPosition = endReadPosition + 1;
 
            // If we were at the end of the buffer and made it all the way here without matching,
            // then our heuristics (i.e.: detecting clearing the screen) got broken.
            // This can happen with repetitive commands when the buffer is full, such as:
 
            // Get-Date
            // cls
            // Get-Date
 
            // Reset them.
            if (inHeuristicContentScan)
            {
                ResetLastScanInfo();
                return ReadOutput(all);
            }
            else
            {
                // We got content - return it.
                return output.ToString().TrimEnd();
            }
        }
 
        private void ResetLastScanInfo()
        {
            lastReadPosition = 0;
            lastRawContent.Clear();
        }
    }
 
    class SendKeysParser
    {
        public static List<string> Parse(string input)
        {
            List<string> output = new List<string>();
 
            bool scanningKeyName = false;
            StringBuilder keyNameBuffer = new StringBuilder();
 
            // Iterate through the string
            for (int index = 0; index < input.Length; index++)
            {
                // Save the current item
                char currentChar = input[index];
 
                // We may have the start of a command
                if (currentChar == '{')
                {
                    if (scanningKeyName)
                    {
                        throw new Exception("The character '{' is not a valid in a key name. " +
                            "To include the '{' character in your text, escape it with another: {{.");
                    }
 
                    // If it's escaped, then add it to output.
                    if ((index < (input.Length - 1)) &&
                        (input[index + 1] == '{'))
                    {
                        output.Add(currentChar.ToString());
                        index++;
                    }
                    else
                    {
                        // Otherwise, we found the start of a key name.
                        scanningKeyName = true;
                    }
                }
                else if (currentChar == '}')
                {
                    // We may have the end of a key name
 
                    // If it's escaped, then add it to output.
                    if ((index < (input.Length - 1)) &&
                        (input[index + 1] == '}'))
                    {
                        // But not if we're scanning a key name
                        if (scanningKeyName)
                        {
                            throw new Exception("The character '}' is not a valid in a key name. " +
                                "To include the '}' character in your text, escape it with another: }}.");
                        }
 
                        output.Add(currentChar.ToString());
                        index++;
                    }
                    else
                    {
                        // Not escaped
 
                        // If we're scanning a key name, record it.
                        if (scanningKeyName)
                        {
                            string keyName = keyNameBuffer.ToString();
                            if (String.IsNullOrEmpty(keyName))
                            {
                                throw new Exception("Key names may not be empty.");
                            }
 
                            output.Add(keyNameBuffer.ToString());
                            scanningKeyName = false;
                        }
                        else
                        {
                            throw new Exception("The character '}' is not a valid by itself. " +
                                "To include the '}' character in your text, escape it with another: }}.");
                        }
                    }
                }
                else
                {
                    // Just a letter
                    if (scanningKeyName)
                    {
                        keyNameBuffer.Append(currentChar);
                    }
                    else
                    {
                        output.Add(currentChar.ToString());
                    }
                }
            }
 
            // We got to the end of the string.
            if (scanningKeyName)
            {
                throw new Exception("The character '{' (representing the start of a key name) did not have a matching '}' " +
                    "character. To include the '{' character in your text, escape it with another: {{.");
            }
 
            return output;
        }
 
        internal static List<NativeMethods.INPUT_RECORD> MapInput(string inputElement)
        {
            List<NativeMethods.INPUT_RECORD> inputs = new List<NativeMethods.INPUT_RECORD>();
 
            NativeMethods.INPUT_RECORD input = new NativeMethods.INPUT_RECORD();
            input.EventType = 0x0001;
 
            NativeMethods.KEY_EVENT_RECORD keypress = new NativeMethods.KEY_EVENT_RECORD();
            keypress.dwControlKeyState = 0;
            keypress.wRepeatCount = 1;
 
            // Just a regular character
            if (inputElement.Length == 1)
            {
                keypress.UnicodeChar = inputElement[0];
            }
            else
            {
                switch (inputElement.ToUpperInvariant())
                {
                    case "BACKSPACE":
                    case "BS":
                    case "BKSP":
                        keypress = GetKeyPressForSimpleKey(keypress, 0x08);
                        break;
 
                    case "BREAK":
                        keypress = GetKeyPressForSimpleKey(keypress, 0x03);
                        keypress.dwControlKeyState = (uint) NativeMethods.ControlKeyStates.LEFT_CTRL_PRESSED;
                        keypress.UnicodeChar = (char) 0;
                        break;
 
                    case "ENTER":
                        keypress = GetKeyPressForSimpleKey(keypress, 0x0D);
                        break;
 
                    case "ESC":
                        keypress = GetKeyPressForSimpleKey(keypress, 0x1B);
                        break;
                }
            }
 
            keypress.bKeyDown = true;
            input.KeyEvent = keypress;
            inputs.Add(input);
 
            keypress.bKeyDown = false;
            keypress.dwControlKeyState = 0;
            input.KeyEvent = keypress;
            inputs.Add(input);
 
            return inputs;
        }
 
        private static NativeMethods.KEY_EVENT_RECORD GetKeyPressForSimpleKey(NativeMethods.KEY_EVENT_RECORD keypress, uint uCode)
        {
            keypress.UnicodeChar = (char)uCode;
            keypress.wVirtualKeyCode = (ushort)uCode;
            keypress.wVirtualScanCode = (ushort)NativeMethods.MapVirtualKey(uCode, MAPVK_VK_TO_VSC);
            return keypress;
        }
 
        const ushort MAPVK_VK_TO_VSC = 0x00;
    }
 
    class NativeMethods
    {
        [DllImport("kernel32.dll", SetLastError = true)]
        internal static extern bool AttachConsole(int pid);
 
        [DllImport("kernel32.dll")]
        internal static extern bool FreeConsole();
 
        [DllImport("kernel32.dll", SetLastError = true)]
        internal static extern bool WriteConsoleInput(IntPtr hConsoleInput, INPUT_RECORD[] lpBuffer, uint nLength, out uint lpNumberOfEventsWritten);
 
        [DllImport("user32.dll")]
        internal static extern uint MapVirtualKey(uint uCode, uint uMapType);
 
        [DllImport("kernel32.dll")]
        internal static extern IntPtr GetStdHandle(uint nStdHandle);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        internal static extern bool FlushConsoleInputBuffer(IntPtr hConsoleInput);
 
        [DllImport("Kernel32")]
        internal static extern bool ReadConsoleOutputCharacter(IntPtr hConsoleOutput, StringBuilder lpCharacter, uint nLength, COORD dwReadCoord, out uint lpNumberOfCharsRead);
 
        [DllImport("kernel32.dll")]
        internal static extern bool GetConsoleScreenBufferInfo(IntPtr hConsoleOutput, out CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo);
 
        [StructLayout(LayoutKind.Sequential)]
        internal struct COORD
        {
            internal short X;
            internal short Y;
        }
 
        internal struct SMALL_RECT
        {
            internal short Left;
            internal short Top;
            internal short Right;
            internal short Bottom;
        }
 
        internal struct CONSOLE_SCREEN_BUFFER_INFO
        {
            internal COORD dwSize;
            internal COORD dwCursorPosition;
            internal short wAttributes;
            internal SMALL_RECT srWindow;
            internal COORD dwMaximumWindowSize;
        }
 
        [StructLayout(LayoutKind.Explicit)]
        internal struct INPUT_RECORD
        {
            [FieldOffset(0)]
            internal ushort EventType;
            [FieldOffset(4)]
            internal KEY_EVENT_RECORD KeyEvent;
        };
 
        [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)]
        internal struct KEY_EVENT_RECORD
        {
            [FieldOffset(0), MarshalAs(UnmanagedType.Bool)]
            internal bool bKeyDown;
            [FieldOffset(4), MarshalAs(UnmanagedType.U2)]
            internal ushort wRepeatCount;
            [FieldOffset(6), MarshalAs(UnmanagedType.U2)]
            internal ushort wVirtualKeyCode;
            [FieldOffset(8), MarshalAs(UnmanagedType.U2)]
            internal ushort wVirtualScanCode;
            [FieldOffset(10)]
            internal char UnicodeChar;
            [FieldOffset(12), MarshalAs(UnmanagedType.U4)]
            internal uint dwControlKeyState;
        }
 
        internal enum ControlKeyStates
        {
            RIGHT_ALT_PRESSED = 0x1,
            LEFT_ALT_PRESSED = 0x2,
            RIGHT_CTRL_PRESSED = 0x4,
            LEFT_CTRL_PRESSED = 0x8,
            SHIFT_PRESSED = 0x10,
            NUMLOCK_ON = 0x20,
            SCROLLLOCK_ON = 0x40,
            CAPSLOCK_ON = 0x80,
            ENHANCED_KEY = 0x100
        }
    }
}