mirror of https://github.com/jetkvm/kvm.git
Merge branch 'dev' into feat/tls
This commit is contained in:
commit
369bd3fb18
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
name: golangci-lint
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "go.sum"
|
||||||
|
- "go.mod"
|
||||||
|
- "**.go"
|
||||||
|
- ".github/workflows/golangci-lint.yml"
|
||||||
|
- ".golangci.yml"
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions: # added using https://github.com/step-security/secure-repo
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
golangci:
|
||||||
|
permissions:
|
||||||
|
contents: read # for actions/checkout to fetch code
|
||||||
|
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
|
||||||
|
name: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||||
|
with:
|
||||||
|
go-version: 1.23.x
|
||||||
|
- name: Lint
|
||||||
|
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
|
||||||
|
with:
|
||||||
|
args: --verbose
|
||||||
|
version: v1.62.0
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
# - goimports
|
||||||
|
# - misspell
|
||||||
|
# - revive
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-rules:
|
||||||
|
- path: _test.go
|
||||||
|
linters:
|
||||||
|
- errcheck
|
|
@ -3,7 +3,6 @@ package kvm
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
@ -94,7 +93,8 @@ func (d *NBDDevice) Start() error {
|
||||||
// Remove the socket file if it already exists
|
// Remove the socket file if it already exists
|
||||||
if _, err := os.Stat(nbdSocketPath); err == nil {
|
if _, err := os.Stat(nbdSocketPath); err == nil {
|
||||||
if err := os.Remove(nbdSocketPath); err != nil {
|
if err := os.Remove(nbdSocketPath); err != nil {
|
||||||
log.Fatalf("Failed to remove existing socket file %s: %v", nbdSocketPath, err)
|
logger.Errorf("Failed to remove existing socket file %s: %v", nbdSocketPath, err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ func (d *NBDDevice) runServerConn() {
|
||||||
MaximumBlockSize: uint32(16 * 1024),
|
MaximumBlockSize: uint32(16 * 1024),
|
||||||
SupportsMultiConn: false,
|
SupportsMultiConn: false,
|
||||||
})
|
})
|
||||||
log.Println("nbd server exited:", err)
|
logger.Infof("nbd server exited: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *NBDDevice) runClientConn() {
|
func (d *NBDDevice) runClientConn() {
|
||||||
|
@ -142,14 +142,14 @@ func (d *NBDDevice) runClientConn() {
|
||||||
ExportName: "jetkvm",
|
ExportName: "jetkvm",
|
||||||
BlockSize: uint32(4 * 1024),
|
BlockSize: uint32(4 * 1024),
|
||||||
})
|
})
|
||||||
log.Println("nbd client exited:", err)
|
logger.Infof("nbd client exited: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *NBDDevice) Close() {
|
func (d *NBDDevice) Close() {
|
||||||
if d.dev != nil {
|
if d.dev != nil {
|
||||||
err := client.Disconnect(d.dev)
|
err := client.Disconnect(d.dev)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("error disconnecting nbd client:", err)
|
logger.Warnf("error disconnecting nbd client: %v", err)
|
||||||
}
|
}
|
||||||
_ = d.dev.Close()
|
_ = d.dev.Close()
|
||||||
}
|
}
|
||||||
|
|
4
cloud.go
4
cloud.go
|
@ -150,8 +150,8 @@ func runWebsocketClient() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer c.CloseNow()
|
defer c.CloseNow() //nolint:errcheck
|
||||||
cloudLogger.Infof("websocket connected to %s", wsURL.String())
|
cloudLogger.Infof("websocket connected to %s", wsURL)
|
||||||
runCtx, cancelRun := context.WithCancel(context.Background())
|
runCtx, cancelRun := context.WithCancel(context.Background())
|
||||||
defer cancelRun()
|
defer cancelRun()
|
||||||
go func() {
|
go func() {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"kvm"
|
"github.com/jetkvm/kvm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
59
config.go
59
config.go
|
@ -5,6 +5,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WakeOnLanDevice struct {
|
type WakeOnLanDevice struct {
|
||||||
|
@ -12,33 +14,26 @@ type WakeOnLanDevice struct {
|
||||||
MacAddress string `json:"macAddress"`
|
MacAddress string `json:"macAddress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UsbConfig struct {
|
|
||||||
VendorId string `json:"vendor_id"`
|
|
||||||
ProductId string `json:"product_id"`
|
|
||||||
SerialNumber string `json:"serial_number"`
|
|
||||||
Manufacturer string `json:"manufacturer"`
|
|
||||||
Product string `json:"product"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
CloudURL string `json:"cloud_url"`
|
CloudURL string `json:"cloud_url"`
|
||||||
CloudAppURL string `json:"cloud_app_url"`
|
CloudAppURL string `json:"cloud_app_url"`
|
||||||
CloudToken string `json:"cloud_token"`
|
CloudToken string `json:"cloud_token"`
|
||||||
GoogleIdentity string `json:"google_identity"`
|
GoogleIdentity string `json:"google_identity"`
|
||||||
JigglerEnabled bool `json:"jiggler_enabled"`
|
JigglerEnabled bool `json:"jiggler_enabled"`
|
||||||
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
||||||
IncludePreRelease bool `json:"include_pre_release"`
|
IncludePreRelease bool `json:"include_pre_release"`
|
||||||
HashedPassword string `json:"hashed_password"`
|
HashedPassword string `json:"hashed_password"`
|
||||||
LocalAuthToken string `json:"local_auth_token"`
|
LocalAuthToken string `json:"local_auth_token"`
|
||||||
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
||||||
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
||||||
EdidString string `json:"hdmi_edid_string"`
|
EdidString string `json:"hdmi_edid_string"`
|
||||||
ActiveExtension string `json:"active_extension"`
|
ActiveExtension string `json:"active_extension"`
|
||||||
DisplayMaxBrightness int `json:"display_max_brightness"`
|
DisplayMaxBrightness int `json:"display_max_brightness"`
|
||||||
DisplayDimAfterSec int `json:"display_dim_after_sec"`
|
DisplayDimAfterSec int `json:"display_dim_after_sec"`
|
||||||
DisplayOffAfterSec int `json:"display_off_after_sec"`
|
DisplayOffAfterSec int `json:"display_off_after_sec"`
|
||||||
TLSMode string `json:"tls_mode"`
|
TLSMode string `json:"tls_mode"`
|
||||||
UsbConfig *UsbConfig `json:"usb_config"`
|
UsbConfig *usbgadget.Config `json:"usb_config"`
|
||||||
|
UsbDevices *usbgadget.Devices `json:"usb_devices"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const configPath = "/userdata/kvm_config.json"
|
const configPath = "/userdata/kvm_config.json"
|
||||||
|
@ -52,13 +47,19 @@ var defaultConfig = &Config{
|
||||||
DisplayDimAfterSec: 120, // 2 minutes
|
DisplayDimAfterSec: 120, // 2 minutes
|
||||||
DisplayOffAfterSec: 1800, // 30 minutes
|
DisplayOffAfterSec: 1800, // 30 minutes
|
||||||
TLSMode: "",
|
TLSMode: "",
|
||||||
UsbConfig: &UsbConfig{
|
UsbConfig: &usbgadget.Config{
|
||||||
VendorId: "0x1d6b", //The Linux Foundation
|
VendorId: "0x1d6b", //The Linux Foundation
|
||||||
ProductId: "0x0104", //Multifunction Composite Gadget
|
ProductId: "0x0104", //Multifunction Composite Gadget
|
||||||
SerialNumber: "",
|
SerialNumber: "",
|
||||||
Manufacturer: "JetKVM",
|
Manufacturer: "JetKVM",
|
||||||
Product: "USB Emulation Device",
|
Product: "USB Emulation Device",
|
||||||
},
|
},
|
||||||
|
UsbDevices: &usbgadget.Devices{
|
||||||
|
AbsoluteMouse: true,
|
||||||
|
RelativeMouse: true,
|
||||||
|
Keyboard: true,
|
||||||
|
MassStorage: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -97,6 +98,10 @@ func LoadConfig() {
|
||||||
loadedConfig.UsbConfig = defaultConfig.UsbConfig
|
loadedConfig.UsbConfig = defaultConfig.UsbConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if loadedConfig.UsbDevices == nil {
|
||||||
|
loadedConfig.UsbDevices = defaultConfig.UsbDevices
|
||||||
|
}
|
||||||
|
|
||||||
config = &loadedConfig
|
config = &loadedConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
35
display.go
35
display.go
|
@ -3,7 +3,6 @@ package kvm
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
@ -25,7 +24,7 @@ const (
|
||||||
func switchToScreen(screen string) {
|
func switchToScreen(screen string) {
|
||||||
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
|
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to switch to screen %s: %v", screen, err)
|
logger.Warnf("failed to switch to screen %s: %v", screen, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentScreen = screen
|
currentScreen = screen
|
||||||
|
@ -41,7 +40,7 @@ func updateLabelIfChanged(objName string, newText string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func switchToScreenIfDifferent(screenName string) {
|
func switchToScreenIfDifferent(screenName string) {
|
||||||
fmt.Println("switching screen from", currentScreen, screenName)
|
logger.Infof("switching screen from %s to %s", currentScreen, screenName)
|
||||||
if currentScreen != screenName {
|
if currentScreen != screenName {
|
||||||
switchToScreen(screenName)
|
switchToScreen(screenName)
|
||||||
}
|
}
|
||||||
|
@ -75,12 +74,12 @@ var displayInited = false
|
||||||
|
|
||||||
func requestDisplayUpdate() {
|
func requestDisplayUpdate() {
|
||||||
if !displayInited {
|
if !displayInited {
|
||||||
fmt.Println("display not inited, skipping updates")
|
logger.Info("display not inited, skipping updates")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
wakeDisplay(false)
|
wakeDisplay(false)
|
||||||
fmt.Println("display updating........................")
|
logger.Info("display updating")
|
||||||
//TODO: only run once regardless how many pending updates
|
//TODO: only run once regardless how many pending updates
|
||||||
updateDisplay()
|
updateDisplay()
|
||||||
}()
|
}()
|
||||||
|
@ -119,7 +118,7 @@ func setDisplayBrightness(brightness int) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("display: set brightness to %v\n", brightness)
|
logger.Infof("display: set brightness to %v", brightness)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +127,7 @@ func setDisplayBrightness(brightness int) error {
|
||||||
func tick_displayDim() {
|
func tick_displayDim() {
|
||||||
err := setDisplayBrightness(config.DisplayMaxBrightness / 2)
|
err := setDisplayBrightness(config.DisplayMaxBrightness / 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("display: failed to dim display: %s\n", err)
|
logger.Warnf("display: failed to dim display: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dimTicker.Stop()
|
dimTicker.Stop()
|
||||||
|
@ -141,7 +140,7 @@ func tick_displayDim() {
|
||||||
func tick_displayOff() {
|
func tick_displayOff() {
|
||||||
err := setDisplayBrightness(0)
|
err := setDisplayBrightness(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("display: failed to turn off display: %s\n", err)
|
logger.Warnf("display: failed to turn off display: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
offTicker.Stop()
|
offTicker.Stop()
|
||||||
|
@ -164,7 +163,7 @@ func wakeDisplay(force bool) {
|
||||||
|
|
||||||
err := setDisplayBrightness(config.DisplayMaxBrightness)
|
err := setDisplayBrightness(config.DisplayMaxBrightness)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("display wake failed, %s\n", err)
|
logger.Warnf("display wake failed, %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.DisplayDimAfterSec != 0 {
|
if config.DisplayDimAfterSec != 0 {
|
||||||
|
@ -184,7 +183,7 @@ func wakeDisplay(force bool) {
|
||||||
func watchTsEvents() {
|
func watchTsEvents() {
|
||||||
ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666)
|
ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("display: failed to open touchscreen device: %s\n", err)
|
logger.Warnf("display: failed to open touchscreen device: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +196,7 @@ func watchTsEvents() {
|
||||||
for {
|
for {
|
||||||
_, err := ts.Read(buf)
|
_, err := ts.Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("display: failed to read from touchscreen device: %s\n", err)
|
logger.Warnf("display: failed to read from touchscreen device: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,17 +211,17 @@ func startBacklightTickers() {
|
||||||
// Don't start the tickers if the display is switched off.
|
// Don't start the tickers if the display is switched off.
|
||||||
// Set the display to off if that's the case.
|
// Set the display to off if that's the case.
|
||||||
if config.DisplayMaxBrightness == 0 {
|
if config.DisplayMaxBrightness == 0 {
|
||||||
setDisplayBrightness(0)
|
_ = setDisplayBrightness(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if dimTicker == nil && config.DisplayDimAfterSec != 0 {
|
if dimTicker == nil && config.DisplayDimAfterSec != 0 {
|
||||||
fmt.Printf("display: dim_ticker has started\n")
|
logger.Info("display: dim_ticker has started")
|
||||||
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
||||||
defer dimTicker.Stop()
|
defer dimTicker.Stop()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for { //nolint:gosimple
|
||||||
select {
|
select {
|
||||||
case <-dimTicker.C:
|
case <-dimTicker.C:
|
||||||
tick_displayDim()
|
tick_displayDim()
|
||||||
|
@ -232,12 +231,12 @@ func startBacklightTickers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if offTicker == nil && config.DisplayOffAfterSec != 0 {
|
if offTicker == nil && config.DisplayOffAfterSec != 0 {
|
||||||
fmt.Printf("display: off_ticker has started\n")
|
logger.Info("display: off_ticker has started")
|
||||||
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
||||||
defer offTicker.Stop()
|
defer offTicker.Stop()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for { //nolint:gosimple
|
||||||
select {
|
select {
|
||||||
case <-offTicker.C:
|
case <-offTicker.C:
|
||||||
tick_displayOff()
|
tick_displayOff()
|
||||||
|
@ -252,11 +251,11 @@ func init() {
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
waitCtrlClientConnected()
|
waitCtrlClientConnected()
|
||||||
fmt.Println("setting initial display contents")
|
logger.Info("setting initial display contents")
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
updateStaticContents()
|
updateStaticContents()
|
||||||
displayInited = true
|
displayInited = true
|
||||||
fmt.Println("display inited")
|
logger.Info("display inited")
|
||||||
startBacklightTickers()
|
startBacklightTickers()
|
||||||
wakeDisplay(true)
|
wakeDisplay(true)
|
||||||
requestDisplayUpdate()
|
requestDisplayUpdate()
|
||||||
|
|
3
fuse.go
3
fuse.go
|
@ -2,7 +2,6 @@ package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
@ -104,7 +103,7 @@ func RunFuseServer() {
|
||||||
var err error
|
var err error
|
||||||
fuseServer, err = fs.Mount(fuseMountPoint, &FuseRoot{}, opts)
|
fuseServer, err = fs.Mount(fuseMountPoint, &FuseRoot{}, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("failed to mount fuse: %w", err)
|
logger.Warnf("failed to mount fuse: %v", err)
|
||||||
}
|
}
|
||||||
fuseServer.Wait()
|
fuseServer.Wait()
|
||||||
}
|
}
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -1,4 +1,4 @@
|
||||||
module kvm
|
module github.com/jetkvm/kvm
|
||||||
|
|
||||||
go 1.21.0
|
go 1.21.0
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ require (
|
||||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
|
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1
|
github.com/hanwen/go-fuse/v2 v2.5.1
|
||||||
github.com/hashicorp/go-envparse v0.1.0
|
github.com/hashicorp/go-envparse v0.1.0
|
||||||
github.com/openstadia/go-usb-gadget v0.0.0-20231115171102-aebd56bbb965
|
|
||||||
github.com/pion/logging v0.2.2
|
github.com/pion/logging v0.2.2
|
||||||
github.com/pion/mdns/v2 v2.0.7
|
github.com/pion/mdns/v2 v2.0.7
|
||||||
github.com/pion/webrtc/v4 v4.0.0
|
github.com/pion/webrtc/v4 v4.0.0
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -87,8 +87,6 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/openstadia/go-usb-gadget v0.0.0-20231115171102-aebd56bbb965 h1:bZGtUfkOl0dqvem8ltx9KCYied0gSlRuDhaZDxgppN4=
|
|
||||||
github.com/openstadia/go-usb-gadget v0.0.0-20231115171102-aebd56bbb965/go.mod h1:6cAIK2c4O3/yETSrRjmNwsBL3yE4Vcu9M9p/Qwx5+gM=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
|
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
|
||||||
|
|
4
hw.go
4
hw.go
|
@ -14,7 +14,7 @@ func extractSerialNumber() (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := regexp.Compile("Serial\\s*:\\s*(\\S+)")
|
r, err := regexp.Compile(`Serial\s*:\s*(\S+)`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to compile regex: %w", err)
|
return "", fmt.Errorf("failed to compile regex: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func extractSerialNumber() (string, error) {
|
||||||
return matches[1], nil
|
return matches[1], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readOtpEntropy() ([]byte, error) {
|
func readOtpEntropy() ([]byte, error) { //nolint:unused
|
||||||
content, err := os.ReadFile("/sys/bus/nvmem/devices/rockchip-otp0/nvmem")
|
content, err := os.ReadFile("/sys/bus/nvmem/devices/rockchip-otp0/nvmem")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -0,0 +1,327 @@
|
||||||
|
package usbgadget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type gadgetConfigItem struct {
|
||||||
|
order uint
|
||||||
|
device string
|
||||||
|
path []string
|
||||||
|
attrs gadgetAttributes
|
||||||
|
configAttrs gadgetAttributes
|
||||||
|
configPath []string
|
||||||
|
reportDesc []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type gadgetAttributes map[string]string
|
||||||
|
|
||||||
|
type gadgetConfigItemWithKey struct {
|
||||||
|
key string
|
||||||
|
item gadgetConfigItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type orderedGadgetConfigItems []gadgetConfigItemWithKey
|
||||||
|
|
||||||
|
var defaultGadgetConfig = map[string]gadgetConfigItem{
|
||||||
|
"base": {
|
||||||
|
order: 0,
|
||||||
|
attrs: gadgetAttributes{
|
||||||
|
"bcdUSB": "0x0200", // USB 2.0
|
||||||
|
"idVendor": "0x1d6b", // The Linux Foundation
|
||||||
|
"idProduct": "0104", // Multifunction Composite Gadget
|
||||||
|
"bcdDevice": "0100",
|
||||||
|
},
|
||||||
|
configAttrs: gadgetAttributes{
|
||||||
|
"MaxPower": "250", // in unit of 2mA
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"base_info": {
|
||||||
|
order: 1,
|
||||||
|
path: []string{"strings", "0x409"},
|
||||||
|
configPath: []string{"strings", "0x409"},
|
||||||
|
attrs: gadgetAttributes{
|
||||||
|
"serialnumber": "",
|
||||||
|
"manufacturer": "JetKVM",
|
||||||
|
"product": "JetKVM USB Emulation Device",
|
||||||
|
},
|
||||||
|
configAttrs: gadgetAttributes{
|
||||||
|
"configuration": "Config 1: HID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// keyboard HID
|
||||||
|
"keyboard": keyboardConfig,
|
||||||
|
// mouse HID
|
||||||
|
"absolute_mouse": absoluteMouseConfig,
|
||||||
|
// relative mouse HID
|
||||||
|
"relative_mouse": relativeMouseConfig,
|
||||||
|
// mass storage
|
||||||
|
"mass_storage_base": massStorageBaseConfig,
|
||||||
|
"mass_storage_lun0": massStorageLun0Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool {
|
||||||
|
switch itemKey {
|
||||||
|
case "absolute_mouse":
|
||||||
|
return u.enabledDevices.AbsoluteMouse
|
||||||
|
case "relative_mouse":
|
||||||
|
return u.enabledDevices.RelativeMouse
|
||||||
|
case "keyboard":
|
||||||
|
return u.enabledDevices.Keyboard
|
||||||
|
case "mass_storage_base":
|
||||||
|
return u.enabledDevices.MassStorage
|
||||||
|
case "mass_storage_lun0":
|
||||||
|
return u.enabledDevices.MassStorage
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) loadGadgetConfig() {
|
||||||
|
if u.customConfig.isEmpty {
|
||||||
|
u.log.Trace("using default gadget config")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u.configMap["base"].attrs["idVendor"] = u.customConfig.VendorId
|
||||||
|
u.configMap["base"].attrs["idProduct"] = u.customConfig.ProductId
|
||||||
|
|
||||||
|
u.configMap["base_info"].attrs["serialnumber"] = u.customConfig.SerialNumber
|
||||||
|
u.configMap["base_info"].attrs["manufacturer"] = u.customConfig.Manufacturer
|
||||||
|
u.configMap["base_info"].attrs["product"] = u.customConfig.Product
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) SetGadgetConfig(config *Config) {
|
||||||
|
u.configLock.Lock()
|
||||||
|
defer u.configLock.Unlock()
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
|
return // nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
u.customConfig = *config
|
||||||
|
u.loadGadgetConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) SetGadgetDevices(devices *Devices) {
|
||||||
|
u.configLock.Lock()
|
||||||
|
defer u.configLock.Unlock()
|
||||||
|
|
||||||
|
if devices == nil {
|
||||||
|
return // nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
u.enabledDevices = *devices
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigPath returns the path to the config item.
|
||||||
|
func (u *UsbGadget) GetConfigPath(itemKey string) (string, error) {
|
||||||
|
item, ok := u.configMap[itemKey]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("config item %s not found", itemKey)
|
||||||
|
}
|
||||||
|
return joinPath(u.kvmGadgetPath, item.configPath), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mountConfigFS() error {
|
||||||
|
_, err := os.Stat(gadgetPath)
|
||||||
|
// TODO: check if it's mounted properly
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = exec.Command("mount", "-t", "configfs", "none", configFSPath).Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to mount configfs: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("unable to access usb gadget path: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) Init() error {
|
||||||
|
u.configLock.Lock()
|
||||||
|
defer u.configLock.Unlock()
|
||||||
|
|
||||||
|
u.loadGadgetConfig()
|
||||||
|
|
||||||
|
udcs := getUdcs()
|
||||||
|
if len(udcs) < 1 {
|
||||||
|
u.log.Error("no udc found, skipping USB stack init")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u.udc = udcs[0]
|
||||||
|
_, err := os.Stat(u.kvmGadgetPath)
|
||||||
|
if err == nil {
|
||||||
|
u.log.Info("usb gadget already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mountConfigFS(); err != nil {
|
||||||
|
u.log.Errorf("failed to mount configfs: %v, usb stack might not function properly", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(u.configC1Path, 0755); err != nil {
|
||||||
|
u.log.Errorf("failed to create config path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.writeGadgetConfig(); err != nil {
|
||||||
|
u.log.Errorf("failed to start gadget: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) UpdateGadgetConfig() error {
|
||||||
|
u.configLock.Lock()
|
||||||
|
defer u.configLock.Unlock()
|
||||||
|
|
||||||
|
u.loadGadgetConfig()
|
||||||
|
|
||||||
|
if err := u.writeGadgetConfig(); err != nil {
|
||||||
|
u.log.Errorf("failed to update gadget: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) getOrderedConfigItems() orderedGadgetConfigItems {
|
||||||
|
items := make([]gadgetConfigItemWithKey, 0)
|
||||||
|
for key, item := range u.configMap {
|
||||||
|
items = append(items, gadgetConfigItemWithKey{key, item})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
return items[i].item.order < items[j].item.order
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) writeGadgetConfig() error {
|
||||||
|
// create kvm gadget path
|
||||||
|
err := os.MkdirAll(u.kvmGadgetPath, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.log.Tracef("writing gadget config")
|
||||||
|
for _, val := range u.getOrderedConfigItems() {
|
||||||
|
key := val.key
|
||||||
|
item := val.item
|
||||||
|
|
||||||
|
// check if the item is enabled in the config
|
||||||
|
if !u.isGadgetConfigItemEnabled(key) {
|
||||||
|
u.log.Tracef("disabling gadget config: %s", key)
|
||||||
|
err = u.disableGadgetItemConfig(item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u.log.Tracef("writing gadget config: %s", key)
|
||||||
|
err = u.writeGadgetItemConfig(item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = u.writeUDC(); err != nil {
|
||||||
|
u.log.Errorf("failed to write UDC: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = u.rebindUsb(true); err != nil {
|
||||||
|
u.log.Infof("failed to rebind usb: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) disableGadgetItemConfig(item gadgetConfigItem) error {
|
||||||
|
// remove symlink if exists
|
||||||
|
if item.configPath == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := joinPath(u.configC1Path, item.configPath)
|
||||||
|
|
||||||
|
if _, err := os.Lstat(configPath); os.IsNotExist(err) {
|
||||||
|
u.log.Tracef("symlink %s does not exist", item.configPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(configPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove symlink %s: %w", item.configPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) writeGadgetItemConfig(item gadgetConfigItem) error {
|
||||||
|
// create directory for the item
|
||||||
|
gadgetItemPath := joinPath(u.kvmGadgetPath, item.path)
|
||||||
|
err := os.MkdirAll(gadgetItemPath, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create path %s: %w", gadgetItemPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(item.attrs) > 0 {
|
||||||
|
// write attributes for the item
|
||||||
|
err = u.writeGadgetAttrs(gadgetItemPath, item.attrs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write attributes for %s: %w", gadgetItemPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write report descriptor if available
|
||||||
|
if item.reportDesc != nil {
|
||||||
|
err = u.writeIfDifferent(path.Join(gadgetItemPath, "report_desc"), item.reportDesc, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create config directory if configAttrs are set
|
||||||
|
if len(item.configAttrs) > 0 {
|
||||||
|
configItemPath := joinPath(u.configC1Path, item.configPath)
|
||||||
|
err = os.MkdirAll(configItemPath, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create path %s: %w", configItemPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = u.writeGadgetAttrs(configItemPath, item.configAttrs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write config attributes for %s: %w", configItemPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create symlink if configPath is set
|
||||||
|
if item.configPath != nil && item.configAttrs == nil {
|
||||||
|
configPath := joinPath(u.configC1Path, item.configPath)
|
||||||
|
u.log.Tracef("Creating symlink from %s to %s", configPath, gadgetItemPath)
|
||||||
|
if err := ensureSymlink(configPath, gadgetItemPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) writeGadgetAttrs(basePath string, attrs gadgetAttributes) error {
|
||||||
|
for key, val := range attrs {
|
||||||
|
filePath := filepath.Join(basePath, key)
|
||||||
|
err := u.writeIfDifferent(filePath, []byte(val), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write to %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package usbgadget
|
||||||
|
|
||||||
|
const dwc3Path = "/sys/bus/platform/drivers/dwc3"
|
|
@ -0,0 +1,11 @@
|
||||||
|
package usbgadget
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
func (u *UsbGadget) resetUserInputTime() {
|
||||||
|
u.lastUserInput = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) GetLastUserInputTime() time.Time {
|
||||||
|
return u.lastUserInput
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package usbgadget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var keyboardConfig = gadgetConfigItem{
|
||||||
|
order: 1000,
|
||||||
|
device: "hid.usb0",
|
||||||
|
path: []string{"functions", "hid.usb0"},
|
||||||
|
configPath: []string{"hid.usb0"},
|
||||||
|
attrs: gadgetAttributes{
|
||||||
|
"protocol": "1",
|
||||||
|
"subclass": "1",
|
||||||
|
"report_length": "8",
|
||||||
|
},
|
||||||
|
reportDesc: keyboardReportDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source: https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt
|
||||||
|
var keyboardReportDesc = []byte{
|
||||||
|
0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
|
||||||
|
0x09, 0x06, /* USAGE (Keyboard) */
|
||||||
|
0xa1, 0x01, /* COLLECTION (Application) */
|
||||||
|
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
|
||||||
|
0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */
|
||||||
|
0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
|
||||||
|
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
|
||||||
|
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
|
||||||
|
0x75, 0x01, /* REPORT_SIZE (1) */
|
||||||
|
0x95, 0x08, /* REPORT_COUNT (8) */
|
||||||
|
0x81, 0x02, /* INPUT (Data,Var,Abs) */
|
||||||
|
0x95, 0x01, /* REPORT_COUNT (1) */
|
||||||
|
0x75, 0x08, /* REPORT_SIZE (8) */
|
||||||
|
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
|
||||||
|
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) */
|
||||||
|
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 */
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
||||||
|
if u.keyboardHidFile == nil {
|
||||||
|
var err error
|
||||||
|
u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open hidg0: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := u.keyboardHidFile.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
u.log.Errorf("failed to write to hidg0: %w", err)
|
||||||
|
u.keyboardHidFile.Close()
|
||||||
|
u.keyboardHidFile = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error {
|
||||||
|
u.keyboardLock.Lock()
|
||||||
|
defer u.keyboardLock.Unlock()
|
||||||
|
|
||||||
|
if len(keys) > 6 {
|
||||||
|
keys = keys[:6]
|
||||||
|
}
|
||||||
|
if len(keys) < 6 {
|
||||||
|
keys = append(keys, make([]uint8, 6-len(keys))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := u.keyboardWriteHidFile([]byte{modifier, 0, keys[0], keys[1], keys[2], keys[3], keys[4], keys[5]})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.resetUserInputTime()
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
package usbgadget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var absoluteMouseConfig = gadgetConfigItem{
|
||||||
|
order: 1001,
|
||||||
|
device: "hid.usb1",
|
||||||
|
path: []string{"functions", "hid.usb1"},
|
||||||
|
configPath: []string{"hid.usb1"},
|
||||||
|
attrs: gadgetAttributes{
|
||||||
|
"protocol": "2",
|
||||||
|
"subclass": "1",
|
||||||
|
"report_length": "6",
|
||||||
|
},
|
||||||
|
reportDesc: absoluteMouseCombinedReportDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
var absoluteMouseCombinedReportDesc = []byte{
|
||||||
|
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
|
||||||
|
0x09, 0x02, // Usage (Mouse)
|
||||||
|
0xA1, 0x01, // Collection (Application)
|
||||||
|
|
||||||
|
// Report ID 1: Absolute Mouse Movement
|
||||||
|
0x85, 0x01, // Report ID (1)
|
||||||
|
0x09, 0x01, // Usage (Pointer)
|
||||||
|
0xA1, 0x00, // Collection (Physical)
|
||||||
|
0x05, 0x09, // Usage Page (Button)
|
||||||
|
0x19, 0x01, // Usage Minimum (0x01)
|
||||||
|
0x29, 0x03, // Usage Maximum (0x03)
|
||||||
|
0x15, 0x00, // Logical Minimum (0)
|
||||||
|
0x25, 0x01, // Logical Maximum (1)
|
||||||
|
0x75, 0x01, // Report Size (1)
|
||||||
|
0x95, 0x03, // Report Count (3)
|
||||||
|
0x81, 0x02, // Input (Data, Var, Abs)
|
||||||
|
0x95, 0x01, // Report Count (1)
|
||||||
|
0x75, 0x05, // Report Size (5)
|
||||||
|
0x81, 0x03, // Input (Cnst, Var, Abs)
|
||||||
|
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
|
||||||
|
0x09, 0x30, // Usage (X)
|
||||||
|
0x09, 0x31, // Usage (Y)
|
||||||
|
0x16, 0x00, 0x00, // Logical Minimum (0)
|
||||||
|
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
|
||||||
|
0x36, 0x00, 0x00, // Physical Minimum (0)
|
||||||
|
0x46, 0xFF, 0x7F, // Physical Maximum (32767)
|
||||||
|
0x75, 0x10, // Report Size (16)
|
||||||
|
0x95, 0x02, // Report Count (2)
|
||||||
|
0x81, 0x02, // Input (Data, Var, Abs)
|
||||||
|
0xC0, // End Collection
|
||||||
|
|
||||||
|
// Report ID 2: Relative Wheel Movement
|
||||||
|
0x85, 0x02, // Report ID (2)
|
||||||
|
0x09, 0x38, // Usage (Wheel)
|
||||||
|
0x15, 0x81, // Logical Minimum (-127)
|
||||||
|
0x25, 0x7F, // Logical Maximum (127)
|
||||||
|
0x75, 0x08, // Report Size (8)
|
||||||
|
0x95, 0x01, // Report Count (1)
|
||||||
|
0x81, 0x06, // Input (Data, Var, Rel)
|
||||||
|
|
||||||
|
0xC0, // End Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) absMouseWriteHidFile(data []byte) error {
|
||||||
|
if u.absMouseHidFile == nil {
|
||||||
|
var err error
|
||||||
|
u.absMouseHidFile, err = os.OpenFile("/dev/hidg1", os.O_RDWR, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open hidg1: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := u.absMouseHidFile.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
u.log.Errorf("failed to write to hidg1: %w", err)
|
||||||
|
u.absMouseHidFile.Close()
|
||||||
|
u.absMouseHidFile = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) AbsMouseReport(x, y int, buttons uint8) error {
|
||||||
|
u.absMouseLock.Lock()
|
||||||
|
defer u.absMouseLock.Unlock()
|
||||||
|
|
||||||
|
err := u.absMouseWriteHidFile([]byte{
|
||||||
|
1, // Report ID 1
|
||||||
|
buttons, // Buttons
|
||||||
|
uint8(x), // X Low Byte
|
||||||
|
uint8(x >> 8), // X High Byte
|
||||||
|
uint8(y), // Y Low Byte
|
||||||
|
uint8(y >> 8), // Y High Byte
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.resetUserInputTime()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) AbsMouseWheelReport(wheelY int8) error {
|
||||||
|
u.absMouseLock.Lock()
|
||||||
|
defer u.absMouseLock.Unlock()
|
||||||
|
|
||||||
|
// Accumulate the wheelY value
|
||||||
|
u.absMouseAccumulatedWheelY += float64(wheelY) / 8.0
|
||||||
|
|
||||||
|
// Only send a report if the accumulated value is significant
|
||||||
|
if abs(u.absMouseAccumulatedWheelY) < 1.0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scaledWheelY := int8(u.absMouseAccumulatedWheelY)
|
||||||
|
|
||||||
|
err := u.absMouseWriteHidFile([]byte{
|
||||||
|
2, // Report ID 2
|
||||||
|
byte(scaledWheelY), // Scaled Wheel Y (signed)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset the accumulator, keeping any remainder
|
||||||
|
u.absMouseAccumulatedWheelY -= float64(scaledWheelY)
|
||||||
|
|
||||||
|
u.resetUserInputTime()
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
package usbgadget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var relativeMouseConfig = gadgetConfigItem{
|
||||||
|
order: 1002,
|
||||||
|
device: "hid.usb2",
|
||||||
|
path: []string{"functions", "hid.usb2"},
|
||||||
|
configPath: []string{"hid.usb2"},
|
||||||
|
attrs: gadgetAttributes{
|
||||||
|
"protocol": "2",
|
||||||
|
"subclass": "1",
|
||||||
|
"report_length": "4",
|
||||||
|
},
|
||||||
|
reportDesc: relativeMouseCombinedReportDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
// from: https://github.com/NicoHood/HID/blob/b16be57caef4295c6cd382a7e4c64db5073647f7/src/SingleReport/BootMouse.cpp#L26
|
||||||
|
var relativeMouseCombinedReportDesc = []byte{
|
||||||
|
0x05, 0x01, // USAGE_PAGE (Generic Desktop) 54
|
||||||
|
0x09, 0x02, // USAGE (Mouse)
|
||||||
|
0xa1, 0x01, // COLLECTION (Application)
|
||||||
|
|
||||||
|
// Pointer and Physical are required by Apple Recovery
|
||||||
|
0x09, 0x01, // USAGE (Pointer)
|
||||||
|
0xa1, 0x00, // COLLECTION (Physical)
|
||||||
|
|
||||||
|
// 8 Buttons
|
||||||
|
0x05, 0x09, // USAGE_PAGE (Button)
|
||||||
|
0x19, 0x01, // USAGE_MINIMUM (Button 1)
|
||||||
|
0x29, 0x08, // USAGE_MAXIMUM (Button 8)
|
||||||
|
0x15, 0x00, // LOGICAL_MINIMUM (0)
|
||||||
|
0x25, 0x01, // LOGICAL_MAXIMUM (1)
|
||||||
|
0x95, 0x08, // REPORT_COUNT (8)
|
||||||
|
0x75, 0x01, // REPORT_SIZE (1)
|
||||||
|
0x81, 0x02, // INPUT (Data,Var,Abs)
|
||||||
|
|
||||||
|
// X, Y, Wheel
|
||||||
|
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
|
||||||
|
0x09, 0x30, // USAGE (X)
|
||||||
|
0x09, 0x31, // USAGE (Y)
|
||||||
|
0x09, 0x38, // USAGE (Wheel)
|
||||||
|
0x15, 0x81, // LOGICAL_MINIMUM (-127)
|
||||||
|
0x25, 0x7f, // LOGICAL_MAXIMUM (127)
|
||||||
|
0x75, 0x08, // REPORT_SIZE (8)
|
||||||
|
0x95, 0x03, // REPORT_COUNT (3)
|
||||||
|
0x81, 0x06, // INPUT (Data,Var,Rel)
|
||||||
|
|
||||||
|
// End
|
||||||
|
0xc0, // End Collection (Physical)
|
||||||
|
0xc0, // End Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) relMouseWriteHidFile(data []byte) error {
|
||||||
|
if u.relMouseHidFile == nil {
|
||||||
|
var err error
|
||||||
|
u.relMouseHidFile, err = os.OpenFile("/dev/hidg2", os.O_RDWR, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open hidg1: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := u.relMouseHidFile.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
u.log.Errorf("failed to write to hidg2: %w", err)
|
||||||
|
u.relMouseHidFile.Close()
|
||||||
|
u.relMouseHidFile = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) RelMouseReport(mx, my int8, buttons uint8) error {
|
||||||
|
u.relMouseLock.Lock()
|
||||||
|
defer u.relMouseLock.Unlock()
|
||||||
|
|
||||||
|
err := u.relMouseWriteHidFile([]byte{
|
||||||
|
buttons, // Buttons
|
||||||
|
uint8(mx), // X
|
||||||
|
uint8(my), // Y
|
||||||
|
0, // Wheel
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.resetUserInputTime()
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package usbgadget
|
||||||
|
|
||||||
|
var massStorageBaseConfig = gadgetConfigItem{
|
||||||
|
order: 3000,
|
||||||
|
device: "mass_storage.usb0",
|
||||||
|
path: []string{"functions", "mass_storage.usb0"},
|
||||||
|
configPath: []string{"mass_storage.usb0"},
|
||||||
|
attrs: gadgetAttributes{
|
||||||
|
"stall": "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var massStorageLun0Config = gadgetConfigItem{
|
||||||
|
order: 3001,
|
||||||
|
path: []string{"functions", "mass_storage.usb0", "lun.0"},
|
||||||
|
attrs: gadgetAttributes{
|
||||||
|
"cdrom": "1",
|
||||||
|
"ro": "1",
|
||||||
|
"removable": "1",
|
||||||
|
"file": "\n",
|
||||||
|
"inquiry_string": "JetKVM Virtual Media",
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package usbgadget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getUdcs() []string {
|
||||||
|
var udcs []string
|
||||||
|
|
||||||
|
files, err := os.ReadDir("/sys/devices/platform/usbdrd")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if !file.IsDir() || !strings.HasSuffix(file.Name(), ".usb") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
udcs = append(udcs, file.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return udcs
|
||||||
|
}
|
||||||
|
|
||||||
|
func rebindUsb(udc string, ignoreUnbindError bool) error {
|
||||||
|
err := os.WriteFile(path.Join(dwc3Path, "unbind"), []byte(udc), 0644)
|
||||||
|
if err != nil && !ignoreUnbindError {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.WriteFile(path.Join(dwc3Path, "bind"), []byte(udc), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) rebindUsb(ignoreUnbindError bool) error {
|
||||||
|
u.log.Infof("rebinding USB gadget to UDC %s", u.udc)
|
||||||
|
return rebindUsb(u.udc, ignoreUnbindError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RebindUsb rebinds the USB gadget to the UDC.
|
||||||
|
func (u *UsbGadget) RebindUsb(ignoreUnbindError bool) error {
|
||||||
|
u.configLock.Lock()
|
||||||
|
defer u.configLock.Unlock()
|
||||||
|
|
||||||
|
return u.rebindUsb(ignoreUnbindError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) writeUDC() error {
|
||||||
|
path := path.Join(u.kvmGadgetPath, "UDC")
|
||||||
|
|
||||||
|
u.log.Tracef("writing UDC %s to %s", u.udc, path)
|
||||||
|
err := u.writeIfDifferent(path, []byte(u.udc), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write UDC: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsbState returns the current state of the USB gadget
|
||||||
|
func (u *UsbGadget) GetUsbState() (state string) {
|
||||||
|
stateFile := path.Join("/sys/class/udc", u.udc, "state")
|
||||||
|
stateBytes, err := os.ReadFile(stateFile)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return "not attached"
|
||||||
|
} else {
|
||||||
|
u.log.Tracef("failed to read usb state: %v", err)
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(stateBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUDCBound checks if the UDC state is bound.
|
||||||
|
func (u *UsbGadget) IsUDCBound() (bool, error) {
|
||||||
|
udcFilePath := path.Join(dwc3Path, u.udc)
|
||||||
|
_, err := os.Stat(udcFilePath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("error checking USB emulation state: %w", err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindUDC binds the gadget to the UDC.
|
||||||
|
func (u *UsbGadget) BindUDC() error {
|
||||||
|
err := os.WriteFile(path.Join(dwc3Path, "bind"), []byte(u.udc), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error binding UDC: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnbindUDC unbinds the gadget from the UDC.
|
||||||
|
func (u *UsbGadget) UnbindUDC() error {
|
||||||
|
err := os.WriteFile(path.Join(dwc3Path, "unbind"), []byte(u.udc), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error unbinding UDC: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
// Package usbgadget provides a high-level interface to manage USB gadgets
|
||||||
|
// THIS PACKAGE IS FOR INTERNAL USE ONLY AND ITS API MAY CHANGE WITHOUT NOTICE
|
||||||
|
package usbgadget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pion/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Devices is a struct that represents the USB devices that can be enabled on a USB gadget.
|
||||||
|
type Devices struct {
|
||||||
|
AbsoluteMouse bool `json:"absolute_mouse"`
|
||||||
|
RelativeMouse bool `json:"relative_mouse"`
|
||||||
|
Keyboard bool `json:"keyboard"`
|
||||||
|
MassStorage bool `json:"mass_storage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is a struct that represents the customizations for a USB gadget.
|
||||||
|
// TODO: rename to something else that won't confuse with the USB gadget configuration
|
||||||
|
type Config struct {
|
||||||
|
VendorId string `json:"vendor_id"`
|
||||||
|
ProductId string `json:"product_id"`
|
||||||
|
SerialNumber string `json:"serial_number"`
|
||||||
|
Manufacturer string `json:"manufacturer"`
|
||||||
|
Product string `json:"product"`
|
||||||
|
|
||||||
|
isEmpty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultUsbGadgetDevices = Devices{
|
||||||
|
AbsoluteMouse: true,
|
||||||
|
RelativeMouse: true,
|
||||||
|
Keyboard: true,
|
||||||
|
MassStorage: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsbGadget is a struct that represents a USB gadget.
|
||||||
|
type UsbGadget struct {
|
||||||
|
name string
|
||||||
|
udc string
|
||||||
|
kvmGadgetPath string
|
||||||
|
configC1Path string
|
||||||
|
|
||||||
|
configMap map[string]gadgetConfigItem
|
||||||
|
customConfig Config
|
||||||
|
|
||||||
|
configLock sync.Mutex
|
||||||
|
|
||||||
|
keyboardHidFile *os.File
|
||||||
|
keyboardLock sync.Mutex
|
||||||
|
absMouseHidFile *os.File
|
||||||
|
absMouseLock sync.Mutex
|
||||||
|
relMouseHidFile *os.File
|
||||||
|
relMouseLock sync.Mutex
|
||||||
|
|
||||||
|
enabledDevices Devices
|
||||||
|
|
||||||
|
absMouseAccumulatedWheelY float64
|
||||||
|
|
||||||
|
lastUserInput time.Time
|
||||||
|
|
||||||
|
log logging.LeveledLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
const configFSPath = "/sys/kernel/config"
|
||||||
|
const gadgetPath = "/sys/kernel/config/usb_gadget"
|
||||||
|
|
||||||
|
var defaultLogger = logging.NewDefaultLoggerFactory().NewLogger("usbgadget")
|
||||||
|
|
||||||
|
// NewUsbGadget creates a new UsbGadget.
|
||||||
|
func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *logging.LeveledLogger) *UsbGadget {
|
||||||
|
if logger == nil {
|
||||||
|
logger = &defaultLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
if enabledDevices == nil {
|
||||||
|
enabledDevices = &defaultUsbGadgetDevices
|
||||||
|
}
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
|
config = &Config{isEmpty: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
g := &UsbGadget{
|
||||||
|
name: name,
|
||||||
|
kvmGadgetPath: path.Join(gadgetPath, name),
|
||||||
|
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
||||||
|
configMap: defaultGadgetConfig,
|
||||||
|
customConfig: *config,
|
||||||
|
configLock: sync.Mutex{},
|
||||||
|
keyboardLock: sync.Mutex{},
|
||||||
|
absMouseLock: sync.Mutex{},
|
||||||
|
relMouseLock: sync.Mutex{},
|
||||||
|
enabledDevices: *enabledDevices,
|
||||||
|
lastUserInput: time.Now(),
|
||||||
|
log: *logger,
|
||||||
|
|
||||||
|
absMouseAccumulatedWheelY: 0,
|
||||||
|
}
|
||||||
|
if err := g.Init(); err != nil {
|
||||||
|
g.log.Errorf("failed to init USB gadget: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return g
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package usbgadget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper function to get absolute value of float64
|
||||||
|
func abs(x float64) float64 {
|
||||||
|
if x < 0 {
|
||||||
|
return -x
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinPath(basePath string, paths []string) string {
|
||||||
|
pathArr := append([]string{basePath}, paths...)
|
||||||
|
return filepath.Join(pathArr...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureSymlink(linkPath string, target string) error {
|
||||||
|
if _, err := os.Lstat(linkPath); err == nil {
|
||||||
|
currentTarget, err := os.Readlink(linkPath)
|
||||||
|
if err != nil || currentTarget != target {
|
||||||
|
err = os.Remove(linkPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to remove existing symlink %s: %w", linkPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to check if symlink exists: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Symlink(target, linkPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to create symlink from %s to %s: %w", linkPath, target, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) writeIfDifferent(filePath string, content []byte, permMode os.FileMode) error {
|
||||||
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
|
oldContent, err := os.ReadFile(filePath)
|
||||||
|
if err == nil {
|
||||||
|
if bytes.Equal(oldContent, content) {
|
||||||
|
u.log.Tracef("skipping writing to %s as it already has the correct content", filePath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(oldContent) == len(content)+1 &&
|
||||||
|
bytes.Equal(oldContent[:len(content)], content) &&
|
||||||
|
oldContent[len(content)] == 10 {
|
||||||
|
u.log.Tracef("skipping writing to %s as it already has the correct content", filePath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u.log.Tracef("writing to %s as it has different content old%v new%v", filePath, oldContent, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return os.WriteFile(filePath, content, permMode)
|
||||||
|
}
|
|
@ -6,10 +6,6 @@ import (
|
||||||
|
|
||||||
var lastUserInput = time.Now()
|
var lastUserInput = time.Now()
|
||||||
|
|
||||||
func resetUserInputTime() {
|
|
||||||
lastUserInput = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
var jigglerEnabled = false
|
var jigglerEnabled = false
|
||||||
|
|
||||||
func rpcSetJigglerState(enabled bool) {
|
func rpcSetJigglerState(enabled bool) {
|
||||||
|
|
120
jsonrpc.go
120
jsonrpc.go
|
@ -5,7 +5,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -15,6 +14,8 @@ import (
|
||||||
|
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
"go.bug.st/serial"
|
"go.bug.st/serial"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JSONRPCRequest struct {
|
type JSONRPCRequest struct {
|
||||||
|
@ -46,12 +47,12 @@ type BacklightSettings struct {
|
||||||
func writeJSONRPCResponse(response JSONRPCResponse, session *Session) {
|
func writeJSONRPCResponse(response JSONRPCResponse, session *Session) {
|
||||||
responseBytes, err := json.Marshal(response)
|
responseBytes, err := json.Marshal(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error marshalling JSONRPC response:", err)
|
logger.Warnf("Error marshalling JSONRPC response: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = session.RPCChannel.SendText(string(responseBytes))
|
err = session.RPCChannel.SendText(string(responseBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error sending JSONRPC response:", err)
|
logger.Warnf("Error sending JSONRPC response: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,16 +65,16 @@ func writeJSONRPCEvent(event string, params interface{}, session *Session) {
|
||||||
}
|
}
|
||||||
requestBytes, err := json.Marshal(request)
|
requestBytes, err := json.Marshal(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error marshalling JSONRPC event:", err)
|
logger.Warnf("Error marshalling JSONRPC event: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if session == nil || session.RPCChannel == nil {
|
if session == nil || session.RPCChannel == nil {
|
||||||
log.Println("RPC channel not available")
|
logger.Info("RPC channel not available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = session.RPCChannel.SendText(string(requestBytes))
|
err = session.RPCChannel.SendText(string(requestBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error sending JSONRPC event:", err)
|
logger.Warnf("Error sending JSONRPC event: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,7 +95,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//log.Printf("Received RPC request: Method=%s, Params=%v, ID=%d", request.Method, request.Params, request.ID)
|
//logger.Infof("Received RPC request: Method=%s, Params=%v, ID=%d", request.Method, request.Params, request.ID)
|
||||||
handler, ok := rpcHandlers[request.Method]
|
handler, ok := rpcHandlers[request.Method]
|
||||||
if !ok {
|
if !ok {
|
||||||
errorResponse := JSONRPCResponse{
|
errorResponse := JSONRPCResponse{
|
||||||
|
@ -147,7 +148,7 @@ func rpcGetStreamQualityFactor() (float64, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetStreamQualityFactor(factor float64) error {
|
func rpcSetStreamQualityFactor(factor float64) error {
|
||||||
log.Printf("Setting stream quality factor to: %f", factor)
|
logger.Infof("Setting stream quality factor to: %f", factor)
|
||||||
var _, err = CallCtrlAction("set_video_quality_factor", map[string]interface{}{"quality_factor": factor})
|
var _, err = CallCtrlAction("set_video_quality_factor", map[string]interface{}{"quality_factor": factor})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -183,10 +184,10 @@ func rpcGetEDID() (string, error) {
|
||||||
|
|
||||||
func rpcSetEDID(edid string) error {
|
func rpcSetEDID(edid string) error {
|
||||||
if edid == "" {
|
if edid == "" {
|
||||||
log.Println("Restoring EDID to default")
|
logger.Info("Restoring EDID to default")
|
||||||
edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
|
edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Setting EDID to: %s", edid)
|
logger.Infof("Setting EDID to: %s", edid)
|
||||||
}
|
}
|
||||||
_, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": edid})
|
_, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": edid})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -195,8 +196,7 @@ func rpcSetEDID(edid string) error {
|
||||||
|
|
||||||
// Save EDID to config, allowing it to be restored on reboot.
|
// Save EDID to config, allowing it to be restored on reboot.
|
||||||
config.EdidString = edid
|
config.EdidString = edid
|
||||||
SaveConfig()
|
_ = SaveConfig()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@ func rpcSetBacklightSettings(params BacklightSettings) error {
|
||||||
return fmt.Errorf("failed to save config: %w", err)
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("rpc: display: settings applied, max_brightness: %d, dim after: %ds, off after: %ds", config.DisplayMaxBrightness, config.DisplayDimAfterSec, config.DisplayOffAfterSec)
|
logger.Infof("rpc: display: settings applied, max_brightness: %d, dim after: %ds, off after: %ds", config.DisplayMaxBrightness, config.DisplayDimAfterSec, config.DisplayOffAfterSec)
|
||||||
|
|
||||||
// If the device started up with auto-dim and/or auto-off set to zero, the display init
|
// If the device started up with auto-dim and/or auto-off set to zero, the display init
|
||||||
// method will not have started the tickers. So in case that has changed, attempt to start the tickers now.
|
// method will not have started the tickers. So in case that has changed, attempt to start the tickers now.
|
||||||
|
@ -478,23 +478,23 @@ type RPCHandler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetMassStorageMode(mode string) (string, error) {
|
func rpcSetMassStorageMode(mode string) (string, error) {
|
||||||
log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode)
|
logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode)
|
||||||
var cdrom bool
|
var cdrom bool
|
||||||
if mode == "cdrom" {
|
if mode == "cdrom" {
|
||||||
cdrom = true
|
cdrom = true
|
||||||
} else if mode != "file" {
|
} else if mode != "file" {
|
||||||
log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Invalid mode provided: %s", mode)
|
logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Invalid mode provided: %s", mode)
|
||||||
return "", fmt.Errorf("invalid mode: %s", mode)
|
return "", fmt.Errorf("invalid mode: %s", mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode)
|
logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode)
|
||||||
|
|
||||||
err := setMassStorageMode(cdrom)
|
err := setMassStorageMode(cdrom)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to set mass storage mode: %w", err)
|
return "", fmt.Errorf("failed to set mass storage mode: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Mass storage mode set to %s", mode)
|
logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Mass storage mode set to %s", mode)
|
||||||
|
|
||||||
// Get the updated mode after setting
|
// Get the updated mode after setting
|
||||||
return rpcGetMassStorageMode()
|
return rpcGetMassStorageMode()
|
||||||
|
@ -518,45 +518,27 @@ func rpcIsUpdatePending() (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetUsbEmulationState() (bool, error) {
|
func rpcGetUsbEmulationState() (bool, error) {
|
||||||
_, err := os.Stat(filepath.Join("/sys/bus/platform/drivers/dwc3", udc))
|
return gadget.IsUDCBound()
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, fmt.Errorf("error checking USB emulation state: %w", err)
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetUsbEmulationState(enabled bool) error {
|
func rpcSetUsbEmulationState(enabled bool) error {
|
||||||
if enabled {
|
if enabled {
|
||||||
return os.WriteFile("/sys/bus/platform/drivers/dwc3/bind", []byte(udc), 0644)
|
return gadget.BindUDC()
|
||||||
} else {
|
} else {
|
||||||
return os.WriteFile("/sys/bus/platform/drivers/dwc3/unbind", []byte(udc), 0644)
|
return gadget.UnbindUDC()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetUsbConfig() (UsbConfig, error) {
|
func rpcGetUsbConfig() (usbgadget.Config, error) {
|
||||||
LoadConfig()
|
LoadConfig()
|
||||||
return *config.UsbConfig, nil
|
return *config.UsbConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetUsbConfig(usbConfig UsbConfig) error {
|
func rpcSetUsbConfig(usbConfig usbgadget.Config) error {
|
||||||
LoadConfig()
|
LoadConfig()
|
||||||
config.UsbConfig = &usbConfig
|
config.UsbConfig = &usbConfig
|
||||||
|
gadget.SetGadgetConfig(config.UsbConfig)
|
||||||
err := UpdateGadgetConfig()
|
return updateUsbRelatedConfig()
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to write gadget config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = SaveConfig()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to save usb config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("[jsonrpc.go:rpcSetUsbConfig] usb config set to %s", usbConfig)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) {
|
func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) {
|
||||||
|
@ -581,7 +563,7 @@ func rpcResetConfig() error {
|
||||||
return fmt.Errorf("failed to reset config: %w", err)
|
return fmt.Errorf("failed to reset config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Configuration reset to default")
|
logger.Info("Configuration reset to default")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -597,7 +579,7 @@ func rpcGetDCPowerState() (DCPowerState, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetDCPowerState(enabled bool) error {
|
func rpcSetDCPowerState(enabled bool) error {
|
||||||
log.Printf("[jsonrpc.go:rpcSetDCPowerState] Setting DC power state to: %v", enabled)
|
logger.Infof("[jsonrpc.go:rpcSetDCPowerState] Setting DC power state to: %v", enabled)
|
||||||
err := setDCPowerState(enabled)
|
err := setDCPowerState(enabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to set DC power state: %w", err)
|
return fmt.Errorf("failed to set DC power state: %w", err)
|
||||||
|
@ -614,18 +596,18 @@ func rpcSetActiveExtension(extensionId string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if config.ActiveExtension == "atx-power" {
|
if config.ActiveExtension == "atx-power" {
|
||||||
unmountATXControl()
|
_ = unmountATXControl()
|
||||||
} else if config.ActiveExtension == "dc-power" {
|
} else if config.ActiveExtension == "dc-power" {
|
||||||
unmountDCControl()
|
_ = unmountDCControl()
|
||||||
}
|
}
|
||||||
config.ActiveExtension = extensionId
|
config.ActiveExtension = extensionId
|
||||||
if err := SaveConfig(); err != nil {
|
if err := SaveConfig(); err != nil {
|
||||||
return fmt.Errorf("failed to save config: %w", err)
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
}
|
}
|
||||||
if extensionId == "atx-power" {
|
if extensionId == "atx-power" {
|
||||||
mountATXControl()
|
_ = mountATXControl()
|
||||||
} else if extensionId == "dc-power" {
|
} else if extensionId == "dc-power" {
|
||||||
mountDCControl()
|
_ = mountDCControl()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -746,11 +728,48 @@ func rpcSetSerialSettings(settings SerialSettings) error {
|
||||||
Parity: parity,
|
Parity: parity,
|
||||||
}
|
}
|
||||||
|
|
||||||
port.SetMode(serialPortMode)
|
_ = port.SetMode(serialPortMode)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcGetUsbDevices() (usbgadget.Devices, error) {
|
||||||
|
return *config.UsbDevices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUsbRelatedConfig() error {
|
||||||
|
if err := gadget.UpdateGadgetConfig(); err != nil {
|
||||||
|
return fmt.Errorf("failed to write gadget config: %w", err)
|
||||||
|
}
|
||||||
|
if err := SaveConfig(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcSetUsbDevices(usbDevices usbgadget.Devices) error {
|
||||||
|
config.UsbDevices = &usbDevices
|
||||||
|
gadget.SetGadgetDevices(config.UsbDevices)
|
||||||
|
return updateUsbRelatedConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcSetUsbDeviceState(device string, enabled bool) error {
|
||||||
|
switch device {
|
||||||
|
case "absoluteMouse":
|
||||||
|
config.UsbDevices.AbsoluteMouse = enabled
|
||||||
|
case "relativeMouse":
|
||||||
|
config.UsbDevices.RelativeMouse = enabled
|
||||||
|
case "keyboard":
|
||||||
|
config.UsbDevices.Keyboard = enabled
|
||||||
|
case "massStorage":
|
||||||
|
config.UsbDevices.MassStorage = enabled
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid device: %s", device)
|
||||||
|
}
|
||||||
|
gadget.SetGadgetDevices(config.UsbDevices)
|
||||||
|
return updateUsbRelatedConfig()
|
||||||
|
}
|
||||||
|
|
||||||
func rpcSetCloudUrl(apiUrl string, appUrl string) error {
|
func rpcSetCloudUrl(apiUrl string, appUrl string) error {
|
||||||
config.CloudURL = apiUrl
|
config.CloudURL = apiUrl
|
||||||
config.CloudAppURL = appUrl
|
config.CloudAppURL = appUrl
|
||||||
|
@ -831,6 +850,9 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}},
|
"setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}},
|
||||||
"getSerialSettings": {Func: rpcGetSerialSettings},
|
"getSerialSettings": {Func: rpcGetSerialSettings},
|
||||||
"setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}},
|
"setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}},
|
||||||
|
"getUsbDevices": {Func: rpcGetUsbDevices},
|
||||||
|
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
|
||||||
|
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
|
||||||
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
|
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
|
||||||
"getScrollSensitivity": {Func: rpcGetScrollSensitivity},
|
"getScrollSensitivity": {Func: rpcGetScrollSensitivity},
|
||||||
"setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}},
|
"setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}},
|
||||||
|
|
1
log.go
1
log.go
|
@ -5,5 +5,4 @@ import "github.com/pion/logging"
|
||||||
// we use logging framework from pion
|
// we use logging framework from pion
|
||||||
// ref: https://github.com/pion/webrtc/wiki/Debugging-WebRTC
|
// ref: https://github.com/pion/webrtc/wiki/Debugging-WebRTC
|
||||||
var logger = logging.NewDefaultLoggerFactory().NewLogger("jetkvm")
|
var logger = logging.NewDefaultLoggerFactory().NewLogger("jetkvm")
|
||||||
var usbLogger = logging.NewDefaultLoggerFactory().NewLogger("usb")
|
|
||||||
var cloudLogger = logging.NewDefaultLoggerFactory().NewLogger("cloud")
|
var cloudLogger = logging.NewDefaultLoggerFactory().NewLogger("cloud")
|
||||||
|
|
11
main.go
11
main.go
|
@ -2,7 +2,6 @@ package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
@ -46,11 +45,13 @@ func Main() {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
initUsbGadget()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(15 * time.Minute)
|
time.Sleep(15 * time.Minute)
|
||||||
for {
|
for {
|
||||||
logger.Debugf("UPDATING - Auto update enabled: %v", config.AutoUpdateEnabled)
|
logger.Debugf("UPDATING - Auto update enabled: %v", config.AutoUpdateEnabled)
|
||||||
if config.AutoUpdateEnabled == false {
|
if !config.AutoUpdateEnabled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if currentSession != nil {
|
if currentSession != nil {
|
||||||
|
@ -80,15 +81,15 @@ func Main() {
|
||||||
sigs := make(chan os.Signal, 1)
|
sigs := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-sigs
|
<-sigs
|
||||||
log.Println("JetKVM Shutting Down")
|
logger.Info("JetKVM Shutting Down")
|
||||||
//if fuseServer != nil {
|
//if fuseServer != nil {
|
||||||
// err := setMassStorageImage(" ")
|
// err := setMassStorageImage(" ")
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
// log.Printf("Failed to unmount mass storage image: %v", err)
|
// logger.Infof("Failed to unmount mass storage image: %v", err)
|
||||||
// }
|
// }
|
||||||
// err = fuseServer.Unmount()
|
// err = fuseServer.Unmount()
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
// log.Printf("Failed to unmount fuse: %v", err)
|
// logger.Infof("Failed to unmount fuse: %v", err)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// os.Exit(0)
|
// os.Exit(0)
|
||||||
|
|
27
native.go
27
native.go
|
@ -5,8 +5,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"kvm/resource"
|
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
@ -14,6 +12,8 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/resource"
|
||||||
|
|
||||||
"github.com/pion/webrtc/v4/pkg/media"
|
"github.com/pion/webrtc/v4/pkg/media"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse
|
||||||
return nil, fmt.Errorf("error marshaling ctrl action: %w", err)
|
return nil, fmt.Errorf("error marshaling ctrl action: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("sending ctrl action", string(jsonData))
|
logger.Infof("sending ctrl action: %s", string(jsonData))
|
||||||
|
|
||||||
err = WriteCtrlMessage(jsonData)
|
err = WriteCtrlMessage(jsonData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -91,8 +91,8 @@ func WriteCtrlMessage(message []byte) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var nativeCtrlSocketListener net.Listener
|
var nativeCtrlSocketListener net.Listener //nolint:unused
|
||||||
var nativeVideoSocketListener net.Listener
|
var nativeVideoSocketListener net.Listener //nolint:unused
|
||||||
|
|
||||||
var ctrlClientConnected = make(chan struct{})
|
var ctrlClientConnected = make(chan struct{})
|
||||||
|
|
||||||
|
@ -104,16 +104,18 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC
|
||||||
// Remove the socket file if it already exists
|
// Remove the socket file if it already exists
|
||||||
if _, err := os.Stat(socketPath); err == nil {
|
if _, err := os.Stat(socketPath); err == nil {
|
||||||
if err := os.Remove(socketPath); err != nil {
|
if err := os.Remove(socketPath); err != nil {
|
||||||
log.Fatalf("Failed to remove existing socket file %s: %v", socketPath, err)
|
logger.Errorf("Failed to remove existing socket file %s: %v", socketPath, err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listener, err := net.Listen("unixpacket", socketPath)
|
listener, err := net.Listen("unixpacket", socketPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to start server on %s: %v", socketPath, err)
|
logger.Errorf("Failed to start server on %s: %v", socketPath, err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Server listening on %s", socketPath)
|
logger.Infof("Server listening on %s", socketPath)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := listener.Accept()
|
conn, err := listener.Accept()
|
||||||
|
@ -188,24 +190,23 @@ func handleCtrlClient(conn net.Conn) {
|
||||||
func handleVideoClient(conn net.Conn) {
|
func handleVideoClient(conn net.Conn) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
log.Printf("Native video socket client connected: %v", conn.RemoteAddr())
|
logger.Infof("Native video socket client connected: %v", conn.RemoteAddr())
|
||||||
|
|
||||||
inboundPacket := make([]byte, maxFrameSize)
|
inboundPacket := make([]byte, maxFrameSize)
|
||||||
lastFrame := time.Now()
|
lastFrame := time.Now()
|
||||||
for {
|
for {
|
||||||
n, err := conn.Read(inboundPacket)
|
n, err := conn.Read(inboundPacket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("error during read: %s", err)
|
logger.Warnf("error during read: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
sinceLastFrame := now.Sub(lastFrame)
|
sinceLastFrame := now.Sub(lastFrame)
|
||||||
lastFrame = now
|
lastFrame = now
|
||||||
//fmt.Println("Video packet received", n, sinceLastFrame)
|
|
||||||
if currentSession != nil {
|
if currentSession != nil {
|
||||||
err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame})
|
err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error writing sample", err)
|
logger.Warnf("error writing sample: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,7 +251,7 @@ func ExtractAndRunNativeBin() error {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
fmt.Printf("Binary started with PID: %d\n", cmd.Process.Pid)
|
logger.Infof("Binary started with PID: %d", cmd.Process.Pid)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
28
network.go
28
network.go
|
@ -56,14 +56,14 @@ func setDhcpClientState(active bool) {
|
||||||
|
|
||||||
cmd := exec.Command("/usr/bin/killall", signal, "udhcpc")
|
cmd := exec.Command("/usr/bin/killall", signal, "udhcpc")
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
fmt.Printf("network: setDhcpClientState: failed to change udhcpc state: %s\n", err)
|
logger.Warnf("network: setDhcpClientState: failed to change udhcpc state: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkNetworkState() {
|
func checkNetworkState() {
|
||||||
iface, err := netlink.LinkByName(NetIfName)
|
iface, err := netlink.LinkByName(NetIfName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("failed to get [%s] interface: %v\n", NetIfName, err)
|
logger.Warnf("failed to get [%s] interface: %v", NetIfName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ func checkNetworkState() {
|
||||||
|
|
||||||
addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL)
|
addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("failed to get addresses for [%s]: %v\n", NetIfName, err)
|
logger.Warnf("failed to get addresses for [%s]: %v", NetIfName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the link is going down, put udhcpc into idle mode.
|
// If the link is going down, put udhcpc into idle mode.
|
||||||
|
@ -89,10 +89,10 @@ func checkNetworkState() {
|
||||||
if addr.IP.To4() != nil {
|
if addr.IP.To4() != nil {
|
||||||
if !newState.Up && networkState.Up {
|
if !newState.Up && networkState.Up {
|
||||||
// If the network is going down, remove all IPv4 addresses from the interface.
|
// If the network is going down, remove all IPv4 addresses from the interface.
|
||||||
fmt.Printf("network: state transitioned to down, removing IPv4 address %s\n", addr.IP.String())
|
logger.Infof("network: state transitioned to down, removing IPv4 address %s", addr.IP.String())
|
||||||
err := netlink.AddrDel(iface, &addr)
|
err := netlink.AddrDel(iface, &addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("network: failed to delete %s", addr.IP.String())
|
logger.Warnf("network: failed to delete %s", addr.IP.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
newState.IPv4 = "..."
|
newState.IPv4 = "..."
|
||||||
|
@ -105,9 +105,9 @@ func checkNetworkState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if newState != networkState {
|
if newState != networkState {
|
||||||
fmt.Println("network state changed")
|
logger.Info("network state changed")
|
||||||
// restart MDNS
|
// restart MDNS
|
||||||
startMDNS()
|
_ = startMDNS()
|
||||||
networkState = newState
|
networkState = newState
|
||||||
requestDisplayUpdate()
|
requestDisplayUpdate()
|
||||||
}
|
}
|
||||||
|
@ -116,15 +116,15 @@ func checkNetworkState() {
|
||||||
func startMDNS() error {
|
func startMDNS() error {
|
||||||
// If server was previously running, stop it
|
// If server was previously running, stop it
|
||||||
if mDNSConn != nil {
|
if mDNSConn != nil {
|
||||||
fmt.Printf("Stopping mDNS server\n")
|
logger.Info("Stopping mDNS server")
|
||||||
err := mDNSConn.Close()
|
err := mDNSConn.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("failed to stop mDNS server: %v\n", err)
|
logger.Warnf("failed to stop mDNS server: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new server
|
// Start a new server
|
||||||
fmt.Printf("Starting mDNS server on jetkvm.local\n")
|
logger.Info("Starting mDNS server on jetkvm.local")
|
||||||
addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4)
|
addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -181,7 +181,7 @@ func getNTPServersFromDHCPInfo() ([]string, error) {
|
||||||
|
|
||||||
for _, server := range strings.Fields(val) {
|
for _, server := range strings.Fields(val) {
|
||||||
if net.ParseIP(server) == nil {
|
if net.ParseIP(server) == nil {
|
||||||
fmt.Printf("invalid NTP server IP: %s, ignoring ... \n", server)
|
logger.Infof("invalid NTP server IP: %s, ignoring", server)
|
||||||
}
|
}
|
||||||
servers = append(servers, server)
|
servers = append(servers, server)
|
||||||
}
|
}
|
||||||
|
@ -196,7 +196,7 @@ func init() {
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
|
||||||
if err := netlink.LinkSubscribe(updates, done); err != nil {
|
if err := netlink.LinkSubscribe(updates, done); err != nil {
|
||||||
fmt.Println("failed to subscribe to link updates: %v", err)
|
logger.Warnf("failed to subscribe to link updates: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,7 +210,7 @@ func init() {
|
||||||
select {
|
select {
|
||||||
case update := <-updates:
|
case update := <-updates:
|
||||||
if update.Link.Attrs().Name == NetIfName {
|
if update.Link.Attrs().Name == NetIfName {
|
||||||
fmt.Printf("link update: %+v\n", update)
|
logger.Infof("link update: %+v", update)
|
||||||
checkNetworkState()
|
checkNetworkState()
|
||||||
}
|
}
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
|
@ -222,6 +222,6 @@ func init() {
|
||||||
}()
|
}()
|
||||||
err := startMDNS()
|
err := startMDNS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("failed to run mDNS: %v", err)
|
logger.Warnf("failed to run mDNS: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
19
ntp.go
19
ntp.go
|
@ -3,7 +3,6 @@ package kvm
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
|
@ -21,7 +20,6 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
timeSynced = false
|
|
||||||
timeSyncRetryInterval = 0 * time.Second
|
timeSyncRetryInterval = 0 * time.Second
|
||||||
defaultNTPServers = []string{
|
defaultNTPServers = []string{
|
||||||
"time.cloudflare.com",
|
"time.cloudflare.com",
|
||||||
|
@ -37,16 +35,16 @@ func TimeSyncLoop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !networkState.Up {
|
if !networkState.Up {
|
||||||
log.Printf("Waiting for network to come up")
|
logger.Infof("Waiting for network to come up")
|
||||||
time.Sleep(timeSyncWaitNetUpInt)
|
time.Sleep(timeSyncWaitNetUpInt)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Syncing system time")
|
logger.Infof("Syncing system time")
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
err := SyncSystemTime()
|
err := SyncSystemTime()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to sync system time: %v", err)
|
logger.Warnf("Failed to sync system time: %v", err)
|
||||||
|
|
||||||
// retry after a delay
|
// retry after a delay
|
||||||
timeSyncRetryInterval += timeSyncRetryStep
|
timeSyncRetryInterval += timeSyncRetryStep
|
||||||
|
@ -58,8 +56,7 @@ func TimeSyncLoop() {
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Printf("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start))
|
logger.Infof("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start))
|
||||||
timeSynced = true
|
|
||||||
time.Sleep(timeSyncInterval) // after the first sync is done
|
time.Sleep(timeSyncInterval) // after the first sync is done
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,20 +76,20 @@ func SyncSystemTime() (err error) {
|
||||||
func queryNetworkTime() (*time.Time, error) {
|
func queryNetworkTime() (*time.Time, error) {
|
||||||
ntpServers, err := getNTPServersFromDHCPInfo()
|
ntpServers, err := getNTPServersFromDHCPInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to get NTP servers from DHCP info: %v\n", err)
|
logger.Warnf("failed to get NTP servers from DHCP info: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ntpServers == nil {
|
if ntpServers == nil {
|
||||||
ntpServers = defaultNTPServers
|
ntpServers = defaultNTPServers
|
||||||
log.Printf("Using default NTP servers: %v\n", ntpServers)
|
logger.Infof("Using default NTP servers: %v\n", ntpServers)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Using NTP servers from DHCP: %v\n", ntpServers)
|
logger.Infof("Using NTP servers from DHCP: %v\n", ntpServers)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, server := range ntpServers {
|
for _, server := range ntpServers {
|
||||||
now, err := queryNtpServer(server, timeSyncTimeout)
|
now, err := queryNtpServer(server, timeSyncTimeout)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Printf("NTP server [%s] returned time: %v\n", server, now)
|
logger.Infof("NTP server [%s] returned time: %v\n", server, now)
|
||||||
return now, nil
|
return now, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
25
ota.go
25
ota.go
|
@ -8,7 +8,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -77,7 +76,7 @@ func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease
|
||||||
query.Set("prerelease", fmt.Sprintf("%v", includePreRelease))
|
query.Set("prerelease", fmt.Sprintf("%v", includePreRelease))
|
||||||
updateUrl.RawQuery = query.Encode()
|
updateUrl.RawQuery = query.Encode()
|
||||||
|
|
||||||
fmt.Println("Checking for updates at:", updateUrl.String())
|
logger.Infof("Checking for updates at: %s", updateUrl)
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", updateUrl.String(), nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", updateUrl.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -230,7 +229,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32) error
|
||||||
}
|
}
|
||||||
|
|
||||||
hashSum := hash.Sum(nil)
|
hashSum := hash.Sum(nil)
|
||||||
fmt.Printf("SHA256 hash of %s: %x\n", path, hashSum)
|
logger.Infof("SHA256 hash of %s: %x", path, hashSum)
|
||||||
|
|
||||||
if hex.EncodeToString(hashSum) != expectedHash {
|
if hex.EncodeToString(hashSum) != expectedHash {
|
||||||
return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash)
|
return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash)
|
||||||
|
@ -272,7 +271,7 @@ var otaState = OTAState{}
|
||||||
func triggerOTAStateUpdate() {
|
func triggerOTAStateUpdate() {
|
||||||
go func() {
|
go func() {
|
||||||
if currentSession == nil {
|
if currentSession == nil {
|
||||||
log.Println("No active RPC session, skipping update state update")
|
logger.Info("No active RPC session, skipping update state update")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONRPCEvent("otaState", otaState, currentSession)
|
writeJSONRPCEvent("otaState", otaState, currentSession)
|
||||||
|
@ -280,7 +279,7 @@ func triggerOTAStateUpdate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error {
|
func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error {
|
||||||
log.Println("Trying to update...")
|
logger.Info("Trying to update...")
|
||||||
if otaState.Updating {
|
if otaState.Updating {
|
||||||
return fmt.Errorf("update already in progress")
|
return fmt.Errorf("update already in progress")
|
||||||
}
|
}
|
||||||
|
@ -315,7 +314,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
rebootNeeded := false
|
rebootNeeded := false
|
||||||
|
|
||||||
if appUpdateAvailable {
|
if appUpdateAvailable {
|
||||||
fmt.Printf("App update available: %s -> %s\n", local.AppVersion, remote.AppVersion)
|
logger.Infof("App update available: %s -> %s", local.AppVersion, remote.AppVersion)
|
||||||
|
|
||||||
err := downloadFile(ctx, "/userdata/jetkvm/jetkvm_app.update", remote.AppUrl, &otaState.AppDownloadProgress)
|
err := downloadFile(ctx, "/userdata/jetkvm/jetkvm_app.update", remote.AppUrl, &otaState.AppDownloadProgress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -341,14 +340,14 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
otaState.AppUpdateProgress = 1
|
otaState.AppUpdateProgress = 1
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
|
|
||||||
fmt.Println("App update downloaded")
|
logger.Info("App update downloaded")
|
||||||
rebootNeeded = true
|
rebootNeeded = true
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("App is up to date")
|
logger.Info("App is up to date")
|
||||||
}
|
}
|
||||||
|
|
||||||
if systemUpdateAvailable {
|
if systemUpdateAvailable {
|
||||||
fmt.Printf("System update available: %s -> %s\n", local.SystemVersion, remote.SystemVersion)
|
logger.Infof("System update available: %s -> %s", local.SystemVersion, remote.SystemVersion)
|
||||||
err := downloadFile(ctx, "/userdata/jetkvm/update_system.tar", remote.SystemUrl, &otaState.SystemDownloadProgress)
|
err := downloadFile(ctx, "/userdata/jetkvm/update_system.tar", remote.SystemUrl, &otaState.SystemDownloadProgress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Error downloading system update: %v", err)
|
otaState.Error = fmt.Sprintf("Error downloading system update: %v", err)
|
||||||
|
@ -366,7 +365,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println("System update downloaded")
|
logger.Info("System update downloaded")
|
||||||
verifyFinished := time.Now()
|
verifyFinished := time.Now()
|
||||||
otaState.SystemVerifiedAt = &verifyFinished
|
otaState.SystemVerifiedAt = &verifyFinished
|
||||||
otaState.SystemVerificationProgress = 1
|
otaState.SystemVerificationProgress = 1
|
||||||
|
@ -413,17 +412,17 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output)
|
return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("rk_ota success, output: %s\n", output)
|
logger.Infof("rk_ota success, output: %s", output)
|
||||||
otaState.SystemUpdateProgress = 1
|
otaState.SystemUpdateProgress = 1
|
||||||
otaState.SystemUpdatedAt = &verifyFinished
|
otaState.SystemUpdatedAt = &verifyFinished
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
rebootNeeded = true
|
rebootNeeded = true
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("System is up to date")
|
logger.Info("System is up to date")
|
||||||
}
|
}
|
||||||
|
|
||||||
if rebootNeeded {
|
if rebootNeeded {
|
||||||
fmt.Println("System Rebooting in 10s...")
|
logger.Info("System Rebooting in 10s")
|
||||||
time.Sleep(10 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
cmd := exec.Command("reboot")
|
cmd := exec.Command("reboot")
|
||||||
err := cmd.Start()
|
err := cmd.Start()
|
||||||
|
|
|
@ -49,7 +49,7 @@ func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) (
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
buf := make([]byte, 0)
|
var buf []byte
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case data := <-diskReadChan:
|
case data := <-diskReadChan:
|
||||||
|
|
14
serial.go
14
serial.go
|
@ -16,14 +16,14 @@ const serialPortPath = "/dev/ttyS3"
|
||||||
var port serial.Port
|
var port serial.Port
|
||||||
|
|
||||||
func mountATXControl() error {
|
func mountATXControl() error {
|
||||||
port.SetMode(defaultMode)
|
_ = port.SetMode(defaultMode)
|
||||||
go runATXControl()
|
go runATXControl()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmountATXControl() error {
|
func unmountATXControl() error {
|
||||||
reopenSerialPort()
|
_ = reopenSerialPort()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,13 +122,13 @@ func pressATXResetButton(duration time.Duration) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func mountDCControl() error {
|
func mountDCControl() error {
|
||||||
port.SetMode(defaultMode)
|
_ = port.SetMode(defaultMode)
|
||||||
go runDCControl()
|
go runDCControl()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmountDCControl() error {
|
func unmountDCControl() error {
|
||||||
reopenSerialPort()
|
_ = reopenSerialPort()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,11 +212,11 @@ var defaultMode = &serial.Mode{
|
||||||
}
|
}
|
||||||
|
|
||||||
func initSerialPort() {
|
func initSerialPort() {
|
||||||
reopenSerialPort()
|
_ = reopenSerialPort()
|
||||||
if config.ActiveExtension == "atx-power" {
|
if config.ActiveExtension == "atx-power" {
|
||||||
mountATXControl()
|
_ = mountATXControl()
|
||||||
} else if config.ActiveExtension == "dc-power" {
|
} else if config.ActiveExtension == "dc-power" {
|
||||||
mountDCControl()
|
_ = mountDCControl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,11 +55,13 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
|
||||||
var size TerminalSize
|
var size TerminalSize
|
||||||
err := json.Unmarshal([]byte(msg.Data), &size)
|
err := json.Unmarshal([]byte(msg.Data), &size)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pty.Setsize(ptmx, &pty.Winsize{
|
err = pty.Setsize(ptmx, &pty.Winsize{
|
||||||
Rows: uint16(size.Rows),
|
Rows: uint16(size.Rows),
|
||||||
Cols: uint16(size.Cols),
|
Cols: uint16(size.Cols),
|
||||||
})
|
})
|
||||||
return
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logger.Errorf("Failed to parse terminal size: %v", err)
|
logger.Errorf("Failed to parse terminal size: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -74,7 +76,7 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
|
||||||
ptmx.Close()
|
ptmx.Close()
|
||||||
}
|
}
|
||||||
if cmd != nil && cmd.Process != nil {
|
if cmd != nil && cmd.Process != nil {
|
||||||
cmd.Process.Kill()
|
_ = cmd.Process.Kill()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,129 @@
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useJsonRpc } from "../hooks/useJsonRpc";
|
||||||
|
import notifications from "../notifications";
|
||||||
|
import { SettingsItem } from "../routes/devices.$id.settings";
|
||||||
|
import Checkbox from "./Checkbox";
|
||||||
|
|
||||||
|
export interface USBConfig {
|
||||||
|
vendor_id: string;
|
||||||
|
product_id: string;
|
||||||
|
serial_number: string;
|
||||||
|
manufacturer: string;
|
||||||
|
product: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsbDeviceConfig {
|
||||||
|
keyboard: boolean;
|
||||||
|
absolute_mouse: boolean;
|
||||||
|
relative_mouse: boolean;
|
||||||
|
mass_storage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultUsbDeviceConfig: UsbDeviceConfig = {
|
||||||
|
keyboard: true,
|
||||||
|
absolute_mouse: true,
|
||||||
|
relative_mouse: true,
|
||||||
|
mass_storage: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UsbDeviceSetting() {
|
||||||
|
const [send] = useJsonRpc();
|
||||||
|
|
||||||
|
const [usbDeviceConfig, setUsbDeviceConfig] = useState<UsbDeviceConfig>(defaultUsbDeviceConfig);
|
||||||
|
const syncUsbDeviceConfig = useCallback(() => {
|
||||||
|
send("getUsbDevices", {}, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
console.error("Failed to load USB devices:", resp.error);
|
||||||
|
notifications.error(
|
||||||
|
`Failed to load USB devices: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log("syncUsbDeviceConfig#getUsbDevices result:", resp.result);
|
||||||
|
const usbConfigState = resp.result as UsbDeviceConfig;
|
||||||
|
setUsbDeviceConfig(usbConfigState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
|
const handleUsbConfigChange = useCallback(
|
||||||
|
(devices: UsbDeviceConfig) => {
|
||||||
|
send("setUsbDevices", { devices }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to set usb devices: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifications.success(
|
||||||
|
`USB Devices updated`
|
||||||
|
);
|
||||||
|
syncUsbDeviceConfig();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[send, syncUsbDeviceConfig],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onUsbConfigItemChange = useCallback((key: keyof UsbDeviceConfig) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setUsbDeviceConfig((val) => {
|
||||||
|
val[key] = e.target.checked;
|
||||||
|
handleUsbConfigChange(val);
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
}, [handleUsbConfigChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
syncUsbDeviceConfig();
|
||||||
|
}, [syncUsbDeviceConfig]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItem
|
||||||
|
title="Enable Keyboard"
|
||||||
|
description="Enable Keyboard"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={usbDeviceConfig.keyboard}
|
||||||
|
onChange={onUsbConfigItemChange("keyboard")}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItem
|
||||||
|
title="Enable Absolute Mouse (Pointer)"
|
||||||
|
description="Enable Absolute Mouse (Pointer)"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={usbDeviceConfig.absolute_mouse}
|
||||||
|
onChange={onUsbConfigItemChange("absolute_mouse")}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItem
|
||||||
|
title="Enable Relative Mouse"
|
||||||
|
description="Enable Relative Mouse"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={usbDeviceConfig.relative_mouse}
|
||||||
|
onChange={onUsbConfigItemChange("relative_mouse")}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItem
|
||||||
|
title="Enable USB Mass Storage"
|
||||||
|
description="Sometimes it might need to be disabled to prevent issues with certain devices"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={usbDeviceConfig.mass_storage}
|
||||||
|
onChange={onUsbConfigItemChange("mass_storage")}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import notifications from "../notifications";
|
import notifications from "../notifications";
|
||||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
import { UsbConfigSetting } from "../components/UsbConfigSetting";
|
import { UsbConfigSetting } from "../components/UsbConfigSetting";
|
||||||
|
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
|
||||||
import { FeatureFlag } from "../components/FeatureFlag";
|
import { FeatureFlag } from "../components/FeatureFlag";
|
||||||
|
|
||||||
export default function SettingsHardwareRoute() {
|
export default function SettingsHardwareRoute() {
|
||||||
|
@ -132,6 +133,10 @@ export default function SettingsHardwareRoute() {
|
||||||
<FeatureFlag minAppVersion="0.3.8">
|
<FeatureFlag minAppVersion="0.3.8">
|
||||||
<UsbConfigSetting />
|
<UsbConfigSetting />
|
||||||
</FeatureFlag>
|
</FeatureFlag>
|
||||||
|
|
||||||
|
<FeatureFlag minAppVersion="0.3.8">
|
||||||
|
<UsbDeviceSetting />
|
||||||
|
</FeatureFlag>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import { useCallback, useEffect, useState } from "react";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { cx } from "../cva.config";
|
import { cx } from "../cva.config";
|
||||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||||
|
import { useFeatureFlag } from "../hooks/useFeatureFlag";
|
||||||
|
import { FeatureFlag } from "../components/FeatureFlag";
|
||||||
|
|
||||||
type ScrollSensitivity = "low" | "default" | "high";
|
type ScrollSensitivity = "low" | "default" | "high";
|
||||||
|
|
||||||
|
@ -21,6 +23,8 @@ export default function SettingsKeyboardMouseRoute() {
|
||||||
state => state.setScrollSensitivity,
|
state => state.setScrollSensitivity,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { isEnabled: isScrollSensitivityEnabled } = useFeatureFlag("0.3.8");
|
||||||
|
|
||||||
const [jiggler, setJiggler] = useState(false);
|
const [jiggler, setJiggler] = useState(false);
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
const [send] = useJsonRpc();
|
||||||
|
@ -31,11 +35,13 @@ export default function SettingsKeyboardMouseRoute() {
|
||||||
setJiggler(resp.result as boolean);
|
setJiggler(resp.result as boolean);
|
||||||
});
|
});
|
||||||
|
|
||||||
send("getScrollSensitivity", {}, resp => {
|
if (isScrollSensitivityEnabled) {
|
||||||
if ("error" in resp) return;
|
send("getScrollSensitivity", {}, resp => {
|
||||||
setScrollSensitivity(resp.result as ScrollSensitivity);
|
if ("error" in resp) return;
|
||||||
});
|
setScrollSensitivity(resp.result as ScrollSensitivity);
|
||||||
}, [send, setScrollSensitivity]);
|
});
|
||||||
|
}
|
||||||
|
}, [isScrollSensitivityEnabled, send, setScrollSensitivity]);
|
||||||
|
|
||||||
const handleJigglerChange = (enabled: boolean) => {
|
const handleJigglerChange = (enabled: boolean) => {
|
||||||
send("setJigglerState", { enabled }, resp => {
|
send("setJigglerState", { enabled }, resp => {
|
||||||
|
@ -82,25 +88,28 @@ export default function SettingsKeyboardMouseRoute() {
|
||||||
onChange={e => setHideCursor(e.target.checked)}
|
onChange={e => setHideCursor(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
<SettingsItem
|
|
||||||
title="Scroll Sensitivity"
|
<FeatureFlag minAppVersion="0.3.8" name="Scroll Sensitivity">
|
||||||
description="Adjust the scroll sensitivity"
|
<SettingsItem
|
||||||
>
|
title="Scroll Sensitivity"
|
||||||
<SelectMenuBasic
|
description="Adjust the scroll sensitivity"
|
||||||
size="SM"
|
>
|
||||||
label=""
|
<SelectMenuBasic
|
||||||
fullWidth
|
size="SM"
|
||||||
value={scrollSensitivity}
|
label=""
|
||||||
onChange={onScrollSensitivityChange}
|
fullWidth
|
||||||
options={
|
value={scrollSensitivity}
|
||||||
[
|
onChange={onScrollSensitivityChange}
|
||||||
{ label: "Low", value: "low" },
|
options={
|
||||||
{ label: "Default", value: "default" },
|
[
|
||||||
{ label: "High", value: "high" },
|
{ label: "Low", value: "low" },
|
||||||
] as { label: string; value: ScrollSensitivity }[]
|
{ label: "Default", value: "default" },
|
||||||
}
|
{ label: "High", value: "high" },
|
||||||
/>
|
] as { label: string; value: ScrollSensitivity }[]
|
||||||
</SettingsItem>
|
}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</FeatureFlag>
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Jiggler"
|
title="Jiggler"
|
||||||
|
|
482
usb.go
482
usb.go
|
@ -1,486 +1,66 @@
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
gadget "github.com/openstadia/go-usb-gadget"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const configFSPath = "/sys/kernel/config"
|
var gadget *usbgadget.UsbGadget
|
||||||
const gadgetPath = "/sys/kernel/config/usb_gadget"
|
|
||||||
const kvmGadgetPath = "/sys/kernel/config/usb_gadget/jetkvm"
|
|
||||||
const configC1Path = "/sys/kernel/config/usb_gadget/jetkvm/configs/c.1"
|
|
||||||
|
|
||||||
func mountConfigFS() error {
|
// initUsbGadget initializes the USB gadget.
|
||||||
_, err := os.Stat(gadgetPath)
|
// call it only after the config is loaded.
|
||||||
if os.IsNotExist(err) {
|
func initUsbGadget() {
|
||||||
err = exec.Command("mount", "-t", "configfs", "none", configFSPath).Run()
|
gadget = usbgadget.NewUsbGadget(
|
||||||
if err != nil {
|
"jetkvm",
|
||||||
return fmt.Errorf("failed to mount configfs: %w", err)
|
config.UsbDevices,
|
||||||
|
config.UsbConfig,
|
||||||
|
&logger,
|
||||||
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
checkUSBState()
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
}
|
}
|
||||||
} else {
|
}()
|
||||||
return fmt.Errorf("unable to access usb gadget path: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
|
||||||
ensureConfigLoaded()
|
|
||||||
|
|
||||||
_ = os.MkdirAll(imagesFolder, 0755)
|
|
||||||
udcs := gadget.GetUdcs()
|
|
||||||
if len(udcs) < 1 {
|
|
||||||
usbLogger.Error("no udc found, skipping USB stack init")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
udc = udcs[0]
|
|
||||||
_, err := os.Stat(kvmGadgetPath)
|
|
||||||
if err == nil {
|
|
||||||
logger.Info("usb gadget already exists, skipping usb gadget initialization")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = mountConfigFS()
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("failed to mount configfs: %v, usb stack might not function properly", err)
|
|
||||||
}
|
|
||||||
err = writeGadgetConfig()
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("failed to start gadget: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: read hid reports(capslock, numlock, etc) from keyboardHidFile
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateGadgetConfig() error {
|
|
||||||
LoadConfig()
|
|
||||||
gadgetAttrs := [][]string{
|
|
||||||
{"idVendor", config.UsbConfig.VendorId},
|
|
||||||
{"idProduct", config.UsbConfig.ProductId},
|
|
||||||
}
|
|
||||||
err := writeGadgetAttrs(kvmGadgetPath, gadgetAttrs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Successfully updated usb gadget attributes: %v", gadgetAttrs)
|
|
||||||
|
|
||||||
strAttrs := [][]string{
|
|
||||||
{"serialnumber", config.UsbConfig.SerialNumber},
|
|
||||||
{"manufacturer", config.UsbConfig.Manufacturer},
|
|
||||||
{"product", config.UsbConfig.Product},
|
|
||||||
}
|
|
||||||
gadgetStringsPath := filepath.Join(kvmGadgetPath, "strings", "0x409")
|
|
||||||
err = os.MkdirAll(gadgetStringsPath, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = writeGadgetAttrs(gadgetStringsPath, strAttrs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Successfully updated usb string attributes: %s", strAttrs)
|
|
||||||
|
|
||||||
err = rebindUsb()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeGadgetAttrs(basePath string, attrs [][]string) error {
|
|
||||||
for _, item := range attrs {
|
|
||||||
filePath := filepath.Join(basePath, item[0])
|
|
||||||
err := os.WriteFile(filePath, []byte(item[1]), 0644)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to write to %s: %w", filePath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeGadgetConfig() error {
|
|
||||||
if _, err := os.Stat(gadgetPath); os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("USB gadget path does not exist: %s", gadgetPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := os.MkdirAll(kvmGadgetPath, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writeGadgetAttrs(kvmGadgetPath, [][]string{
|
|
||||||
{"bcdUSB", "0x0200"}, //USB 2.0
|
|
||||||
{"idVendor", config.UsbConfig.VendorId}, //The Linux Foundation
|
|
||||||
{"idProduct", config.UsbConfig.ProductId}, //Multifunction Composite Gadget¬
|
|
||||||
{"bcdDevice", "0100"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
gadgetStringsPath := filepath.Join(kvmGadgetPath, "strings", "0x409")
|
|
||||||
err = os.MkdirAll(gadgetStringsPath, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writeGadgetAttrs(gadgetStringsPath, [][]string{
|
|
||||||
{"serialnumber", GetDeviceID()},
|
|
||||||
{"manufacturer", config.UsbConfig.Manufacturer},
|
|
||||||
{"product", config.UsbConfig.Product},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
configC1StringsPath := path.Join(configC1Path, "strings", "0x409")
|
|
||||||
err = os.MkdirAll(configC1StringsPath, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writeGadgetAttrs(configC1Path, [][]string{
|
|
||||||
{"MaxPower", "250"}, //in unit of 2mA
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writeGadgetAttrs(configC1StringsPath, [][]string{
|
|
||||||
{"configuration", "Config 1: HID"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
//keyboard HID
|
|
||||||
hid0Path := path.Join(kvmGadgetPath, "functions", "hid.usb0")
|
|
||||||
err = os.MkdirAll(hid0Path, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = writeGadgetAttrs(hid0Path, [][]string{
|
|
||||||
{"protocol", "1"},
|
|
||||||
{"subclass", "1"},
|
|
||||||
{"report_length", "8"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(path.Join(hid0Path, "report_desc"), KeyboardReportDesc, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
//mouse HID
|
|
||||||
hid1Path := path.Join(kvmGadgetPath, "functions", "hid.usb1")
|
|
||||||
err = os.MkdirAll(hid1Path, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = writeGadgetAttrs(hid1Path, [][]string{
|
|
||||||
{"protocol", "2"},
|
|
||||||
{"subclass", "1"},
|
|
||||||
{"report_length", "6"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(path.Join(hid1Path, "report_desc"), CombinedMouseReportDesc, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
//mass storage
|
|
||||||
massStoragePath := path.Join(kvmGadgetPath, "functions", "mass_storage.usb0")
|
|
||||||
err = os.MkdirAll(massStoragePath, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writeGadgetAttrs(massStoragePath, [][]string{
|
|
||||||
{"stall", "1"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
lun0Path := path.Join(massStoragePath, "lun.0")
|
|
||||||
err = os.MkdirAll(lun0Path, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = writeGadgetAttrs(lun0Path, [][]string{
|
|
||||||
{"cdrom", "1"},
|
|
||||||
{"ro", "1"},
|
|
||||||
{"removable", "1"},
|
|
||||||
{"file", "\n"},
|
|
||||||
{"inquiry_string", "JetKVM Virtual Media"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Symlink(hid0Path, path.Join(configC1Path, "hid.usb0"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Symlink(hid1Path, path.Join(configC1Path, "hid.usb1"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Symlink(massStoragePath, path.Join(configC1Path, "mass_storage.usb0"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(path.Join(kvmGadgetPath, "UDC"), []byte(udc), 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func rebindUsb() error {
|
|
||||||
err := os.WriteFile("/sys/bus/platform/drivers/dwc3/unbind", []byte(udc), 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = os.WriteFile("/sys/bus/platform/drivers/dwc3/bind", []byte(udc), 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var keyboardHidFile *os.File
|
|
||||||
var keyboardLock = sync.Mutex{}
|
|
||||||
var mouseHidFile *os.File
|
|
||||||
var mouseLock = sync.Mutex{}
|
|
||||||
|
|
||||||
func rpcKeyboardReport(modifier uint8, keys []uint8) error {
|
func rpcKeyboardReport(modifier uint8, keys []uint8) error {
|
||||||
keyboardLock.Lock()
|
return gadget.KeyboardReport(modifier, keys)
|
||||||
defer keyboardLock.Unlock()
|
|
||||||
if keyboardHidFile == nil {
|
|
||||||
var err error
|
|
||||||
keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open hidg0: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(keys) > 6 {
|
|
||||||
keys = keys[:6]
|
|
||||||
}
|
|
||||||
if len(keys) < 6 {
|
|
||||||
keys = append(keys, make([]uint8, 6-len(keys))...)
|
|
||||||
}
|
|
||||||
_, err := keyboardHidFile.Write([]byte{modifier, 0, keys[0], keys[1], keys[2], keys[3], keys[4], keys[5]})
|
|
||||||
if err != nil {
|
|
||||||
keyboardHidFile.Close()
|
|
||||||
keyboardHidFile = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
resetUserInputTime()
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcAbsMouseReport(x, y int, buttons uint8) error {
|
func rpcAbsMouseReport(x, y int, buttons uint8) error {
|
||||||
mouseLock.Lock()
|
return gadget.AbsMouseReport(x, y, buttons)
|
||||||
defer mouseLock.Unlock()
|
|
||||||
if mouseHidFile == nil {
|
|
||||||
var err error
|
|
||||||
mouseHidFile, err = os.OpenFile("/dev/hidg1", os.O_RDWR, 0666)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open hidg1: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resetUserInputTime()
|
|
||||||
_, err := mouseHidFile.Write([]byte{
|
|
||||||
1, // Report ID 1
|
|
||||||
buttons, // Buttons
|
|
||||||
uint8(x), // X Low Byte
|
|
||||||
uint8(x >> 8), // X High Byte
|
|
||||||
uint8(y), // Y Low Byte
|
|
||||||
uint8(y >> 8), // Y High Byte
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
mouseHidFile.Close()
|
|
||||||
mouseHidFile = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var accumulatedWheelY float64 = 0
|
|
||||||
|
|
||||||
func rpcWheelReport(wheelY int8) error {
|
func rpcWheelReport(wheelY int8) error {
|
||||||
if mouseHidFile == nil {
|
return gadget.AbsMouseWheelReport(wheelY)
|
||||||
return errors.New("hid not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accumulate the wheelY value with finer granularity
|
|
||||||
// Reduce divisor from 8.0 to a smaller value (e.g., 2.0 or 4.0)
|
|
||||||
accumulatedWheelY += float64(wheelY) / 4.0
|
|
||||||
|
|
||||||
// Lower the threshold for sending a report (0.25 instead of 1.0)
|
|
||||||
if abs(accumulatedWheelY) >= 0.25 {
|
|
||||||
// Scale the wheel value appropriately for the HID report
|
|
||||||
// The descriptor uses an 8-bit signed value (-127 to 127)
|
|
||||||
scaledWheelY := int8(accumulatedWheelY * 0.5) // Scale down to prevent too much scrolling
|
|
||||||
|
|
||||||
_, err := mouseHidFile.Write([]byte{
|
|
||||||
2, // Report ID 2
|
|
||||||
byte(scaledWheelY), // Scaled Wheel Y (signed)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Reset the accumulator, keeping any remainder
|
|
||||||
accumulatedWheelY -= float64(scaledWheelY) / 0.5 // Adjust based on the scaling factor
|
|
||||||
|
|
||||||
resetUserInputTime()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to get absolute value of float64
|
|
||||||
func abs(x float64) float64 {
|
|
||||||
if x < 0 {
|
|
||||||
return -x
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var usbState = "unknown"
|
var usbState = "unknown"
|
||||||
|
|
||||||
func rpcGetUSBState() (state string) {
|
func rpcGetUSBState() (state string) {
|
||||||
stateBytes, err := os.ReadFile("/sys/class/udc/ffb00000.usb/state")
|
return gadget.GetUsbState()
|
||||||
if err != nil {
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(string(stateBytes))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func triggerUSBStateUpdate() {
|
func triggerUSBStateUpdate() {
|
||||||
go func() {
|
go func() {
|
||||||
if currentSession == nil {
|
if currentSession == nil {
|
||||||
log.Println("No active RPC session, skipping update state update")
|
logger.Info("No active RPC session, skipping update state update")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONRPCEvent("usbState", usbState, currentSession)
|
writeJSONRPCEvent("usbState", usbState, currentSession)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
var udc string
|
func checkUSBState() {
|
||||||
|
newState := gadget.GetUsbState()
|
||||||
|
if newState == usbState {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
usbState = newState
|
||||||
|
|
||||||
func init() {
|
logger.Infof("USB state changed from %s to %s", usbState, newState)
|
||||||
ensureConfigLoaded()
|
requestDisplayUpdate()
|
||||||
|
triggerUSBStateUpdate()
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
newState := rpcGetUSBState()
|
|
||||||
if newState != usbState {
|
|
||||||
log.Printf("USB state changed from %s to %s", usbState, newState)
|
|
||||||
usbState = newState
|
|
||||||
requestDisplayUpdate()
|
|
||||||
triggerUSBStateUpdate()
|
|
||||||
}
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Source: https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt
|
|
||||||
var KeyboardReportDesc = []byte{
|
|
||||||
0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
|
|
||||||
0x09, 0x06, /* USAGE (Keyboard) */
|
|
||||||
0xa1, 0x01, /* COLLECTION (Application) */
|
|
||||||
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
|
|
||||||
0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */
|
|
||||||
0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
|
|
||||||
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
|
|
||||||
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
|
|
||||||
0x75, 0x01, /* REPORT_SIZE (1) */
|
|
||||||
0x95, 0x08, /* REPORT_COUNT (8) */
|
|
||||||
0x81, 0x02, /* INPUT (Data,Var,Abs) */
|
|
||||||
0x95, 0x01, /* REPORT_COUNT (1) */
|
|
||||||
0x75, 0x08, /* REPORT_SIZE (8) */
|
|
||||||
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
|
|
||||||
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) */
|
|
||||||
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 */
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combined absolute and relative mouse report descriptor with report ID
|
|
||||||
var CombinedMouseReportDesc = []byte{
|
|
||||||
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
|
|
||||||
0x09, 0x02, // Usage (Mouse)
|
|
||||||
0xA1, 0x01, // Collection (Application)
|
|
||||||
|
|
||||||
// Report ID 1: Absolute Mouse Movement
|
|
||||||
0x85, 0x01, // Report ID (1)
|
|
||||||
0x09, 0x01, // Usage (Pointer)
|
|
||||||
0xA1, 0x00, // Collection (Physical)
|
|
||||||
0x05, 0x09, // Usage Page (Button)
|
|
||||||
0x19, 0x01, // Usage Minimum (0x01)
|
|
||||||
0x29, 0x03, // Usage Maximum (0x03)
|
|
||||||
0x15, 0x00, // Logical Minimum (0)
|
|
||||||
0x25, 0x01, // Logical Maximum (1)
|
|
||||||
0x75, 0x01, // Report Size (1)
|
|
||||||
0x95, 0x03, // Report Count (3)
|
|
||||||
0x81, 0x02, // Input (Data, Var, Abs)
|
|
||||||
0x95, 0x01, // Report Count (1)
|
|
||||||
0x75, 0x05, // Report Size (5)
|
|
||||||
0x81, 0x03, // Input (Cnst, Var, Abs)
|
|
||||||
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
|
|
||||||
0x09, 0x30, // Usage (X)
|
|
||||||
0x09, 0x31, // Usage (Y)
|
|
||||||
0x16, 0x00, 0x00, // Logical Minimum (0)
|
|
||||||
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
|
|
||||||
0x36, 0x00, 0x00, // Physical Minimum (0)
|
|
||||||
0x46, 0xFF, 0x7F, // Physical Maximum (32767)
|
|
||||||
0x75, 0x10, // Report Size (16)
|
|
||||||
0x95, 0x02, // Report Count (2)
|
|
||||||
0x81, 0x02, // Input (Data, Var, Abs)
|
|
||||||
0xC0, // End Collection
|
|
||||||
|
|
||||||
// Report ID 2: Relative Wheel Movement
|
|
||||||
0x85, 0x02, // Report ID (2)
|
|
||||||
0x09, 0x38, // Usage (Wheel)
|
|
||||||
0x15, 0x81, // Logical Minimum (-127)
|
|
||||||
0x25, 0x7F, // Logical Maximum (127)
|
|
||||||
0x75, 0x08, // Report Size (8)
|
|
||||||
0x95, 0x01, // Report Count (1)
|
|
||||||
0x81, 0x06, // Input (Data, Var, Rel)
|
|
||||||
|
|
||||||
0xC0, // End Collection
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"kvm/resource"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
@ -17,54 +15,58 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/psanford/httpreadat"
|
"github.com/psanford/httpreadat"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/resource"
|
||||||
)
|
)
|
||||||
|
|
||||||
const massStorageName = "mass_storage.usb0"
|
|
||||||
|
|
||||||
var massStorageFunctionPath = path.Join(gadgetPath, "jetkvm", "functions", massStorageName)
|
|
||||||
|
|
||||||
func writeFile(path string, data string) error {
|
func writeFile(path string, data string) error {
|
||||||
return os.WriteFile(path, []byte(data), 0644)
|
return os.WriteFile(path, []byte(data), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setMassStorageImage(imagePath string) error {
|
func setMassStorageImage(imagePath string) error {
|
||||||
err := writeFile(path.Join(massStorageFunctionPath, "lun.0", "file"), imagePath)
|
massStorageFunctionPath, err := gadget.GetConfigPath("mass_storage_lun0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get mass storage path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeFile(path.Join(massStorageFunctionPath, "file"), imagePath); err != nil {
|
||||||
return fmt.Errorf("failed to set image path: %w", err)
|
return fmt.Errorf("failed to set image path: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setMassStorageMode(cdrom bool) error {
|
func setMassStorageMode(cdrom bool) error {
|
||||||
|
massStorageFunctionPath, err := gadget.GetConfigPath("mass_storage_lun0")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get mass storage path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
mode := "0"
|
mode := "0"
|
||||||
if cdrom {
|
if cdrom {
|
||||||
mode = "1"
|
mode = "1"
|
||||||
}
|
}
|
||||||
err := writeFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"), mode)
|
if err := writeFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"), mode); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set cdrom mode: %w", err)
|
return fmt.Errorf("failed to set cdrom mode: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func onDiskMessage(msg webrtc.DataChannelMessage) {
|
func onDiskMessage(msg webrtc.DataChannelMessage) {
|
||||||
fmt.Println("Disk Message, len:", len(msg.Data))
|
logger.Infof("Disk Message, len: %d", len(msg.Data))
|
||||||
diskReadChan <- msg.Data
|
diskReadChan <- msg.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
func mountImage(imagePath string) error {
|
func mountImage(imagePath string) error {
|
||||||
err := setMassStorageImage("")
|
err := setMassStorageImage("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Remove Mass Storage Image Error", err)
|
return fmt.Errorf("Remove Mass Storage Image Error: %w", err)
|
||||||
}
|
}
|
||||||
err = setMassStorageImage(imagePath)
|
err = setMassStorageImage(imagePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Set Mass Storage Image Error", err)
|
return fmt.Errorf("Set Mass Storage Image Error: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -74,7 +76,7 @@ var nbdDevice *NBDDevice
|
||||||
const imagesFolder = "/userdata/jetkvm/images"
|
const imagesFolder = "/userdata/jetkvm/images"
|
||||||
|
|
||||||
func rpcMountBuiltInImage(filename string) error {
|
func rpcMountBuiltInImage(filename string) error {
|
||||||
log.Println("Mount Built-In Image", filename)
|
logger.Infof("Mount Built-In Image: %s", filename)
|
||||||
_ = os.MkdirAll(imagesFolder, 0755)
|
_ = os.MkdirAll(imagesFolder, 0755)
|
||||||
imagePath := filepath.Join(imagesFolder, filename)
|
imagePath := filepath.Join(imagesFolder, filename)
|
||||||
|
|
||||||
|
@ -108,6 +110,11 @@ func rpcMountBuiltInImage(filename string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMassStorageMode() (bool, error) {
|
func getMassStorageMode() (bool, error) {
|
||||||
|
massStorageFunctionPath, err := gadget.GetConfigPath("mass_storage_lun0")
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get mass storage path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"))
|
data, err := os.ReadFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to read cdrom mode: %w", err)
|
return false, fmt.Errorf("failed to read cdrom mode: %w", err)
|
||||||
|
@ -166,7 +173,7 @@ func rpcUnmountImage() error {
|
||||||
defer virtualMediaStateMutex.Unlock()
|
defer virtualMediaStateMutex.Unlock()
|
||||||
err := setMassStorageImage("\n")
|
err := setMassStorageImage("\n")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Remove Mass Storage Image Error", err)
|
logger.Warnf("Remove Mass Storage Image Error: %v", err)
|
||||||
}
|
}
|
||||||
//TODO: check if we still need it
|
//TODO: check if we still need it
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
3
video.go
3
video.go
|
@ -2,7 +2,6 @@ package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// max frame size for 1080p video, specified in mpp venc setting
|
// max frame size for 1080p video, specified in mpp venc setting
|
||||||
|
@ -39,7 +38,7 @@ func HandleVideoStateMessage(event CtrlResponse) {
|
||||||
videoState := VideoInputState{}
|
videoState := VideoInputState{}
|
||||||
err := json.Unmarshal(event.Data, &videoState)
|
err := json.Unmarshal(event.Data, &videoState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error parsing video state json:", err)
|
logger.Warnf("Error parsing video state json: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lastVideoState = videoState
|
lastVideoState = videoState
|
||||||
|
|
13
webrtc.go
13
webrtc.go
|
@ -3,7 +3,6 @@ package kvm
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -74,17 +73,17 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
|
|
||||||
if config.IsCloud {
|
if config.IsCloud {
|
||||||
if config.ICEServers == nil {
|
if config.ICEServers == nil {
|
||||||
fmt.Printf("ICE Servers not provided by cloud")
|
logger.Info("ICE Servers not provided by cloud")
|
||||||
} else {
|
} else {
|
||||||
iceServer.URLs = config.ICEServers
|
iceServer.URLs = config.ICEServers
|
||||||
fmt.Printf("Using ICE Servers provided by cloud: %v\n", iceServer.URLs)
|
logger.Infof("Using ICE Servers provided by cloud: %v", iceServer.URLs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.LocalIP == "" || net.ParseIP(config.LocalIP) == nil {
|
if config.LocalIP == "" || net.ParseIP(config.LocalIP) == nil {
|
||||||
fmt.Printf("Local IP address %v not provided or invalid, won't set NAT1To1IPs\n", config.LocalIP)
|
logger.Infof("Local IP address %v not provided or invalid, won't set NAT1To1IPs", config.LocalIP)
|
||||||
} else {
|
} else {
|
||||||
webrtcSettingEngine.SetNAT1To1IPs([]string{config.LocalIP}, webrtc.ICECandidateTypeSrflx)
|
webrtcSettingEngine.SetNAT1To1IPs([]string{config.LocalIP}, webrtc.ICECandidateTypeSrflx)
|
||||||
fmt.Printf("Setting NAT1To1IPs to %s\n", config.LocalIP)
|
logger.Infof("Setting NAT1To1IPs to %s", config.LocalIP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +97,7 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
session := &Session{peerConnection: peerConnection}
|
session := &Session{peerConnection: peerConnection}
|
||||||
|
|
||||||
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
|
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
|
||||||
fmt.Printf("New DataChannel %s %d\n", d.Label(), d.ID())
|
logger.Infof("New DataChannel %s %d", d.Label(), d.ID())
|
||||||
switch d.Label() {
|
switch d.Label() {
|
||||||
case "rpc":
|
case "rpc":
|
||||||
session.RPCChannel = d
|
session.RPCChannel = d
|
||||||
|
@ -146,7 +145,7 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
var isConnected bool
|
var isConnected bool
|
||||||
|
|
||||||
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
|
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
|
||||||
fmt.Printf("Connection State has changed %s \n", connectionState.String())
|
logger.Infof("Connection State has changed %s", connectionState)
|
||||||
if connectionState == webrtc.ICEConnectionStateConnected {
|
if connectionState == webrtc.ICEConnectionStateConnected {
|
||||||
if !isConnected {
|
if !isConnected {
|
||||||
isConnected = true
|
isConnected = true
|
||||||
|
|
2
wol.go
2
wol.go
|
@ -43,7 +43,7 @@ func createMagicPacket(mac net.HardwareAddr) []byte {
|
||||||
|
|
||||||
// Write the target MAC address 16 times
|
// Write the target MAC address 16 times
|
||||||
for i := 0; i < 16; i++ {
|
for i := 0; i < 16; i++ {
|
||||||
binary.Write(&buf, binary.BigEndian, mac)
|
_ = binary.Write(&buf, binary.BigEndian, mac)
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
|
|
Loading…
Reference in New Issue