mirror of https://github.com/jetkvm/kvm.git
Compare commits
17 Commits
6c84a315a3
...
61ee27e931
| Author | SHA1 | Date |
|---|---|---|
|
|
61ee27e931 | |
|
|
19ff472f8b | |
|
|
b144d9926f | |
|
|
e755a6e1b1 | |
|
|
99a8c2711c | |
|
|
317218a682 | |
|
|
62b8dee170 | |
|
|
bdd6f4247b | |
|
|
63aa940f42 | |
|
|
043ef9ddfc | |
|
|
437f0b854a | |
|
|
a45d55123c | |
|
|
213e750e04 | |
|
|
6dcb0286e3 | |
|
|
74ccca0b1a | |
|
|
0ad435475b | |
|
|
23bf3978fa |
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "JetKVM",
|
||||
"name": "JetKVM docker devcontainer",
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
|
|
@ -33,3 +33,4 @@ wget https://github.com/jetkvm/rv1106-system/releases/download/${BUILDKIT_VERSIO
|
|||
sudo tar --use-compress-program="unzstd --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \
|
||||
rm buildkit.tar.zst
|
||||
popd
|
||||
rm -rf "${BUILDKIT_TMPDIR}"
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "JetKVM podman devcontainer",
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
// Should match what is defined in ui/package.json
|
||||
"version": "22.19.0"
|
||||
}
|
||||
},
|
||||
"runArgs": [
|
||||
"--userns=keep-id",
|
||||
"--security-opt=label=disable",
|
||||
"--security-opt=label=nested"
|
||||
],
|
||||
"containerUser": "vscode",
|
||||
"containerEnv": {
|
||||
"HOME": "/home/vscode"
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ jobs:
|
|||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v6.0.0
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.25.1"
|
||||
- name: Build frontend
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: oldstable
|
||||
- name: Create empty resource directory
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ jobs:
|
|||
EOF
|
||||
ssh jkci "cat /tmp/device-tests.json" > device-tests.json
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v5.5.0
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24.4"
|
||||
- name: Golang Test Report
|
||||
|
|
|
|||
|
|
@ -97,21 +97,38 @@ tail -f /var/log/jetkvm.log
|
|||
|
||||
```
|
||||
/kvm/
|
||||
├── main.go # App entry point
|
||||
├── config.go # Settings & configuration
|
||||
├── web.go # API endpoints
|
||||
├── ui/ # React frontend
|
||||
│ ├── src/routes/ # Pages (login, settings, etc.)
|
||||
│ └── src/components/ # UI components
|
||||
├── internal/ # Internal Go packages
|
||||
│ ├── native/ # CGO / Native code glue layer
|
||||
│ ├── native/cgo/ # C files for the native library (HDMI, Touchscreen, etc.)
|
||||
│ ├── native/eez/ # EEZ Studio Project files (for Touchscreen)
|
||||
│ ├── hidrpc/ # HIDRPC implementation for HID devices (keyboard, mouse, etc.)
|
||||
│ ├── logging/ # Logging implementation
|
||||
│ ├── usbgadget/ # USB gadget
|
||||
│ └── websecurity/ # TLS certificate management
|
||||
└── resource # netboot iso and other resources
|
||||
├── main.go # App entry point
|
||||
├── config.go # Settings & configuration
|
||||
├── display.go # Device UI control
|
||||
├── web.go # API endpoints
|
||||
├── cmd/ # Command line main
|
||||
├── internal/ # Internal Go packages
|
||||
│ ├── confparser/ # Configuration file implementation
|
||||
│ ├── hidrpc/ # HIDRPC implementation for HID devices (keyboard, mouse, etc.)
|
||||
│ ├── logging/ # Logging implementation
|
||||
│ ├── mdns/ # mDNS implementation
|
||||
│ ├── native/ # CGO / Native code glue layer (on-device hardware)
|
||||
│ │ ├── cgo/ # C files for the native library (HDMI, Touchscreen, etc.)
|
||||
│ │ └── eez/ # EEZ Studio Project files (for Touchscreen)
|
||||
│ ├── network/ # Network implementation
|
||||
│ ├── timesync/ # Time sync/NTP implementation
|
||||
│ ├── tzdata/ # Timezone data and generation
|
||||
│ ├── udhcpc/ # DHCP implementation
|
||||
│ ├── usbgadget/ # USB gadget
|
||||
│ ├── utils/ # SSH handling
|
||||
│ └── websecure/ # TLS certificate management
|
||||
├── resource/ # netboot iso and other resources
|
||||
├── scripts/ # Bash shell scripts for building and deploying
|
||||
└── static/ # (react client build output)
|
||||
└── ui/ # React frontend
|
||||
├── public/ # UI website static images and fonts
|
||||
└── src/ # Client React UI
|
||||
├── assets/ # UI in-page images
|
||||
├── components/ # UI components
|
||||
├── hooks/ # Hooks (stores, RPC handling, virtual devices)
|
||||
├── keyboardLayouts/ # Keyboard layout definitions
|
||||
├── providers/ # Feature flags
|
||||
└── routes/ # Pages (login, settings, etc.)
|
||||
```
|
||||
|
||||
**Key files for beginners:**
|
||||
|
|
@ -252,6 +269,47 @@ rm -rf node_modules
|
|||
npm install
|
||||
```
|
||||
|
||||
### "Device UI Fails to Build"
|
||||
|
||||
If while trying to build you run into an error message similar to :
|
||||
```plaintext
|
||||
In file included from /workspaces/kvm/internal/native/cgo/ctrl.c:15:
|
||||
/workspaces/kvm/internal/native/cgo/ui_index.h:4:10: fatal error: ui/ui.h: No such file or directory
|
||||
#include "ui/ui.h"
|
||||
^~~~~~~~~
|
||||
compilation terminated.
|
||||
```
|
||||
This means that your system didn't create the directory-link to from _./internal/native/cgo/ui_ to ./internal/native/eez/src/ui when the repository was checked out. You can verify this is the case if _./internal/native/cgo/ui_ appears as a plain text file with only the textual contents:
|
||||
```plaintext
|
||||
../eez/src/ui
|
||||
```
|
||||
|
||||
If this happens to you need to [enable git creation of symbolic links](https://stackoverflow.com/a/59761201/2076) either globally or for the KVM repository:
|
||||
```bash
|
||||
# Globally enable git to create symlinks
|
||||
git config --global core.symlinks true
|
||||
git restore internal/native/cgo/ui
|
||||
```
|
||||
```bash
|
||||
# Enable git to create symlinks only in this project
|
||||
git config core.symlinks true
|
||||
git restore internal/native/cgo/ui
|
||||
```
|
||||
|
||||
Or if you want to manually create the symlink use:
|
||||
```bash
|
||||
# linux
|
||||
cd internal/native/cgo
|
||||
rm ui
|
||||
ln -s ../eez/src/ui ui
|
||||
```
|
||||
```dos
|
||||
rem Windows
|
||||
cd internal/native/cgo
|
||||
del ui
|
||||
mklink /d ui ..\eez\src\ui
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
|
|
|||
2
cloud.go
2
cloud.go
|
|
@ -478,7 +478,7 @@ func handleSessionRequest(
|
|||
cloudLogger.Trace().Interface("session", session).Msg("new session accepted")
|
||||
|
||||
// Cancel any ongoing keyboard macro when session changes
|
||||
cancelKeyboardMacro()
|
||||
cancelAllRunningKeyboardMacros()
|
||||
|
||||
currentSession = session
|
||||
_ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd})
|
||||
|
|
|
|||
39
go.mod
39
go.mod
|
|
@ -5,13 +5,14 @@ go 1.24.4
|
|||
require (
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/beevik/ntp v1.4.3
|
||||
github.com/coder/websocket v1.8.13
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/coreos/go-oidc/v3 v3.15.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/gin-contrib/logger v1.2.6
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-co-op/gocron/v2 v2.16.5
|
||||
github.com/go-co-op/gocron/v2 v2.16.6
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/guregu/null/v6 v6.0.0
|
||||
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f
|
||||
|
|
@ -19,8 +20,8 @@ require (
|
|||
github.com/pion/mdns/v2 v2.0.7
|
||||
github.com/pion/webrtc/v4 v4.1.4
|
||||
github.com/pojntfx/go-nbd v0.3.2
|
||||
github.com/prometheus/client_golang v1.23.0
|
||||
github.com/prometheus/common v0.66.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/common v0.66.1
|
||||
github.com/prometheus/procfs v0.17.0
|
||||
github.com/psanford/httpreadat v0.1.0
|
||||
github.com/rs/xid v1.6.0
|
||||
|
|
@ -30,37 +31,31 @@ require (
|
|||
github.com/vearutop/statigz v1.5.0
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
go.bug.st/serial v1.6.4
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/sys v0.35.0
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/net v0.44.0
|
||||
golang.org/x/sys v0.36.0
|
||||
)
|
||||
|
||||
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
||||
|
||||
require (
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/creack/goselect v0.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.4 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/jpillora/overseer v1.1.6 // indirect
|
||||
github.com/jpillora/s3 v1.1.4 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
|
|
@ -90,10 +85,10 @@ require (
|
|||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/arch v0.18.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
86
go.sum
86
go.sum
|
|
@ -1,7 +1,5 @@
|
|||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
|
||||
|
|
@ -10,20 +8,18 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
|||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bool64/dev v0.2.39 h1:kP8DnMGlWXhGYJEZE/J0l/gVBdbuhoPGL+MJG4QbofE=
|
||||
github.com/bool64/dev v0.2.39/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs=
|
||||
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b/go.mod h1:SehHnbi2e8NiSAKby42Itm8SIoS7b+wAprsfPH3qgYk=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
|
||||
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
|
|
@ -46,20 +42,18 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
|||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-co-op/gocron/v2 v2.16.5 h1:j228Jxk7bb9CF8LKR3gS+bK3rcjRUINjlVI+ZMp26Ss=
|
||||
github.com/go-co-op/gocron/v2 v2.16.5/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc=
|
||||
github.com/go-co-op/gocron/v2 v2.16.6 h1:zI2Ya9sqvuLcgqJgV79LwoJXM8h20Z/drtB7ATbpRWo=
|
||||
github.com/go-co-op/gocron/v2 v2.16.6/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
|
|
@ -68,26 +62,18 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
|||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
|
||||
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
|
||||
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f h1:08t2PbrkDgW2+mwCQ3jhKUBrCM9Bc9SeH5j2Dst3B+0=
|
||||
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/jpillora/overseer v1.1.6 h1:3ygYfNcR3FfOr22miu3vR1iQcXKMHbmULBh98rbkIyo=
|
||||
github.com/jpillora/overseer v1.1.6/go.mod h1:aPXQtxuVb9PVWRWTXpo+LdnC/YXQ0IBLNXqKMJmgk88=
|
||||
github.com/jpillora/s3 v1.1.4 h1:YCCKDWzb/Ye9EBNd83ATRF/8wPEy0xd43Rezb6u6fzc=
|
||||
github.com/jpillora/s3 v1.1.4/go.mod h1:yedE603V+crlFi1Kl/5vZJaBu9pUzE9wvKegU/lF2zs=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
|
|
@ -152,12 +138,12 @@ github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7d
|
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsMeVY=
|
||||
github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
||||
|
|
@ -170,15 +156,12 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
|||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/gunit v1.1.3/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f h1:VgoRCP1efSCEZIcF2THLQ46+pIBzzgNiaUBe9wEDwYU=
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
|
|
@ -200,12 +183,14 @@ go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
|||
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
@ -213,20 +198,17 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
|
|
|||
42
hidrpc.go
42
hidrpc.go
|
|
@ -26,20 +26,45 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
|
|||
return
|
||||
}
|
||||
session.hidRPCAvailable = true
|
||||
|
||||
case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport:
|
||||
rpcErr = handleHidRPCKeyboardInput(message)
|
||||
|
||||
case hidrpc.TypeKeyboardMacroReport:
|
||||
keyboardMacroReport, err := message.KeyboardMacroReport()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to get keyboard macro report")
|
||||
return
|
||||
}
|
||||
rpcErr = rpcExecuteKeyboardMacro(keyboardMacroReport.Steps)
|
||||
token := rpcExecuteKeyboardMacro(keyboardMacroReport.IsPaste, keyboardMacroReport.Steps)
|
||||
logger.Debug().Str("token", token.String()).Msg("started keyboard macro")
|
||||
message, err := hidrpc.NewKeyboardMacroTokenMessage(token).Marshal()
|
||||
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to marshal running macro token message")
|
||||
return
|
||||
}
|
||||
if err := session.HidChannel.Send(message); err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to send running macro token message")
|
||||
return
|
||||
}
|
||||
|
||||
case hidrpc.TypeCancelKeyboardMacroReport:
|
||||
rpcCancelKeyboardMacro()
|
||||
return
|
||||
|
||||
case hidrpc.TypeKeyboardMacroTokenState:
|
||||
tokenState, err := message.KeyboardMacroTokenState()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to get keyboard macro token")
|
||||
return
|
||||
}
|
||||
rpcCancelKeyboardMacroByToken(tokenState.Token)
|
||||
return
|
||||
|
||||
case hidrpc.TypeKeypressKeepAliveReport:
|
||||
rpcErr = handleHidRPCKeypressKeepAlive(session)
|
||||
|
||||
case hidrpc.TypePointerReport:
|
||||
pointerReport, err := message.PointerReport()
|
||||
if err != nil {
|
||||
|
|
@ -47,6 +72,7 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
|
|||
return
|
||||
}
|
||||
rpcErr = rpcAbsMouseReport(pointerReport.X, pointerReport.Y, pointerReport.Button)
|
||||
|
||||
case hidrpc.TypeMouseReport:
|
||||
mouseReport, err := message.MouseReport()
|
||||
if err != nil {
|
||||
|
|
@ -54,6 +80,7 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
|
|||
return
|
||||
}
|
||||
rpcErr = rpcRelMouseReport(mouseReport.DX, mouseReport.DY, mouseReport.Button)
|
||||
|
||||
default:
|
||||
logger.Warn().Uint8("type", uint8(message.Type())).Msg("unknown HID RPC message type")
|
||||
}
|
||||
|
|
@ -65,15 +92,18 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
|
|||
|
||||
func onHidMessage(msg hidQueueMessage, session *Session) {
|
||||
data := msg.Data
|
||||
dataLen := len(data)
|
||||
|
||||
scopedLogger := hidRPCLogger.With().
|
||||
Str("channel", msg.channel).
|
||||
Bytes("data", data).
|
||||
Dur("timelimit", msg.timelimit).
|
||||
Int("data_len", dataLen).
|
||||
Bytes("data", data[:min(dataLen, 32)]).
|
||||
Logger()
|
||||
scopedLogger.Debug().Msg("HID RPC message received")
|
||||
|
||||
if len(data) < 1 {
|
||||
scopedLogger.Warn().Int("length", len(data)).Msg("received empty data in HID RPC message handler")
|
||||
if dataLen < 1 {
|
||||
scopedLogger.Warn().Msg("received empty data in HID RPC message handler")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +126,7 @@ func onHidMessage(msg hidQueueMessage, session *Session) {
|
|||
r <- nil
|
||||
}()
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
case <-time.After(msg.timelimit * time.Second):
|
||||
scopedLogger.Warn().Msg("HID RPC message timed out")
|
||||
case <-r:
|
||||
scopedLogger.Debug().Dur("duration", time.Since(t)).Msg("HID RPC message handled")
|
||||
|
|
@ -212,6 +242,8 @@ func reportHidRPC(params any, session *Session) {
|
|||
message, err = hidrpc.NewKeydownStateMessage(params).Marshal()
|
||||
case hidrpc.KeyboardMacroState:
|
||||
message, err = hidrpc.NewKeyboardMacroStateMessage(params.State, params.IsPaste).Marshal()
|
||||
case hidrpc.KeyboardMacroTokenState:
|
||||
message, err = hidrpc.NewKeyboardMacroTokenMessage(params.Token).Marshal()
|
||||
default:
|
||||
err = fmt.Errorf("unknown HID RPC message type: %T", params)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package hidrpc
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||
)
|
||||
|
||||
|
|
@ -22,26 +24,34 @@ const (
|
|||
TypeKeyboardLedState MessageType = 0x32
|
||||
TypeKeydownState MessageType = 0x33
|
||||
TypeKeyboardMacroState MessageType = 0x34
|
||||
TypeKeyboardMacroTokenState MessageType = 0x35
|
||||
)
|
||||
|
||||
type QueueIndex int
|
||||
|
||||
const (
|
||||
Version byte = 0x01 // Version of the HID RPC protocol
|
||||
Version byte = 0x01 // Version of the HID RPC protocol
|
||||
HandshakeQueue int = 0 // Queue index for handshake messages
|
||||
KeyboardQueue int = 1 // Queue index for keyboard messages
|
||||
MouseQueue int = 2 // Queue index for mouse messages
|
||||
MacroQueue int = 3 // Queue index for macro messages
|
||||
OtherQueue int = 4 // Queue index for other messages
|
||||
)
|
||||
|
||||
// GetQueueIndex returns the index of the queue to which the message should be enqueued.
|
||||
func GetQueueIndex(messageType MessageType) int {
|
||||
func GetQueueIndex(messageType MessageType) (int, time.Duration) {
|
||||
switch messageType {
|
||||
case TypeHandshake:
|
||||
return 0
|
||||
case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardMacroReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroState:
|
||||
return 1
|
||||
return HandshakeQueue, 1
|
||||
case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroState:
|
||||
return KeyboardQueue, 1
|
||||
case TypePointerReport, TypeMouseReport, TypeWheelReport:
|
||||
return 2
|
||||
// we don't want to block the queue for this message
|
||||
case TypeCancelKeyboardMacroReport:
|
||||
return 3
|
||||
return MouseQueue, 1
|
||||
// we don't want to block the queue for these messages
|
||||
case TypeKeyboardMacroReport, TypeCancelKeyboardMacroReport, TypeKeyboardMacroTokenState:
|
||||
return MacroQueue, 60 // 1 minute timeout
|
||||
default:
|
||||
return 3
|
||||
return OtherQueue, 5
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,3 +131,13 @@ func NewKeyboardMacroStateMessage(state bool, isPaste bool) *Message {
|
|||
d: data,
|
||||
}
|
||||
}
|
||||
|
||||
// NewKeyboardMacroTokenMessage creates a new keyboard macro token message.
|
||||
func NewKeyboardMacroTokenMessage(token uuid.UUID) *Message {
|
||||
data, _ := token.MarshalBinary()
|
||||
|
||||
return &Message{
|
||||
t: TypeKeyboardMacroTokenState,
|
||||
d: data,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package hidrpc
|
|||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Message ..
|
||||
|
|
@ -23,6 +25,9 @@ func (m *Message) Type() MessageType {
|
|||
func (m *Message) String() string {
|
||||
switch m.t {
|
||||
case TypeHandshake:
|
||||
if len(m.d) != 0 {
|
||||
return fmt.Sprintf("Handshake{Malformed: %v}", m.d)
|
||||
}
|
||||
return "Handshake"
|
||||
case TypeKeypressReport:
|
||||
if len(m.d) < 2 {
|
||||
|
|
@ -45,12 +50,45 @@ func (m *Message) String() string {
|
|||
}
|
||||
return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0], m.d[1], m.d[2])
|
||||
case TypeKeypressKeepAliveReport:
|
||||
if len(m.d) != 0 {
|
||||
return fmt.Sprintf("KeypressKeepAliveReport{Malformed: %v}", m.d)
|
||||
}
|
||||
return "KeypressKeepAliveReport"
|
||||
case TypeWheelReport:
|
||||
if len(m.d) < 3 {
|
||||
return fmt.Sprintf("WheelReport{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("WheelReport{Vertical: %d, Horizontal: %d}", int8(m.d[0]), int8(m.d[1]))
|
||||
case TypeKeyboardMacroReport:
|
||||
if len(m.d) < 5 {
|
||||
return fmt.Sprintf("KeyboardMacroReport{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("KeyboardMacroReport{IsPaste: %v, Length: %d}", m.d[0] == uint8(1), binary.BigEndian.Uint32(m.d[1:5]))
|
||||
case TypeCancelKeyboardMacroReport:
|
||||
if len(m.d) != 0 {
|
||||
return fmt.Sprintf("CancelKeyboardMacroReport{Malformed: %v}", m.d)
|
||||
}
|
||||
return "CancelKeyboardMacroReport"
|
||||
case TypeKeyboardMacroTokenState:
|
||||
if len(m.d) != 16 {
|
||||
return fmt.Sprintf("KeyboardMacroTokenState{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("KeyboardMacroTokenState{Token: %s}", uuid.Must(uuid.FromBytes(m.d)).String())
|
||||
case TypeKeyboardLedState:
|
||||
if len(m.d) < 1 {
|
||||
return fmt.Sprintf("KeyboardLedState{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("KeyboardLedState{State: %d}", m.d[0])
|
||||
case TypeKeydownState:
|
||||
if len(m.d) < 1 {
|
||||
return fmt.Sprintf("KeydownState{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("KeydownState{State: %d}", m.d[0])
|
||||
case TypeKeyboardMacroState:
|
||||
if len(m.d) < 2 {
|
||||
return fmt.Sprintf("KeyboardMacroState{Malformed: %v}", m.d)
|
||||
}
|
||||
return fmt.Sprintf("KeyboardMacroState{State: %v, IsPaste: %v}", m.d[0] == uint8(1), m.d[1] == uint8(1))
|
||||
default:
|
||||
return fmt.Sprintf("Unknown{Type: %d, Data: %v}", m.t, m.d)
|
||||
}
|
||||
|
|
@ -67,7 +105,9 @@ func (m *Message) KeypressReport() (KeypressReport, error) {
|
|||
if m.t != TypeKeypressReport {
|
||||
return KeypressReport{}, fmt.Errorf("invalid message type: %d", m.t)
|
||||
}
|
||||
|
||||
if len(m.d) < 2 {
|
||||
return KeypressReport{}, fmt.Errorf("invalid message data length: %d", len(m.d))
|
||||
}
|
||||
return KeypressReport{
|
||||
Key: m.d[0],
|
||||
Press: m.d[1] == uint8(1),
|
||||
|
|
@ -95,7 +135,7 @@ func (m *Message) KeyboardReport() (KeyboardReport, error) {
|
|||
// Macro ..
|
||||
type KeyboardMacroStep struct {
|
||||
Modifier byte // 1 byte
|
||||
Keys []byte // 6 bytes: hidKeyBufferSize
|
||||
Keys []byte // 6 bytes: HidKeyBufferSize
|
||||
Delay uint16 // 2 bytes
|
||||
}
|
||||
type KeyboardMacroReport struct {
|
||||
|
|
@ -105,7 +145,7 @@ type KeyboardMacroReport struct {
|
|||
}
|
||||
|
||||
// HidKeyBufferSize is the size of the keys buffer in the keyboard report.
|
||||
const HidKeyBufferSize = 6
|
||||
const HidKeyBufferSize int = 6
|
||||
|
||||
// KeyboardMacroReport returns the keyboard macro report from the message.
|
||||
func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) {
|
||||
|
|
@ -205,3 +245,29 @@ func (m *Message) KeyboardMacroState() (KeyboardMacroState, error) {
|
|||
IsPaste: m.d[1] == uint8(1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type KeyboardMacroTokenState struct {
|
||||
Token uuid.UUID
|
||||
}
|
||||
|
||||
// KeyboardMacroTokenState returns the keyboard macro token UUID from the message.
|
||||
func (m *Message) KeyboardMacroTokenState() (KeyboardMacroTokenState, error) {
|
||||
if m.t != TypeKeyboardMacroTokenState {
|
||||
return KeyboardMacroTokenState{}, fmt.Errorf("invalid message type: %d", m.t)
|
||||
}
|
||||
|
||||
if len(m.d) == 0 {
|
||||
return KeyboardMacroTokenState{Token: uuid.Nil}, nil
|
||||
}
|
||||
|
||||
if len(m.d) != 16 {
|
||||
return KeyboardMacroTokenState{}, fmt.Errorf("invalid UUID length: %d", len(m.d))
|
||||
}
|
||||
|
||||
token, err := uuid.FromBytes(m.d)
|
||||
if err != nil {
|
||||
return KeyboardMacroTokenState{}, fmt.Errorf("invalid UUID: %v", err)
|
||||
}
|
||||
|
||||
return KeyboardMacroTokenState{Token: token}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,7 +118,6 @@ func uiInit(rotation uint16) {
|
|||
defer cgoLock.Unlock()
|
||||
|
||||
cRotation := C.u_int16_t(rotation)
|
||||
defer C.free(unsafe.Pointer(&cRotation))
|
||||
|
||||
C.jetkvm_ui_init(cRotation)
|
||||
}
|
||||
|
|
@ -350,7 +349,6 @@ func uiDispSetRotation(rotation uint16) (bool, error) {
|
|||
nativeLogger.Info().Uint16("rotation", rotation).Msg("setting rotation")
|
||||
|
||||
cRotation := C.u_int16_t(rotation)
|
||||
defer C.free(unsafe.Pointer(&cRotation))
|
||||
|
||||
C.jetkvm_ui_set_rotation(cRotation)
|
||||
return true, nil
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ var keyboardReportDesc = []byte{
|
|||
0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
|
||||
0x09, 0x06, /* USAGE (Keyboard) */
|
||||
0xa1, 0x01, /* COLLECTION (Application) */
|
||||
|
||||
/* 8 modifier bits */
|
||||
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
|
||||
0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */
|
||||
0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
|
||||
|
|
@ -39,27 +41,47 @@ var keyboardReportDesc = []byte{
|
|||
0x75, 0x01, /* REPORT_SIZE (1) */
|
||||
0x95, 0x08, /* REPORT_COUNT (8) */
|
||||
0x81, 0x02, /* INPUT (Data,Var,Abs) */
|
||||
|
||||
/* 8 bits of padding */
|
||||
0x95, 0x01, /* REPORT_COUNT (1) */
|
||||
0x75, 0x08, /* REPORT_SIZE (8) */
|
||||
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
|
||||
|
||||
/* 6 key codes for the 104 key keyboard */
|
||||
0x95, 0x06, /* REPORT_COUNT (6) */
|
||||
0x75, 0x08, /* REPORT_SIZE (8) */
|
||||
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
|
||||
0x25, 0xE7, /* LOGICAL_MAXIMUM (104-key HID) */
|
||||
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
|
||||
0x19, 0x00, /* USAGE_MINIMUM (Reserved) */
|
||||
0x29, 0xE7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
|
||||
0x81, 0x00, /* INPUT (Data,Ary,Abs) */
|
||||
|
||||
/* LED report 5 bits for Num Lock through Kana */
|
||||
0x95, 0x05, /* REPORT_COUNT (5) */
|
||||
0x75, 0x01, /* REPORT_SIZE (1) */
|
||||
|
||||
0x05, 0x08, /* USAGE_PAGE (LEDs) */
|
||||
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
|
||||
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
|
||||
0x91, 0x02, /* OUTPUT (Data,Var,Abs) */
|
||||
|
||||
/* 1 bit of padding for the Power LED (ignored) */
|
||||
0x95, 0x01, /* REPORT_COUNT (1) */
|
||||
0x75, 0x03, /* REPORT_SIZE (3) */
|
||||
0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
|
||||
|
||||
/* LED report 1 bit for Shift */
|
||||
0x95, 0x01, /* REPORT_COUNT (1) */
|
||||
0x75, 0x01, /* REPORT_SIZE (1) */
|
||||
0x05, 0x08, /* USAGE_PAGE (LEDs) */
|
||||
0x19, 0x07, /* USAGE_MINIMUM (Shift) */
|
||||
0x29, 0x07, /* USAGE_MAXIMUM (Shift) */
|
||||
0x91, 0x02, /* OUTPUT (Data,Var,Abs) */
|
||||
|
||||
/* 1 bit of padding for the rest of the byte */
|
||||
0x95, 0x01, /* REPORT_COUNT (1) */
|
||||
0x75, 0x03, /* REPORT_SIZE (3) */
|
||||
0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
|
||||
0x95, 0x06, /* REPORT_COUNT (6) */
|
||||
0x75, 0x08, /* REPORT_SIZE (8) */
|
||||
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
|
||||
0x25, 0x65, /* LOGICAL_MAXIMUM (101) */
|
||||
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
|
||||
0x19, 0x00, /* USAGE_MINIMUM (Reserved) */
|
||||
0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */
|
||||
0x81, 0x00, /* INPUT (Data,Ary,Abs) */
|
||||
0xc0, /* END_COLLECTION */
|
||||
}
|
||||
|
||||
|
|
@ -153,6 +175,16 @@ func (u *UsbGadget) SetOnKeysDownChange(f func(state KeysDownState)) {
|
|||
u.onKeysDownChange = &f
|
||||
}
|
||||
|
||||
var suspendedKeyDownMessages bool = false
|
||||
|
||||
func (u *UsbGadget) SuspendKeyDownMessages() {
|
||||
suspendedKeyDownMessages = true
|
||||
}
|
||||
|
||||
func (u *UsbGadget) ResumeSuspendKeyDownMessages() {
|
||||
suspendedKeyDownMessages = false
|
||||
}
|
||||
|
||||
func (u *UsbGadget) SetOnKeepAliveReset(f func()) {
|
||||
u.onKeepAliveReset = &f
|
||||
}
|
||||
|
|
@ -169,9 +201,9 @@ func (u *UsbGadget) scheduleAutoRelease(key byte) {
|
|||
}
|
||||
|
||||
// TODO: make this configurable
|
||||
// We currently hardcode the duration to 100ms
|
||||
// We currently hardcode the duration to the default of 100ms
|
||||
// However, it should be the same as the duration of the keep-alive reset called baseExtension.
|
||||
u.kbdAutoReleaseTimers[key] = time.AfterFunc(100*time.Millisecond, func() {
|
||||
u.kbdAutoReleaseTimers[key] = time.AfterFunc(DefaultAutoReleaseDuration, func() {
|
||||
u.performAutoRelease(key)
|
||||
})
|
||||
}
|
||||
|
|
@ -314,6 +346,7 @@ var keyboardWriteHidFileLock sync.Mutex
|
|||
func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error {
|
||||
keyboardWriteHidFileLock.Lock()
|
||||
defer keyboardWriteHidFileLock.Unlock()
|
||||
|
||||
if err := u.openKeyboardHidFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -353,7 +386,7 @@ func (u *UsbGadget) UpdateKeysDown(modifier byte, keys []byte) KeysDownState {
|
|||
u.keysDownState = state
|
||||
u.keyboardStateLock.Unlock()
|
||||
|
||||
if u.onKeysDownChange != nil {
|
||||
if u.onKeysDownChange != nil && !suspendedKeyDownMessages {
|
||||
(*u.onKeysDownChange)(state) // this enques to the outgoing hidrpc queue via usb.go → currentSession.enqueueKeysDownState(...)
|
||||
}
|
||||
return state
|
||||
|
|
@ -484,6 +517,10 @@ func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error)
|
|||
}
|
||||
|
||||
err := u.keyboardWriteHidFile(modifier, keys)
|
||||
if err != nil {
|
||||
u.log.Warn().Uint8("modifier", modifier).Uints8("keys", keys).Msg("Could not write keyboard report to hidg0")
|
||||
}
|
||||
|
||||
return u.UpdateKeysDown(modifier, keys), err
|
||||
}
|
||||
|
||||
|
|
|
|||
153
jsonrpc.go
153
jsonrpc.go
|
|
@ -1,7 +1,6 @@
|
|||
package kvm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
|
@ -14,6 +13,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/rs/zerolog"
|
||||
"go.bug.st/serial"
|
||||
|
|
@ -1084,91 +1084,154 @@ func rpcSetLocalLoopbackOnly(enabled bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type RunningMacro struct {
|
||||
cancel context.CancelFunc
|
||||
isPaste bool
|
||||
}
|
||||
|
||||
var (
|
||||
keyboardMacroCancel context.CancelFunc
|
||||
keyboardMacroLock sync.Mutex
|
||||
keyboardMacroCancelMap map[uuid.UUID]RunningMacro
|
||||
keyboardMacroLock sync.Mutex
|
||||
keyboardMacroOnce sync.Once
|
||||
)
|
||||
|
||||
// cancelKeyboardMacro cancels any ongoing keyboard macro execution
|
||||
func cancelKeyboardMacro() {
|
||||
func getKeyboardMacroCancelMap() map[uuid.UUID]RunningMacro {
|
||||
keyboardMacroOnce.Do(func() {
|
||||
keyboardMacroCancelMap = make(map[uuid.UUID]RunningMacro)
|
||||
})
|
||||
return keyboardMacroCancelMap
|
||||
}
|
||||
|
||||
func addKeyboardMacro(isPaste bool, cancel context.CancelFunc) uuid.UUID {
|
||||
keyboardMacroLock.Lock()
|
||||
defer keyboardMacroLock.Unlock()
|
||||
cancelMap := getKeyboardMacroCancelMap()
|
||||
|
||||
if keyboardMacroCancel != nil {
|
||||
keyboardMacroCancel()
|
||||
logger.Info().Msg("canceled keyboard macro")
|
||||
keyboardMacroCancel = nil
|
||||
token := uuid.New() // Generate a unique token
|
||||
cancelMap[token] = RunningMacro{
|
||||
isPaste: isPaste,
|
||||
cancel: cancel,
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func removeRunningKeyboardMacro(token uuid.UUID) {
|
||||
keyboardMacroLock.Lock()
|
||||
defer keyboardMacroLock.Unlock()
|
||||
cancelMap := getKeyboardMacroCancelMap()
|
||||
|
||||
delete(cancelMap, token)
|
||||
}
|
||||
|
||||
func cancelRunningKeyboardMacro(token uuid.UUID) {
|
||||
keyboardMacroLock.Lock()
|
||||
defer keyboardMacroLock.Unlock()
|
||||
cancelMap := getKeyboardMacroCancelMap()
|
||||
|
||||
if runningMacro, exists := cancelMap[token]; exists {
|
||||
runningMacro.cancel()
|
||||
delete(cancelMap, token)
|
||||
logger.Info().Interface("token", token).Msg("canceled keyboard macro by token")
|
||||
} else {
|
||||
logger.Debug().Interface("token", token).Msg("no running keyboard macro found for token")
|
||||
}
|
||||
}
|
||||
|
||||
func setKeyboardMacroCancel(cancel context.CancelFunc) {
|
||||
func cancelAllRunningKeyboardMacros() {
|
||||
keyboardMacroLock.Lock()
|
||||
defer keyboardMacroLock.Unlock()
|
||||
cancelMap := getKeyboardMacroCancelMap()
|
||||
|
||||
keyboardMacroCancel = cancel
|
||||
for token, runningMacro := range cancelMap {
|
||||
runningMacro.cancel()
|
||||
delete(cancelMap, token)
|
||||
logger.Info().Interface("token", token).Msg("cancelled keyboard macro")
|
||||
}
|
||||
}
|
||||
|
||||
func rpcExecuteKeyboardMacro(macro []hidrpc.KeyboardMacroStep) error {
|
||||
cancelKeyboardMacro()
|
||||
func reportRunningMacrosState() {
|
||||
if currentSession != nil {
|
||||
keyboardMacroLock.Lock()
|
||||
defer keyboardMacroLock.Unlock()
|
||||
cancelMap := getKeyboardMacroCancelMap()
|
||||
|
||||
isPaste := false
|
||||
for _, runningMacro := range cancelMap {
|
||||
if runningMacro.isPaste {
|
||||
isPaste = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
state := hidrpc.KeyboardMacroState{
|
||||
State: len(cancelMap) > 0,
|
||||
IsPaste: isPaste,
|
||||
}
|
||||
|
||||
currentSession.reportHidRPCKeyboardMacroState(state)
|
||||
}
|
||||
}
|
||||
|
||||
func rpcExecuteKeyboardMacro(isPaste bool, macro []hidrpc.KeyboardMacroStep) uuid.UUID {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
setKeyboardMacroCancel(cancel)
|
||||
token := addKeyboardMacro(isPaste, cancel)
|
||||
reportRunningMacrosState()
|
||||
|
||||
s := hidrpc.KeyboardMacroState{
|
||||
State: true,
|
||||
IsPaste: true,
|
||||
}
|
||||
go func() {
|
||||
defer reportRunningMacrosState() // this executes last, so the map is already updated
|
||||
defer removeRunningKeyboardMacro(token) // this executes first, to update the map
|
||||
|
||||
if currentSession != nil {
|
||||
currentSession.reportHidRPCKeyboardMacroState(s)
|
||||
}
|
||||
err := executeKeyboardMacro(ctx, isPaste, macro)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Interface("token", token).Bool("isPaste", isPaste).Msg("keyboard macro execution failed")
|
||||
}
|
||||
}()
|
||||
|
||||
err := rpcDoExecuteKeyboardMacro(ctx, macro)
|
||||
|
||||
setKeyboardMacroCancel(nil)
|
||||
|
||||
s.State = false
|
||||
if currentSession != nil {
|
||||
currentSession.reportHidRPCKeyboardMacroState(s)
|
||||
}
|
||||
|
||||
return err
|
||||
return token
|
||||
}
|
||||
|
||||
func rpcCancelKeyboardMacro() {
|
||||
cancelKeyboardMacro()
|
||||
defer reportRunningMacrosState()
|
||||
cancelAllRunningKeyboardMacros()
|
||||
}
|
||||
|
||||
var keyboardClearStateKeys = make([]byte, hidrpc.HidKeyBufferSize)
|
||||
func rpcCancelKeyboardMacroByToken(token uuid.UUID) {
|
||||
defer reportRunningMacrosState()
|
||||
|
||||
func isClearKeyStep(step hidrpc.KeyboardMacroStep) bool {
|
||||
return step.Modifier == 0 && bytes.Equal(step.Keys, keyboardClearStateKeys)
|
||||
if token == uuid.Nil {
|
||||
cancelAllRunningKeyboardMacros()
|
||||
} else {
|
||||
cancelRunningKeyboardMacro(token)
|
||||
}
|
||||
}
|
||||
|
||||
func rpcDoExecuteKeyboardMacro(ctx context.Context, macro []hidrpc.KeyboardMacroStep) error {
|
||||
logger.Debug().Interface("macro", macro).Msg("Executing keyboard macro")
|
||||
func executeKeyboardMacro(ctx context.Context, isPaste bool, macro []hidrpc.KeyboardMacroStep) error {
|
||||
logger.Debug().
|
||||
Int("macro_steps", len(macro)).
|
||||
Bool("isPaste", isPaste).
|
||||
Msg("Executing keyboard macro")
|
||||
|
||||
// don't report keyboard state changes while executing the macro
|
||||
gadget.SuspendKeyDownMessages()
|
||||
defer gadget.ResumeSuspendKeyDownMessages()
|
||||
|
||||
for i, step := range macro {
|
||||
delay := time.Duration(step.Delay) * time.Millisecond
|
||||
|
||||
err := rpcKeyboardReport(step.Modifier, step.Keys)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to execute keyboard macro")
|
||||
logger.Warn().Err(err).Int("step", i).Msg("failed to execute keyboard macro")
|
||||
return err
|
||||
}
|
||||
|
||||
// notify the device that the keyboard state is being cleared
|
||||
if isClearKeyStep(step) {
|
||||
gadget.UpdateKeysDown(0, keyboardClearStateKeys)
|
||||
}
|
||||
|
||||
// Use context-aware sleep that can be cancelled
|
||||
select {
|
||||
case <-time.After(delay):
|
||||
// Sleep completed normally
|
||||
case <-ctx.Done():
|
||||
// make sure keyboard state is reset
|
||||
err := rpcKeyboardReport(0, keyboardClearStateKeys)
|
||||
// make sure keyboard state is reset and the client gets notified
|
||||
gadget.ResumeSuspendKeyDownMessages()
|
||||
err := rpcKeyboardReport(0, make([]byte, hidrpc.HidKeyBufferSize))
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to reset keyboard state")
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "kvm-ui",
|
||||
"private": true,
|
||||
"version": "2025.09.26.01300",
|
||||
"version": "2025.10.01.1900",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^22.15.0"
|
||||
|
|
@ -42,12 +42,13 @@
|
|||
"react-hot-toast": "^2.6.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router": "^7.9.3",
|
||||
"react-simple-keyboard": "^3.8.122",
|
||||
"react-simple-keyboard": "^3.8.125",
|
||||
"react-use-websocket": "^4.13.0",
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"recharts": "^3.2.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"uuid": "^13.0.0",
|
||||
"validator": "^13.15.15",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
|
|
@ -56,15 +57,15 @@
|
|||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@types/react": "^19.1.14",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/react": "^19.1.17",
|
||||
"@types/react-dom": "^19.1.10",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/validator": "^13.15.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
||||
"@typescript-eslint/parser": "^8.44.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
||||
"@typescript-eslint/parser": "^8.45.0",
|
||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.36.0",
|
||||
|
|
@ -77,8 +78,8 @@
|
|||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"typescript": "^5.9.2",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.7",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,18 +188,18 @@ export default function PasteModal() {
|
|||
type="number"
|
||||
label="Delay between keys"
|
||||
placeholder="Delay between keys"
|
||||
min={50}
|
||||
min={0}
|
||||
max={65534}
|
||||
value={delayValue}
|
||||
onChange={e => {
|
||||
setDelayValue(parseInt(e.target.value, 10));
|
||||
}}
|
||||
/>
|
||||
{delayValue < 50 || delayValue > 65534 && (
|
||||
{(delayValue < defaultDelay || delayValue > 65534) && (
|
||||
<div className="mt-2 flex items-center gap-x-2">
|
||||
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
|
||||
<span className="text-xs text-red-500 dark:text-red-400">
|
||||
Delay must be between 50 and 65534
|
||||
Delay should be between 20 and 65534
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { parse as uuidParse , stringify as uuidStringify } from "uuid";
|
||||
|
||||
import { hidKeyBufferSize, KeyboardLedState, KeysDownState } from "./stores";
|
||||
|
||||
export const HID_RPC_MESSAGE_TYPES = {
|
||||
|
|
@ -13,6 +15,7 @@ export const HID_RPC_MESSAGE_TYPES = {
|
|||
KeyboardLedState: 0x32,
|
||||
KeysDownState: 0x33,
|
||||
KeyboardMacroState: 0x34,
|
||||
CancelKeyboardMacroByTokenReport: 0x35,
|
||||
}
|
||||
|
||||
export type HidRpcMessageType = typeof HID_RPC_MESSAGE_TYPES[keyof typeof HID_RPC_MESSAGE_TYPES];
|
||||
|
|
@ -299,7 +302,7 @@ export class KeyboardMacroStateMessage extends RpcMessage {
|
|||
}
|
||||
|
||||
public static unmarshal(data: Uint8Array): KeyboardMacroStateMessage | undefined {
|
||||
if (data.length < 1) {
|
||||
if (data.length < 2) {
|
||||
throw new Error(`Invalid keyboard macro state report message length: ${data.length}`);
|
||||
}
|
||||
|
||||
|
|
@ -378,13 +381,30 @@ export class PointerReportMessage extends RpcMessage {
|
|||
}
|
||||
|
||||
export class CancelKeyboardMacroReportMessage extends RpcMessage {
|
||||
token: string;
|
||||
|
||||
constructor() {
|
||||
constructor(token: string) {
|
||||
super(HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport);
|
||||
this.token = (token == null || token === undefined || token === "")
|
||||
? "00000000-0000-0000-0000-000000000000"
|
||||
: token;
|
||||
}
|
||||
|
||||
marshal(): Uint8Array {
|
||||
return new Uint8Array([this.messageType]);
|
||||
const tokenBytes = uuidParse(this.token);
|
||||
return new Uint8Array([this.messageType, ...tokenBytes]);
|
||||
}
|
||||
|
||||
public static unmarshal(data: Uint8Array): CancelKeyboardMacroReportMessage | undefined {
|
||||
if (data.length == 0) {
|
||||
return new CancelKeyboardMacroReportMessage("00000000-0000-0000-0000-000000000000");
|
||||
}
|
||||
|
||||
if (data.length != 16) {
|
||||
throw new Error(`Invalid cancel message length: ${data.length}`);
|
||||
}
|
||||
|
||||
return new CancelKeyboardMacroReportMessage(uuidStringify(data.slice(0, 16)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -430,6 +450,7 @@ export const messageRegistry = {
|
|||
[HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport]: CancelKeyboardMacroReportMessage,
|
||||
[HID_RPC_MESSAGE_TYPES.KeyboardMacroState]: KeyboardMacroStateMessage,
|
||||
[HID_RPC_MESSAGE_TYPES.KeypressKeepAliveReport]: KeypressKeepAliveMessage,
|
||||
[HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroByTokenReport]: CancelKeyboardMacroReportMessage,
|
||||
}
|
||||
|
||||
export const unmarshalHidRpcMessage = (data: Uint8Array): RpcMessage | undefined => {
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
|||
|
||||
const cancelOngoingKeyboardMacro = useCallback(
|
||||
() => {
|
||||
sendMessage(new CancelKeyboardMacroReportMessage());
|
||||
sendMessage(new CancelKeyboardMacroReportMessage(""));
|
||||
},
|
||||
[sendMessage],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -277,7 +277,6 @@ export default function useKeyboard() {
|
|||
cancelKeepAlive();
|
||||
}, [cancelKeepAlive]);
|
||||
|
||||
|
||||
// executeMacro is used to execute a macro consisting of multiple steps.
|
||||
// Each step can have multiple keys, multiple modifiers and a delay.
|
||||
// The keys and modifiers are pressed together and held for the delay duration.
|
||||
|
|
@ -292,9 +291,7 @@ export default function useKeyboard() {
|
|||
for (const [_, step] of steps.entries()) {
|
||||
const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean);
|
||||
const modifierMask: number = (step.modifiers || [])
|
||||
|
||||
.map(mod => modifiers[mod])
|
||||
|
||||
.reduce((acc, val) => acc + val, 0);
|
||||
|
||||
// If the step has keys and/or modifiers, press them and hold for the delay
|
||||
|
|
@ -306,6 +303,7 @@ export default function useKeyboard() {
|
|||
|
||||
sendKeyboardMacroEventHidRpc(macro);
|
||||
}, [sendKeyboardMacroEventHidRpc]);
|
||||
|
||||
const executeMacroClientSide = useCallback(async (steps: MacroSteps) => {
|
||||
const promises: (() => Promise<void>)[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -153,13 +153,13 @@ body {
|
|||
|
||||
@property --grid-color-start {
|
||||
syntax: "<color>";
|
||||
initial-value: var(--color-blue-50/10);
|
||||
initial-value: oklch(97% 0.014 254.604 / 10); /* var(--color-blue-50/10) */
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@property --grid-color-end {
|
||||
syntax: "<color>";
|
||||
initial-value: var(--color-blue-50/100);
|
||||
initial-value: oklch(97% 0.014 254.604 / 100); /* var(--color-blue-50/100) */
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
|
|
@ -175,8 +175,8 @@ body {
|
|||
}
|
||||
|
||||
.group:hover .grid-card {
|
||||
--grid-color-start: var(--color-blue-100/50);
|
||||
--grid-color-end: var(--color-blue-50/50);
|
||||
--grid-color-start: oklch(from var(--color-blue-100) l c h / 50);
|
||||
--grid-color-end: oklch(from var(--color-blue-50) l c h / 50);
|
||||
}
|
||||
|
||||
video::-webkit-media-controls {
|
||||
|
|
|
|||
|
|
@ -374,8 +374,8 @@ function UrlView({
|
|||
icon: FedoraIcon,
|
||||
},
|
||||
{
|
||||
name: "openSUSE Leap 15.6",
|
||||
url: "https://download.opensuse.org/distribution/leap/15.6/iso/openSUSE-Leap-15.6-NET-x86_64-Media.iso",
|
||||
name: "openSUSE Leap 16.0",
|
||||
url: "https://download.opensuse.org/distribution/leap/16.0/offline/Leap-16.0-online-installer-x86_64.install.iso",
|
||||
icon: OpenSUSEIcon,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
2
web.go
2
web.go
|
|
@ -230,7 +230,7 @@ func handleWebRTCSession(c *gin.Context) {
|
|||
}
|
||||
|
||||
// Cancel any ongoing keyboard macro when session changes
|
||||
cancelKeyboardMacro()
|
||||
cancelAllRunningKeyboardMacros()
|
||||
|
||||
currentSession = session
|
||||
c.JSON(http.StatusOK, gin.H{"sd": sd})
|
||||
|
|
|
|||
56
webrtc.go
56
webrtc.go
|
|
@ -34,7 +34,7 @@ type Session struct {
|
|||
lastTimerResetTime time.Time // Track when auto-release timer was last reset
|
||||
keepAliveJitterLock sync.Mutex // Protect jitter compensation timing state
|
||||
hidQueueLock sync.Mutex
|
||||
hidQueue []chan hidQueueMessage
|
||||
hidQueues []chan hidQueueMessage
|
||||
|
||||
keysDownStateQueue chan usbgadget.KeysDownState
|
||||
}
|
||||
|
|
@ -48,7 +48,8 @@ func (s *Session) resetKeepAliveTime() {
|
|||
|
||||
type hidQueueMessage struct {
|
||||
webrtc.DataChannelMessage
|
||||
channel string
|
||||
channel string
|
||||
timelimit time.Duration
|
||||
}
|
||||
|
||||
type SessionConfig struct {
|
||||
|
|
@ -93,19 +94,20 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) {
|
|||
return base64.StdEncoding.EncodeToString(localDescription), nil
|
||||
}
|
||||
|
||||
func (s *Session) initQueues() {
|
||||
func (s *Session) initHidQueues() {
|
||||
s.hidQueueLock.Lock()
|
||||
defer s.hidQueueLock.Unlock()
|
||||
|
||||
s.hidQueue = make([]chan hidQueueMessage, 0)
|
||||
for i := 0; i < 4; i++ {
|
||||
q := make(chan hidQueueMessage, 256)
|
||||
s.hidQueue = append(s.hidQueue, q)
|
||||
}
|
||||
s.hidQueues = make([]chan hidQueueMessage, hidrpc.OtherQueue+1)
|
||||
s.hidQueues[hidrpc.HandshakeQueue] = make(chan hidQueueMessage, 2) // we don't really want to queue many handshake messages
|
||||
s.hidQueues[hidrpc.KeyboardQueue] = make(chan hidQueueMessage, 256)
|
||||
s.hidQueues[hidrpc.MouseQueue] = make(chan hidQueueMessage, 256)
|
||||
s.hidQueues[hidrpc.MacroQueue] = make(chan hidQueueMessage, 10) // macros can be long, but we don't want to queue too many
|
||||
s.hidQueues[hidrpc.OtherQueue] = make(chan hidQueueMessage, 256)
|
||||
}
|
||||
|
||||
func (s *Session) handleQueues(index int) {
|
||||
for msg := range s.hidQueue[index] {
|
||||
func (s *Session) handleQueue(queue chan hidQueueMessage) {
|
||||
for msg := range queue {
|
||||
onHidMessage(msg, s)
|
||||
}
|
||||
}
|
||||
|
|
@ -160,17 +162,18 @@ func getOnHidMessageHandler(session *Session, scopedLogger *zerolog.Logger, chan
|
|||
l.Trace().Msg("received data in HID RPC message handler")
|
||||
|
||||
// Enqueue to ensure ordered processing
|
||||
queueIndex := hidrpc.GetQueueIndex(hidrpc.MessageType(msg.Data[0]))
|
||||
if queueIndex >= len(session.hidQueue) || queueIndex < 0 {
|
||||
queueIndex, timelimit := hidrpc.GetQueueIndex(hidrpc.MessageType(msg.Data[0]))
|
||||
if queueIndex >= len(session.hidQueues) || queueIndex < 0 {
|
||||
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue index not found")
|
||||
queueIndex = 3
|
||||
queueIndex = hidrpc.OtherQueue
|
||||
}
|
||||
|
||||
queue := session.hidQueue[queueIndex]
|
||||
queue := session.hidQueues[queueIndex]
|
||||
if queue != nil {
|
||||
queue <- hidQueueMessage{
|
||||
DataChannelMessage: msg,
|
||||
channel: channel,
|
||||
timelimit: timelimit,
|
||||
}
|
||||
} else {
|
||||
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue is nil")
|
||||
|
|
@ -220,7 +223,7 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
|
||||
session := &Session{peerConnection: peerConnection}
|
||||
session.rpcQueue = make(chan webrtc.DataChannelMessage, 256)
|
||||
session.initQueues()
|
||||
session.initHidQueues()
|
||||
session.initKeysDownStateQueue()
|
||||
|
||||
go func() {
|
||||
|
|
@ -230,8 +233,8 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
}
|
||||
}()
|
||||
|
||||
for i := 0; i < len(session.hidQueue); i++ {
|
||||
go session.handleQueues(i)
|
||||
for queue := range session.hidQueues {
|
||||
go session.handleQueue(session.hidQueues[queue])
|
||||
}
|
||||
|
||||
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
|
||||
|
|
@ -256,7 +259,11 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
session.RPCChannel = d
|
||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
// Enqueue to ensure ordered processing
|
||||
session.rpcQueue <- msg
|
||||
if session.rpcQueue != nil {
|
||||
session.rpcQueue <- msg
|
||||
} else {
|
||||
scopedLogger.Warn().Msg("RPC message received but rpcQueue is nil")
|
||||
}
|
||||
})
|
||||
triggerOTAStateUpdate()
|
||||
triggerVideoStateUpdate()
|
||||
|
|
@ -325,22 +332,23 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
_ = peerConnection.Close()
|
||||
}
|
||||
if connectionState == webrtc.ICEConnectionStateClosed {
|
||||
scopedLogger.Debug().Msg("ICE Connection State is closed, unmounting virtual media")
|
||||
scopedLogger.Debug().Msg("ICE Connection State is closed, tearing down session")
|
||||
if session == currentSession {
|
||||
// Cancel any ongoing keyboard report multi when session closes
|
||||
cancelKeyboardMacro()
|
||||
cancelAllRunningKeyboardMacros()
|
||||
currentSession = nil
|
||||
}
|
||||
|
||||
// Stop RPC processor
|
||||
if session.rpcQueue != nil {
|
||||
close(session.rpcQueue)
|
||||
session.rpcQueue = nil
|
||||
}
|
||||
|
||||
// Stop HID RPC processor
|
||||
for i := 0; i < len(session.hidQueue); i++ {
|
||||
close(session.hidQueue[i])
|
||||
session.hidQueue[i] = nil
|
||||
// Stop HID RPC processors
|
||||
for i := 0; i < len(session.hidQueues); i++ {
|
||||
close(session.hidQueues[i])
|
||||
session.hidQueues[i] = nil
|
||||
}
|
||||
|
||||
close(session.keysDownStateQueue)
|
||||
|
|
|
|||
Loading…
Reference in New Issue