mirror of
https://github.com/jetkvm/kvm.git
synced 2026-05-21 05:20:35 +00:00
df5dbea431
* fix(keyboard): keep modifiers out of auto-release Prevent per-key auto-release from dropping held modifiers during jitter while keeping explicit cleanup paths covered by E2E tests. * fix(keyboard): keep modifiers out of auto-release Prevent per-key auto-release from dropping held modifiers during jitter while keeping explicit cleanup paths covered by E2E tests. * chore(keyboard): trim autorelease comments Keep comments focused on keyboard behavior and remove branch-specific narrative from the tests. * fix(keyboard): reset keepalive timing on key state changes Reset session keepalive timing on every keyboard state change so stale gaps do not poison later holds under modifiers.
255 lines
6.5 KiB
Go
255 lines
6.5 KiB
Go
package kvm
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
|
)
|
|
|
|
var gadget *usbgadget.UsbGadget
|
|
|
|
// initUsbGadget initializes the USB gadget.
|
|
// call it only after the config is loaded.
|
|
func initUsbGadget() {
|
|
gadget = usbgadget.NewUsbGadget(
|
|
"jetkvm",
|
|
config.UsbDevices,
|
|
config.UsbConfig,
|
|
usbLogger,
|
|
)
|
|
|
|
setUSBRecoveryTimer(time.Now())
|
|
|
|
go func() {
|
|
for {
|
|
checkUSBState()
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
}()
|
|
|
|
gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) {
|
|
if currentSession != nil {
|
|
currentSession.reportHidRPCKeyboardLedState(state)
|
|
}
|
|
})
|
|
|
|
gadget.SetOnKeysDownChange(func(state usbgadget.KeysDownState) {
|
|
if currentSession != nil {
|
|
currentSession.enqueueKeysDownState(state)
|
|
currentSession.resetKeepAliveTime()
|
|
}
|
|
})
|
|
|
|
// open the keyboard hid file to listen for keyboard events
|
|
if err := gadget.OpenKeyboardHidFile(); err != nil {
|
|
usbLogger.Error().Err(err).Msg("failed to open keyboard hid file")
|
|
}
|
|
}
|
|
|
|
// rpcHidReport wraps a HID gadget call with the common guard (skip if USB not
|
|
// ready) and error suppression (swallow transient HID errors during rebind).
|
|
func rpcHidReport(fn func() error) error {
|
|
if !usbReadyForHidReports() {
|
|
return nil
|
|
}
|
|
if err := fn(); err != nil && !usbgadget.IsHIDTemporarilyUnavailableError(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func rpcKeyboardReport(modifier byte, keys []byte) error {
|
|
return rpcHidReport(func() error { return gadget.KeyboardReport(modifier, keys) })
|
|
}
|
|
|
|
func rpcKeypressReport(key byte, press bool) error {
|
|
return rpcHidReport(func() error { return gadget.KeypressReport(key, press) })
|
|
}
|
|
|
|
func rpcAbsMouseReport(x int, y int, buttons uint8) error {
|
|
return rpcHidReport(func() error { return gadget.AbsMouseReport(x, y, buttons) })
|
|
}
|
|
|
|
func rpcRelMouseReport(dx int8, dy int8, buttons uint8) error {
|
|
return rpcHidReport(func() error { return gadget.RelMouseReport(dx, dy, buttons) })
|
|
}
|
|
|
|
func rpcWheelReport(wheelY int8, wheelX int8) error {
|
|
return rpcHidReport(func() error {
|
|
if gadget.HasAbsoluteMouse() {
|
|
return gadget.AbsMouseWheelReport(wheelY, wheelX)
|
|
}
|
|
return gadget.RelMouseWheelReport(wheelY, wheelX)
|
|
})
|
|
}
|
|
|
|
func rpcGetKeyboardLedState() (state usbgadget.KeyboardState) {
|
|
return gadget.GetKeyboardState()
|
|
}
|
|
|
|
func rpcGetKeysDownState() (state usbgadget.KeysDownState) {
|
|
return gadget.GetKeysDownState()
|
|
}
|
|
|
|
var (
|
|
usbState = usbgadget.USBStateUnknown
|
|
usbStateLock sync.Mutex
|
|
|
|
usbEmulationDesired = true
|
|
lastUSBRecoveryTry time.Time
|
|
)
|
|
|
|
func usbReadyForHidReports() bool {
|
|
usbStateLock.Lock()
|
|
state := usbState
|
|
usbStateLock.Unlock()
|
|
return state != usbgadget.USBStateNotAttached && state != usbgadget.USBStateUnknown
|
|
}
|
|
|
|
func rpcGetUSBState() (state string) {
|
|
return gadget.GetUsbState()
|
|
}
|
|
|
|
func setUSBEmulationDesired(enabled bool) {
|
|
usbStateLock.Lock()
|
|
defer usbStateLock.Unlock()
|
|
|
|
usbEmulationDesired = enabled
|
|
}
|
|
|
|
func setUSBRecoveryTimer(lastAttempt time.Time) {
|
|
usbStateLock.Lock()
|
|
defer usbStateLock.Unlock()
|
|
|
|
lastUSBRecoveryTry = lastAttempt
|
|
}
|
|
|
|
func attemptUSBRecovery(state string) string {
|
|
now := time.Now()
|
|
|
|
usbStateLock.Lock()
|
|
desired := usbEmulationDesired
|
|
lastAttempt := lastUSBRecoveryTry
|
|
shouldRecover := usbgadget.ShouldAttemptUSBRecovery(state, desired, lastAttempt, now)
|
|
if shouldRecover {
|
|
lastUSBRecoveryTry = now
|
|
}
|
|
usbStateLock.Unlock()
|
|
|
|
if !shouldRecover {
|
|
return state
|
|
}
|
|
|
|
usbLogger.Warn().Msg("USB gadget is detached while USB emulation should be enabled; rebinding USB gadget")
|
|
|
|
if err := gadget.RebindUsb(true); err != nil {
|
|
usbLogger.Warn().Err(err).Msg("failed to recover USB gadget by rebinding USB device controller")
|
|
return state
|
|
}
|
|
|
|
// Clear stale /dev/hidg* handles from the pre-rebind gadget instance.
|
|
// The next write/open must use the newly recreated device nodes.
|
|
gadget.ResetHIDFiles()
|
|
|
|
// After rebind, the kernel recreates /dev/hidg* but the character
|
|
// devices take several seconds to become usable (ENXIO until the
|
|
// function driver attaches). Retry the keyboard HID file open with
|
|
// increasing delays up to ~20 seconds total.
|
|
delays := []time.Duration{
|
|
1 * time.Second,
|
|
1 * time.Second,
|
|
2 * time.Second,
|
|
2 * time.Second,
|
|
3 * time.Second,
|
|
3 * time.Second,
|
|
4 * time.Second,
|
|
4 * time.Second,
|
|
}
|
|
tryReopenKeyboard := func(openDelays []time.Duration, reason string) bool {
|
|
for _, delay := range openDelays {
|
|
time.Sleep(delay)
|
|
if err := gadget.ReopenKeyboardHidFile(); err == nil {
|
|
usbLogger.Info().Str("reason", reason).Msg("keyboard HID file reopened successfully after USB recovery")
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
if tryReopenKeyboard(delays, "udc_rebind") {
|
|
return gadget.GetUsbState()
|
|
}
|
|
|
|
usbLogger.Warn().Msg("keyboard HID file not ready after UDC rebind; attempting full USB gadget reconfigure")
|
|
|
|
if err := gadget.UpdateGadgetConfig(); err != nil {
|
|
usbLogger.Warn().Err(err).Msg("failed to recover USB gadget with full gadget reconfigure")
|
|
return gadget.GetUsbState()
|
|
}
|
|
gadget.ResetHIDFiles()
|
|
|
|
if !tryReopenKeyboard(delays, "gadget_reconfigure") {
|
|
usbLogger.Warn().Msg("keyboard HID file not ready after full USB recovery retry window")
|
|
}
|
|
|
|
return gadget.GetUsbState()
|
|
}
|
|
|
|
func triggerUSBStateUpdate() {
|
|
go func() {
|
|
if currentSession == nil {
|
|
usbLogger.Info().Msg("No active RPC session, skipping USB state update")
|
|
return
|
|
}
|
|
writeJSONRPCEvent("usbState", usbState, currentSession)
|
|
}()
|
|
}
|
|
|
|
func checkUSBState() {
|
|
newState := gadget.GetUsbState()
|
|
if newState == usbgadget.USBStateNotAttached {
|
|
newState = attemptUSBRecovery(newState)
|
|
}
|
|
|
|
usbStateLock.Lock()
|
|
defer usbStateLock.Unlock()
|
|
|
|
if newState != usbgadget.USBStateNotAttached {
|
|
// Once USB is attached again, clear recovery rate limiting so any future
|
|
// detach can be recovered immediately.
|
|
lastUSBRecoveryTry = time.Time{}
|
|
}
|
|
|
|
if newState == usbState {
|
|
return
|
|
}
|
|
|
|
oldState := usbState
|
|
usbState = newState
|
|
usbLogger.Info().Str("from", oldState).Str("to", newState).Msg("USB state changed")
|
|
|
|
if newState != usbgadget.USBStateNotAttached {
|
|
openErr := gadget.OpenKeyboardHidFile()
|
|
if openErr != nil {
|
|
usbLogger.Warn().Err(openErr).Str("state", newState).Msg("HID chardev broken after state change, attempting corrective rebind")
|
|
|
|
lastUSBRecoveryTry = time.Now()
|
|
usbStateLock.Unlock()
|
|
|
|
gadget.ResetHIDFiles()
|
|
if rebindErr := gadget.RebindUsb(true); rebindErr == nil {
|
|
time.Sleep(1 * time.Second)
|
|
_ = gadget.OpenKeyboardHidFile()
|
|
}
|
|
|
|
usbStateLock.Lock()
|
|
usbState = gadget.GetUsbState()
|
|
lastUSBRecoveryTry = time.Now()
|
|
}
|
|
}
|
|
|
|
requestDisplayUpdate(false, "usb_state_changed")
|
|
triggerUSBStateUpdate()
|
|
}
|