diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
new file mode 100644
index 0000000..8c828cf
--- /dev/null
+++ b/.github/workflows/golangci-lint.yml
@@ -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
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..ddf4443
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,12 @@
+---
+linters:
+ enable:
+ # - goimports
+ # - misspell
+ # - revive
+
+issues:
+ exclude-rules:
+ - path: _test.go
+ linters:
+ - errcheck
diff --git a/block_device.go b/block_device.go
index 1e34884..3a44135 100644
--- a/block_device.go
+++ b/block_device.go
@@ -3,7 +3,6 @@ package kvm
import (
"context"
"errors"
- "log"
"net"
"os"
"time"
@@ -94,7 +93,8 @@ func (d *NBDDevice) Start() error {
// Remove the socket file if it already exists
if _, err := os.Stat(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),
SupportsMultiConn: false,
})
- log.Println("nbd server exited:", err)
+ logger.Infof("nbd server exited: %v", err)
}
func (d *NBDDevice) runClientConn() {
@@ -142,14 +142,14 @@ func (d *NBDDevice) runClientConn() {
ExportName: "jetkvm",
BlockSize: uint32(4 * 1024),
})
- log.Println("nbd client exited:", err)
+ logger.Infof("nbd client exited: %v", err)
}
func (d *NBDDevice) Close() {
if d.dev != nil {
err := client.Disconnect(d.dev)
if err != nil {
- log.Println("error disconnecting nbd client:", err)
+ logger.Warnf("error disconnecting nbd client: %v", err)
}
_ = d.dev.Close()
}
diff --git a/cloud.go b/cloud.go
index 628837b..a30a14c 100644
--- a/cloud.go
+++ b/cloud.go
@@ -150,8 +150,8 @@ func runWebsocketClient() error {
if err != nil {
return err
}
- defer c.CloseNow()
- cloudLogger.Infof("websocket connected to %s", wsURL.String())
+ defer c.CloseNow() //nolint:errcheck
+ cloudLogger.Infof("websocket connected to %s", wsURL)
runCtx, cancelRun := context.WithCancel(context.Background())
defer cancelRun()
go func() {
diff --git a/cmd/main.go b/cmd/main.go
index 6080aff..ab44ac9 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -1,7 +1,7 @@
package main
import (
- "kvm"
+ "github.com/jetkvm/kvm"
)
func main() {
diff --git a/config.go b/config.go
index 304d8f6..642f113 100644
--- a/config.go
+++ b/config.go
@@ -5,6 +5,8 @@ import (
"fmt"
"os"
"sync"
+
+ "github.com/jetkvm/kvm/internal/usbgadget"
)
type WakeOnLanDevice struct {
@@ -12,33 +14,26 @@ type WakeOnLanDevice struct {
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 {
- CloudURL string `json:"cloud_url"`
- CloudAppURL string `json:"cloud_app_url"`
- CloudToken string `json:"cloud_token"`
- GoogleIdentity string `json:"google_identity"`
- JigglerEnabled bool `json:"jiggler_enabled"`
- AutoUpdateEnabled bool `json:"auto_update_enabled"`
- IncludePreRelease bool `json:"include_pre_release"`
- HashedPassword string `json:"hashed_password"`
- LocalAuthToken string `json:"local_auth_token"`
- LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
- WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
- EdidString string `json:"hdmi_edid_string"`
- ActiveExtension string `json:"active_extension"`
- DisplayMaxBrightness int `json:"display_max_brightness"`
- DisplayDimAfterSec int `json:"display_dim_after_sec"`
- DisplayOffAfterSec int `json:"display_off_after_sec"`
- TLSMode string `json:"tls_mode"`
- UsbConfig *UsbConfig `json:"usb_config"`
+ CloudURL string `json:"cloud_url"`
+ CloudAppURL string `json:"cloud_app_url"`
+ CloudToken string `json:"cloud_token"`
+ GoogleIdentity string `json:"google_identity"`
+ JigglerEnabled bool `json:"jiggler_enabled"`
+ AutoUpdateEnabled bool `json:"auto_update_enabled"`
+ IncludePreRelease bool `json:"include_pre_release"`
+ HashedPassword string `json:"hashed_password"`
+ LocalAuthToken string `json:"local_auth_token"`
+ LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
+ WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
+ EdidString string `json:"hdmi_edid_string"`
+ ActiveExtension string `json:"active_extension"`
+ DisplayMaxBrightness int `json:"display_max_brightness"`
+ DisplayDimAfterSec int `json:"display_dim_after_sec"`
+ DisplayOffAfterSec int `json:"display_off_after_sec"`
+ TLSMode string `json:"tls_mode"`
+ UsbConfig *usbgadget.Config `json:"usb_config"`
+ UsbDevices *usbgadget.Devices `json:"usb_devices"`
}
const configPath = "/userdata/kvm_config.json"
@@ -52,13 +47,19 @@ var defaultConfig = &Config{
DisplayDimAfterSec: 120, // 2 minutes
DisplayOffAfterSec: 1800, // 30 minutes
TLSMode: "",
- UsbConfig: &UsbConfig{
+ UsbConfig: &usbgadget.Config{
VendorId: "0x1d6b", //The Linux Foundation
ProductId: "0x0104", //Multifunction Composite Gadget
SerialNumber: "",
Manufacturer: "JetKVM",
Product: "USB Emulation Device",
},
+ UsbDevices: &usbgadget.Devices{
+ AbsoluteMouse: true,
+ RelativeMouse: true,
+ Keyboard: true,
+ MassStorage: true,
+ },
}
var (
@@ -97,6 +98,10 @@ func LoadConfig() {
loadedConfig.UsbConfig = defaultConfig.UsbConfig
}
+ if loadedConfig.UsbDevices == nil {
+ loadedConfig.UsbDevices = defaultConfig.UsbDevices
+ }
+
config = &loadedConfig
}
diff --git a/display.go b/display.go
index a956830..f4e8cf7 100644
--- a/display.go
+++ b/display.go
@@ -3,7 +3,6 @@ package kvm
import (
"errors"
"fmt"
- "log"
"os"
"strconv"
"time"
@@ -25,7 +24,7 @@ const (
func switchToScreen(screen string) {
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
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
}
currentScreen = screen
@@ -41,7 +40,7 @@ func updateLabelIfChanged(objName string, newText 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 {
switchToScreen(screenName)
}
@@ -75,12 +74,12 @@ var displayInited = false
func requestDisplayUpdate() {
if !displayInited {
- fmt.Println("display not inited, skipping updates")
+ logger.Info("display not inited, skipping updates")
return
}
go func() {
wakeDisplay(false)
- fmt.Println("display updating........................")
+ logger.Info("display updating")
//TODO: only run once regardless how many pending updates
updateDisplay()
}()
@@ -119,7 +118,7 @@ func setDisplayBrightness(brightness int) error {
return err
}
- fmt.Printf("display: set brightness to %v\n", brightness)
+ logger.Infof("display: set brightness to %v", brightness)
return nil
}
@@ -128,7 +127,7 @@ func setDisplayBrightness(brightness int) error {
func tick_displayDim() {
err := setDisplayBrightness(config.DisplayMaxBrightness / 2)
if err != nil {
- fmt.Printf("display: failed to dim display: %s\n", err)
+ logger.Warnf("display: failed to dim display: %s", err)
}
dimTicker.Stop()
@@ -141,7 +140,7 @@ func tick_displayDim() {
func tick_displayOff() {
err := setDisplayBrightness(0)
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()
@@ -164,7 +163,7 @@ func wakeDisplay(force bool) {
err := setDisplayBrightness(config.DisplayMaxBrightness)
if err != nil {
- fmt.Printf("display wake failed, %s\n", err)
+ logger.Warnf("display wake failed, %s", err)
}
if config.DisplayDimAfterSec != 0 {
@@ -184,7 +183,7 @@ func wakeDisplay(force bool) {
func watchTsEvents() {
ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666)
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
}
@@ -197,7 +196,7 @@ func watchTsEvents() {
for {
_, err := ts.Read(buf)
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
}
@@ -212,17 +211,17 @@ func startBacklightTickers() {
// Don't start the tickers if the display is switched off.
// Set the display to off if that's the case.
if config.DisplayMaxBrightness == 0 {
- setDisplayBrightness(0)
+ _ = setDisplayBrightness(0)
return
}
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)
defer dimTicker.Stop()
go func() {
- for {
+ for { //nolint:gosimple
select {
case <-dimTicker.C:
tick_displayDim()
@@ -232,12 +231,12 @@ func startBacklightTickers() {
}
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)
defer offTicker.Stop()
go func() {
- for {
+ for { //nolint:gosimple
select {
case <-offTicker.C:
tick_displayOff()
@@ -252,11 +251,11 @@ func init() {
go func() {
waitCtrlClientConnected()
- fmt.Println("setting initial display contents")
+ logger.Info("setting initial display contents")
time.Sleep(500 * time.Millisecond)
updateStaticContents()
displayInited = true
- fmt.Println("display inited")
+ logger.Info("display inited")
startBacklightTickers()
wakeDisplay(true)
requestDisplayUpdate()
diff --git a/fuse.go b/fuse.go
index 6ecc49c..29b11f7 100644
--- a/fuse.go
+++ b/fuse.go
@@ -2,7 +2,6 @@ package kvm
import (
"context"
- "fmt"
"os"
"sync"
"syscall"
@@ -104,7 +103,7 @@ func RunFuseServer() {
var err error
fuseServer, err = fs.Mount(fuseMountPoint, &FuseRoot{}, opts)
if err != nil {
- fmt.Println("failed to mount fuse: %w", err)
+ logger.Warnf("failed to mount fuse: %v", err)
}
fuseServer.Wait()
}
diff --git a/go.mod b/go.mod
index 5748e64..93fedab 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module kvm
+module github.com/jetkvm/kvm
go 1.21.0
@@ -15,7 +15,6 @@ require (
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
github.com/hanwen/go-fuse/v2 v2.5.1
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/mdns/v2 v2.0.7
github.com/pion/webrtc/v4 v4.0.0
diff --git a/go.sum b/go.sum
index a5ce4cd..b5769d8 100644
--- a/go.sum
+++ b/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/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/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/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
diff --git a/hw.go b/hw.go
index efe8f5c..03e9d4b 100644
--- a/hw.go
+++ b/hw.go
@@ -14,7 +14,7 @@ func extractSerialNumber() (string, error) {
return "", err
}
- r, err := regexp.Compile("Serial\\s*:\\s*(\\S+)")
+ r, err := regexp.Compile(`Serial\s*:\s*(\S+)`)
if err != nil {
return "", fmt.Errorf("failed to compile regex: %w", err)
}
@@ -27,7 +27,7 @@ func extractSerialNumber() (string, error) {
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")
if err != nil {
return nil, err
diff --git a/internal/usbgadget/config.go b/internal/usbgadget/config.go
new file mode 100644
index 0000000..5f08733
--- /dev/null
+++ b/internal/usbgadget/config.go
@@ -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
+}
diff --git a/internal/usbgadget/consts.go b/internal/usbgadget/consts.go
new file mode 100644
index 0000000..8204d0a
--- /dev/null
+++ b/internal/usbgadget/consts.go
@@ -0,0 +1,3 @@
+package usbgadget
+
+const dwc3Path = "/sys/bus/platform/drivers/dwc3"
diff --git a/internal/usbgadget/hid.go b/internal/usbgadget/hid.go
new file mode 100644
index 0000000..5faac89
--- /dev/null
+++ b/internal/usbgadget/hid.go
@@ -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
+}
diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go
new file mode 100644
index 0000000..030f7af
--- /dev/null
+++ b/internal/usbgadget/hid_keyboard.go
@@ -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
+}
diff --git a/internal/usbgadget/hid_mouse_absolute.go b/internal/usbgadget/hid_mouse_absolute.go
new file mode 100644
index 0000000..c59b591
--- /dev/null
+++ b/internal/usbgadget/hid_mouse_absolute.go
@@ -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
+}
diff --git a/internal/usbgadget/hid_mouse_relative.go b/internal/usbgadget/hid_mouse_relative.go
new file mode 100644
index 0000000..df844dc
--- /dev/null
+++ b/internal/usbgadget/hid_mouse_relative.go
@@ -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
+}
diff --git a/internal/usbgadget/mass_storage.go b/internal/usbgadget/mass_storage.go
new file mode 100644
index 0000000..f962cb4
--- /dev/null
+++ b/internal/usbgadget/mass_storage.go
@@ -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",
+ },
+}
diff --git a/internal/usbgadget/udc.go b/internal/usbgadget/udc.go
new file mode 100644
index 0000000..6316b83
--- /dev/null
+++ b/internal/usbgadget/udc.go
@@ -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
+}
diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go
new file mode 100644
index 0000000..9fc34d5
--- /dev/null
+++ b/internal/usbgadget/usbgadget.go
@@ -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
+}
diff --git a/internal/usbgadget/utils.go b/internal/usbgadget/utils.go
new file mode 100644
index 0000000..3f0adda
--- /dev/null
+++ b/internal/usbgadget/utils.go
@@ -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)
+}
diff --git a/jiggler.go b/jiggler.go
index 11ce313..daec192 100644
--- a/jiggler.go
+++ b/jiggler.go
@@ -6,10 +6,6 @@ import (
var lastUserInput = time.Now()
-func resetUserInputTime() {
- lastUserInput = time.Now()
-}
-
var jigglerEnabled = false
func rpcSetJigglerState(enabled bool) {
diff --git a/jsonrpc.go b/jsonrpc.go
index 40be85c..298a810 100644
--- a/jsonrpc.go
+++ b/jsonrpc.go
@@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
- "log"
"os"
"os/exec"
"path/filepath"
@@ -15,6 +14,8 @@ import (
"github.com/pion/webrtc/v4"
"go.bug.st/serial"
+
+ "github.com/jetkvm/kvm/internal/usbgadget"
)
type JSONRPCRequest struct {
@@ -46,12 +47,12 @@ type BacklightSettings struct {
func writeJSONRPCResponse(response JSONRPCResponse, session *Session) {
responseBytes, err := json.Marshal(response)
if err != nil {
- log.Println("Error marshalling JSONRPC response:", err)
+ logger.Warnf("Error marshalling JSONRPC response: %v", err)
return
}
err = session.RPCChannel.SendText(string(responseBytes))
if err != nil {
- log.Println("Error sending JSONRPC response:", err)
+ logger.Warnf("Error sending JSONRPC response: %v", err)
return
}
}
@@ -64,16 +65,16 @@ func writeJSONRPCEvent(event string, params interface{}, session *Session) {
}
requestBytes, err := json.Marshal(request)
if err != nil {
- log.Println("Error marshalling JSONRPC event:", err)
+ logger.Warnf("Error marshalling JSONRPC event: %v", err)
return
}
if session == nil || session.RPCChannel == nil {
- log.Println("RPC channel not available")
+ logger.Info("RPC channel not available")
return
}
err = session.RPCChannel.SendText(string(requestBytes))
if err != nil {
- log.Println("Error sending JSONRPC event:", err)
+ logger.Warnf("Error sending JSONRPC event: %v", err)
return
}
}
@@ -94,7 +95,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
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]
if !ok {
errorResponse := JSONRPCResponse{
@@ -147,7 +148,7 @@ func rpcGetStreamQualityFactor() (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})
if err != nil {
return err
@@ -183,10 +184,10 @@ func rpcGetEDID() (string, error) {
func rpcSetEDID(edid string) error {
if edid == "" {
- log.Println("Restoring EDID to default")
+ logger.Info("Restoring EDID to default")
edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
} else {
- log.Printf("Setting EDID to: %s", edid)
+ logger.Infof("Setting EDID to: %s", edid)
}
_, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": edid})
if err != nil {
@@ -195,8 +196,7 @@ func rpcSetEDID(edid string) error {
// Save EDID to config, allowing it to be restored on reboot.
config.EdidString = edid
- SaveConfig()
-
+ _ = SaveConfig()
return nil
}
@@ -257,7 +257,7 @@ func rpcSetBacklightSettings(params BacklightSettings) error {
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
// 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) {
- 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
if mode == "cdrom" {
cdrom = true
} 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)
}
- 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)
if err != nil {
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
return rpcGetMassStorageMode()
@@ -518,45 +518,27 @@ func rpcIsUpdatePending() (bool, error) {
}
func rpcGetUsbEmulationState() (bool, error) {
- _, err := os.Stat(filepath.Join("/sys/bus/platform/drivers/dwc3", udc))
- if err != nil {
- if os.IsNotExist(err) {
- return false, nil
- }
- return false, fmt.Errorf("error checking USB emulation state: %w", err)
- }
- return true, nil
+ return gadget.IsUDCBound()
}
func rpcSetUsbEmulationState(enabled bool) error {
if enabled {
- return os.WriteFile("/sys/bus/platform/drivers/dwc3/bind", []byte(udc), 0644)
+ return gadget.BindUDC()
} 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()
return *config.UsbConfig, nil
}
-func rpcSetUsbConfig(usbConfig UsbConfig) error {
+func rpcSetUsbConfig(usbConfig usbgadget.Config) error {
LoadConfig()
config.UsbConfig = &usbConfig
-
- err := UpdateGadgetConfig()
- 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
+ gadget.SetGadgetConfig(config.UsbConfig)
+ return updateUsbRelatedConfig()
}
func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) {
@@ -581,7 +563,7 @@ func rpcResetConfig() error {
return fmt.Errorf("failed to reset config: %w", err)
}
- log.Println("Configuration reset to default")
+ logger.Info("Configuration reset to default")
return nil
}
@@ -597,7 +579,7 @@ func rpcGetDCPowerState() (DCPowerState, 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)
if err != nil {
return fmt.Errorf("failed to set DC power state: %w", err)
@@ -614,18 +596,18 @@ func rpcSetActiveExtension(extensionId string) error {
return nil
}
if config.ActiveExtension == "atx-power" {
- unmountATXControl()
+ _ = unmountATXControl()
} else if config.ActiveExtension == "dc-power" {
- unmountDCControl()
+ _ = unmountDCControl()
}
config.ActiveExtension = extensionId
if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
if extensionId == "atx-power" {
- mountATXControl()
+ _ = mountATXControl()
} else if extensionId == "dc-power" {
- mountDCControl()
+ _ = mountDCControl()
}
return nil
}
@@ -746,11 +728,48 @@ func rpcSetSerialSettings(settings SerialSettings) error {
Parity: parity,
}
- port.SetMode(serialPortMode)
+ _ = port.SetMode(serialPortMode)
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 {
config.CloudURL = apiUrl
config.CloudAppURL = appUrl
@@ -831,6 +850,9 @@ var rpcHandlers = map[string]RPCHandler{
"setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}},
"getSerialSettings": {Func: rpcGetSerialSettings},
"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"}},
"getScrollSensitivity": {Func: rpcGetScrollSensitivity},
"setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}},
diff --git a/log.go b/log.go
index dbc5f03..7718a28 100644
--- a/log.go
+++ b/log.go
@@ -5,5 +5,4 @@ import "github.com/pion/logging"
// we use logging framework from pion
// ref: https://github.com/pion/webrtc/wiki/Debugging-WebRTC
var logger = logging.NewDefaultLoggerFactory().NewLogger("jetkvm")
-var usbLogger = logging.NewDefaultLoggerFactory().NewLogger("usb")
var cloudLogger = logging.NewDefaultLoggerFactory().NewLogger("cloud")
diff --git a/main.go b/main.go
index 2c8c22a..6a55595 100644
--- a/main.go
+++ b/main.go
@@ -2,7 +2,6 @@ package kvm
import (
"context"
- "log"
"net/http"
"os"
"os/signal"
@@ -46,11 +45,13 @@ func Main() {
}
}()
+ initUsbGadget()
+
go func() {
time.Sleep(15 * time.Minute)
for {
logger.Debugf("UPDATING - Auto update enabled: %v", config.AutoUpdateEnabled)
- if config.AutoUpdateEnabled == false {
+ if !config.AutoUpdateEnabled {
return
}
if currentSession != nil {
@@ -80,15 +81,15 @@ func Main() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
- log.Println("JetKVM Shutting Down")
+ logger.Info("JetKVM Shutting Down")
//if fuseServer != nil {
// err := setMassStorageImage(" ")
// 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()
// if err != nil {
- // log.Printf("Failed to unmount fuse: %v", err)
+ // logger.Infof("Failed to unmount fuse: %v", err)
// }
// os.Exit(0)
diff --git a/native.go b/native.go
index 7940f64..8960304 100644
--- a/native.go
+++ b/native.go
@@ -5,8 +5,6 @@ import (
"encoding/json"
"fmt"
"io"
- "kvm/resource"
- "log"
"net"
"os"
"os/exec"
@@ -14,6 +12,8 @@ import (
"syscall"
"time"
+ "github.com/jetkvm/kvm/resource"
+
"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)
}
- fmt.Println("sending ctrl action", string(jsonData))
+ logger.Infof("sending ctrl action: %s", string(jsonData))
err = WriteCtrlMessage(jsonData)
if err != nil {
@@ -91,8 +91,8 @@ func WriteCtrlMessage(message []byte) error {
return err
}
-var nativeCtrlSocketListener net.Listener
-var nativeVideoSocketListener net.Listener
+var nativeCtrlSocketListener net.Listener //nolint:unused
+var nativeVideoSocketListener net.Listener //nolint:unused
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
if _, err := os.Stat(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)
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() {
conn, err := listener.Accept()
@@ -188,24 +190,23 @@ func handleCtrlClient(conn net.Conn) {
func handleVideoClient(conn net.Conn) {
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)
lastFrame := time.Now()
for {
n, err := conn.Read(inboundPacket)
if err != nil {
- log.Println("error during read: %s", err)
+ logger.Warnf("error during read: %v", err)
return
}
now := time.Now()
sinceLastFrame := now.Sub(lastFrame)
lastFrame = now
- //fmt.Println("Video packet received", n, sinceLastFrame)
if currentSession != nil {
err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame})
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
}
diff --git a/network.go b/network.go
index 3553015..66b8616 100644
--- a/network.go
+++ b/network.go
@@ -56,14 +56,14 @@ func setDhcpClientState(active bool) {
cmd := exec.Command("/usr/bin/killall", signal, "udhcpc")
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() {
iface, err := netlink.LinkByName(NetIfName)
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
}
@@ -76,7 +76,7 @@ func checkNetworkState() {
addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL)
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.
@@ -89,10 +89,10 @@ func checkNetworkState() {
if addr.IP.To4() != nil {
if !newState.Up && networkState.Up {
// 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)
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 = "..."
@@ -105,9 +105,9 @@ func checkNetworkState() {
}
if newState != networkState {
- fmt.Println("network state changed")
+ logger.Info("network state changed")
// restart MDNS
- startMDNS()
+ _ = startMDNS()
networkState = newState
requestDisplayUpdate()
}
@@ -116,15 +116,15 @@ func checkNetworkState() {
func startMDNS() error {
// If server was previously running, stop it
if mDNSConn != nil {
- fmt.Printf("Stopping mDNS server\n")
+ logger.Info("Stopping mDNS server")
err := mDNSConn.Close()
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
- fmt.Printf("Starting mDNS server on jetkvm.local\n")
+ logger.Info("Starting mDNS server on jetkvm.local")
addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4)
if err != nil {
return err
@@ -181,7 +181,7 @@ func getNTPServersFromDHCPInfo() ([]string, error) {
for _, server := range strings.Fields(val) {
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)
}
@@ -196,7 +196,7 @@ func init() {
done := make(chan struct{})
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
}
@@ -210,7 +210,7 @@ func init() {
select {
case update := <-updates:
if update.Link.Attrs().Name == NetIfName {
- fmt.Printf("link update: %+v\n", update)
+ logger.Infof("link update: %+v", update)
checkNetworkState()
}
case <-ticker.C:
@@ -222,6 +222,6 @@ func init() {
}()
err := startMDNS()
if err != nil {
- fmt.Println("failed to run mDNS: %v", err)
+ logger.Warnf("failed to run mDNS: %v", err)
}
}
diff --git a/ntp.go b/ntp.go
index 92d0471..39ea7af 100644
--- a/ntp.go
+++ b/ntp.go
@@ -3,7 +3,6 @@ package kvm
import (
"errors"
"fmt"
- "log"
"net/http"
"os/exec"
"time"
@@ -21,7 +20,6 @@ const (
)
var (
- timeSynced = false
timeSyncRetryInterval = 0 * time.Second
defaultNTPServers = []string{
"time.cloudflare.com",
@@ -37,16 +35,16 @@ func TimeSyncLoop() {
}
if !networkState.Up {
- log.Printf("Waiting for network to come up")
+ logger.Infof("Waiting for network to come up")
time.Sleep(timeSyncWaitNetUpInt)
continue
}
- log.Printf("Syncing system time")
+ logger.Infof("Syncing system time")
start := time.Now()
err := SyncSystemTime()
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
timeSyncRetryInterval += timeSyncRetryStep
@@ -58,8 +56,7 @@ func TimeSyncLoop() {
continue
}
- log.Printf("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start))
- timeSynced = true
+ logger.Infof("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start))
time.Sleep(timeSyncInterval) // after the first sync is done
}
}
@@ -79,20 +76,20 @@ func SyncSystemTime() (err error) {
func queryNetworkTime() (*time.Time, error) {
ntpServers, err := getNTPServersFromDHCPInfo()
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 {
ntpServers = defaultNTPServers
- log.Printf("Using default NTP servers: %v\n", ntpServers)
+ logger.Infof("Using default NTP servers: %v\n", ntpServers)
} 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 {
now, err := queryNtpServer(server, timeSyncTimeout)
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
}
}
diff --git a/ota.go b/ota.go
index 9f9cb6f..f813c09 100644
--- a/ota.go
+++ b/ota.go
@@ -8,7 +8,6 @@ import (
"encoding/json"
"fmt"
"io"
- "log"
"net/http"
"net/url"
"os"
@@ -77,7 +76,7 @@ func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease
query.Set("prerelease", fmt.Sprintf("%v", includePreRelease))
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)
if err != nil {
@@ -230,7 +229,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32) error
}
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 {
return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash)
@@ -272,7 +271,7 @@ var otaState = OTAState{}
func triggerOTAStateUpdate() {
go func() {
if currentSession == nil {
- log.Println("No active RPC session, skipping update state update")
+ logger.Info("No active RPC session, skipping update state update")
return
}
writeJSONRPCEvent("otaState", otaState, currentSession)
@@ -280,7 +279,7 @@ func triggerOTAStateUpdate() {
}
func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error {
- log.Println("Trying to update...")
+ logger.Info("Trying to update...")
if otaState.Updating {
return fmt.Errorf("update already in progress")
}
@@ -315,7 +314,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
rebootNeeded := false
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)
if err != nil {
@@ -341,14 +340,14 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
otaState.AppUpdateProgress = 1
triggerOTAStateUpdate()
- fmt.Println("App update downloaded")
+ logger.Info("App update downloaded")
rebootNeeded = true
} else {
- fmt.Println("App is up to date")
+ logger.Info("App is up to date")
}
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)
if err != nil {
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()
return err
}
- fmt.Println("System update downloaded")
+ logger.Info("System update downloaded")
verifyFinished := time.Now()
otaState.SystemVerifiedAt = &verifyFinished
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)
}
- fmt.Printf("rk_ota success, output: %s\n", output)
+ logger.Infof("rk_ota success, output: %s", output)
otaState.SystemUpdateProgress = 1
otaState.SystemUpdatedAt = &verifyFinished
triggerOTAStateUpdate()
rebootNeeded = true
} else {
- fmt.Println("System is up to date")
+ logger.Info("System is up to date")
}
if rebootNeeded {
- fmt.Println("System Rebooting in 10s...")
+ logger.Info("System Rebooting in 10s")
time.Sleep(10 * time.Second)
cmd := exec.Command("reboot")
err := cmd.Start()
diff --git a/remote_mount.go b/remote_mount.go
index e6e7322..5b10695 100644
--- a/remote_mount.go
+++ b/remote_mount.go
@@ -49,7 +49,7 @@ func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) (
if err != nil {
return nil, err
}
- buf := make([]byte, 0)
+ var buf []byte
for {
select {
case data := <-diskReadChan:
diff --git a/serial.go b/serial.go
index 3ad56d3..a4ab7d5 100644
--- a/serial.go
+++ b/serial.go
@@ -16,14 +16,14 @@ const serialPortPath = "/dev/ttyS3"
var port serial.Port
func mountATXControl() error {
- port.SetMode(defaultMode)
+ _ = port.SetMode(defaultMode)
go runATXControl()
return nil
}
func unmountATXControl() error {
- reopenSerialPort()
+ _ = reopenSerialPort()
return nil
}
@@ -122,13 +122,13 @@ func pressATXResetButton(duration time.Duration) error {
}
func mountDCControl() error {
- port.SetMode(defaultMode)
+ _ = port.SetMode(defaultMode)
go runDCControl()
return nil
}
func unmountDCControl() error {
- reopenSerialPort()
+ _ = reopenSerialPort()
return nil
}
@@ -212,11 +212,11 @@ var defaultMode = &serial.Mode{
}
func initSerialPort() {
- reopenSerialPort()
+ _ = reopenSerialPort()
if config.ActiveExtension == "atx-power" {
- mountATXControl()
+ _ = mountATXControl()
} else if config.ActiveExtension == "dc-power" {
- mountDCControl()
+ _ = mountDCControl()
}
}
diff --git a/terminal.go b/terminal.go
index 1a1ac1c..3e64020 100644
--- a/terminal.go
+++ b/terminal.go
@@ -55,11 +55,13 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
var size TerminalSize
err := json.Unmarshal([]byte(msg.Data), &size)
if err == nil {
- pty.Setsize(ptmx, &pty.Winsize{
+ err = pty.Setsize(ptmx, &pty.Winsize{
Rows: uint16(size.Rows),
Cols: uint16(size.Cols),
})
- return
+ if err == nil {
+ return
+ }
}
logger.Errorf("Failed to parse terminal size: %v", err)
}
@@ -74,7 +76,7 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
ptmx.Close()
}
if cmd != nil && cmd.Process != nil {
- cmd.Process.Kill()
+ _ = cmd.Process.Kill()
}
})
}
diff --git a/ui/src/assets/netboot-icon.svg b/ui/src/assets/netboot-icon.svg
index e341c99..39b5fb2 100644
--- a/ui/src/assets/netboot-icon.svg
+++ b/ui/src/assets/netboot-icon.svg
@@ -1,18 +1 @@
-
+
\ No newline at end of file
diff --git a/ui/src/components/UsbDeviceSetting.tsx b/ui/src/components/UsbDeviceSetting.tsx
new file mode 100644
index 0000000..1c8812c
--- /dev/null
+++ b/ui/src/components/UsbDeviceSetting.tsx
@@ -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(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) => {
+ setUsbDeviceConfig((val) => {
+ val[key] = e.target.checked;
+ handleUsbConfigChange(val);
+ return val;
+ });
+ }, [handleUsbConfigChange]);
+
+ useEffect(() => {
+ syncUsbDeviceConfig();
+ }, [syncUsbDeviceConfig]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/ui/src/routes/devices.$id.settings.hardware.tsx b/ui/src/routes/devices.$id.settings.hardware.tsx
index 5fb744b..3a60466 100644
--- a/ui/src/routes/devices.$id.settings.hardware.tsx
+++ b/ui/src/routes/devices.$id.settings.hardware.tsx
@@ -7,6 +7,7 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "../notifications";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { UsbConfigSetting } from "../components/UsbConfigSetting";
+import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
import { FeatureFlag } from "../components/FeatureFlag";
export default function SettingsHardwareRoute() {
@@ -132,6 +133,10 @@ export default function SettingsHardwareRoute() {
+
+
+
+
);
}
diff --git a/ui/src/routes/devices.$id.settings.mouse.tsx b/ui/src/routes/devices.$id.settings.mouse.tsx
index 9956ecb..c8c351a 100644
--- a/ui/src/routes/devices.$id.settings.mouse.tsx
+++ b/ui/src/routes/devices.$id.settings.mouse.tsx
@@ -10,6 +10,8 @@ import { useCallback, useEffect, useState } from "react";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { cx } from "../cva.config";
import { SelectMenuBasic } from "../components/SelectMenuBasic";
+import { useFeatureFlag } from "../hooks/useFeatureFlag";
+import { FeatureFlag } from "../components/FeatureFlag";
type ScrollSensitivity = "low" | "default" | "high";
@@ -21,6 +23,8 @@ export default function SettingsKeyboardMouseRoute() {
state => state.setScrollSensitivity,
);
+ const { isEnabled: isScrollSensitivityEnabled } = useFeatureFlag("0.3.8");
+
const [jiggler, setJiggler] = useState(false);
const [send] = useJsonRpc();
@@ -31,11 +35,13 @@ export default function SettingsKeyboardMouseRoute() {
setJiggler(resp.result as boolean);
});
- send("getScrollSensitivity", {}, resp => {
- if ("error" in resp) return;
- setScrollSensitivity(resp.result as ScrollSensitivity);
- });
- }, [send, setScrollSensitivity]);
+ if (isScrollSensitivityEnabled) {
+ send("getScrollSensitivity", {}, resp => {
+ if ("error" in resp) return;
+ setScrollSensitivity(resp.result as ScrollSensitivity);
+ });
+ }
+ }, [isScrollSensitivityEnabled, send, setScrollSensitivity]);
const handleJigglerChange = (enabled: boolean) => {
send("setJigglerState", { enabled }, resp => {
@@ -82,25 +88,28 @@ export default function SettingsKeyboardMouseRoute() {
onChange={e => setHideCursor(e.target.checked)}
/>
-
-
-
+
+
+
+
+
+
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
+ return gadget.KeyboardReport(modifier, keys)
}
func rpcAbsMouseReport(x, y int, buttons uint8) error {
- mouseLock.Lock()
- 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
+ return gadget.AbsMouseReport(x, y, buttons)
}
-var accumulatedWheelY float64 = 0
-
func rpcWheelReport(wheelY int8) error {
- if mouseHidFile == nil {
- 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
+ return gadget.AbsMouseWheelReport(wheelY)
}
var usbState = "unknown"
func rpcGetUSBState() (state string) {
- stateBytes, err := os.ReadFile("/sys/class/udc/ffb00000.usb/state")
- if err != nil {
- return "unknown"
- }
- return strings.TrimSpace(string(stateBytes))
+ return gadget.GetUsbState()
}
func triggerUSBStateUpdate() {
go func() {
if currentSession == nil {
- log.Println("No active RPC session, skipping update state update")
+ logger.Info("No active RPC session, skipping update state update")
return
}
writeJSONRPCEvent("usbState", usbState, currentSession)
}()
}
-var udc string
+func checkUSBState() {
+ newState := gadget.GetUsbState()
+ if newState == usbState {
+ return
+ }
+ usbState = newState
-func init() {
- ensureConfigLoaded()
-
- 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
+ logger.Infof("USB state changed from %s to %s", usbState, newState)
+ requestDisplayUpdate()
+ triggerUSBStateUpdate()
}
diff --git a/usb_mass_storage.go b/usb_mass_storage.go
index b897c20..45f613f 100644
--- a/usb_mass_storage.go
+++ b/usb_mass_storage.go
@@ -5,8 +5,6 @@ import (
"errors"
"fmt"
"io"
- "kvm/resource"
- "log"
"net/http"
"os"
"path"
@@ -17,54 +15,58 @@ import (
"time"
"github.com/gin-gonic/gin"
-
"github.com/psanford/httpreadat"
-
"github.com/google/uuid"
"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 {
return os.WriteFile(path, []byte(data), 0644)
}
func setMassStorageImage(imagePath string) error {
- err := writeFile(path.Join(massStorageFunctionPath, "lun.0", "file"), imagePath)
+ massStorageFunctionPath, err := gadget.GetConfigPath("mass_storage_lun0")
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 nil
}
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"
if cdrom {
mode = "1"
}
- err := writeFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"), mode)
- if err != nil {
+ if err := writeFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"), mode); err != nil {
return fmt.Errorf("failed to set cdrom mode: %w", err)
}
return nil
}
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
}
func mountImage(imagePath string) error {
err := setMassStorageImage("")
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)
if err != nil {
- return fmt.Errorf("Set Mass Storage Image Error", err)
+ return fmt.Errorf("Set Mass Storage Image Error: %w", err)
}
return nil
}
@@ -74,7 +76,7 @@ var nbdDevice *NBDDevice
const imagesFolder = "/userdata/jetkvm/images"
func rpcMountBuiltInImage(filename string) error {
- log.Println("Mount Built-In Image", filename)
+ logger.Infof("Mount Built-In Image: %s", filename)
_ = os.MkdirAll(imagesFolder, 0755)
imagePath := filepath.Join(imagesFolder, filename)
@@ -108,6 +110,11 @@ func rpcMountBuiltInImage(filename string) 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"))
if err != nil {
return false, fmt.Errorf("failed to read cdrom mode: %w", err)
@@ -166,7 +173,7 @@ func rpcUnmountImage() error {
defer virtualMediaStateMutex.Unlock()
err := setMassStorageImage("\n")
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
time.Sleep(500 * time.Millisecond)
diff --git a/video.go b/video.go
index 8fc2bfa..ade9353 100644
--- a/video.go
+++ b/video.go
@@ -2,7 +2,6 @@ package kvm
import (
"encoding/json"
- "log"
)
// max frame size for 1080p video, specified in mpp venc setting
@@ -39,7 +38,7 @@ func HandleVideoStateMessage(event CtrlResponse) {
videoState := VideoInputState{}
err := json.Unmarshal(event.Data, &videoState)
if err != nil {
- log.Println("Error parsing video state json:", err)
+ logger.Warnf("Error parsing video state json: %v", err)
return
}
lastVideoState = videoState
diff --git a/webrtc.go b/webrtc.go
index 5e9ce3d..12d4f95 100644
--- a/webrtc.go
+++ b/webrtc.go
@@ -3,7 +3,6 @@ package kvm
import (
"encoding/base64"
"encoding/json"
- "fmt"
"net"
"strings"
@@ -74,17 +73,17 @@ func newSession(config SessionConfig) (*Session, error) {
if config.IsCloud {
if config.ICEServers == nil {
- fmt.Printf("ICE Servers not provided by cloud")
+ logger.Info("ICE Servers not provided by cloud")
} else {
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 {
- 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 {
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}
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() {
case "rpc":
session.RPCChannel = d
@@ -146,7 +145,7 @@ func newSession(config SessionConfig) (*Session, error) {
var isConnected bool
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 !isConnected {
isConnected = true
diff --git a/wol.go b/wol.go
index 43c9e53..f0e68bb 100644
--- a/wol.go
+++ b/wol.go
@@ -43,7 +43,7 @@ func createMagicPacket(mac net.HardwareAddr) []byte {
// Write the target MAC address 16 times
for i := 0; i < 16; i++ {
- binary.Write(&buf, binary.BigEndian, mac)
+ _ = binary.Write(&buf, binary.BigEndian, mac)
}
return buf.Bytes()