fix stopping

This commit is contained in:
KingKDot
2026-07-12 14:55:43 -05:00
parent 1d2b50b333
commit 082716081e
5 changed files with 88 additions and 26 deletions
@@ -30,6 +30,7 @@ var (
virtualMonitorIndex int virtualMonitorIndex int
virtualInitialized bool virtualInitialized bool
virtualMu sync.Mutex virtualMu sync.Mutex
virtualPlacementGen atomic.Uint64
virtualCursorEnabled bool virtualCursorEnabled bool
virtualInputMu sync.Mutex virtualInputMu sync.Mutex
@@ -131,6 +132,8 @@ func CleanupVirtualMode() {
virtualMu.Lock() virtualMu.Lock()
defer virtualMu.Unlock() defer virtualMu.Unlock()
virtualPlacementGen.Add(1)
virtualDupState.mu.Lock() virtualDupState.mu.Lock()
virtualDupState.close() virtualDupState.close()
virtualDupState.mu.Unlock() virtualDupState.mu.Unlock()
@@ -750,7 +753,8 @@ func StartVirtualProcess(filePath string) (uint32, error) {
pid := pi.dwProcessId pid := pi.dwProcessId
log.Printf("virtual: started process %q (pid=%d), moving windows to virtual monitor", filePath, pid) log.Printf("virtual: started process %q (pid=%d), moving windows to virtual monitor", filePath, pid)
go moveProcessWindowsToVirtualMonitor(pid, bounds, baseline) placementGen := virtualPlacementGen.Load()
go moveProcessWindowsToVirtualMonitor(pid, bounds, baseline, placementGen)
if pi.hThread != 0 { if pi.hThread != 0 {
procCloseHandle.Call(pi.hThread) procCloseHandle.Call(pi.hThread)
@@ -789,10 +793,13 @@ func virtualTopLevelWindowSet() map[uintptr]struct{} {
return result return result
} }
func moveProcessWindowsToVirtualMonitor(pid uint32, bounds image.Rectangle, baseline map[uintptr]struct{}) { func moveProcessWindowsToVirtualMonitor(pid uint32, bounds image.Rectangle, baseline map[uintptr]struct{}, placementGen uint64) {
hwndMap := make(map[uintptr]bool) hwndMap := make(map[uintptr]bool)
for attempt := 0; attempt < 300; attempt++ { for attempt := 0; attempt < 300; attempt++ {
if virtualPlacementGen.Load() != placementGen {
return
}
deskHwnd, _, _ := procGetDesktopWindow.Call() deskHwnd, _, _ := procGetDesktopWindow.Call()
hwnd := getTopWindow(deskHwnd) hwnd := getTopWindow(deskHwnd)
if hwnd == 0 { if hwnd == 0 {
@@ -803,6 +810,10 @@ func moveProcessWindowsToVirtualMonitor(pid uint32, bounds image.Rectangle, base
moved := 0 moved := 0
for hwnd != 0 { for hwnd != 0 {
visible := isWindowVisible(hwnd) visible := isWindowVisible(hwnd)
if !visible {
hwnd = getWindow(hwnd, GW_HWNDNEXT)
continue
}
var winPID uint32 var winPID uint32
procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&winPID))) procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&winPID)))
@@ -836,10 +847,7 @@ func moveProcessWindowsToVirtualMonitor(pid uint32, bounds image.Rectangle, base
if winH > bounds.Dy() { if winH > bounds.Dy() {
winH = bounds.Dy() winH = bounds.Dy()
} }
flags := uintptr(SWP_NOZORDER | SWP_NOACTIVATE) flags := uintptr(SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW)
if visible {
flags |= SWP_SHOWWINDOW
}
procSetWindowPos.Call(hwnd, 0, uintptr(newX), uintptr(newY), uintptr(winW), uintptr(winH), flags) procSetWindowPos.Call(hwnd, 0, uintptr(newX), uintptr(newY), uintptr(winW), uintptr(winH), flags)
hwndMap[hwnd] = visible hwndMap[hwnd] = visible
+21 -10
View File
@@ -172,6 +172,18 @@ func waitStreamStop(done <-chan struct{}, name string) {
} }
} }
func stopVirtualStreamLocked(env *runtime.Env) {
env.VirtualMouseControl = false
env.VirtualKeyboardControl = false
env.VirtualCursorCapture = false
if env.VirtualCancel != nil {
env.VirtualCancel()
}
waitStreamStop(env.VirtualDone, "hidden")
env.VirtualCancel = nil
env.VirtualDone = nil
}
func ensureHVNCInputWorker() { func ensureHVNCInputWorker() {
hvncInputOnce.Do(func() { hvncInputOnce.Do(func() {
hvncInputQueue = make(chan hvncInputEvent, 1024) hvncInputQueue = make(chan hvncInputEvent, 1024)
@@ -696,6 +708,12 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
env.DesktopCancel = nil env.DesktopCancel = nil
env.DesktopDone = nil env.DesktopDone = nil
env.DesktopMu.Unlock() env.DesktopMu.Unlock()
env.VirtualMu.Lock()
if env.VirtualCancel != nil {
log.Printf("hidden: stop requested via desktop_stop")
stopVirtualStreamLocked(env)
}
env.VirtualMu.Unlock()
if privacy.IsEnabled() { if privacy.IsEnabled() {
privacy.Stop() privacy.Stop()
log.Printf("privacy: auto-disabled on desktop stop") log.Printf("privacy: auto-disabled on desktop stop")
@@ -1065,22 +1083,15 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
sendCommandResultSafe(env, cmdID, true, "") sendCommandResultSafe(env, cmdID, true, "")
return nil return nil
case "hvnc_stop": case "hvnc_stop":
if env.VirtualCancel != nil {
env.VirtualMu.Lock() env.VirtualMu.Lock()
log.Printf("hidden: stop requested")
env.VirtualMouseControl = false
env.VirtualKeyboardControl = false
env.VirtualCursorCapture = false
if env.VirtualCancel != nil { if env.VirtualCancel != nil {
env.VirtualCancel() log.Printf("hidden: stop requested")
} stopVirtualStreamLocked(env)
waitStreamStop(env.VirtualDone, "hidden")
env.VirtualCancel = nil
env.VirtualDone = nil
env.VirtualMu.Unlock() env.VirtualMu.Unlock()
sendCommandResultSafe(env, cmdID, true, "") sendCommandResultSafe(env, cmdID, true, "")
return nil return nil
} }
env.VirtualMu.Unlock()
env.HVNCMu.Lock() env.HVNCMu.Lock()
log.Printf("hvnc: stop requested") log.Printf("hvnc: stop requested")
env.HVNCMouseControl = false env.HVNCMouseControl = false
@@ -250,7 +250,7 @@ type WsLifecycleDeps = {
cleanupDesktopAudioViewer: (ws: ServerWebSocket<SocketData>) => void; cleanupDesktopAudioViewer: (ws: ServerWebSocket<SocketData>) => void;
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void; stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void; sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
sendHVNCCommand: (target: ClientInfo, commandType: string, payload: Record<string, unknown>) => void; sendHVNCCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
notifyConsoleClosed: (clientId: string, reason: string) => void; notifyConsoleClosed: (clientId: string, reason: string) => void;
clearPendingNotificationScreenshots: (clientId: string) => void; clearPendingNotificationScreenshots: (clientId: string) => void;
clearClientPluginState: (clientId: string) => void; clearClientPluginState: (clientId: string) => void;
@@ -5,8 +5,11 @@ import * as sessionManager from "../sessions/sessionManager";
import type { SocketData } from "../sessions/types"; import type { SocketData } from "../sessions/types";
import type { ClientInfo } from "../types"; import type { ClientInfo } from "../types";
import { import {
handleHVNCViewerMessage,
handleHVNCViewerOpen,
handleRemoteDesktopViewerMessage, handleRemoteDesktopViewerMessage,
handleRemoteDesktopViewerOpen, handleRemoteDesktopViewerOpen,
hvncStreamingState,
rdStreamingState, rdStreamingState,
} from "./ws-console-rd-hvnc"; } from "./ws-console-rd-hvnc";
@@ -70,7 +73,11 @@ afterEach(() => {
for (const session of sessionManager.getRdSessionsForClient(clientId)) { for (const session of sessionManager.getRdSessionsForClient(clientId)) {
sessionManager.deleteRdSession(session.id); sessionManager.deleteRdSession(session.id);
} }
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
sessionManager.deleteHvncSession(session.id);
}
rdStreamingState.delete(clientId); rdStreamingState.delete(clientId);
hvncStreamingState.delete(clientId);
clientManager.deleteClient(clientId); clientManager.deleteClient(clientId);
} }
clientIdsToCleanup.clear(); clientIdsToCleanup.clear();
@@ -150,3 +157,29 @@ describe("remote desktop viewer control", () => {
expect(rdStreamingState.get(clientId)?.isStreaming).toBe(true); expect(rdStreamingState.get(clientId)?.isStreaming).toBe(true);
}); });
}); });
describe("hvnc viewer control", () => {
test("forwards hvnc_stop even when server stream state is stale", () => {
const clientId = `hvnc-stale-stop-${Date.now().toString(36)}`;
const { agentWs } = createClient(clientId);
const viewer = createMockWs({ role: "hvnc_viewer", clientId });
handleHVNCViewerOpen(viewer as any);
hvncStreamingState.set(clientId, {
isStreaming: false,
virtualMode: true,
display: 0,
quality: 90,
codec: "",
maxFps: 120,
lastFps: 0,
});
handleHVNCViewerMessage(viewer as any, JSON.stringify({ type: "hvnc_stop" }));
const commands = agentCommands(agentWs);
expect(commands.filter((msg) => msg.commandType === "hvnc_stop")).toHaveLength(1);
expect(commands.filter((msg) => msg.commandType === "webrtc_stop")).toHaveLength(1);
expect(hvncStreamingState.get(clientId)?.isStreaming).toBe(false);
});
});
@@ -1179,13 +1179,11 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
const otherHvncViewers = sessionManager.getHvncSessionsForClient(clientId) const otherHvncViewers = sessionManager.getHvncSessionsForClient(clientId)
.filter(s => s.id !== ws.data.sessionId); .filter(s => s.id !== ws.data.sessionId);
if (otherHvncViewers.length === 0) { if (otherHvncViewers.length === 0) {
if (state.isStreaming) {
sendHVNCCommand(target, "hvnc_stop", {}); sendHVNCCommand(target, "hvnc_stop", {});
sendHVNCCommand(target, "webrtc_stop", { kind: "hvnc" }); sendHVNCCommand(target, "webrtc_stop", { kind: "hvnc" });
state.isStreaming = false; state.isStreaming = false;
hvncStreamingState.set(clientId, state); hvncStreamingState.set(clientId, state);
logger.debug(`[hvnc] stopped streaming for client ${clientId}`); logger.debug(`[hvnc] stopped streaming for client ${clientId}`);
}
} else { } else {
logger.debug(`[hvnc] ignoring hvnc_stop for client ${clientId} - ${otherHvncViewers.length} other viewer(s) still active`); logger.debug(`[hvnc] ignoring hvnc_stop for client ${clientId} - ${otherHvncViewers.length} other viewer(s) still active`);
} }
@@ -1408,8 +1406,20 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
} }
} }
export function sendHVNCCommand(target: ClientInfo, commandType: string, payload: any) { export function sendHVNCCommand(target: ClientInfo | undefined, commandType: string, payload: any) {
if (!target) {
logger.warn(`[hvnc] send command skipped, target missing command=${commandType}`);
return false;
}
try {
logger.debug(`[hvnc] send command command=${commandType} client=${target.id} payload=${JSON.stringify(payload || {})}`);
target.ws.send(encodeMessage({ type: "command", commandType: commandType as any, id: uuidv4(), payload })); target.ws.send(encodeMessage({ type: "command", commandType: commandType as any, id: uuidv4(), payload }));
metrics.recordCommand(commandType);
return true;
} catch (err) {
logger.error("[hvnc] send command failed", err);
return false;
}
} }
(globalThis as any).__hvncBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => { (globalThis as any).__hvncBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {