# Vash broadcast desktop helper. # # The worker starts this in the signed-in desktop session and sends one JSON # request per line on stdin; each gets one JSON line back on stdout: # {"id":1,"cmd":"windows","under":"C:\\...\\osu"} -> {"id":1,"ok":true,"result":[...]} # # It needs Windows PowerShell 5.1 (powershell.exe, part of Windows): the # Windows.Media.Ocr API used for reading osu!tourney's room list is not # reachable from PowerShell 7. Coordinates are physical screen pixels. $ErrorActionPreference = 'Stop' # Explicit UTF-8 streams: [Console]::InputEncoding cannot be set when there is no console window. $utf8 = New-Object System.Text.UTF8Encoding $false $stdin = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), $utf8) $stdout = New-Object System.IO.StreamWriter([Console]::OpenStandardOutput(), $utf8) $stdout.AutoFlush = $true Add-Type -AssemblyName System.Drawing Add-Type -ReferencedAssemblies System.Drawing -TypeDefinition @' using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Runtime.InteropServices; using System.Text; using System.Threading; public class VashWindow { public long hwnd { get; set; } public int pid { get; set; } public string title { get; set; } public string className { get; set; } public string exe { get; set; } public bool visible { get; set; } public bool minimized { get; set; } public int x { get; set; } public int y { get; set; } public int width { get; set; } public int height { get; set; } public int clientX { get; set; } public int clientY { get; set; } public int clientWidth { get; set; } public int clientHeight { get; set; } } public class VashProcess { public int pid { get; set; } public string path { get; set; } } public static class VashDesktop { [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; } [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X, Y; } [StructLayout(LayoutKind.Sequential)] struct MOUSEINPUT { public int dx; public int dy; public uint mouseData; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; } [StructLayout(LayoutKind.Sequential)] struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; } [StructLayout(LayoutKind.Sequential)] struct HARDWAREINPUT { public uint uMsg; public ushort wParamL; public ushort wParamH; } [StructLayout(LayoutKind.Explicit)] struct InputUnion { [FieldOffset(0)] public MOUSEINPUT mi; [FieldOffset(0)] public KEYBDINPUT ki; [FieldOffset(0)] public HARDWAREINPUT hi; } [StructLayout(LayoutKind.Sequential)] struct INPUT { public uint type; public InputUnion U; } [StructLayout(LayoutKind.Sequential)] struct LASTINPUTINFO { public uint cbSize; public uint dwTime; } delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); [DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lParam); [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid); [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowText(IntPtr hWnd, StringBuilder sb, int max); [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr hWnd); [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetClassName(IntPtr hWnd, StringBuilder sb, int max); [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hWnd); [DllImport("user32.dll")] static extern bool IsWindow(IntPtr hWnd); [DllImport("user32.dll")] static extern bool IsIconic(IntPtr hWnd); [DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr hWnd, out RECT r); [DllImport("user32.dll")] static extern bool GetClientRect(IntPtr hWnd, out RECT r); [DllImport("user32.dll")] static extern bool ClientToScreen(IntPtr hWnd, ref POINT p); [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint msg, IntPtr w, string l, uint flags, uint timeout, out IntPtr result); [DllImport("user32.dll")] static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hWnd, int cmd); [DllImport("user32.dll")] static extern bool BringWindowToTop(IntPtr hWnd); [DllImport("user32.dll")] static extern bool AttachThreadInput(uint a, uint b, bool attach); [DllImport("kernel32.dll")] static extern uint GetCurrentThreadId(); [DllImport("user32.dll")] static extern bool SetCursorPos(int x, int y); [DllImport("user32.dll")] static extern uint SendInput(uint n, INPUT[] inputs, int size); [DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr hWnd, IntPtr after, int x, int y, int cx, int cy, uint flags); [DllImport("user32.dll")] static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr w, IntPtr l); [DllImport("user32.dll")] static extern IntPtr OpenInputDesktop(uint flags, bool inherit, uint access); [DllImport("user32.dll")] static extern bool CloseDesktop(IntPtr h); [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern bool GetUserObjectInformation(IntPtr h, int index, StringBuilder sb, int len, out int needed); [DllImport("user32.dll")] static extern bool SetProcessDpiAwarenessContext(IntPtr value); [DllImport("user32.dll")] static extern int GetSystemMetrics(int index); [DllImport("user32.dll")] static extern bool GetLastInputInfo(ref LASTINPUTINFO info); [DllImport("kernel32.dll")] static extern IntPtr OpenProcess(uint access, bool inherit, uint pid); [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] static extern bool QueryFullProcessImageName(IntPtr h, uint flags, StringBuilder sb, ref uint size); [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr h); [DllImport("kernel32.dll")] static extern uint WTSGetActiveConsoleSessionId(); [DllImport("kernel32.dll")] static extern bool ProcessIdToSessionId(uint pid, out uint session); [DllImport("kernel32.dll")] static extern uint SetThreadExecutionState(uint flags); /** Keeps the PC awake and the display on while this helper runs (it reverts when the helper exits). */ public static bool StayAwake(bool on) { // ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED uint flags = on ? 0x80000000u | 0x00000001u | 0x00000002u : 0x80000000u; return SetThreadExecutionState(flags) != 0; } public static string Init() { try { // Per-monitor aware v2: every rectangle and click is in physical pixels. if (SetProcessDpiAwarenessContext(new IntPtr(-4))) return "per-monitor-v2"; return "unchanged"; } catch (Exception e) { return "unavailable: " + e.Message; } } public static string ProcessPath(uint pid) { IntPtr h = OpenProcess(0x1000, false, pid); if (h == IntPtr.Zero) return null; try { var sb = new StringBuilder(1024); uint size = (uint)sb.Capacity; return QueryFullProcessImageName(h, 0, sb, ref size) ? sb.ToString() : null; } finally { CloseHandle(h); } } static bool Under(string path, string dir) { if (String.IsNullOrEmpty(dir)) return true; if (path == null) return false; string prefix = dir.TrimEnd('\\') + "\\"; return path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase); } public static VashWindow Describe(IntPtr h) { uint pid; GetWindowThreadProcessId(h, out pid); var title = new StringBuilder(GetWindowTextLength(h) + 1); GetWindowText(h, title, title.Capacity); var cls = new StringBuilder(256); GetClassName(h, cls, cls.Capacity); RECT r, c; GetWindowRect(h, out r); GetClientRect(h, out c); var origin = new POINT(); ClientToScreen(h, ref origin); string path = ProcessPath(pid); return new VashWindow { hwnd = h.ToInt64(), pid = (int)pid, title = title.ToString(), className = cls.ToString(), exe = path, visible = IsWindowVisible(h), minimized = IsIconic(h), x = r.Left, y = r.Top, width = r.Right - r.Left, height = r.Bottom - r.Top, clientX = origin.X, clientY = origin.Y, clientWidth = c.Right - c.Left, clientHeight = c.Bottom - c.Top }; } /** Top-level windows of processes whose executable is under dir (all when empty). */ public static VashWindow[] Windows(string dir) { var handles = new List(); EnumWindows((h, l) => { handles.Add(h); return true; }, IntPtr.Zero); var result = new List(); var paths = new Dictionary(); foreach (var h in handles) { uint pid; GetWindowThreadProcessId(h, out pid); string path; if (!paths.TryGetValue(pid, out path)) { path = ProcessPath(pid); paths[pid] = path; } if (!Under(path, dir)) continue; result.Add(Describe(h)); } return result.ToArray(); } public static VashWindow Window(long hwnd) { var h = new IntPtr(hwnd); return IsWindow(h) ? Describe(h) : null; } public static VashProcess[] Processes(string dir) { var result = new List(); foreach (var p in Process.GetProcesses()) { try { string path = ProcessPath((uint)p.Id); if (path != null && Under(path, dir)) result.Add(new VashProcess { pid = p.Id, path = path }); } catch { } finally { p.Dispose(); } } return result.ToArray(); } public static int Kill(int[] pids) { int killed = 0; foreach (int pid in pids) { try { using (var p = Process.GetProcessById(pid)) { p.Kill(); killed++; } } catch { } } return killed; } /** Asks every top-level window of a process to close, like clicking its X. */ public static int CloseProcessWindows(int pid) { int posted = 0; foreach (var w in Windows(null)) { if (w.pid != pid) continue; if (PostMessage(new IntPtr(w.hwnd), 0x0010, IntPtr.Zero, IntPtr.Zero)) posted++; } return posted; } /** "Default" when the desktop is unlocked; "Winlogon" (or null) when locked or on a secure screen. */ public static string InputDesktop() { IntPtr d = OpenInputDesktop(0, false, 0x0001); if (d == IntPtr.Zero) return null; try { var sb = new StringBuilder(256); int needed; return GetUserObjectInformation(d, 2, sb, sb.Capacity * 2, out needed) ? sb.ToString() : null; } finally { CloseDesktop(d); } } public static Dictionary Session() { uint session; ProcessIdToSessionId((uint)Process.GetCurrentProcess().Id, out session); var info = new LASTINPUTINFO { cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO)) }; GetLastInputInfo(ref info); return new Dictionary { { "sessionId", (int)session }, { "consoleSessionId", (int)WTSGetActiveConsoleSessionId() }, { "inputDesktop", InputDesktop() }, { "screenWidth", GetSystemMetrics(0) }, { "screenHeight", GetSystemMetrics(1) }, { "idleMs", unchecked(Environment.TickCount - (int)info.dwTime) } }; } static void Key(ushort vk, bool up) { var input = new INPUT { type = 1 }; input.U.ki.wVk = vk; input.U.ki.dwFlags = up ? 0x0002u : 0u; SendInput(1, new[] { input }, Marshal.SizeOf(typeof(INPUT))); } /** Brings a window to the front; Windows only allows this in some cases, so try the known ways. */ public static bool Focus(long hwnd) { var h = new IntPtr(hwnd); if (!IsWindow(h)) return false; if (IsIconic(h)) ShowWindow(h, 9); if (GetForegroundWindow() == h) return true; IntPtr fg = GetForegroundWindow(); uint ignored; uint fgThread = fg == IntPtr.Zero ? 0 : GetWindowThreadProcessId(fg, out ignored); uint me = GetCurrentThreadId(); bool attached = fgThread != 0 && fgThread != me && AttachThreadInput(me, fgThread, true); BringWindowToTop(h); SetForegroundWindow(h); if (attached) AttachThreadInput(me, fgThread, false); Thread.Sleep(80); if (GetForegroundWindow() == h) return true; // The process that sent the last input may set the foreground window: tap Alt, then retry. Key(0x12, false); Key(0x12, true); SetForegroundWindow(h); Thread.Sleep(120); return GetForegroundWindow() == h; } public static bool Move(long hwnd, int x, int y) { // SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE return SetWindowPos(new IntPtr(hwnd), IntPtr.Zero, x, y, 0, 0, 0x0001 | 0x0004 | 0x0010); } public static bool SetTitle(long hwnd, string title) { IntPtr result; // WM_SETTEXT with a timeout, so a busy window cannot hang the helper. return SendMessageTimeout(new IntPtr(hwnd), 0x000C, IntPtr.Zero, title, 0x0002, 2000, out result) != IntPtr.Zero; } static ImageCodecInfo Codec(string mime) { foreach (var codec in ImageCodecInfo.GetImageEncoders()) if (codec.MimeType == mime) return codec; return null; } static void Save(Bitmap bmp, string path, string format, long quality) { if (format == "jpeg") { var parameters = new EncoderParameters(1); parameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality); bmp.Save(path, Codec("image/jpeg"), parameters); } else { bmp.Save(path, ImageFormat.Png); } } /** * Copies a screen rectangle to an image file, optionally scaled (to at most maxWidth, * or by scale) and inverted (dark text on light reads better for OCR). Returns * the saved size. Fails while the desktop is locked. */ public static int[] Capture(int x, int y, int w, int h, string path, string format, int maxWidth, double scale, bool invert, long quality) { using (var bmp = new Bitmap(w, h, PixelFormat.Format32bppArgb)) { using (var g = Graphics.FromImage(bmp)) { g.CopyFromScreen(x, y, 0, 0, new Size(w, h), CopyPixelOperation.SourceCopy); } double factor = scale > 0 ? scale : 1.0; if (maxWidth > 0 && w * factor > maxWidth) factor = (double)maxWidth / w; int ow = Math.Max(1, (int)Math.Round(w * factor)); int oh = Math.Max(1, (int)Math.Round(h * factor)); if (ow == w && oh == h && !invert) { Save(bmp, path, format, quality); return new[] { ow, oh }; } using (var output = new Bitmap(ow, oh, PixelFormat.Format24bppRgb)) { using (var g = Graphics.FromImage(output)) { g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.PixelOffsetMode = PixelOffsetMode.HighQuality; var attributes = new ImageAttributes(); if (invert) { attributes.SetColorMatrix(new ColorMatrix(new float[][] { new float[] { -1, 0, 0, 0, 0 }, new float[] { 0, -1, 0, 0, 0 }, new float[] { 0, 0, -1, 0, 0 }, new float[] { 0, 0, 0, 1, 0 }, new float[] { 1, 1, 1, 0, 1 } })); } g.DrawImage(bmp, new Rectangle(0, 0, ow, oh), 0, 0, w, h, GraphicsUnit.Pixel, attributes); } Save(output, path, format, quality); } return new[] { ow, oh }; } } public static bool Click(int x, int y, bool right) { if (!SetCursorPos(x, y)) return false; Thread.Sleep(60); var down = new INPUT { type = 0 }; down.U.mi.dwFlags = right ? 0x0008u : 0x0002u; var up = new INPUT { type = 0 }; up.U.mi.dwFlags = right ? 0x0010u : 0x0004u; uint sent = SendInput(1, new[] { down }, Marshal.SizeOf(typeof(INPUT))); Thread.Sleep(60); sent += SendInput(1, new[] { up }, Marshal.SizeOf(typeof(INPUT))); return sent == 2; } public static int TypeText(string text) { int sent = 0; foreach (char ch in text) { var down = new INPUT { type = 1 }; down.U.ki.wScan = ch; down.U.ki.dwFlags = 0x0004; var up = new INPUT { type = 1 }; up.U.ki.wScan = ch; up.U.ki.dwFlags = 0x0004 | 0x0002; sent += (int)SendInput(1, new[] { down }, Marshal.SizeOf(typeof(INPUT))); sent += (int)SendInput(1, new[] { up }, Marshal.SizeOf(typeof(INPUT))); Thread.Sleep(15); } return sent / 2; } public static void PressKey(ushort vk) { Key(vk, false); Thread.Sleep(30); Key(vk, true); } } '@ $script:dpi = [VashDesktop]::Init() # ── Windows OCR (Windows.Media.Ocr, part of Windows 10 and 11) ───────────── $script:ocr = $null function Initialize-Ocr { if ($null -ne $script:ocr) { return } Add-Type -AssemblyName System.Runtime.WindowsRuntime $null = [Windows.Storage.StorageFile, Windows.Storage, ContentType = WindowsRuntime] $null = [Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime] $null = [Windows.Foundation.IAsyncOperation`1, Windows.Foundation, ContentType = WindowsRuntime] $null = [Windows.Graphics.Imaging.SoftwareBitmap, Windows.Foundation, ContentType = WindowsRuntime] $null = [Windows.Graphics.Imaging.BitmapDecoder, Windows.Foundation, ContentType = WindowsRuntime] $null = [Windows.Storage.Streams.RandomAccessStream, Windows.Storage.Streams, ContentType = WindowsRuntime] $script:asTask = [System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' } | Select-Object -First 1 $engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages() if ($null -eq $engine) { try { $null = [Windows.Globalization.Language, Windows.Globalization, ContentType = WindowsRuntime] $engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage([Windows.Globalization.Language]::new('en-US')) } catch { } } if ($null -eq $engine) { throw 'Windows OCR has no language available. Add English under Settings > Time & language > Language & region.' } $script:ocr = $engine } function Await($operation, [Type]$type) { $task = $script:asTask.MakeGenericMethod($type).Invoke($null, @($operation)) if (-not $task.Wait(20000)) { throw 'Windows OCR timed out' } $task.Result } function Read-Text([string]$path) { Initialize-Ocr $file = Await ([Windows.Storage.StorageFile]::GetFileFromPathAsync($path)) ([Windows.Storage.StorageFile]) $stream = Await ($file.OpenAsync([Windows.Storage.FileAccessMode]::Read)) ([Windows.Storage.Streams.IRandomAccessStream]) try { $decoder = Await ([Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream)) ([Windows.Graphics.Imaging.BitmapDecoder]) $bitmap = Await ($decoder.GetSoftwareBitmapAsync()) ([Windows.Graphics.Imaging.SoftwareBitmap]) $result = Await ($script:ocr.RecognizeAsync($bitmap)) ([Windows.Media.Ocr.OcrResult]) } finally { $stream.Dispose() } $lines = New-Object System.Collections.ArrayList foreach ($line in $result.Lines) { $left = [double]::MaxValue; $top = [double]::MaxValue; $right = 0.0; $bottom = 0.0 foreach ($word in $line.Words) { $r = $word.BoundingRect $left = [Math]::Min($left, $r.X); $top = [Math]::Min($top, $r.Y) $right = [Math]::Max($right, $r.X + $r.Width); $bottom = [Math]::Max($bottom, $r.Y + $r.Height) } if ($right -le 0) { continue } $null = $lines.Add(@{ text = $line.Text; x = $left; y = $top; width = $right - $left; height = $bottom - $top }) } return , $lines.ToArray() } # ── Request loop ────────────────────────────────────────────────────────── function Reply($id, [bool]$ok, $value) { $message = @{ id = $id; ok = $ok } if ($ok) { $message.result = $value } else { $message.error = [string]$value } $stdout.WriteLine((ConvertTo-Json -InputObject $message -Compress -Depth 6)) } Reply 0 $true @{ ready = $true; dpi = $script:dpi; powershell = $PSVersionTable.PSVersion.ToString() } while ($true) { $line = $stdin.ReadLine() if ($null -eq $line) { break } if (-not $line.Trim()) { continue } $id = $null try { $request = ConvertFrom-Json $line $id = $request.id $cmd = [string]$request.cmd $result = $null if ($cmd -eq 'ping') { $result = @{ pong = $true } } elseif ($cmd -eq 'awake') { $result = [VashDesktop]::StayAwake([bool]$request.on) } elseif ($cmd -eq 'session') { $result = [VashDesktop]::Session() } elseif ($cmd -eq 'windows') { $result = [VashDesktop]::Windows([string]$request.under) } elseif ($cmd -eq 'window') { $result = [VashDesktop]::Window([long]$request.hwnd) } elseif ($cmd -eq 'processes') { $result = [VashDesktop]::Processes([string]$request.under) } elseif ($cmd -eq 'kill') { $result = [VashDesktop]::Kill([int[]]@($request.pids)) } elseif ($cmd -eq 'close') { $result = [VashDesktop]::CloseProcessWindows([int]$request.pid) } elseif ($cmd -eq 'focus') { $result = [VashDesktop]::Focus([long]$request.hwnd) } elseif ($cmd -eq 'move') { $result = [VashDesktop]::Move([long]$request.hwnd, [int]$request.x, [int]$request.y) } elseif ($cmd -eq 'title') { $result = [VashDesktop]::SetTitle([long]$request.hwnd, [string]$request.title) } elseif ($cmd -eq 'capture') { $format = if ($request.format) { [string]$request.format } else { 'png' } $quality = if ($request.quality) { [long]$request.quality } else { 80 } $size = [VashDesktop]::Capture([int]$request.x, [int]$request.y, [int]$request.width, [int]$request.height, [string]$request.path, $format, [int]$request.maxWidth, [double]$request.scale, [bool]$request.invert, $quality) $result = @{ width = $size[0]; height = $size[1] } } elseif ($cmd -eq 'ocr') { $result = Read-Text ([string]$request.path) } elseif ($cmd -eq 'ocrReady') { Initialize-Ocr $result = @{ language = $script:ocr.RecognizerLanguage.LanguageTag; maxDimension = [Windows.Media.Ocr.OcrEngine]::MaxImageDimension } } elseif ($cmd -eq 'click') { $result = [VashDesktop]::Click([int]$request.x, [int]$request.y, [bool]$request.right) } elseif ($cmd -eq 'type') { $result = [VashDesktop]::TypeText([string]$request.text) } elseif ($cmd -eq 'key') { [VashDesktop]::PressKey([uint16]$request.vk) $result = $true } else { throw "Unknown command $cmd" } Reply $id $true $result } catch { Reply $id $false $_.Exception.Message } }