diff --git a/client/vnc/server/capture_windows.go b/client/vnc/server/capture_windows.go index 5ae17ffdf..3189be528 100644 --- a/client/vnc/server/capture_windows.go +++ b/client/vnc/server/capture_windows.go @@ -133,6 +133,10 @@ type gdiCapturer struct { memDC uintptr bmp uintptr bits uintptr + // prevBmp is the bitmap memDC held before bmp was selected into it. It + // has to go back in before bmp can be deleted: DeleteObject refuses a + // bitmap that is still selected into a DC, and the handle leaks. + prevBmp uintptr } func newGDICapturer() (*gdiCapturer, error) { @@ -183,11 +187,12 @@ func (c *gdiCapturer) allocGDI() error { return fmt.Errorf("CreateDIBSection returned 0") } - _, _, _ = procSelectObject.Call(memDC, bmp) + prevBmp, _, _ := procSelectObject.Call(memDC, bmp) c.memDC = memDC c.bmp = bmp c.bits = bits + c.prevBmp = prevBmp return nil } @@ -195,6 +200,10 @@ func (c *gdiCapturer) close() { c.freeGDI() } // freeGDI releases pre-allocated GDI resources. func (c *gdiCapturer) freeGDI() { + if c.memDC != 0 && c.prevBmp != 0 { + _, _, _ = procSelectObject.Call(c.memDC, c.prevBmp) + c.prevBmp = 0 + } if c.bmp != 0 { _, _, _ = procDeleteObject.Call(c.bmp) c.bmp = 0 diff --git a/client/vnc/server/input_windows.go b/client/vnc/server/input_windows.go index d85d51ab7..ab3f8b891 100644 --- a/client/vnc/server/input_windows.go +++ b/client/vnc/server/input_windows.go @@ -123,6 +123,29 @@ type inputCmd struct { serverW int serverH int clipText string + // st is the input state of the client that sent this command. + st *windowsInputState +} + +// windowsInputState is what one client has pressed. The OS thread and queue are +// shared by every attach-mode session, but this is not: shared, one client's +// pointer sample releases a button another client is holding, and one client's +// Delete completes another client's Ctrl+Alt into a Secure Attention Sequence. +type windowsInputState struct { + // prevButtonMask, ctrlDown and altDown are read and written only on the + // input thread, in dispatch. + prevButtonMask uint16 + ctrlDown bool + altDown bool + + // queueMu guards the enqueue-side bookkeeping, written on the caller's + // goroutine. lastQueuedButtonMask is the most recent buttonMask submitted + // by InjectPointer, compared against the next sample to decide whether it + // is move-only (lossy enqueue) or carries a button/wheel transition + // (reliable enqueue). + queueMu sync.Mutex + lastQueuedButtonMask uint16 + lastQueuedMaskValid bool } // WindowsInputInjector delivers input events from a dedicated OS thread that @@ -130,20 +153,21 @@ type inputCmd struct { // calling thread's desktop, so the injection thread must be on the same // desktop the user sees. type WindowsInputInjector struct { - ch chan inputCmd - closed chan struct{} - done chan struct{} - closeOnce sync.Once - prevButtonMask uint16 - // lastQueuedButtonMask is the most recent buttonMask submitted to ch - // by InjectPointer. Compared against the incoming sample to decide - // whether the new event is move-only (lossy enqueue) or carries a - // button/wheel transition (reliable enqueue). - lastQueuedButtonMask uint16 - lastQueuedMaskValid bool - queueMu sync.Mutex - ctrlDown bool - altDown bool + ch chan inputCmd + closed chan struct{} + done chan struct{} + closeOnce sync.Once + // st is the input state used by callers that do not ask for a per-session + // view through ForSession. + st *windowsInputState +} + +// windowsSessionInjector is one session's view of a shared +// WindowsInputInjector: the same input thread, its own input state. It does +// not implement Close, so a session cannot shut the shared injector down. +type windowsSessionInjector struct { + w *WindowsInputInjector + st *windowsInputState } // NewWindowsInputInjector creates a desktop-aware input injector. @@ -152,6 +176,7 @@ func NewWindowsInputInjector() *WindowsInputInjector { ch: make(chan inputCmd, 64), closed: make(chan struct{}), done: make(chan struct{}), + st: &windowsInputState{}, } go w.loop() return w @@ -185,7 +210,7 @@ func (w *WindowsInputInjector) tryEnqueue(cmd inputCmd) { // enqueueReliable posts a command and blocks until it's accepted or the // injector closes. Used for edge-triggered events (button/wheel) where a -// drop would desynchronize prevButtonMask in dispatch(). +// drop would desynchronize the sender's prevButtonMask in dispatch(). func (w *WindowsInputInjector) enqueueReliable(cmd inputCmd) { select { case <-w.closed: @@ -248,11 +273,11 @@ func (w *WindowsInputInjector) dispatch(cmd inputCmd) { case cmd.isType: w.typeUnicodeText(cmd.clipText) case cmd.isScancode: - w.doInjectKeyScancode(cmd.scancode, cmd.keysym, cmd.down) + cmd.st.doInjectKeyScancode(cmd.scancode, cmd.keysym, cmd.down) case cmd.isKey: - w.doInjectKey(cmd.keysym, cmd.down) + cmd.st.doInjectKey(cmd.keysym, cmd.down) default: - w.doInjectPointer(cmd.buttonMask, cmd.x, cmd.y, cmd.serverW, cmd.serverH) + cmd.st.doInjectPointer(cmd.buttonMask, cmd.x, cmd.y, cmd.serverW, cmd.serverH) } } @@ -263,7 +288,11 @@ func (w *WindowsInputInjector) dispatch(cmd inputCmd) { // on the host with nothing to lift it. That covers the releases // releaseStickyInput sends when a client disconnects mid-keystroke. func (w *WindowsInputInjector) InjectKey(keysym uint32, down bool) { - w.enqueueReliable(inputCmd{isKey: true, keysym: keysym, down: down}) + w.injectKey(w.st, keysym, down) +} + +func (w *WindowsInputInjector) injectKey(st *windowsInputState, keysym uint32, down bool) { + w.enqueueReliable(inputCmd{isKey: true, keysym: keysym, down: down, st: st}) } // InjectKeyScancode queues a raw-scancode key event. PC AT Set 1 maps @@ -272,11 +301,15 @@ func (w *WindowsInputInjector) InjectKey(keysym uint32, down bool) { // KEYEVENTF_EXTENDEDKEY flag. keysym is the client-provided fallback we // reach for if the scancode is zero. func (w *WindowsInputInjector) InjectKeyScancode(scancode uint32, keysym uint32, down bool) { + w.injectKeyScancode(w.st, scancode, keysym, down) +} + +func (w *WindowsInputInjector) injectKeyScancode(st *windowsInputState, scancode, keysym uint32, down bool) { if scancode == 0 { - w.InjectKey(keysym, down) + w.injectKey(st, keysym, down) return } - w.enqueueReliable(inputCmd{isScancode: true, scancode: scancode, keysym: keysym, down: down}) + w.enqueueReliable(inputCmd{isScancode: true, scancode: scancode, keysym: keysym, down: down, st: st}) } // InjectPointer queues a pointer event for injection on the input desktop @@ -285,12 +318,16 @@ func (w *WindowsInputInjector) InjectKeyScancode(scancode uint32, keysym uint32, // queued mask is enqueued reliably so wheel ticks and button transitions // can't be dropped under backpressure. func (w *WindowsInputInjector) InjectPointer(buttonMask uint16, x, y, serverW, serverH int) { - cmd := inputCmd{buttonMask: buttonMask, x: x, y: y, serverW: serverW, serverH: serverH} - w.queueMu.Lock() - transition := !w.lastQueuedMaskValid || w.lastQueuedButtonMask != buttonMask - w.lastQueuedButtonMask = buttonMask - w.lastQueuedMaskValid = true - w.queueMu.Unlock() + w.injectPointer(w.st, buttonMask, x, y, serverW, serverH) +} + +func (w *WindowsInputInjector) injectPointer(st *windowsInputState, buttonMask uint16, x, y, serverW, serverH int) { + cmd := inputCmd{buttonMask: buttonMask, x: x, y: y, serverW: serverW, serverH: serverH, st: st} + st.queueMu.Lock() + transition := !st.lastQueuedMaskValid || st.lastQueuedButtonMask != buttonMask + st.lastQueuedButtonMask = buttonMask + st.lastQueuedMaskValid = true + st.queueMu.Unlock() if transition { w.enqueueReliable(cmd) return @@ -303,14 +340,14 @@ func (w *WindowsInputInjector) InjectPointer(buttonMask uint16, x, y, serverW, s // natively via KEYEVENTF_SCANCODE, so the only work is splitting the // optional 0xE0 prefix off into the EXTENDEDKEY flag and tracking // modifier state for the SAS Ctrl+Alt+Del shortcut. -func (w *WindowsInputInjector) doInjectKeyScancode(scancode, keysym uint32, down bool) { +func (st *windowsInputState) doInjectKeyScancode(scancode, keysym uint32, down bool) { switch keysym { case 0xffe3, 0xffe4: - w.ctrlDown = down + st.ctrlDown = down case 0xffe9, 0xffea: - w.altDown = down + st.altDown = down } - if (keysym == 0xff9f || keysym == 0xffff) && w.ctrlDown && w.altDown && down { + if (keysym == 0xff9f || keysym == 0xffff) && st.ctrlDown && st.altDown && down { signalSAS() return } @@ -324,15 +361,15 @@ func (w *WindowsInputInjector) doInjectKeyScancode(scancode, keysym uint32, down sendKeyInput(0, qemuScancodeLowByte(scancode), flags) } -func (w *WindowsInputInjector) doInjectKey(keysym uint32, down bool) { +func (st *windowsInputState) doInjectKey(keysym uint32, down bool) { switch keysym { case 0xffe3, 0xffe4: - w.ctrlDown = down + st.ctrlDown = down case 0xffe9, 0xffea: - w.altDown = down + st.altDown = down } - if (keysym == 0xff9f || keysym == 0xffff) && w.ctrlDown && w.altDown && down { + if (keysym == 0xff9f || keysym == 0xffff) && st.ctrlDown && st.altDown && down { signalSAS() return } @@ -377,7 +414,7 @@ func signalSAS() { } } -func (w *WindowsInputInjector) doInjectPointer(buttonMask uint16, x, y, serverW, serverH int) { +func (st *windowsInputState) doInjectPointer(buttonMask uint16, x, y, serverW, serverH int) { if serverW == 0 || serverH == 0 { return } @@ -387,8 +424,8 @@ func (w *WindowsInputInjector) doInjectPointer(buttonMask uint16, x, y, serverW, sendMouseInput(mouseeventfMove|mouseeventfAbsolute, absX, absY, 0) - changed := buttonMask ^ w.prevButtonMask - w.prevButtonMask = buttonMask + changed := buttonMask ^ st.prevButtonMask + st.prevButtonMask = buttonMask type btnMap struct { bit uint16 @@ -446,6 +483,12 @@ func (w *WindowsInputInjector) doInjectPointer(buttonMask uint16, x, y, serverW, func keysym2VK(keysym uint32) (vk uint16, scan uint16, extended bool) { if keysym >= 0x20 && keysym <= 0x7e { r, _, _ := procVkKeyScanA.Call(uintptr(keysym)) + // VkKeyScanA returns -1 when the active layout has no key for the + // character. Narrowing that to its low byte gives VK 0xff, a bogus + // key event, so report no mapping instead. + if int16(r) == -1 { + return 0, 0, false + } vk = uint16(r & 0xff) return } @@ -530,15 +573,21 @@ const ( // current input desktop. Secure desktops (Winlogon, UAC) have isolated // clipboards we cannot reach, so the call is a no-op there; use TypeText // to enter text into a secure desktop instead. +// +// Enqueued reliably: this is content the user asked to send, and the lossy +// enqueue is for samples a later one supersedes, like pointer motion. Dropping +// it under backpressure would lose the paste with nothing to say so. func (w *WindowsInputInjector) SetClipboard(text string) { - w.tryEnqueue(inputCmd{isClipboard: true, clipText: text}) + w.enqueueReliable(inputCmd{isClipboard: true, clipText: text}) } // TypeText queues a request to synthesize the given text as Unicode // keystrokes on the current input desktop. Targets the secure desktop // when the user is on Winlogon/UAC, where the clipboard is unreachable. +// +// Enqueued reliably, for the reason given on SetClipboard. func (w *WindowsInputInjector) TypeText(text string) { - w.tryEnqueue(inputCmd{isType: true, clipText: text}) + w.enqueueReliable(inputCmd{isType: true, clipText: text}) } func (w *WindowsInputInjector) doSetClipboard(text string) { @@ -636,3 +685,37 @@ func (w *WindowsInputInjector) GetClipboard() string { var _ InputInjector = (*WindowsInputInjector)(nil) var _ ScreenCapturer = (*DesktopCapturer)(nil) + +// ForSession returns an injector for one client that shares w's input thread +// and queue but keeps that client's buttons and modifiers to itself. +func (w *WindowsInputInjector) ForSession() InputInjector { + return &windowsSessionInjector{w: w, st: &windowsInputState{}} +} + +// InjectKey queues a key event under this session's input state. +func (s *windowsSessionInjector) InjectKey(keysym uint32, down bool) { + s.w.injectKey(s.st, keysym, down) +} + +// InjectKeyScancode queues a raw-scancode key event under this session's input +// state. +func (s *windowsSessionInjector) InjectKeyScancode(scancode, keysym uint32, down bool) { + s.w.injectKeyScancode(s.st, scancode, keysym, down) +} + +// InjectPointer queues a pointer event under this session's input state. +func (s *windowsSessionInjector) InjectPointer(buttonMask uint16, x, y, serverW, serverH int) { + s.w.injectPointer(s.st, buttonMask, x, y, serverW, serverH) +} + +// SetClipboard sets the shared clipboard; there is only one per desktop. +func (s *windowsSessionInjector) SetClipboard(text string) { s.w.SetClipboard(text) } + +// GetClipboard reads the shared clipboard. +func (s *windowsSessionInjector) GetClipboard() string { return s.w.GetClipboard() } + +// TypeText types text on the current input desktop. It carries no modifier +// state, so it needs no per-session view. +func (s *windowsSessionInjector) TypeText(text string) { s.w.TypeText(text) } + +var _ InputInjector = (*windowsSessionInjector)(nil) diff --git a/client/vnc/server/server.go b/client/vnc/server/server.go index c5fcae8ea..ad1453f21 100644 --- a/client/vnc/server/server.go +++ b/client/vnc/server/server.go @@ -134,6 +134,13 @@ type InputInjector interface { TypeText(text string) } +// sessionInputFactory is implemented by injectors that keep input state per +// client. ForSession returns a view that shares the injector's resources but +// not what the client has pressed; the view must not close the injector. +type sessionInputFactory interface { + ForSession() InputInjector +} + // connectionHeader is sent by the client before the RFB handshake to specify // the VNC session mode and authenticate. type connectionHeader struct { @@ -1180,6 +1187,13 @@ func (s *Server) handleConnection(conn net.Conn) { }, conn) defer s.removeSession(sessionID) + // An injector shared by every attach-mode session can hand each one its + // own view, so what one client holds down is not released, or completed + // into a shortcut, by another client's events. + if f, ok := injector.(sessionInputFactory); ok { + injector = f.ForSession() + } + conn = newMetricsConn(conn, s.sessionRecorder) sess := &session{ conn: conn,