Files
NanoKVM-MIRROR/server/service/vm/jiggler/jiggler.go
SiYue-ZO 64ce12cc0d feat(mcp): add authenticated remote control backend
Add an MCP Streamable HTTP server with token authentication, screenshot capture, keyboard, and mouse tools.

Introduce shared control-mode and input-control coordination so MCP, PicoClaw, and local HID paths serialize ownership safely.

Integrate PicoClaw gateway/runtime control handoff with PID-managed startup and focused unit coverage.
2026-07-29 15:16:06 +08:00

170 lines
2.7 KiB
Go

package jiggler
import (
"context"
"os"
"strings"
"sync"
"time"
"NanoKVM-Server/service/controlmode"
"NanoKVM-Server/service/hid"
"NanoKVM-Server/service/inputcontrol"
)
const (
ConfigFile = "/etc/kvm/mouse-jiggler"
Interval = 15 * time.Second
)
var (
jiggler Jiggler
once sync.Once
)
type Jiggler struct {
mutex sync.Mutex
enabled bool
running bool
mode string
lastUpdated time.Time
}
func GetJiggler() *Jiggler {
once.Do(func() {
jiggler = Jiggler{
mutex: sync.Mutex{},
enabled: false,
running: false,
mode: "relative",
lastUpdated: time.Now(),
}
content, err := os.ReadFile(ConfigFile)
if err != nil {
return
}
mode := strings.ReplaceAll(string(content), "\n", "")
if mode != "" {
jiggler.mode = mode
}
jiggler.enabled = true
})
return &jiggler
}
func (j *Jiggler) Enable(mode string) error {
err := os.WriteFile(ConfigFile, []byte(mode), 0644)
if err != nil {
return err
}
j.enabled = true
j.mode = mode
j.Run()
return nil
}
func (j *Jiggler) Disable() error {
if err := os.Remove(ConfigFile); err != nil {
return err
}
j.enabled = false
j.mode = "relative"
return nil
}
func (j *Jiggler) Run() {
if !j.enabled || j.running {
return
}
j.mutex.Lock()
j.running = true
j.mutex.Unlock()
j.Update()
go func() {
ticker := time.NewTicker(Interval)
defer ticker.Stop()
for range ticker.C {
if !j.enabled {
j.running = false
return
}
if time.Since(j.lastUpdated) > Interval {
move(j.mode)
j.Update()
}
}
}()
}
func (j *Jiggler) Update() {
if j.running {
j.lastUpdated = time.Now()
}
}
func (j *Jiggler) IsEnabled() bool {
return j.enabled
}
func (j *Jiggler) GetMode() string {
return j.mode
}
func move(mode string) {
_, releaseMode, err := controlmode.GetManager().AcquireStable()
if err != nil {
return
}
defer releaseMode()
ctx, release, err := inputcontrol.GetCoordinator().BeginBackground(context.Background())
if err != nil {
return
}
defer release()
h := hid.GetHid()
if mode == "absolute" {
if err := h.WriteAbsoluteMouseReport([]byte{0x00, 0x00, 0x3f, 0x00, 0x3f, 0x00}); err != nil {
return
}
defer func() {
_ = h.WriteAbsoluteMouseReport([]byte{0x00, 0xff, 0x3f, 0xff, 0x3f, 0x00})
}()
_ = waitMove(ctx, 100*time.Millisecond)
} else {
if err := h.WriteRelativeMouseReport([]byte{0x00, 0xa, 0xa, 0x00}); err != nil {
return
}
defer func() {
_ = h.WriteRelativeMouseReport([]byte{0x00, 0xf6, 0xf6, 0x00})
}()
_ = waitMove(ctx, 100*time.Millisecond)
}
}
func waitMove(ctx context.Context, delay time.Duration) bool {
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return false
case <-timer.C:
return true
}
}