mirror of https://github.com/jetkvm/kvm.git
feat: Add HID keyboard auto-release and async callbacks
Co-authored-by: adam <adam@buildjet.com>
This commit is contained in:
parent
1717549578
commit
20783e6118
|
@ -4,4 +4,13 @@ import "time"
|
||||||
|
|
||||||
const dwc3Path = "/sys/bus/platform/drivers/dwc3"
|
const dwc3Path = "/sys/bus/platform/drivers/dwc3"
|
||||||
|
|
||||||
const hidWriteTimeout = 10 * time.Millisecond
|
// Timeout for HID writes to gadget endpoints. Some hosts poll at ~10ms, but
|
||||||
|
// under load this can be longer. Use a slightly higher value to reduce
|
||||||
|
// spurious timeouts while still preventing indefinite blocking.
|
||||||
|
const hidWriteTimeout = 50 * time.Millisecond
|
||||||
|
|
||||||
|
// Auto-release configuration. When keys are down and no successful HID write
|
||||||
|
// has happened for this duration, clear the keyboard state to prevent stuck
|
||||||
|
// keys after network hiccups.
|
||||||
|
const keyAutoReleaseAfter = 2 * time.Second
|
||||||
|
const keyAutoReleaseCheckInterval = 200 * time.Millisecond
|
||||||
|
|
|
@ -163,9 +163,16 @@ func (u *UsbGadget) updateKeyDownState(state KeysDownState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if u.onKeysDownChange != nil {
|
if u.onKeysDownChange != nil {
|
||||||
u.log.Trace().Interface("state", state).Msg("calling onKeysDownChange")
|
// Call the callback asynchronously to avoid blocking the HID RPC handler path
|
||||||
(*u.onKeysDownChange)(state)
|
// and to prevent timeouts when the network is slow or the channel back-pressures.
|
||||||
u.log.Trace().Interface("state", state).Msg("onKeysDownChange called")
|
st := state
|
||||||
|
// make a copy of the slice to avoid data races
|
||||||
|
st.Keys = append([]byte(nil), state.Keys...)
|
||||||
|
go func(s KeysDownState) {
|
||||||
|
u.log.Trace().Interface("state", s).Msg("calling onKeysDownChange")
|
||||||
|
(*u.onKeysDownChange)(s)
|
||||||
|
u.log.Trace().Interface("state", s).Msg("onKeysDownChange called")
|
||||||
|
}(st)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,6 +241,8 @@ func (u *UsbGadget) openKeyboardHidFile() error {
|
||||||
|
|
||||||
u.keyboardStateCtx, u.keyboardStateCancel = context.WithCancel(context.Background())
|
u.keyboardStateCtx, u.keyboardStateCancel = context.WithCancel(context.Background())
|
||||||
u.listenKeyboardEvents()
|
u.listenKeyboardEvents()
|
||||||
|
// Start auto-release monitor to clear stuck keys after inactivity
|
||||||
|
u.listenKeyboardAutoRelease()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -400,3 +409,59 @@ func (u *UsbGadget) KeypressReport(key byte, press bool) (KeysDownState, error)
|
||||||
|
|
||||||
return u.UpdateKeysDown(modifier, keys), err
|
return u.UpdateKeysDown(modifier, keys), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listenKeyboardAutoRelease starts a background monitor that automatically
|
||||||
|
// releases all keys if no input has been successfully written to the HID device
|
||||||
|
// for a period of time. This acts as a safety net to avoid stuck keys when
|
||||||
|
// key-up events are lost due to network issues.
|
||||||
|
func (u *UsbGadget) listenKeyboardAutoRelease() {
|
||||||
|
var path string
|
||||||
|
if u.keyboardHidFile != nil {
|
||||||
|
path = u.keyboardHidFile.Name()
|
||||||
|
}
|
||||||
|
l := u.log.With().Str("listener", "keyboardAutoRelease").Str("path", path).Logger()
|
||||||
|
l.Trace().Msg("starting")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(keyAutoReleaseCheckInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-u.keyboardStateCtx.Done():
|
||||||
|
l.Info().Msg("context done")
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
// Fast path: if nothing is currently down, skip work
|
||||||
|
u.keyboardStateLock.Lock()
|
||||||
|
modifier := u.keysDownState.Modifier
|
||||||
|
keys := append([]byte(nil), u.keysDownState.Keys...)
|
||||||
|
u.keyboardStateLock.Unlock()
|
||||||
|
|
||||||
|
allZero := modifier == 0
|
||||||
|
if allZero {
|
||||||
|
for _, k := range keys {
|
||||||
|
if k != 0 {
|
||||||
|
allZero = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allZero {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have any key/modifier down and we've been idle long enough, clear state
|
||||||
|
if time.Since(u.GetLastUserInputTime()) >= keyAutoReleaseAfter {
|
||||||
|
l.Debug().Dur("idle", time.Since(u.GetLastUserInputTime())).Msg("auto-releasing stuck keys")
|
||||||
|
zeros := make([]byte, hidKeyBufferSize)
|
||||||
|
_, err := u.KeyboardReport(0x00, zeros)
|
||||||
|
if err != nil {
|
||||||
|
u.logWithSuppression("keyboardAutoReleaseWrite", 100, &l, err, "failed to auto-release keys")
|
||||||
|
} else {
|
||||||
|
u.resetLogSuppressionCounter("keyboardAutoReleaseWrite")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
|
@ -116,23 +116,22 @@ func (u *UsbGadget) writeWithTimeout(file *os.File, data []byte) (n int, err err
|
||||||
}
|
}
|
||||||
|
|
||||||
n, err = file.Write(data)
|
n, err = file.Write(data)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||||
|
// Promote visibility of timeouts and allow callers to recover by
|
||||||
|
// reopening the file. Do not swallow the error.
|
||||||
u.logWithSuppression(
|
u.logWithSuppression(
|
||||||
fmt.Sprintf("writeWithTimeout_%s", file.Name()),
|
fmt.Sprintf("writeWithTimeout_%s", file.Name()),
|
||||||
1000,
|
100,
|
||||||
u.log,
|
u.log,
|
||||||
err,
|
err,
|
||||||
"write timed out: %s",
|
"write timed out: %s",
|
||||||
file.Name(),
|
file.Name(),
|
||||||
)
|
)
|
||||||
err = nil
|
|
||||||
}
|
}
|
||||||
|
return n, err
|
||||||
return
|
}
|
||||||
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) logWithSuppression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...any) {
|
func (u *UsbGadget) logWithSuppression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...any) {
|
||||||
|
|
11
webrtc.go
11
webrtc.go
|
@ -274,6 +274,17 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
session.rpcQueue = nil
|
session.rpcQueue = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Best-effort: clear any potentially stuck keys on channel close
|
||||||
|
if gadget != nil {
|
||||||
|
go func() {
|
||||||
|
zeros := make([]byte, 6)
|
||||||
|
_, err := rpcKeyboardReport(0x00, zeros)
|
||||||
|
if err != nil {
|
||||||
|
scopedLogger.Debug().Err(err).Msg("failed to auto-release keys on channel close")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// Stop HID RPC processor
|
// Stop HID RPC processor
|
||||||
for i := 0; i < len(session.hidQueue); i++ {
|
for i := 0; i < len(session.hidQueue); i++ {
|
||||||
close(session.hidQueue[i])
|
close(session.hidQueue[i])
|
||||||
|
|
Loading…
Reference in New Issue