-
Notifications
You must be signed in to change notification settings - Fork 4
/
AwaitDriver.cs
555 lines (476 loc) · 21.2 KB
/
AwaitDriver.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
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
}
}
}