Repository navigation
Expand file tree
/
Copy pathKeySender.cs
More file actions
729 lines (649 loc) · 32 KB
/
Copy pathKeySender.cs
File metadata and controls
729 lines (649 loc) · 32 KB
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
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
// Keystroke injection. Everything that talks to user32 lives here.
//
// C# 5 only (in-box csc).
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
namespace RSPaster
{
internal static class Native
{
[StructLayout(LayoutKind.Sequential)]
public struct MOUSEINPUT
{
public int dx;
public int dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
public struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
public struct HARDWAREINPUT
{
public uint uMsg;
public ushort wParamL;
public ushort wParamH;
}
// All three members overlay at offset 0; INPUT is sequential so the
// union lands at the right offset on both x86 and x64.
[StructLayout(LayoutKind.Explicit)]
public struct InputUnion
{
[FieldOffset(0)] public MOUSEINPUT mi;
[FieldOffset(0)] public KEYBDINPUT ki;
[FieldOffset(0)] public HARDWAREINPUT hi;
}
[StructLayout(LayoutKind.Sequential)]
public struct INPUT
{
public uint type;
public InputUnion U;
}
public const uint INPUT_KEYBOARD = 1;
public const uint KEYEVENTF_EXTENDEDKEY = 0x0001;
public const uint KEYEVENTF_KEYUP = 0x0002;
public const uint KEYEVENTF_UNICODE = 0x0004;
[DllImport("user32.dll")]
public static extern short GetAsyncKeyState(int vKey);
// GetAsyncKeyState reports the physical key; this one reports the
// toggle bit, which is what CapsLock actually means to a layout.
[DllImport("user32.dll")]
public static extern short GetKeyState(int nVirtKey);
// Asks a layout what a key combination would produce, without sending
// anything. Used to check a CapsLock-affected key rather than assume.
//
// CharSet.Unicode is load-bearing here for a harsher reason than on
// VkKeyScanEx below: DllImport defaults to ANSI, which marshals the
// char[] as a one-byte-per-char buffer, so the runtime hands this API
// cchBuff bytes while it writes cchBuff WCHARs. That overruns the
// native buffer and corrupts the heap - it crashed the test host with
// STATUS_HEAP_CORRUPTION, several calls after the damage was done.
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int ToUnicodeEx(uint wVirtKey, uint wScanCode, byte[] lpKeyState,
[Out] char[] pwszBuff, int cchBuff, uint wFlags, IntPtr dwhkl);
[DllImport("user32.dll")]
public static extern bool SetProcessDPIAware();
[DllImport("user32.dll", SetLastError = true)]
public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
// CharSet.Unicode is load-bearing: DllImport defaults to ANSI, which
// marshals this char through the system code page. A character outside
// it (Greek, box drawing, checkmarks) became '?', which VkKeyScanExA
// happily resolved to the question-mark key - so instead of the unicode
// fallback the target received a literal '?'.
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern short VkKeyScanEx(char ch, IntPtr dwhkl);
[DllImport("user32.dll")]
public static extern uint MapVirtualKeyEx(uint uCode, uint uMapType, IntPtr dwhkl);
[DllImport("user32.dll")]
public static extern IntPtr GetKeyboardLayout(uint idThread);
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
// Identifying the window that is about to receive the keystrokes, and
// its integrity level. SendInput cannot report a UIPI block - the API
// documents that neither the return value nor GetLastError indicates
// it - so an elevated target has to be detected by looking first.
public const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
public const uint TOKEN_QUERY = 0x0008;
public const int TokenIntegrityLevel = 25;
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(uint access, bool inherit, uint pid);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr h);
[DllImport("kernel32.dll")]
public static extern uint GetCurrentProcessId();
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool QueryFullProcessImageName(IntPtr process, uint flags,
StringBuilder name, ref int size);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool OpenProcessToken(IntPtr process, uint access, out IntPtr token);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool GetTokenInformation(IntPtr token, int infoClass,
IntPtr info, int len, out int retLen);
[DllImport("advapi32.dll")]
public static extern IntPtr GetSidSubAuthority(IntPtr sid, uint index);
[DllImport("advapi32.dll")]
public static extern IntPtr GetSidSubAuthorityCount(IntPtr sid);
// The input desktop is unreachable when the workstation is locked or a
// UAC prompt owns the secure desktop - both silent-drop cases.
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr OpenInputDesktop(uint flags, bool inherit, uint access);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool CloseDesktop(IntPtr h);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
[DllImport("user32.dll")]
public static extern bool UnregisterHotKey(IntPtr hWnd, int id);
// Icon handles from Bitmap.GetHicon() are not owned by the Icon wrapper
// and leak a GDI handle per theme switch unless destroyed explicitly.
[DllImport("user32.dll", SetLastError = true)]
public static extern bool DestroyIcon(IntPtr hIcon);
// The two bits of chrome WinForms cannot recolor: a TextBox's scrollbars
// and the window title bar. Both are in-box on Win 10 1809+ / Win 11.
[DllImport("uxtheme.dll", CharSet = CharSet.Unicode)]
public static extern int SetWindowTheme(IntPtr hWnd, string pszSubAppName, string pszSubIdList);
[DllImport("dwmapi.dll")]
public static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int value, int size);
// Scrollbars are the one piece that needs undocumented uxtheme ordinals:
// SetWindowTheme("DarkMode_Explorer") alone is ignored until the process
// has opted into dark mode. These are exported by ordinal only and are
// absent before Win10 1809, so every call site swallows the lookup
// failure and settles for light scrollbars.
[DllImport("uxtheme.dll", EntryPoint = "#135")]
public static extern int SetPreferredAppMode(int mode);
[DllImport("uxtheme.dll", EntryPoint = "#133")]
public static extern bool AllowDarkModeForWindow(IntPtr hwnd, bool allow);
public const int WM_THEMECHANGED = 0x031A;
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
public const uint RDW_INVALIDATE = 0x0001;
public const uint RDW_FRAME = 0x0400;
public const uint RDW_UPDATENOW = 0x0100;
[DllImport("user32.dll")]
public static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprc, IntPtr hrgn, uint flags);
}
// Where the keystrokes are about to land.
//
// This exists because SendInput cannot tell us it failed. When UIPI blocks
// an injected keystroke - which is exactly what happens when an unelevated
// process types into an elevated window - the call still returns the full
// event count and GetLastError still says success. The keystrokes simply
// vanish. Inferring a block from the return value therefore cannot work for
// the one case the warning exists for, so the check has to happen before
// typing rather than after.
public static class Target
{
public class Info
{
public IntPtr Window;
public uint ProcessId;
public string ProcessName; // null when it could not be read
public int Integrity = -1; // -1 when it could not be read
public bool IsSelf;
}
// Integrity RIDs, from the mandatory label SID's last sub-authority.
public const int LOW = 0x1000;
public const int MEDIUM = 0x2000;
public const int HIGH = 0x3000;
public static Info Foreground()
{
Info info = new Info();
info.Window = Native.GetForegroundWindow();
if (info.Window == IntPtr.Zero) return info;
uint pid;
Native.GetWindowThreadProcessId(info.Window, out pid);
info.ProcessId = pid;
if (pid == 0) return info;
info.IsSelf = pid == Native.GetCurrentProcessId();
IntPtr h = Native.OpenProcess(Native.PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
if (h == IntPtr.Zero) return info; // a protected process; leave unknown
try
{
info.ProcessName = ImageName(h);
info.Integrity = IntegrityOf(h);
}
finally { Native.CloseHandle(h); }
return info;
}
public static int Own()
{
IntPtr h = Native.OpenProcess(Native.PROCESS_QUERY_LIMITED_INFORMATION,
false, Native.GetCurrentProcessId());
if (h == IntPtr.Zero) return -1;
try { return IntegrityOf(h); }
finally { Native.CloseHandle(h); }
}
// False when the workstation is locked or a UAC prompt has taken the
// secure desktop. Injected input goes nowhere in both cases, and like
// UIPI neither one reports itself through SendInput.
//
// Two signals, and both must say unusable before this refuses. Opening
// the input desktop is denied with ERROR_ACCESS_DENIED while the secure
// desktop owns input - verified against a locked session, where it
// fails with 5 and there is no foreground window either. Requiring the
// second signal keeps an unexpected denial on some hardened
// configuration from making the app refuse to type at all, which for a
// tool whose only job is typing is a worse failure than typing into a
// window that turns out to be wrong.
public static bool InputDesktopReachable()
{
const uint DESKTOP_READOBJECTS = 0x0001;
IntPtr d = Native.OpenInputDesktop(0, false, DESKTOP_READOBJECTS);
if (d != IntPtr.Zero)
{
Native.CloseDesktop(d);
return true;
}
return Native.GetForegroundWindow() != IntPtr.Zero;
}
static string ImageName(IntPtr process)
{
int size = 260;
StringBuilder sb = new StringBuilder(size);
if (!Native.QueryFullProcessImageName(process, 0, sb, ref size)) return null;
string path = sb.ToString();
int slash = path.LastIndexOf('\\');
if (slash >= 0 && slash < path.Length - 1) path = path.Substring(slash + 1);
if (path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
path = path.Substring(0, path.Length - 4);
return path.Length == 0 ? null : path;
}
// The integrity level is the last sub-authority of the mandatory label
// SID in TokenIntegrityLevel. Returns -1 if anything along the way is
// refused, so an unreadable target is never treated as a blocked one.
static int IntegrityOf(IntPtr process)
{
IntPtr token;
if (!Native.OpenProcessToken(process, Native.TOKEN_QUERY, out token)) return -1;
IntPtr buffer = IntPtr.Zero;
try
{
int needed;
Native.GetTokenInformation(token, Native.TokenIntegrityLevel,
IntPtr.Zero, 0, out needed);
if (needed <= 0) return -1;
buffer = Marshal.AllocHGlobal(needed);
if (!Native.GetTokenInformation(token, Native.TokenIntegrityLevel,
buffer, needed, out needed)) return -1;
// TOKEN_MANDATORY_LABEL is a SID_AND_ATTRIBUTES, so the PSID is
// the first pointer-sized field.
IntPtr sid = Marshal.ReadIntPtr(buffer);
if (sid == IntPtr.Zero) return -1;
IntPtr countPtr = Native.GetSidSubAuthorityCount(sid);
if (countPtr == IntPtr.Zero) return -1;
int count = Marshal.ReadByte(countPtr);
if (count <= 0) return -1;
IntPtr ridPtr = Native.GetSidSubAuthority(sid, (uint)(count - 1));
if (ridPtr == IntPtr.Zero) return -1;
return Marshal.ReadInt32(ridPtr);
}
catch (OutOfMemoryException) { return -1; }
finally
{
if (buffer != IntPtr.Zero) Marshal.FreeHGlobal(buffer);
Native.CloseHandle(token);
}
}
}
public class TypeResult
{
public int KeysSent;
public int Blocked;
public bool Cancelled;
}
public class TypeOptions
{
public int PerKeyDelayMs = 15;
// Pause after each Enter before the next line is typed, for consoles
// where a command needs time to finish before the next can be entered.
public int LineDelayMs;
public bool UnicodeMode;
// Sends a final Enter. A flag rather than the caller appending "\n",
// which would copy the whole secret to add one character.
public bool AppendEnter;
public Func<bool> Cancelled;
public Action<int, int> Progress; // characters done, total
public Action<int, int, int> LineWait; // ms remaining, next line, total lines
}
public static class KeySender
{
const ushort VK_SHIFT = 0x10;
const ushort VK_CONTROL = 0x11;
const ushort VK_MENU = 0x12;
const ushort VK_RETURN = 0x0D;
const ushort VK_TAB = 0x09;
const ushort VK_CAPITAL = 0x14;
const ushort SCAN_LSHIFT = 0x2A;
const ushort SCAN_LCONTROL = 0x1D;
const ushort SCAN_RALT = 0x38; // with the extended bit: right Alt / AltGr
// Types 'text' into the focused window. Newlines become Enter, tabs
// become Tab. In scancode mode each character is mapped through the
// target window's keyboard layout to a virtual key + scancode, which is
// what VM/IPMI/VNC consoles listen for; characters needing AltGr or
// absent from the layout fall back to a KEYEVENTF_UNICODE event. In
// unicode mode every character is sent as a unicode event.
public static TypeResult TypeText(string text, TypeOptions o)
{
if (text.IndexOf('\r') >= 0)
text = text.Replace("\r\n", "\n").Replace("\r", "\n");
IntPtr hkl = ForegroundLayout();
TypeResult result = new TypeResult();
// The trailing Enter is generated in the loop rather than appended
// to the string, so a secret is not copied to add one character.
int total = text.Length + (o.AppendEnter ? 1 : 0);
// CapsLock is a property of the machine, not of the text. Sampled
// once: the layout question it changes is the same for every
// character, and the operator is not at the keyboard during a run.
bool capsOn = (Native.GetKeyState(VK_CAPITAL) & 1) != 0;
Func<bool> cancelled = o.Cancelled;
// With a start delay of 0 and the hotkey, the user's fingers are
// still on Ctrl+Alt+V when typing begins, so the first injected
// keys arrive at the target as Ctrl+Alt chords - shortcuts, killed
// commands. Hold until the physical keys are up before sending.
if (!WaitForKeysUp(cancelled))
{
result.Cancelled = true;
return result;
}
Action<int, int> progress = o.Progress;
bool unicodeMode = o.UnicodeMode;
int perKeyDelayMs = o.PerKeyDelayMs;
// A trailing newline is the "press Enter at end" one and does not
// start another line, so it must not inflate the count.
int totalLines = 1;
for (int i = 0; i < text.Length; i++)
if (text[i] == '\n' && i < total - 1) totalLines++;
int lineNo = 1;
try
{
for (int i = 0; i < total; i++)
{
if (cancelled != null && cancelled())
{
result.Cancelled = true;
break;
}
char c = i < text.Length ? text[i] : '\n';
bool ok;
if (c == '\n')
{
ok = SendVk(VK_RETURN, false, hkl);
}
else if (c == '\t')
{
ok = SendVk(VK_TAB, false, hkl);
}
else if (char.IsHighSurrogate(c) && i + 1 < text.Length
&& char.IsLowSurrogate(text[i + 1]))
{
// A surrogate pair only composes if both halves reach the
// target in one SendInput call; sent separately they arrive
// as two lone surrogates and produce nothing.
ok = SendUnicodePair(c, text[i + 1]);
i++; // both halves consumed; counted once below
}
else if (unicodeMode)
{
ok = SendUnicode(c);
}
else
{
short vks = Native.VkKeyScanEx(c, hkl);
if (vks == -1)
{
ok = SendUnicode(c);
}
else
{
ushort vk = (ushort)(vks & 0xFF);
int shiftState = (vks >> 8) & 0xFF;
ushort scan = (ushort)Native.MapVirtualKeyEx(vk, 0 /* MAPVK_VK_TO_VSC */, hkl);
// Bit 0 = Shift, bit 1 = Ctrl, bit 2 = Alt.
bool shift = (shiftState & 1) != 0;
bool altGr = (shiftState & 6) == 6;
if (scan == 0)
{
// The key exists in the layout but has no scancode
// on this keyboard. Windows would resolve the
// virtual key anyway, but a console that forwards
// raw scancodes has nothing to forward and the
// character would vanish while the counter said it
// was typed. Treat it the same as "not in this
// layout" and let the unicode event carry it.
ok = SendUnicode(c);
}
else if ((shiftState & ~1) == 0)
{
ok = SendVk(vk, scan, ShiftForCaps(c, vk, scan, shift, false, hkl, capsOn));
}
else if (altGr)
{
// Ctrl+Alt together is AltGr: on European layouts
// that is @ { } [ ] \ | ~ and more. Sent as a real
// AltGr scancode chord, because the KVM consoles
// this tool exists for ignore unicode events.
ok = SendAltGr(vk, scan, ShiftForCaps(c, vk, scan, shift, true, hkl, capsOn));
}
else
{
ok = SendUnicode(c);
}
}
}
if (ok) result.KeysSent++; else result.Blocked++;
if (progress != null && (i % 20 == 0 || i == total - 1))
progress(i + 1, total);
if (perKeyDelayMs > 0)
Thread.Sleep(perKeyDelayMs);
// Hold after the Enter that ends a line, but not after the last
// one: nothing follows it, so waiting only delays the finish.
// That includes the Enter-at-end keystroke - it submits the
// last command, but no keystroke follows it for the delay to
// protect, and a countdown with nothing after it reads as a hang.
if (c == '\n' && o.LineDelayMs > 0 && i < total - 1)
{
lineNo++;
if (!Wait(o.LineDelayMs, cancelled, o.LineWait, lineNo, totalLines))
{
result.Cancelled = true;
break;
}
}
}
}
finally
{
// Every exit from the loop, including cancellation and an
// exception: nothing downstream releases a modifier, so this
// is the last chance to.
ReleaseModifiers();
}
return result;
}
// Sleeps in short slices so Cancel stays responsive: a line delay is
// measured in seconds, and one long Thread.Sleep would leave Esc and
// the hotkey looking dead for the whole of it. Returns false if the run
// was cancelled while waiting.
static bool Wait(int totalMs, Func<bool> cancelled,
Action<int, int, int> tick, int lineNo, int totalLines)
{
const int SLICE = 100;
int waited = 0;
while (waited < totalMs)
{
if (cancelled != null && cancelled()) return false;
if (tick != null && waited % 1000 == 0)
tick(totalMs - waited, lineNo, totalLines);
int slice = Math.Min(SLICE, totalMs - waited);
Thread.Sleep(slice);
waited += slice;
}
return true;
}
// VkKeyScanEx answers for a keyboard with CapsLock off. With CapsLock
// on, the same key produces the other case, so 'rootpw' arrives as
// 'ROOTPW' - silently, because every keystroke was accepted and the
// counter still says Done. That is the worst shape of bug this tool
// can have.
//
// The obvious fix, "invert shift for letters", is wrong on Turkish and
// on any layout using SGCAPS, where CapsLock is not simply Shift. So
// ask the layout instead: ToUnicodeEx is a pure query, and it knows
// exactly what each combination produces under the current CapsLock
// state. Only flip when flipping is what recovers the character.
//
// This corrects targets that translate keys locally, which is the
// common case (native windows, and browser-based KVM consoles, where
// the browser does the translation before forwarding). A console that
// forwards raw scancodes applies the guest's own CapsLock, which
// nothing on this side can see; DEPLOY.md says so.
static bool ShiftForCaps(char c, ushort vk, ushort scan, bool shift,
bool altGr, IntPtr hkl, bool capsOn)
{
if (!capsOn) return shift;
if (Produces(vk, scan, shift, altGr, hkl) == c) return shift;
if (Produces(vk, scan, !shift, altGr, hkl) == c) return !shift;
return shift; // neither matches; leave the layout's own answer
}
// What this key combination would type right now, or '\0' if it is not
// a single character. CapsLock is read from the live keyboard state,
// which is the whole point of the call.
static char Produces(ushort vk, ushort scan, bool shift, bool altGr, IntPtr hkl)
{
byte[] state = new byte[256];
state[VK_CAPITAL] = 1; // toggled on
if (shift) state[VK_SHIFT] = 0x80;
if (altGr) { state[VK_CONTROL] = 0x80; state[VK_MENU] = 0x80; }
char[] buf = new char[8];
int n = Native.ToUnicodeEx(vk, scan, state, buf, buf.Length, 0, hkl);
if (n < 0)
{
// A dead key. ToUnicodeEx leaves it pending in the layout's
// state, where it would compose with whatever is asked next,
// so call again to flush it back out.
Native.ToUnicodeEx(vk, scan, state, buf, buf.Length, 0, hkl);
return '\0';
}
return n == 1 ? buf[0] : '\0';
}
static bool SendVk(ushort vk, bool shift, IntPtr hkl)
{
return SendVk(vk, (ushort)Native.MapVirtualKeyEx(vk, 0 /* MAPVK_VK_TO_VSC */, hkl), shift);
}
static bool SendVk(ushort vk, ushort scan, bool shift)
{
List<Native.INPUT> events = new List<Native.INPUT>(4);
if (shift) events.Add(KeyEvent(VK_SHIFT, SCAN_LSHIFT, false));
events.Add(KeyEvent(vk, scan, false));
events.Add(KeyEvent(vk, scan, true));
if (shift) events.Add(KeyEvent(VK_SHIFT, SCAN_LSHIFT, true));
return Send(events.ToArray());
}
// A physical AltGr press emits LControl (0x1D) followed by the extended
// right Alt (E0 0x38); this mirrors that exactly, so a console that
// forwards raw scancodes hands the guest the same chord a real keyboard
// would have produced.
static bool SendAltGr(ushort vk, ushort scan, bool shift)
{
List<Native.INPUT> events = new List<Native.INPUT>(8);
events.Add(KeyEvent(VK_CONTROL, SCAN_LCONTROL, false));
events.Add(KeyEvent(VK_MENU, SCAN_RALT, false, true));
if (shift) events.Add(KeyEvent(VK_SHIFT, SCAN_LSHIFT, false));
events.Add(KeyEvent(vk, scan, false));
events.Add(KeyEvent(vk, scan, true));
if (shift) events.Add(KeyEvent(VK_SHIFT, SCAN_LSHIFT, true));
events.Add(KeyEvent(VK_MENU, SCAN_RALT, true, true));
events.Add(KeyEvent(VK_CONTROL, SCAN_LCONTROL, true));
return Send(events.ToArray());
}
// Blocks until Shift, Ctrl, Alt, Win and V are all physically up, so a
// hotkey trigger with no start delay cannot mix the user's own held
// modifiers into the injected stream. The timeout covers keys that
// report stuck (some KVM passthroughs): typing anyway beats hanging.
static bool WaitForKeysUp(Func<bool> cancelled)
{
int[] keys = { 0x10, 0x11, 0x12, 0x5B, 0x5C, 0x56 };
const int TIMEOUT_MS = 3000;
const int SLICE = 30;
int waited = 0;
while (waited < TIMEOUT_MS)
{
if (cancelled != null && cancelled()) return false;
bool held = false;
for (int i = 0; i < keys.Length; i++)
{
if ((Native.GetAsyncKeyState(keys[i]) & 0x8000) != 0) { held = true; break; }
}
if (!held) return true;
Thread.Sleep(SLICE);
waited += SLICE;
}
return true;
}
static bool SendUnicode(char c)
{
Native.INPUT[] events = new Native.INPUT[2];
events[0] = UnicodeEvent(c, false);
events[1] = UnicodeEvent(c, true);
return Send(events);
}
// Both halves in one call: split across two calls they arrive as lone
// surrogates and compose into nothing.
static bool SendUnicodePair(char high, char low)
{
Native.INPUT[] events = new Native.INPUT[4];
events[0] = UnicodeEvent(high, false);
events[1] = UnicodeEvent(high, true);
events[2] = UnicodeEvent(low, false);
events[3] = UnicodeEvent(low, true);
return Send(events);
}
static Native.INPUT KeyEvent(ushort vk, ushort scan, bool up)
{
return KeyEvent(vk, scan, up, false);
}
static Native.INPUT KeyEvent(ushort vk, ushort scan, bool up, bool extended)
{
Native.INPUT input = new Native.INPUT();
input.type = Native.INPUT_KEYBOARD;
input.U.ki.wVk = vk;
input.U.ki.wScan = scan;
input.U.ki.dwFlags = (up ? Native.KEYEVENTF_KEYUP : 0)
| (extended ? Native.KEYEVENTF_EXTENDEDKEY : 0);
return input;
}
static Native.INPUT UnicodeEvent(char c, bool up)
{
Native.INPUT input = new Native.INPUT();
input.type = Native.INPUT_KEYBOARD;
input.U.ki.wVk = 0;
input.U.ki.wScan = c;
input.U.ki.dwFlags = Native.KEYEVENTF_UNICODE | (up ? Native.KEYEVENTF_KEYUP : 0);
return input;
}
static bool Send(Native.INPUT[] events)
{
uint sent = Native.SendInput((uint)events.Length, events,
Marshal.SizeOf(typeof(Native.INPUT)));
// SendInput can insert some events and stop - another thread's
// injected stream interrupts it, and the API documents the partial
// insert for exactly that reason. A chord cut in half leaves its
// modifier pressed with no key-up to follow, and the system goes on
// believing Shift or Ctrl is held. In front of a root shell that
// turns the operator's next keystrokes into signals, at the moment
// they are already puzzling over the missing characters.
if (sent != (uint)events.Length) ReleaseModifiers();
return sent == (uint)events.Length;
}
// Unconditional key-up for every modifier this class ever presses.
// Sending an up for a key that is already up is harmless; leaving one
// down is not. Calls SendInput directly so a failure cannot recurse.
public static void ReleaseModifiers()
{
Native.INPUT[] up = new Native.INPUT[3];
up[0] = KeyEvent(VK_SHIFT, SCAN_LSHIFT, true);
up[1] = KeyEvent(VK_CONTROL, SCAN_LCONTROL, true);
up[2] = KeyEvent(VK_MENU, SCAN_RALT, true, true);
Native.SendInput((uint)up.Length, up, Marshal.SizeOf(typeof(Native.INPUT)));
}
static IntPtr ForegroundLayout()
{
uint pid;
uint tid = Native.GetWindowThreadProcessId(Native.GetForegroundWindow(), out pid);
return Native.GetKeyboardLayout(tid);
}
}
}