mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
feat: show remote keyboard lock status (#838)
* feat: show remote keyboard lock status * fix(keyboard-led-status): eliminate polling and improve delivery * fix(keyboard-led-status): simplify indicator labels
This commit is contained in:
@@ -4,6 +4,14 @@ type GetHidModeRsp struct {
|
||||
Mode string `json:"mode"` // normal or hid-only
|
||||
}
|
||||
|
||||
type GetKeyboardLedStatusRsp struct {
|
||||
NumLock bool `json:"numLock"`
|
||||
CapsLock bool `json:"capsLock"`
|
||||
ScrollLock bool `json:"scrollLock"`
|
||||
Known bool `json:"known"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type SetHidModeReq struct {
|
||||
Mode string `validate:"required"` // normal or hid-only
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ func hidRouter(r *gin.Engine) {
|
||||
api.GET("/hid/mode", service.GetHidMode) // get hid mode
|
||||
api.POST("/hid/mode", service.SetHidMode) // set hid mode
|
||||
api.POST("/hid/reset", service.ResetHid) // reset hid
|
||||
api.GET("/hid/leds", service.GetKeyboardLedStatus)
|
||||
|
||||
localAPI.POST("/usb/recover", service.RecoverUSB)
|
||||
}
|
||||
|
||||
@@ -13,11 +13,17 @@ import (
|
||||
)
|
||||
|
||||
type Hid struct {
|
||||
g0 *os.File
|
||||
g1 *os.File
|
||||
g2 *os.File
|
||||
kbMutex sync.Mutex
|
||||
mouseMutex sync.Mutex
|
||||
g0 *os.File
|
||||
g0Reader *os.File
|
||||
g1 *os.File
|
||||
g2 *os.File
|
||||
ledReaderNotifyReader *os.File
|
||||
ledReaderNotifyWriter *os.File
|
||||
ledReaderNotifyReadFD int
|
||||
ledReaderNotifyWriteFD int
|
||||
kbMutex sync.Mutex
|
||||
mouseMutex sync.Mutex
|
||||
ledReaderStartOnce sync.Once
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -124,6 +130,15 @@ func (h *Hid) OpenNoLock() error {
|
||||
}
|
||||
}
|
||||
|
||||
if h.g0 != nil {
|
||||
if err := h.openKeyboardLedReaderNoLock(); err != nil {
|
||||
log.Errorf("open keyboard LED reader failed: %s", err)
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
h.startKeyboardLedReader()
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
@@ -162,6 +177,8 @@ func openNoLockWithRetry(open func() error, timeout, delay time.Duration) error
|
||||
}
|
||||
|
||||
func (h *Hid) CloseNoLock() {
|
||||
h.closeKeyboardLedReaderNoLock()
|
||||
|
||||
for _, device := range h.devices() {
|
||||
h.closeDeviceNoLock(device)
|
||||
}
|
||||
@@ -193,6 +210,41 @@ func (h *Hid) closeDeviceNoLock(device hidDevice) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hid) openKeyboardLedReaderNoLock() error {
|
||||
if h.g0Reader != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := h.ensureKeyboardLedReaderNotifierNoLock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Keep this descriptor blocking. The LED reader waits for either an output
|
||||
// report or a lifecycle notification, so an idle host does not cause a
|
||||
// periodic EAGAIN retry loop.
|
||||
file, err := os.OpenFile(HID0, os.O_RDONLY, 0o666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", HID0, err)
|
||||
}
|
||||
|
||||
h.g0Reader = file
|
||||
h.notifyKeyboardLedReaderNoLock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Hid) closeKeyboardLedReaderNoLock() {
|
||||
if h.g0Reader == nil {
|
||||
return
|
||||
}
|
||||
|
||||
file := h.g0Reader
|
||||
h.g0Reader = nil
|
||||
h.notifyKeyboardLedReaderNoLock()
|
||||
if err := file.Close(); err != nil {
|
||||
log.Debugf("close keyboard LED reader failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// writeWithTimeout bounds how long callers hold HID locks when writing to a
|
||||
// nonblocking descriptor. EAGAIN means the host is not accepting HID reports
|
||||
// yet, so retry until the caller's deadline expires.
|
||||
@@ -302,6 +354,13 @@ func (h *Hid) writeHID(device hidDevice, data []byte) error {
|
||||
if err := h.openDeviceNoLock(device); err != nil {
|
||||
return err
|
||||
}
|
||||
if device.path == HID0 {
|
||||
if err := h.openKeyboardLedReaderNoLock(); err != nil {
|
||||
log.Debugf("open keyboard LED reader failed: %s", err)
|
||||
} else {
|
||||
h.startKeyboardLedReader()
|
||||
}
|
||||
}
|
||||
|
||||
file := device.get()
|
||||
if file == nil {
|
||||
@@ -314,6 +373,9 @@ func (h *Hid) writeHID(device hidDevice, data []byte) error {
|
||||
}
|
||||
|
||||
if err := writeWithTimeout(file, data, hidWriteTimeout); err != nil {
|
||||
if device.path == HID0 {
|
||||
h.closeKeyboardLedReaderNoLock()
|
||||
}
|
||||
h.closeDeviceNoLock(device)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrClosed):
|
||||
|
||||
279
server/service/hid/leds.go
Normal file
279
server/service/hid/leds.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package hid
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
KeyboardLedStatusEvent = "hid-led-status"
|
||||
)
|
||||
|
||||
// KeyboardLedStatus is the lock-key LED state last reported by the remote host
|
||||
// through the keyboard HID output report.
|
||||
type KeyboardLedStatus struct {
|
||||
NumLock bool `json:"numLock"`
|
||||
CapsLock bool `json:"capsLock"`
|
||||
ScrollLock bool `json:"scrollLock"`
|
||||
Known bool `json:"known"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type keyboardLedStatusStore struct {
|
||||
mutex sync.RWMutex
|
||||
status KeyboardLedStatus
|
||||
subscribers map[int]func(KeyboardLedStatus)
|
||||
nextID int
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
var keyboardLeds = newKeyboardLedStatusStore(time.Now)
|
||||
|
||||
func newKeyboardLedStatusStore(now func() time.Time) *keyboardLedStatusStore {
|
||||
return &keyboardLedStatusStore{
|
||||
subscribers: make(map[int]func(KeyboardLedStatus)),
|
||||
now: now,
|
||||
}
|
||||
}
|
||||
|
||||
// GetKeyboardLedStatus returns a snapshot. Known is false until the host has
|
||||
// sent at least one keyboard HID output report.
|
||||
func GetKeyboardLedStatus() KeyboardLedStatus {
|
||||
return keyboardLeds.Get()
|
||||
}
|
||||
|
||||
// SubscribeKeyboardLedStatus subscribes to state changes and returns a
|
||||
// function that removes the subscription.
|
||||
func SubscribeKeyboardLedStatus(subscriber func(KeyboardLedStatus)) func() {
|
||||
return keyboardLeds.Subscribe(subscriber)
|
||||
}
|
||||
|
||||
func (s *keyboardLedStatusStore) Get() KeyboardLedStatus {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
return s.status
|
||||
}
|
||||
|
||||
func (s *keyboardLedStatusStore) Subscribe(subscriber func(KeyboardLedStatus)) func() {
|
||||
s.mutex.Lock()
|
||||
id := s.nextID
|
||||
s.nextID++
|
||||
s.subscribers[id] = subscriber
|
||||
s.mutex.Unlock()
|
||||
|
||||
return func() {
|
||||
s.mutex.Lock()
|
||||
delete(s.subscribers, id)
|
||||
s.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *keyboardLedStatusStore) Update(report byte) {
|
||||
next := keyboardLedStatusFromReport(report, s.now())
|
||||
|
||||
s.mutex.Lock()
|
||||
previous := s.status
|
||||
s.status = next
|
||||
if sameKeyboardLedState(previous, next) {
|
||||
s.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
subscribers := make([]func(KeyboardLedStatus), 0, len(s.subscribers))
|
||||
for _, subscriber := range s.subscribers {
|
||||
subscribers = append(subscribers, subscriber)
|
||||
}
|
||||
s.mutex.Unlock()
|
||||
|
||||
for _, subscriber := range subscribers {
|
||||
subscriber(next)
|
||||
}
|
||||
}
|
||||
|
||||
func keyboardLedStatusFromReport(report byte, updatedAt time.Time) KeyboardLedStatus {
|
||||
return KeyboardLedStatus{
|
||||
NumLock: report&0x01 != 0,
|
||||
CapsLock: report&0x02 != 0,
|
||||
ScrollLock: report&0x04 != 0,
|
||||
Known: true,
|
||||
UpdatedAt: updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func sameKeyboardLedState(a, b KeyboardLedStatus) bool {
|
||||
return a.NumLock == b.NumLock &&
|
||||
a.CapsLock == b.CapsLock &&
|
||||
a.ScrollLock == b.ScrollLock &&
|
||||
a.Known == b.Known
|
||||
}
|
||||
|
||||
func (h *Hid) startKeyboardLedReader() {
|
||||
h.ledReaderStartOnce.Do(func() {
|
||||
go h.readKeyboardLeds()
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Hid) readKeyboardLeds() {
|
||||
buf := make([]byte, 64)
|
||||
|
||||
for {
|
||||
file, notifierFD := h.keyboardLedReaderHandles()
|
||||
if file == nil {
|
||||
if err := waitForKeyboardLedReaderChange(notifierFD); err != nil {
|
||||
log.Debugf("wait for keyboard LED reader failed: %s", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
changed, err := waitForKeyboardLedReportOrChange(file, notifierFD)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrClosed) {
|
||||
log.Debugf("wait for keyboard LED report failed: %s", err)
|
||||
}
|
||||
h.reopenKeyboardLedReader(file)
|
||||
continue
|
||||
}
|
||||
if changed {
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := file.Read(buf)
|
||||
if n > 0 {
|
||||
// Both keyboard gadget descriptors in kvmapp/system/init.d declare
|
||||
// boot-keyboard reports without a Report ID (no 0x85 item). Their LED
|
||||
// output report is one byte: bits 0-4 are the LED bitmap and bits 5-7
|
||||
// are padding. report_length is 8 because it also covers input reports.
|
||||
keyboardLeds.Update(buf[0])
|
||||
}
|
||||
|
||||
if err == nil && n > 0 {
|
||||
continue
|
||||
}
|
||||
if err != nil && !errors.Is(err, os.ErrClosed) {
|
||||
log.Debugf("read keyboard LED report failed: %s", err)
|
||||
}
|
||||
h.reopenKeyboardLedReader(file)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hid) keyboardLedReaderHandles() (*os.File, int) {
|
||||
h.kbMutex.Lock()
|
||||
defer h.kbMutex.Unlock()
|
||||
if h.ledReaderNotifyReader == nil {
|
||||
return h.g0Reader, -1
|
||||
}
|
||||
return h.g0Reader, h.ledReaderNotifyReadFD
|
||||
}
|
||||
|
||||
func (h *Hid) reopenKeyboardLedReader(file *os.File) {
|
||||
h.kbMutex.Lock()
|
||||
defer h.kbMutex.Unlock()
|
||||
|
||||
// A reset may have installed a new reader while the old descriptor was
|
||||
// reporting an error. Never let that stale reader close the replacement.
|
||||
if h.g0Reader != file {
|
||||
return
|
||||
}
|
||||
h.closeKeyboardLedReaderNoLock()
|
||||
if h.g0 == nil {
|
||||
return
|
||||
}
|
||||
if err := h.openKeyboardLedReaderNoLock(); err != nil {
|
||||
log.Debugf("reopen keyboard LED reader failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hid) ensureKeyboardLedReaderNotifierNoLock() error {
|
||||
if h.ledReaderNotifyReader != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
reader, writer, err := os.Pipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create keyboard LED reader notifier: %w", err)
|
||||
}
|
||||
readerFD := int(reader.Fd())
|
||||
writerFD := int(writer.Fd())
|
||||
if err := unix.SetNonblock(readerFD, true); err != nil {
|
||||
_ = reader.Close()
|
||||
_ = writer.Close()
|
||||
return fmt.Errorf("set keyboard LED reader notifier read end nonblocking: %w", err)
|
||||
}
|
||||
if err := unix.SetNonblock(writerFD, true); err != nil {
|
||||
_ = reader.Close()
|
||||
_ = writer.Close()
|
||||
return fmt.Errorf("set keyboard LED reader notifier write end nonblocking: %w", err)
|
||||
}
|
||||
h.ledReaderNotifyReader = reader
|
||||
h.ledReaderNotifyWriter = writer
|
||||
h.ledReaderNotifyReadFD = readerFD
|
||||
h.ledReaderNotifyWriteFD = writerFD
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Hid) notifyKeyboardLedReaderNoLock() {
|
||||
if h.ledReaderNotifyWriter == nil {
|
||||
return
|
||||
}
|
||||
_, err := unix.Write(h.ledReaderNotifyWriteFD, []byte{1})
|
||||
if err != nil && !errors.Is(err, syscall.EAGAIN) && !errors.Is(err, syscall.EWOULDBLOCK) {
|
||||
log.Debugf("notify keyboard LED reader failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForKeyboardLedReaderChange(notifierFD int) error {
|
||||
if notifierFD < 0 {
|
||||
return fmt.Errorf("keyboard LED reader notifier is nil")
|
||||
}
|
||||
_, err := unix.Poll([]unix.PollFd{{Fd: int32(notifierFD), Events: unix.POLLIN}}, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
drainKeyboardLedReaderNotifier(notifierFD)
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitForKeyboardLedReportOrChange blocks until a HID output report arrives or
|
||||
// the reader is replaced/closed. Its bool result is true for the latter.
|
||||
func waitForKeyboardLedReportOrChange(file *os.File, notifierFD int) (bool, error) {
|
||||
if file == nil || notifierFD < 0 {
|
||||
return false, fmt.Errorf("keyboard LED reader or notifier is nil")
|
||||
}
|
||||
|
||||
fds := []unix.PollFd{
|
||||
{Fd: int32(file.Fd()), Events: unix.POLLIN},
|
||||
{Fd: int32(notifierFD), Events: unix.POLLIN},
|
||||
}
|
||||
_, err := unix.Poll(fds, -1)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if fds[1].Revents&unix.POLLIN != 0 {
|
||||
drainKeyboardLedReaderNotifier(notifierFD)
|
||||
return true, nil
|
||||
}
|
||||
if fds[0].Revents&(unix.POLLIN|unix.POLLERR|unix.POLLHUP|unix.POLLNVAL) != 0 {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("keyboard LED poll returned without an event")
|
||||
}
|
||||
|
||||
func drainKeyboardLedReaderNotifier(notifierFD int) {
|
||||
buf := make([]byte, 64)
|
||||
for {
|
||||
_, err := unix.Read(notifierFD, buf)
|
||||
if err != nil {
|
||||
if !errors.Is(err, syscall.EAGAIN) && !errors.Is(err, syscall.EWOULDBLOCK) {
|
||||
log.Debugf("drain keyboard LED reader notifier failed: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
168
server/service/hid/leds_test.go
Normal file
168
server/service/hid/leds_test.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package hid
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestKeyboardLedStatusFromReport(t *testing.T) {
|
||||
updatedAt := time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC)
|
||||
status := keyboardLedStatusFromReport(0x17, updatedAt)
|
||||
|
||||
if !status.NumLock || !status.CapsLock || !status.ScrollLock || !status.Known {
|
||||
t.Fatalf("unexpected parsed status: %+v", status)
|
||||
}
|
||||
if !status.UpdatedAt.Equal(updatedAt) {
|
||||
t.Fatalf("updatedAt = %s, want %s", status.UpdatedAt, updatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyboardLedStatusStoreOnlyNotifiesStateChanges(t *testing.T) {
|
||||
now := time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC)
|
||||
store := newKeyboardLedStatusStore(func() time.Time { return now })
|
||||
notifications := make(chan KeyboardLedStatus, 2)
|
||||
store.Subscribe(func(status KeyboardLedStatus) {
|
||||
notifications <- status
|
||||
})
|
||||
|
||||
store.Update(0x01)
|
||||
store.Update(0x01)
|
||||
now = now.Add(time.Second)
|
||||
store.Update(0x03)
|
||||
|
||||
first := <-notifications
|
||||
second := <-notifications
|
||||
if !first.NumLock || first.CapsLock || !second.NumLock || !second.CapsLock {
|
||||
t.Fatalf("unexpected notifications: first=%+v second=%+v", first, second)
|
||||
}
|
||||
|
||||
select {
|
||||
case status := <-notifications:
|
||||
t.Fatalf("unexpected duplicate notification: %+v", status)
|
||||
default:
|
||||
}
|
||||
|
||||
if status := store.Get(); !status.UpdatedAt.Equal(now) {
|
||||
t.Fatalf("latest updatedAt = %s, want %s", status.UpdatedAt, now)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForKeyboardLedReaderChangeWaitsForNotification(t *testing.T) {
|
||||
h := &Hid{}
|
||||
h.kbMutex.Lock()
|
||||
if err := h.ensureKeyboardLedReaderNotifierNoLock(); err != nil {
|
||||
h.kbMutex.Unlock()
|
||||
t.Fatal(err)
|
||||
}
|
||||
notifierFD := h.ledReaderNotifyReadFD
|
||||
h.kbMutex.Unlock()
|
||||
defer h.ledReaderNotifyReader.Close()
|
||||
defer h.ledReaderNotifyWriter.Close()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- waitForKeyboardLedReaderChange(notifierFD) }()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
t.Fatalf("wait returned before a notification: %v", err)
|
||||
case <-time.After(25 * time.Millisecond):
|
||||
}
|
||||
|
||||
h.kbMutex.Lock()
|
||||
h.notifyKeyboardLedReaderNoLock()
|
||||
h.kbMutex.Unlock()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatalf("wait returned error: %v", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("wait did not wake after a notification")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForKeyboardLedReportOrChangeReturnsForReport(t *testing.T) {
|
||||
reader, writer, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer reader.Close()
|
||||
defer writer.Close()
|
||||
|
||||
h := &Hid{}
|
||||
h.kbMutex.Lock()
|
||||
if err := h.ensureKeyboardLedReaderNotifierNoLock(); err != nil {
|
||||
h.kbMutex.Unlock()
|
||||
t.Fatal(err)
|
||||
}
|
||||
notifierFD := h.ledReaderNotifyReadFD
|
||||
h.kbMutex.Unlock()
|
||||
defer h.ledReaderNotifyReader.Close()
|
||||
defer h.ledReaderNotifyWriter.Close()
|
||||
|
||||
done := make(chan struct {
|
||||
changed bool
|
||||
err error
|
||||
}, 1)
|
||||
go func() {
|
||||
changed, err := waitForKeyboardLedReportOrChange(reader, notifierFD)
|
||||
done <- struct {
|
||||
changed bool
|
||||
err error
|
||||
}{changed, err}
|
||||
}()
|
||||
|
||||
if _, err := writer.Write([]byte{0x03}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case result := <-done:
|
||||
if result.err != nil || result.changed {
|
||||
t.Fatalf("result = changed=%t, err=%v; want report", result.changed, result.err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("wait did not wake for report")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReopenKeyboardLedReaderDoesNotCloseReplacement(t *testing.T) {
|
||||
oldReader, oldWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer oldReader.Close()
|
||||
defer oldWriter.Close()
|
||||
replacement, replacementWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer replacement.Close()
|
||||
defer replacementWriter.Close()
|
||||
|
||||
h := &Hid{g0Reader: oldReader}
|
||||
h.kbMutex.Lock()
|
||||
if err := h.ensureKeyboardLedReaderNotifierNoLock(); err != nil {
|
||||
h.kbMutex.Unlock()
|
||||
t.Fatal(err)
|
||||
}
|
||||
h.g0Reader = replacement // Simulate reset/reopen replacing the reader.
|
||||
h.kbMutex.Unlock()
|
||||
defer h.ledReaderNotifyReader.Close()
|
||||
defer h.ledReaderNotifyWriter.Close()
|
||||
|
||||
h.reopenKeyboardLedReader(oldReader)
|
||||
current, _ := h.keyboardLedReaderHandles()
|
||||
if current != replacement {
|
||||
t.Fatal("stale reader close replaced the active reader")
|
||||
}
|
||||
if _, err := replacementWriter.Write([]byte{0x01}); err != nil {
|
||||
t.Fatalf("replacement reader was closed: %v", err)
|
||||
}
|
||||
buf := make([]byte, 1)
|
||||
if _, err := replacement.Read(buf); err != nil {
|
||||
t.Fatalf("replacement reader was closed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,23 @@ func (s *Service) GetHidMode(c *gin.Context) {
|
||||
log.Debugf("get hid mode: %s", mode)
|
||||
}
|
||||
|
||||
func (s *Service) GetKeyboardLedStatus(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
status := GetKeyboardLedStatus()
|
||||
updatedAt := ""
|
||||
if !status.UpdatedAt.IsZero() {
|
||||
updatedAt = status.UpdatedAt.UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetKeyboardLedStatusRsp{
|
||||
NumLock: status.NumLock,
|
||||
CapsLock: status.CapsLock,
|
||||
ScrollLock: status.ScrollLock,
|
||||
Known: status.Known,
|
||||
UpdatedAt: updatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) SetHidMode(c *gin.Context) {
|
||||
var req proto.SetHidModeReq
|
||||
var rsp proto.Response
|
||||
|
||||
@@ -29,13 +29,15 @@ const (
|
||||
|
||||
func NewClient(ws *websocket.Conn) *Client {
|
||||
client := &Client{
|
||||
ws: ws,
|
||||
hid: hid.GetHid(),
|
||||
manual: inputcontrol.NewManualSession(controlmode.GetManager(), inputcontrol.GetCoordinator()),
|
||||
keyboard: make(chan hid.QueuedReport, 200),
|
||||
mouse: make(chan hid.QueuedReport, 200),
|
||||
heartbeatTimeout: clientHeartbeatTimeout,
|
||||
lastHeartbeat: time.Time{},
|
||||
ws: ws,
|
||||
hid: hid.GetHid(),
|
||||
manual: inputcontrol.NewManualSession(controlmode.GetManager(), inputcontrol.GetCoordinator()),
|
||||
keyboard: make(chan hid.QueuedReport, 200),
|
||||
mouse: make(chan hid.QueuedReport, 200),
|
||||
heartbeatTimeout: clientHeartbeatTimeout,
|
||||
keyboardLedNotify: make(chan struct{}, 1),
|
||||
keyboardLedDone: make(chan struct{}),
|
||||
lastHeartbeat: time.Time{},
|
||||
}
|
||||
|
||||
client.hid.Open()
|
||||
@@ -44,6 +46,8 @@ func NewClient(ws *websocket.Conn) *Client {
|
||||
}
|
||||
|
||||
func (c *Client) Start() {
|
||||
c.startKeyboardLedStatusWorker()
|
||||
|
||||
c.workers.Add(2)
|
||||
go func() {
|
||||
defer c.workers.Done()
|
||||
@@ -194,6 +198,7 @@ func (c *Client) Close() {
|
||||
if c.ws != nil {
|
||||
_ = c.ws.Close()
|
||||
}
|
||||
c.stopKeyboardLedStatusWorker()
|
||||
|
||||
if c.keyboard != nil {
|
||||
close(c.keyboard)
|
||||
@@ -203,12 +208,104 @@ func (c *Client) Close() {
|
||||
}
|
||||
})
|
||||
c.workers.Wait()
|
||||
c.keyboardLedWorkers.Wait()
|
||||
if c.manual != nil {
|
||||
c.manual.Close()
|
||||
}
|
||||
log.Debug("websocket disconnected")
|
||||
}
|
||||
|
||||
// enqueueKeyboardLedStatus records only the newest status for this client. It
|
||||
// never waits for a WebSocket write, so a client which stops reading cannot
|
||||
// delay LED updates for the other connected clients.
|
||||
func (c *Client) enqueueKeyboardLedStatus(status hid.KeyboardLedStatus) {
|
||||
c.keyboardLedMutex.Lock()
|
||||
if c.keyboardLedClosed || c.keyboardLedNotify == nil {
|
||||
c.keyboardLedMutex.Unlock()
|
||||
return
|
||||
}
|
||||
c.keyboardLedStatus = &status
|
||||
notify := c.keyboardLedNotify
|
||||
c.keyboardLedMutex.Unlock()
|
||||
|
||||
select {
|
||||
case notify <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) startKeyboardLedStatusWorker() {
|
||||
c.keyboardLedMutex.Lock()
|
||||
if c.keyboardLedClosed {
|
||||
c.keyboardLedMutex.Unlock()
|
||||
return
|
||||
}
|
||||
if c.keyboardLedNotify == nil {
|
||||
c.keyboardLedNotify = make(chan struct{}, 1)
|
||||
}
|
||||
if c.keyboardLedDone == nil {
|
||||
c.keyboardLedDone = make(chan struct{})
|
||||
}
|
||||
c.keyboardLedMutex.Unlock()
|
||||
|
||||
c.keyboardLedOnce.Do(func() {
|
||||
c.keyboardLedWorkers.Add(1)
|
||||
go func() {
|
||||
defer c.keyboardLedWorkers.Done()
|
||||
c.runKeyboardLedStatusWorker()
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) stopKeyboardLedStatusWorker() {
|
||||
c.keyboardLedMutex.Lock()
|
||||
defer c.keyboardLedMutex.Unlock()
|
||||
if c.keyboardLedClosed {
|
||||
return
|
||||
}
|
||||
c.keyboardLedClosed = true
|
||||
if c.keyboardLedDone != nil {
|
||||
close(c.keyboardLedDone)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) runKeyboardLedStatusWorker() {
|
||||
for {
|
||||
c.keyboardLedMutex.Lock()
|
||||
notify := c.keyboardLedNotify
|
||||
done := c.keyboardLedDone
|
||||
c.keyboardLedMutex.Unlock()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-notify:
|
||||
}
|
||||
|
||||
for {
|
||||
status, ok := c.takeKeyboardLedStatus()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if err := sendKeyboardLedStatus(c, status); err != nil {
|
||||
log.Errorf("failed to send keyboard LED status: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) takeKeyboardLedStatus() (hid.KeyboardLedStatus, bool) {
|
||||
c.keyboardLedMutex.Lock()
|
||||
defer c.keyboardLedMutex.Unlock()
|
||||
if c.keyboardLedStatus == nil {
|
||||
return hid.KeyboardLedStatus{}, false
|
||||
}
|
||||
|
||||
status := *c.keyboardLedStatus
|
||||
c.keyboardLedStatus = nil
|
||||
return status, true
|
||||
}
|
||||
|
||||
func writeQueue(queue chan hid.QueuedReport, report hid.QueuedReport) bool {
|
||||
if !sendQueue(queue, report) {
|
||||
report.Complete(false)
|
||||
|
||||
94
server/service/ws/keyboard_led_status.go
Normal file
94
server/service/ws/keyboard_led_status.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
|
||||
"NanoKVM-Server/service/hid"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var keyboardLedBroadcaster = newKeyboardLedStatusBroadcaster(broadcastKeyboardLedStatus)
|
||||
|
||||
func init() {
|
||||
go keyboardLedBroadcaster.Run()
|
||||
|
||||
hid.SubscribeKeyboardLedStatus(func(status hid.KeyboardLedStatus) {
|
||||
keyboardLedBroadcaster.Enqueue(status)
|
||||
})
|
||||
}
|
||||
|
||||
type keyboardLedStatusBroadcaster struct {
|
||||
mutex sync.Mutex
|
||||
pending *hid.KeyboardLedStatus
|
||||
notify chan struct{}
|
||||
broadcast func(hid.KeyboardLedStatus)
|
||||
}
|
||||
|
||||
func newKeyboardLedStatusBroadcaster(
|
||||
broadcast func(hid.KeyboardLedStatus),
|
||||
) *keyboardLedStatusBroadcaster {
|
||||
return &keyboardLedStatusBroadcaster{
|
||||
notify: make(chan struct{}, 1),
|
||||
broadcast: broadcast,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *keyboardLedStatusBroadcaster) Enqueue(status hid.KeyboardLedStatus) {
|
||||
b.mutex.Lock()
|
||||
b.pending = &status
|
||||
b.mutex.Unlock()
|
||||
|
||||
select {
|
||||
case b.notify <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (b *keyboardLedStatusBroadcaster) Run() {
|
||||
for range b.notify {
|
||||
for {
|
||||
status, ok := b.takePending()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
b.broadcast(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *keyboardLedStatusBroadcaster) takePending() (hid.KeyboardLedStatus, bool) {
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
|
||||
if b.pending == nil {
|
||||
return hid.KeyboardLedStatus{}, false
|
||||
}
|
||||
|
||||
status := *b.pending
|
||||
b.pending = nil
|
||||
return status, true
|
||||
}
|
||||
|
||||
func sendKeyboardLedStatusSnapshot(client *Client) {
|
||||
if err := sendKeyboardLedStatus(client, hid.GetKeyboardLedStatus()); err != nil {
|
||||
log.Errorf("failed to send keyboard LED status snapshot: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func broadcastKeyboardLedStatus(status hid.KeyboardLedStatus) {
|
||||
for _, client := range GetManager().GetClients() {
|
||||
client.enqueueKeyboardLedStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
func sendKeyboardLedStatus(client *Client, status hid.KeyboardLedStatus) error {
|
||||
payload, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return client.Write(hid.KeyboardLedStatusEvent, string(payload))
|
||||
}
|
||||
79
server/service/ws/keyboard_led_status_test.go
Normal file
79
server/service/ws/keyboard_led_status_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"NanoKVM-Server/service/hid"
|
||||
)
|
||||
|
||||
func TestKeyboardLedStatusQueueKeepsLatestStatus(t *testing.T) {
|
||||
client := &Client{
|
||||
keyboardLedNotify: make(chan struct{}, 1),
|
||||
keyboardLedDone: make(chan struct{}),
|
||||
}
|
||||
first := hid.KeyboardLedStatus{NumLock: true}
|
||||
latest := hid.KeyboardLedStatus{CapsLock: true}
|
||||
|
||||
client.enqueueKeyboardLedStatus(first)
|
||||
client.enqueueKeyboardLedStatus(latest)
|
||||
|
||||
status, ok := client.takeKeyboardLedStatus()
|
||||
if !ok {
|
||||
t.Fatal("queued LED status was not available")
|
||||
}
|
||||
if status != latest {
|
||||
t.Fatalf("queued status = %#v, want latest %#v", status, latest)
|
||||
}
|
||||
if _, ok := client.takeKeyboardLedStatus(); ok {
|
||||
t.Fatal("queue retained more than one LED status")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyboardLedStatusQueueDoesNotBlockWhenWorkerIsBusy(t *testing.T) {
|
||||
client := &Client{
|
||||
keyboardLedNotify: make(chan struct{}, 1),
|
||||
keyboardLedDone: make(chan struct{}),
|
||||
}
|
||||
// A pending notification models a worker currently busy writing the prior
|
||||
// status to a slow websocket client.
|
||||
client.keyboardLedNotify <- struct{}{}
|
||||
|
||||
finished := make(chan struct{})
|
||||
go func() {
|
||||
client.enqueueKeyboardLedStatus(hid.KeyboardLedStatus{CapsLock: true})
|
||||
close(finished)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-finished:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("enqueue blocked while the keyboard LED worker was busy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyboardLedStatusWorkerStopsOnClientClose(t *testing.T) {
|
||||
client := &Client{
|
||||
keyboardLedNotify: make(chan struct{}, 1),
|
||||
keyboardLedDone: make(chan struct{}),
|
||||
}
|
||||
client.startKeyboardLedStatusWorker()
|
||||
client.stopKeyboardLedStatusWorker()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
client.workers.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("keyboard LED worker did not stop after client close")
|
||||
}
|
||||
|
||||
client.enqueueKeyboardLedStatus(hid.KeyboardLedStatus{ScrollLock: true})
|
||||
if _, ok := client.takeKeyboardLedStatus(); ok {
|
||||
t.Fatal("closed client accepted a keyboard LED status")
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ func (s *Service) Connect(c *gin.Context) {
|
||||
|
||||
sendCaptureStatusSnapshot(client)
|
||||
sendH264ModeStatusSnapshot(client)
|
||||
sendKeyboardLedStatusSnapshot(client)
|
||||
|
||||
client.Start()
|
||||
}
|
||||
|
||||
@@ -16,16 +16,23 @@ type Manager struct {
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ws *websocket.Conn
|
||||
hid *hid.Hid
|
||||
manual *inputcontrol.ManualSession
|
||||
keyboard chan hid.QueuedReport
|
||||
mouse chan hid.QueuedReport
|
||||
heartbeatTimeout time.Duration
|
||||
lastHeartbeat time.Time
|
||||
mutex sync.Mutex
|
||||
closeOnce sync.Once
|
||||
workers sync.WaitGroup
|
||||
ws *websocket.Conn
|
||||
hid *hid.Hid
|
||||
manual *inputcontrol.ManualSession
|
||||
keyboard chan hid.QueuedReport
|
||||
mouse chan hid.QueuedReport
|
||||
heartbeatTimeout time.Duration
|
||||
lastHeartbeat time.Time
|
||||
mutex sync.Mutex
|
||||
keyboardLedMutex sync.Mutex
|
||||
keyboardLedStatus *hid.KeyboardLedStatus
|
||||
keyboardLedNotify chan struct{}
|
||||
keyboardLedDone chan struct{}
|
||||
keyboardLedClosed bool
|
||||
keyboardLedOnce sync.Once
|
||||
keyboardLedWorkers sync.WaitGroup
|
||||
closeOnce sync.Once
|
||||
workers sync.WaitGroup
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
|
||||
Reference in New Issue
Block a user