diff --git a/client/internal/engine_vnc_x11.go b/client/internal/engine_vnc_x11.go index 3b0ddf8ac..7c2a124ea 100644 --- a/client/internal/engine_vnc_x11.go +++ b/client/internal/engine_vnc_x11.go @@ -10,12 +10,25 @@ import ( func newPlatformVNC() (vncserver.ScreenCapturer, vncserver.InputInjector, bool) { // Prefer X11 when an X server is reachable. NewX11InputInjector probes - // DISPLAY (and /proc) eagerly, so a non-nil error here means no X. + // DISPLAY (and /proc) eagerly. injector, err := vncserver.NewX11InputInjector("", "", "") if err == nil { return vncserver.NewX11Poller("", ""), injector, true } - log.Debugf("VNC: X11 not available: %v", err) + log.Debugf("VNC: X11 input injection unavailable: %v", err) + + // That failure covers two different things: no display at all, and a + // display whose server has no XTest extension. Only the first rules X11 + // out. A host with a screen worth streaming and no way to inject input is + // still better served by a view-only X11 session than by falling through + // to a framebuffer console that is blank while X owns the VT. + probe, capErr := vncserver.NewX11Capturer("", "") + if capErr == nil { + probe.Close() + log.Infof("VNC: X11 capture available without input injection, serving view-only") + return vncserver.NewX11Poller("", ""), &vncserver.StubInputInjector{}, true + } + log.Debugf("VNC: X11 capture unavailable: %v", capErr) // Fallback for headless / pre-X states (kernel console, login manager // without X, physical server in recovery): stream the framebuffer and diff --git a/client/vnc/server/input_uinput_linux.go b/client/vnc/server/input_uinput_linux.go index 971aef9ef..1f8ab5bf0 100644 --- a/client/vnc/server/input_uinput_linux.go +++ b/client/vnc/server/input_uinput_linux.go @@ -24,6 +24,7 @@ const ( uiDevSetup = (1 << 30) | (92 << 16) | (0x55 << 8) | 3 uiSetEvBit = (1 << 30) | (4 << 16) | (0x55 << 8) | 100 uiSetKeyBit = (1 << 30) | (4 << 16) | (0x55 << 8) | 101 + uiSetRelBit = (1 << 30) | (4 << 16) | (0x55 << 8) | 102 uiSetAbsBit = (1 << 30) | (4 << 16) | (0x55 << 8) | 103 uinputAbsSize = 64 // legacy struct uses absmin/absmax/absfuzz/absflat[64]. ) @@ -32,11 +33,16 @@ const ( const ( evSyn = 0x00 evKey = 0x01 + evRel = 0x02 evAbs = 0x03 evRep = 0x14 synReport = 0 + // relWheel carries notched vertical scroll, one unit per detent, which is + // what the RFB wheel-up/wheel-down button bits describe. + relWheel = 0x08 + // keyMaxCode is the kernel's KEY_MAX: the largest code UI_SET_KEYBIT // accepts. Anything above it would make the ioctl fail. keyMaxCode = 0x2ff @@ -101,6 +107,16 @@ func NewUInputInjector(w, h int) (*UInputInjector, error) { unix.Close(fd) return nil, err } + // Scroll arrives as the RFB wheel button bits, which have no KEY_ code to + // press: the kernel expects a relative wheel delta instead. + if err := setBit(fd, uiSetEvBit, evRel); err != nil { + unix.Close(fd) + return nil, err + } + if err := setBit(fd, uiSetRelBit, relWheel); err != nil { + unix.Close(fd) + return nil, err + } // Advertise key auto-repeat so the kernel input core repeats held // keys at the configured rate (default ~250 ms delay, ~33 ms period). // Without this, holding Backspace etc. only deletes one character. @@ -271,6 +287,22 @@ func (u *UInputInjector) InjectPointer(buttonMask uint16, x, y, serverW, serverH _ = u.emit(evKey, b.key, 0) } } + + // RFB spells a wheel notch as a press of button 4 (up) or 5 (down), + // immediately released. There is no KEY_ code for either, so each press + // edge becomes one relative wheel detent; the release edge carries no + // further movement and is ignored. + const ( + wheelUpBit = 1 << 3 + wheelDownBit = 1 << 4 + ) + if buttonMask&wheelUpBit != 0 && u.prevButtons&wheelUpBit == 0 { + _ = u.emit(evRel, relWheel, 1) + } + if buttonMask&wheelDownBit != 0 && u.prevButtons&wheelDownBit == 0 { + _ = u.emit(evRel, relWheel, -1) + } + u.prevButtons = buttonMask u.sync() } @@ -433,6 +465,12 @@ func keymapByKeysym(_ []uint16) map[uint32]uint16 { 0xffe3: keyLeftCtrl, 0xffe4: keyRightCtrl, 0xffe9: keyLeftAlt, 0xffea: keyRightAlt, 0xffeb: keyLeftMeta, 0xffec: keyRightMeta, + // Caps_Lock is in stickyModifierKeysyms, so a client that drops + // mid-press relies on this entry to get the release delivered. + 0xffe5: keyCapsLock, + // Meta_L / Meta_R. X11 clients send these as well as Super_L/Super_R + // above, and Linux has no separate Meta code. + 0xffe7: keyLeftMeta, 0xffe8: keyRightMeta, } // Letters: register both lowercase and uppercase keysyms onto the same // KEY_ code. The client sends Shift separately for uppercase.