diff options
Diffstat (limited to 'scripts/windows_input/Native.cs')
| -rw-r--r-- | scripts/windows_input/Native.cs | 439 |
1 files changed, 439 insertions, 0 deletions
diff --git a/scripts/windows_input/Native.cs b/scripts/windows_input/Native.cs new file mode 100644 index 00000000..5f941f1a --- /dev/null +++ b/scripts/windows_input/Native.cs @@ -0,0 +1,439 @@ +// Test-only Windows desktop/console adapter. No calls occur when this file is compiled. +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +namespace HerdrInputGauntlet { + [ComImport, Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")] + class ApplicationActivationManager { } + + [ComImport, Guid("2E941141-7F97-4756-BA1D-9DECDE894A3D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + interface IApplicationActivationManager { + [PreserveSig] + int ActivateApplication([MarshalAs(UnmanagedType.LPWStr)] string appUserModelId, + [MarshalAs(UnmanagedType.LPWStr)] string arguments, uint options, out uint processId); + } + + public static class Desktop { + [StructLayout(LayoutKind.Sequential)] public struct Point { public int X, Y; } + [StructLayout(LayoutKind.Sequential)] public struct Rect { public int Left, Top, Right, Bottom; } + [StructLayout(LayoutKind.Sequential)] struct Mouse { public int X, Y; public uint Data, Flags, Time; public UIntPtr Extra; } + [StructLayout(LayoutKind.Sequential)] struct Key { public ushort Vk, Scan; public uint Flags, Time; public UIntPtr Extra; } + [StructLayout(LayoutKind.Explicit)] struct Union { [FieldOffset(0)] public Mouse Mouse; [FieldOffset(0)] public Key Key; } + [StructLayout(LayoutKind.Sequential)] struct Input { public uint Type; public Union Data; } + delegate bool EnumProc(IntPtr hwnd, IntPtr arg); + delegate IntPtr HookProc(int code,IntPtr message,IntPtr data); + [StructLayout(LayoutKind.Sequential)] struct Message { public IntPtr Window; public uint Id; public UIntPtr WParam; public IntPtr LParam; public uint Time; public Point Point; public uint Private; } + [DllImport("user32.dll",SetLastError=true)] static extern IntPtr SetWindowsHookEx(int kind,HookProc callback,IntPtr module,uint thread); + [DllImport("user32.dll")] static extern bool UnhookWindowsHookEx(IntPtr hook); + [DllImport("user32.dll")] static extern IntPtr CallNextHookEx(IntPtr hook,int code,IntPtr message,IntPtr data); + [DllImport("user32.dll")] static extern int GetMessage(out Message message,IntPtr window,uint min,uint max); + [DllImport("user32.dll")] static extern bool PostThreadMessage(uint thread,uint message,UIntPtr wparam,IntPtr lparam); + [DllImport("kernel32.dll")] static extern uint GetCurrentThreadId(); + [DllImport("kernel32.dll",CharSet=CharSet.Unicode)] static extern IntPtr GetModuleHandle(string name); + static volatile bool stopped; + static Thread stopThread; + static uint stopThreadId; + static HookProc stopHook; + static readonly ManualResetEventSlim stopReady=new ManualResetEventSlim(); + static string stopError; + public static void StartEmergencyStop() { + stopThread=new Thread(()=> { + IntPtr hook=IntPtr.Zero; + try { + stopThreadId=GetCurrentThreadId(); + stopHook=(code,message,data)=> { + if(code>=0 && (message.ToInt64()==0x100 || message.ToInt64()==0x104) && Marshal.ReadInt32(data)==0x7B) stopped=true; + return CallNextHookEx(IntPtr.Zero,code,message,data); + }; + hook=SetWindowsHookEx(13,stopHook,GetModuleHandle(null),0); + if(hook==IntPtr.Zero) throw new Win32Exception(); + stopReady.Set(); + Message message; + while(GetMessage(out message,IntPtr.Zero,0,0)>0) {} + } catch(Exception e) { stopError=e.Message; stopped=true; stopReady.Set(); } + finally { if(hook!=IntPtr.Zero) UnhookWindowsHookEx(hook); } + }) { IsBackground=true }; + stopThread.Start(); + if(!stopReady.Wait(3000) || stopError!=null) throw new Exception("Cannot install emergency-stop latch: "+stopError); + } + public static void StopEmergencyStop() { + if(stopThread!=null) { PostThreadMessage(stopThreadId,0x12,UIntPtr.Zero,IntPtr.Zero); stopThread.Join(1000); } + } + [DllImport("kernel32.dll",SetLastError=true)] static extern IntPtr OpenProcess(uint access,bool inherit,int pid); + [DllImport("advapi32.dll",SetLastError=true)] static extern bool OpenProcessToken(IntPtr process,uint access,out IntPtr token); + [DllImport("advapi32.dll",SetLastError=true)] static extern bool GetTokenInformation(IntPtr token,int kind,out uint value,int size,out int returned); + [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle); + [StructLayout(LayoutKind.Sequential)] struct FileInformation { + public uint Attributes,CreationLow,CreationHigh,AccessLow,AccessHigh,WriteLow,WriteHigh; + public uint Volume,SizeHigh,SizeLow,Links,IndexHigh,IndexLow; + } + [DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern Microsoft.Win32.SafeHandles.SafeFileHandle CreateFile(string path,uint access,uint share,IntPtr security,uint creation,uint flags,IntPtr template); + [DllImport("kernel32.dll",SetLastError=true)] static extern bool GetFileInformationByHandle(Microsoft.Win32.SafeHandles.SafeFileHandle file,out FileInformation info); + public static string FileIdentity(string path) { + using(var file=CreateFile(path,0,7,IntPtr.Zero,3,0x02000000,IntPtr.Zero)) { + FileInformation info; + if(file.IsInvalid || !GetFileInformationByHandle(file,out info)) throw new Win32Exception(Marshal.GetLastWin32Error(),"Cannot establish Terminal file/installation identity"); + return info.Volume.ToString("x8")+":"+info.IndexHigh.ToString("x8")+info.IndexLow.ToString("x8"); + } + } + public static void AssertNotElevated(int pid) { + var process=OpenProcess(0x1000,false,pid); + if(process==IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error(),"Cannot verify process elevation; refusing desktop input"); + IntPtr token=IntPtr.Zero; + try { + uint elevated; int returned; + if(!OpenProcessToken(process,8,out token) || !GetTokenInformation(token,20,out elevated,4,out returned) || returned!=4) + throw new Win32Exception(Marshal.GetLastWin32Error(),"Cannot verify process elevation; refusing desktop input"); + if(elevated!=0) throw new Exception("Elevated controller/Terminal is not allowed. Start PowerShell and Terminal without Run as administrator."); + } finally { if(token!=IntPtr.Zero) CloseHandle(token); CloseHandle(process); } + } + [DllImport("user32.dll")] static extern bool EnumWindows(EnumProc callback, IntPtr arg); + [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr hwnd, StringBuilder value, int count); + [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); + [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint pid); + [DllImport("user32.dll")] static extern IntPtr GetKeyboardLayout(uint thread); + [DllImport("user32.dll")] static extern bool SetForegroundWindow(IntPtr hwnd); + [DllImport("user32.dll")] static extern bool GetCursorPos(out Point point); + [DllImport("user32.dll")] static extern bool SetCursorPos(int x,int y); + [DllImport("user32.dll")] static extern int GetSystemMetrics(int index); + [DllImport("user32.dll")] static extern short GetAsyncKeyState(int vk); + [DllImport("user32.dll")] static extern uint MapVirtualKeyEx(uint key, uint kind, IntPtr layout); + [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int ToUnicodeEx(uint key,uint scan,byte[] state,StringBuilder text,int count,uint flags,IntPtr layout); + [DllImport("user32.dll", SetLastError=true)] static extern uint SendInput(uint count, Input[] events, int size); + [DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr hwnd, out Rect rect); + [DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr hwnd, IntPtr after, int x, int y, int width, int height, uint flags); + [DllImport("user32.dll")] public static extern int CountClipboardFormats(); + [DllImport("user32.dll")] public static extern uint GetClipboardSequenceNumber(); + [DllImport("user32.dll")] static extern IntPtr GetClipboardOwner(); + [DllImport("user32.dll")] static extern bool OpenClipboard(IntPtr owner); + [DllImport("user32.dll")] static extern bool CloseClipboard(); + [DllImport("user32.dll")] static extern bool EmptyClipboard(); + [DllImport("user32.dll")] static extern IntPtr SetClipboardData(uint format,IntPtr value); + [DllImport("kernel32.dll")] static extern IntPtr GlobalAlloc(uint flags,UIntPtr size); + [DllImport("kernel32.dll")] static extern IntPtr GlobalLock(IntPtr memory); + [DllImport("kernel32.dll")] static extern bool GlobalUnlock(IntPtr memory); + [DllImport("kernel32.dll")] static extern IntPtr GlobalFree(IntPtr memory); + + static string QuoteArgument(string value) { + if(value.Length>0 && value.IndexOfAny(new[]{' ', '\t', '\n', '\v', '"'})<0) return value; + var result=new StringBuilder("\""); + int slashes=0; + foreach(char c in value) { + if(c=='\\') { slashes++; continue; } + if(c=='"') result.Append('\\',slashes*2+1); + else result.Append('\\',slashes); + result.Append(c); slashes=0; + } + result.Append('\\',slashes*2).Append('"'); + return result.ToString(); + } + public static int ActivateApplication(string appUserModelId,string[] arguments) { + var manager=(IApplicationActivationManager)new ApplicationActivationManager(); + uint pid; + int result=manager.ActivateApplication(appUserModelId,string.Join(" ",Array.ConvertAll(arguments,QuoteArgument)),0,out pid); + if(result<0) Marshal.ThrowExceptionForHR(result); + return checked((int)pid); + } + + public static uint SetEmptyClipboard(IntPtr owner,string text) { + if(!OpenClipboard(owner)) throw new Exception("Clipboard busy; refusing replacement"); + IntPtr memory=IntPtr.Zero; + try { + if(CountClipboardFormats()!=0) throw new Exception("Clipboard changed or contains user data; refusing replacement"); + byte[] data=Encoding.Unicode.GetBytes(text+"\0"); + memory=GlobalAlloc(2,new UIntPtr((uint)data.Length)); + if(memory==IntPtr.Zero) throw new Exception("Clipboard allocation failed"); + var pointer=GlobalLock(memory); + if(pointer==IntPtr.Zero) throw new Exception("Clipboard lock failed"); + try { Marshal.Copy(data,0,pointer,data.Length); } finally { GlobalUnlock(memory); } + if(!EmptyClipboard() || SetClipboardData(13,memory)==IntPtr.Zero) throw new Exception("Clipboard write failed"); + memory=IntPtr.Zero; // ownership transferred to Windows + } finally { if(memory!=IntPtr.Zero) GlobalFree(memory); CloseClipboard(); } + // Closing can synthesize additional formats and advance the sequence. + // Reopen before adopting it so another writer cannot become our lease. + if(!OpenClipboard(owner)) throw new Exception("Clipboard busy; cannot establish test ownership"); + try { + if(owner==IntPtr.Zero || GetClipboardOwner()!=owner) throw new Exception("Clipboard ownership changed; refusing cleanup lease"); + return GetClipboardSequenceNumber(); + } finally { CloseClipboard(); } + } + public static bool ClearOwnedClipboard(uint sequence) { + if(!OpenClipboard(IntPtr.Zero)) return false; + try { return GetClipboardSequenceNumber()!=sequence || EmptyClipboard(); } + finally { CloseClipboard(); } + } + public static string Title(IntPtr hwnd) { var text=new StringBuilder(1024); GetWindowText(hwnd,text,text.Capacity); return text.ToString(); } + public static IntPtr Find(string nonce) { + var matches=new List<IntPtr>(); + EnumWindows((h,a)=> { if (Title(h).Contains(nonce)) matches.Add(h); return true; },IntPtr.Zero); + if(matches.Count>1) throw new Exception("Ambiguous test window identity"); + return matches.Count==1 ? matches[0] : IntPtr.Zero; + } + public static int Pid(IntPtr hwnd) { uint pid; GetWindowThreadProcessId(hwnd,out pid); return checked((int)pid); } + public static string Layout(IntPtr hwnd) { uint pid; return GetKeyboardLayout(GetWindowThreadProcessId(hwnd,out pid)).ToInt64().ToString("x"); } + public static bool IsOwned(IntPtr hwnd, string nonce, int pid) { return hwnd!=IntPtr.Zero && Pid(hwnd)==pid && Title(hwnd).Contains(nonce); } + public static void Guard(IntPtr hwnd, string nonce, int pid) { + if(!IsOwned(hwnd,nonce,pid) || GetForegroundWindow()!=hwnd) throw new Exception("Lost owned test-window focus; injection aborted"); + AssertNotElevated(pid); + if(stopped || (GetAsyncKeyState(0x7B)&0x8000)!=0) throw new Exception("F12 emergency stop"); + } + public static void Focus(IntPtr hwnd, string nonce, int pid) { + if(!IsOwned(hwnd,nonce,pid)) throw new Exception("Lost window ownership"); + AssertNotElevated(pid); + var deadline=DateTime.UtcNow.AddSeconds(3); + do { + SetForegroundWindow(hwnd); + if(GetForegroundWindow()==hwnd) return; + Thread.Sleep(100); + } while(DateTime.UtcNow<deadline); + throw new Exception("Cannot focus test window; no input sent"); + } + public static void FocusAwayAndBack(IntPtr other,IntPtr hwnd,string nonce,int pid) { + if(other==IntPtr.Zero || other==hwnd) throw new Exception("No separate controller window for focus-cycle qualification"); + AssertNotElevated(Pid(other)); + var deadline=DateTime.UtcNow.AddSeconds(3); + do { + SetForegroundWindow(other); + if(GetForegroundWindow()==other) break; + Thread.Sleep(100); + } while(DateTime.UtcNow<deadline); + if(GetForegroundWindow()!=other) throw new Exception("Cannot focus controller window for focus-cycle qualification"); + Thread.Sleep(250); + Focus(hwnd,nonce,pid); + Thread.Sleep(250); + } + public static void Neutral() { + foreach(int key in new[]{0x10,0x11,0x12,0x5B,0x5C,1,2,4}) + if((GetAsyncKeyState(key)&0x8000)!=0) throw new Exception("Release physical modifiers and mouse buttons before running"); + } + static Input Event(ushort vk, bool release, IntPtr layout) { + uint mapped=MapVirtualKeyEx(vk,4,layout); + if(mapped==0) throw new Exception("No scan code for virtual key "+vk); + return new Input { Type=1, Data=new Union { Key=new Key { Scan=(ushort)(mapped&255), Flags=8u|(release?2u:0u)|((mapped&0xff00)!=0?1u:0u) } } }; + } + public static int[] Scans(IntPtr hwnd,int[] keys) { + uint ignored; var layout=GetKeyboardLayout(GetWindowThreadProcessId(hwnd,out ignored)); + var scans=new List<int>(); + foreach(int key in keys) scans.Add(Event((ushort)key,false,layout).Data.Key.Scan); + return scans.ToArray(); + } + static int[] LayoutChord(int vk,int modifiers) { + var keys=new List<int>(); + if((modifiers&1)!=0) keys.Add(0x10); + if((modifiers&2)!=0) keys.Add(0x11); + if((modifiers&4)!=0) keys.Add(0x12); + keys.Add(vk); + return keys.ToArray(); + } + public static int[] DeadKeyChord(IntPtr hwnd,char accent) { + uint ignored; var layout=GetKeyboardLayout(GetWindowThreadProcessId(hwnd,out ignored)); + for(int modifiers=0;modifiers<8;modifiers++) for(uint vk=1;vk<255;vk++) { + uint scan=MapVirtualKeyEx(vk,4,layout); + if(scan==0) continue; + var state=new byte[256]; + if((modifiers&1)!=0) state[0x10]=0x80; + if((modifiers&2)!=0) state[0x11]=0x80; + if((modifiers&4)!=0) state[0x12]=0x80; + var text=new StringBuilder(4); + // Flag 4 discovers dead keys without changing keyboard state. + if(ToUnicodeEx(vk,scan,state,text,text.Capacity,4,layout)<0 && text.Length>0 && text[0]==accent) + return LayoutChord((int)vk,modifiers); + } + throw new Exception("Active layout has no "+accent+" dead key"); + } + // One balanced batch; no modifier is intentionally held between calls. + public static int Chord(IntPtr hwnd, string nonce, int pid, int[] keys) { + Guard(hwnd,nonce,pid); Neutral(); + uint ignored; var layout=GetKeyboardLayout(GetWindowThreadProcessId(hwnd,out ignored)); + var events=new List<Input>(); + foreach(int key in keys) events.Add(Event(checked((ushort)key),false,layout)); + for(int i=keys.Length-1;i>=0;i--) events.Add(Event(checked((ushort)keys[i]),true,layout)); + Guard(hwnd,nonce,pid); + uint sent=SendInput((uint)events.Count,events.ToArray(),Marshal.SizeOf<Input>()); + if(sent!=events.Count) { + // Release only keys whose down event was actually submitted and not yet released. + var held=new List<Input>(); + for(int i=0;i<keys.Length && i<sent;i++) + if(sent <= 2*keys.Length-1-i) held.Add(Event((ushort)keys[i],true,layout)); + if(held.Count>0) { + uint released=SendInput((uint)held.Count,held.ToArray(),Marshal.SizeOf<Input>()); + if(released!=held.Count) throw new Exception("SendInput cleanup incomplete: "+released+"/"+held.Count+" key-up events; release test modifiers manually before continuing"); + } + throw new Exception("SendInput incomplete (possible UIPI restriction): "+sent+"/"+events.Count); + } + return (int)sent; + } + public static long Cursor() { + Point point; + if(!GetCursorPos(out point)) throw new Win32Exception(); + return ((long)(uint)point.X<<32)|(uint)point.Y; + } + public static void RestoreCursor(long packed) { + if(!SetCursorPos(unchecked((int)(packed>>32)),unchecked((int)packed))) throw new Win32Exception(); + } + public static void MouseMoveInside(IntPtr hwnd,string nonce,int pid,int offset) { + Guard(hwnd,nonce,pid); Neutral(); Rect rect; + if(!GetWindowRect(hwnd,out rect)) throw new Win32Exception(); + int x=(rect.Left+rect.Right)/2+offset,y=(rect.Top+rect.Bottom)/2; + if(x<=rect.Left+20 || x>=rect.Right-20 || y<=rect.Top+20 || y>=rect.Bottom-20) throw new Exception("Mouse target is outside the safe window interior"); + int left=GetSystemMetrics(76),top=GetSystemMetrics(77),width=GetSystemMetrics(78),height=GetSystemMetrics(79); + if(width<2 || height<2) throw new Exception("Virtual desktop geometry unavailable"); + int nx=(int)Math.Round((x-left)*65535.0/(width-1)),ny=(int)Math.Round((y-top)*65535.0/(height-1)); + var input=new Input { Type=0, Data=new Union { Mouse=new Mouse { X=nx,Y=ny,Flags=0xC001 } } }; + if(SendInput(1,new[]{input},Marshal.SizeOf<Input>())!=1) throw new Win32Exception(Marshal.GetLastWin32Error(),"Mouse injection failed"); + } + public static void MouseClickWheelInside(IntPtr hwnd,string nonce,int pid,int offset) { + Guard(hwnd,nonce,pid); Neutral(); Rect rect; + if(!GetWindowRect(hwnd,out rect)) throw new Win32Exception(); + int x=(rect.Left+rect.Right)/2+offset,y=(rect.Top+rect.Bottom)/2; + if(x<=rect.Left+20 || x>=rect.Right-20 || y<=rect.Top+20 || y>=rect.Bottom-20) throw new Exception("Mouse target is outside the safe window interior"); + int left=GetSystemMetrics(76),top=GetSystemMetrics(77),width=GetSystemMetrics(78),height=GetSystemMetrics(79); + if(width<2 || height<2) throw new Exception("Virtual desktop geometry unavailable"); + int nx=(int)Math.Round((x-left)*65535.0/(width-1)),ny=(int)Math.Round((y-top)*65535.0/(height-1)); + var inputs=new[]{ + new Input { Type=0, Data=new Union { Mouse=new Mouse { X=nx,Y=ny,Flags=0xC001 } } }, + new Input { Type=0, Data=new Union { Mouse=new Mouse { Flags=0x0002 } } }, + new Input { Type=0, Data=new Union { Mouse=new Mouse { Flags=0x0004 } } }, + new Input { Type=0, Data=new Union { Mouse=new Mouse { Data=120,Flags=0x0800 } } } + }; + uint sent=SendInput((uint)inputs.Length,inputs,Marshal.SizeOf<Input>()); + if(sent!=inputs.Length) { + int error=Marshal.GetLastWin32Error(); + if(sent==2 && SendInput(1,new[]{inputs[2]},Marshal.SizeOf<Input>())!=1) + throw new Win32Exception(Marshal.GetLastWin32Error(),"Mouse injection failed after button-down; release the left mouse button manually"); + throw new Win32Exception(error,"Mouse click/wheel injection failed"); + } + } + public static void Resize(IntPtr hwnd,string nonce,int pid,int dx,int dy) { + Guard(hwnd,nonce,pid); Rect r; + if(!GetWindowRect(hwnd,out r)) throw new Win32Exception(); + int w=r.Right-r.Left+dx,h=r.Bottom-r.Top+dy; + if(w<300 || h<200 || w>10000 || h>10000) throw new Exception("Unsafe resize request"); + if(!SetWindowPos(hwnd,IntPtr.Zero,0,0,w,h,0x16)) throw new Win32Exception(); + } + } + + public sealed class ConsoleProbe : IDisposable { + [StructLayout(LayoutKind.Sequential)] struct Coord { public short X,Y; } + [StructLayout(LayoutKind.Sequential)] struct SmallRect { public short Left,Top,Right,Bottom; } + [StructLayout(LayoutKind.Sequential)] struct Info { public Coord Size,Cursor; public ushort Attributes; public SmallRect Window; public Coord Maximum; } + [StructLayout(LayoutKind.Explicit,Size=20)] struct Record { + [FieldOffset(0)] public ushort Type; + [FieldOffset(4)] public int Down; + [FieldOffset(8)] public ushort Repeat; + [FieldOffset(10)] public ushort Vk; + [FieldOffset(12)] public ushort Scan; + [FieldOffset(14)] public ushort Unicode; + [FieldOffset(16)] public uint Control; + } + [DllImport("kernel32.dll")] static extern IntPtr GetStdHandle(int kind); + [DllImport("kernel32.dll",SetLastError=true)] static extern bool GetConsoleMode(IntPtr handle,out uint mode); + [DllImport("kernel32.dll",SetLastError=true)] static extern bool SetConsoleMode(IntPtr handle,uint mode); + [DllImport("kernel32.dll")] static extern bool GetConsoleScreenBufferInfo(IntPtr handle,out Info info); + [DllImport("kernel32.dll")] static extern uint GetConsoleCP(); + [DllImport("kernel32.dll")] static extern bool SetConsoleCP(uint cp); + [DllImport("kernel32.dll")] static extern bool ReadFile(IntPtr handle,byte[] buffer,uint size,out uint count,IntPtr overlap); + [DllImport("kernel32.dll")] static extern bool WriteFile(IntPtr handle,byte[] buffer,uint size,out uint count,IntPtr overlap); + [DllImport("kernel32.dll")] static extern bool ReadConsoleInputW(IntPtr handle,Record[] records,uint size,out uint count); + [DllImport("kernel32.dll")] static extern uint GetCurrentThreadId(); + [DllImport("kernel32.dll")] static extern IntPtr OpenThread(uint access,bool inherit,uint id); + [DllImport("kernel32.dll")] static extern bool CancelSynchronousIo(IntPtr thread); + [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle); + readonly object gate=new object(); + readonly List<byte> bytes=new List<byte>(); + readonly List<long[]> records=new List<long[]>(); + readonly IntPtr input=GetStdHandle(-10), output=GetStdHandle(-11); + readonly uint original, outputMode, cp; + readonly bool native; + volatile bool running=true; + Thread reader; + IntPtr threadHandle; + string error; + public string Error { + get { return Volatile.Read(ref error); } + private set { Volatile.Write(ref error,value); } + } + public ConsoleProbe(string mode) { + if(!GetConsoleMode(input,out original) || !GetConsoleMode(output,out outputMode)) throw new Exception("Probe needs a real console"); + cp=GetConsoleCP(); native=mode=="native"; + try { + if(!SetConsoleCP(65001) || !SetConsoleMode(output,outputMode|4) || !SetConsoleMode(input,native ? (original & ~0x247u)|0x98u : (original & ~7u)|0x200u)) throw new Win32Exception(); + Print("\x1b[?2004h"+(mode=="kitty"?"\x1b[>1u\x1b[?u":mode=="mok2"?"\x1b[>4;2m":"")); + reader=new Thread(Read) { IsBackground=true }; reader.Start(); + } catch { SetConsoleMode(input,original); SetConsoleMode(output,outputMode); SetConsoleCP(cp); throw; } + } + public static long[] Geometry() { + Info info; uint mode; + if(!GetConsoleScreenBufferInfo(GetStdHandle(-11),out info) || !GetConsoleMode(GetStdHandle(-10),out mode)) throw new Exception("Console geometry unavailable"); + return new long[]{ info.Window.Right-info.Window.Left+1,info.Window.Bottom-info.Window.Top+1,mode }; + } + public static void Print(string text) { + byte[] data=Encoding.UTF8.GetBytes(text); uint count; + if(!WriteFile(GetStdHandle(-11),data,(uint)data.Length,out count,IntPtr.Zero) || count!=data.Length) throw new Exception("Console output failed"); + } + public void SetKeyboardMode(string mode) { + string sequence="\x1b[<u\x1b[>4;0m"; + if(mode=="mok2") sequence+="\x1b[>4;2m"; + else if(mode=="kitty") sequence+="\x1b[>1u"; + else if(mode!="legacy") throw new Exception("Unknown keyboard mode transition"); + Print(sequence); + } + void Read() { + threadHandle=OpenThread(1,false,GetCurrentThreadId()); + if(threadHandle==IntPtr.Zero) { Error="Cannot acquire reader cancellation handle"; return; } + try { + while(running) { + if(Count()>1048576) { Error="Probe input limit exceeded"; break; } + uint count; + if(native) { + var batch=new Record[64]; + if(!ReadConsoleInputW(input,batch,64,out count)) { if(running) Error="ReadConsoleInputW failed"; break; } + lock(gate) for(int i=0;i<count;i++) { + var r=batch[i]; + // Preserve every native record, including non-key event type and payload. + records.Add(new long[]{r.Type,r.Down,r.Repeat,r.Vk,r.Scan,r.Unicode,r.Control}); + } + } else { + var batch=new byte[4096]; + if(!ReadFile(input,batch,4096,out count,IntPtr.Zero)) { if(running) Error="ReadFile failed"; break; } + lock(gate) for(int i=0;i<count;i++) bytes.Add(batch[i]); + } + } + } catch(Exception e) { Error=e.Message; } + } + public void Clear() { lock(gate) { bytes.Clear(); records.Clear(); } } + public bool ClearIfCount(int expected) { + lock(gate) { + if(bytes.Count+records.Count!=expected) return false; + bytes.Clear(); records.Clear(); return true; + } + } + public string Hex() { lock(gate) return BitConverter.ToString(bytes.ToArray()).Replace("-","").ToLowerInvariant(); } + public long[][] Records() { lock(gate) return records.ToArray(); } + public int Count() { lock(gate) return bytes.Count+records.Count; } + public string[] CleanupErrors { get; private set; } = new string[0]; + public void Dispose() { + var errors=new List<string>(); + running=false; + if(threadHandle!=IntPtr.Zero) CancelSynchronousIo(threadHandle); + if(reader!=null && !reader.Join(1000)) errors.Add("Reader did not stop"); + if(threadHandle!=IntPtr.Zero) CloseHandle(threadHandle); + try { Print("\x1b[<u\x1b[>4;0m\x1b[?2004l\x1b[?1000l\x1b[?1006l"); } + catch(Exception e) { errors.Add(e.Message); } + if(!SetConsoleMode(input,original)) errors.Add("Input-mode restoration failed"); + if(!SetConsoleMode(output,outputMode)) errors.Add("Output-mode restoration failed"); + if(!SetConsoleCP(cp)) errors.Add("Code-page restoration failed"); + uint actual; + if(!GetConsoleMode(input,out actual) || actual!=original) errors.Add("Input mode differs after restoration"); + if(!GetConsoleMode(output,out actual) || actual!=outputMode) errors.Add("Output mode differs after restoration"); + if(GetConsoleCP()!=cp) errors.Add("Code page differs after restoration"); + CleanupErrors=errors.ToArray(); + } + } +} |
