diff --git a/client/vnc/server/capture_fb_freebsd_test.go b/client/vnc/server/capture_fb_freebsd_test.go index 85e188044..2f60f591f 100644 --- a/client/vnc/server/capture_fb_freebsd_test.go +++ b/client/vnc/server/capture_fb_freebsd_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // fb_depth is colour depth, not storage: a KMS console is depth 24 stored as @@ -32,3 +33,33 @@ func TestFreebsdStorageBits(t *testing.T) { }) } } + +// The row pitch is derived from the mapping, not computed from width and +// depth: a KMS framebuffer pads its rows, and reading at the unpadded width +// shears the image. A mapping too small for the reported geometry would have +// rows read off its end, so it is refused rather than guessed at. +func TestFreebsdFBStride(t *testing.T) { + tests := []struct { + name string + fbt fbType + want int + wantErr bool + }{ + {"unpadded 32-bit", fbType{FbWidth: 1024, FbHeight: 768, FbDepth: 32, FbSize: 1024 * 768 * 4}, 4096, false}, + {"padded rows", fbType{FbWidth: 1000, FbHeight: 768, FbDepth: 32, FbSize: 4096 * 768}, 4096, false}, + {"packed 24-bit", fbType{FbWidth: 1024, FbHeight: 768, FbDepth: 24, FbSize: 1024 * 768 * 3}, 3072, false}, + {"16-bit", fbType{FbWidth: 800, FbHeight: 600, FbDepth: 16, FbSize: 800 * 600 * 2}, 1600, false}, + {"size cannot hold the geometry", fbType{FbWidth: 1024, FbHeight: 768, FbDepth: 32, FbSize: 1024 * 768 * 2}, 0, true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := freebsdFBStride(tc.fbt) + if tc.wantErr { + require.Error(t, err, "a mapping smaller than the geometry must be refused") + return + } + require.NoError(t, err) + assert.Equal(t, tc.want, got, "row pitch in bytes") + }) + } +} diff --git a/client/vnc/server/input_darwin.go b/client/vnc/server/input_darwin.go index 1668f444e..270b935d9 100644 --- a/client/vnc/server/input_darwin.go +++ b/client/vnc/server/input_darwin.go @@ -97,6 +97,9 @@ var ( // event, so the receiving app gets those characters whatever the active // keyboard layout would have produced for the keycode. cgEventKeyboardSetUnicodeString func(uintptr, uintptr, *uint16) + // cgEventSourceFlagsState reads the modifier flags macOS currently holds + // for an event-source state, which is where the real Caps Lock state is. + cgEventSourceFlagsState func(int32) uint64 // CGEventCreateScrollWheelEvent is variadic, call via SyscallN. cgEventCreateScrollWheelEventAddr uintptr @@ -171,6 +174,9 @@ func initDarwinInput() { if sym, err := purego.Dlsym(cg, "CGEventKeyboardSetUnicodeString"); err == nil { purego.RegisterFunc(&cgEventKeyboardSetUnicodeString, sym) } + if sym, err := purego.Dlsym(cg, "CGEventSourceFlagsState"); err == nil { + purego.RegisterFunc(&cgEventSourceFlagsState, sym) + } sym, err := purego.Dlsym(cg, "CGEventCreateScrollWheelEvent") if err == nil { @@ -616,13 +622,22 @@ func (m *MacInputInjector) postModifier(src uintptr, keycode uint16, down bool, if capsLock && !down { return } + // Toggle from the state macOS actually has, not from the one this injector + // has built up: Caps Lock may already be on when the injector starts, or be + // toggled at the local keyboard in the meantime, and toggling a stale copy + // would post the state the system is already in instead of a transition. + systemCaps, haveSystemCaps := systemCapsLock() var flags uint64 for { old := m.modifiers.Load() switch { case capsLock: - flags = old ^ bit + current := old & bit + if haveSystemCaps { + current = systemCaps + } + flags = (old &^ bit) | (current ^ bit) case down: flags = old | bit default: @@ -893,8 +908,11 @@ func (m *MacInputInjector) SetClipboard(text string) { // TypeText synthesizes the given text as keystrokes via Core Graphics. // Lets a client push host clipboard content to the focused remote app // even when the app doesn't honor pbpaste-style clipboard sync (e.g. -// login screens, locked-down apps). ASCII printable runes only; others -// are skipped. +// login screens, locked-down apps). Printable runes, non-ASCII included, are +// attached to the events as literal text, so they arrive as written whatever +// the host's keyboard layout. Return and Tab go out as their keys. Where the +// literal-text call is unavailable, ASCII falls back to US-layout keycodes and +// anything else is skipped. func (m *MacInputInjector) TypeText(text string) { // Same permission the other injection paths need: without it the posted // events are swallowed, so asking here is what makes the prompt appear @@ -1149,3 +1167,12 @@ func (m *MacInputInjector) typeUnicodeRune(src uintptr, r rune) bool { } return true } + +// systemCapsLock returns the Caps Lock bit macOS currently holds, and false +// when that cannot be read. +func systemCapsLock() (uint64, bool) { + if cgEventSourceFlagsState == nil { + return 0, false + } + return cgEventSourceFlagsState(kCGEventSourceStateCombinedSessionState) & kCGEventFlagMaskAlphaShift, true +} diff --git a/client/vnc/server/input_uinput_keymap_linux.go b/client/vnc/server/input_uinput_keymap_linux.go index 7baf76769..d32f49acc 100644 --- a/client/vnc/server/input_uinput_keymap_linux.go +++ b/client/vnc/server/input_uinput_keymap_linux.go @@ -17,12 +17,14 @@ const consoleTTY = "/dev/tty0" // kdgkbent is KDGKBENT from linux/kd.h, which reads one keymap entry. const kdgkbent = 0x4B46 -// Keymap tables (linux/keyboard.h): the character a key produces unmodified, -// with Shift, and with AltGr. +// Keymap tables (linux/keyboard.h), indexed by the modifier bits held: the +// character a key produces unmodified, with Shift (KG_SHIFT), with AltGr +// (KG_ALTGR), and with both. const ( - kNormTab = 0 - kShiftTab = 1 - kAltGrTab = 2 + kNormTab = 0 + kShiftTab = 1 + kAltGrTab = 2 + kShiftAltGrTab = 3 ) // Key types (linux/keyboard.h). KT_LATIN and KT_LETTER carry a Latin-1 @@ -50,9 +52,10 @@ type consoleKey struct { } // readConsoleKeymap reads the active console keymap and returns, for every -// printable character it can type, the key that types it. The unmodified table -// wins over the Shift one, and both over AltGr, so a character reachable more -// than one way gets the simplest. +// printable character it can type, the key that types it. Tables are read from +// fewest modifiers to most (plain, Shift, AltGr, Shift+AltGr), and the first +// one to produce a character wins, so a character reachable more than one way +// gets the simplest. // // The kernel decodes uinput key codes with this same keymap, which is what makes // it the right source. A fixed US table types the wrong characters on any other @@ -71,6 +74,7 @@ func readConsoleKeymap(tty string) (map[rune]consoleKey, error) { {kNormTab, consoleKey{}}, {kShiftTab, consoleKey{shift: true}}, {kAltGrTab, consoleKey{altGr: true}}, + {kShiftAltGrTab, consoleKey{shift: true, altGr: true}}, } out := make(map[rune]consoleKey) diff --git a/client/vnc/server/input_x11.go b/client/vnc/server/input_x11.go index b56811b7f..2c0c4b28a 100644 --- a/client/vnc/server/input_x11.go +++ b/client/vnc/server/input_x11.go @@ -132,6 +132,10 @@ func (x *X11InputInjector) ensureConnLocked() { x.conn = conn x.root = screen.Root x.screen = &screen + // The new server has no buttons down. Keeping the old mask would make a + // button still held across the restart look already pressed, so it would + // never get the ButtonPress it now needs. + x.lastButtons = 0 x.cacheKeyboardMapping() log.Infof("X11 input injector reconnected (display=%s)", x.display) }