diff --git a/config.go b/config.go index ba38a60..cfd7f5f 100644 --- a/config.go +++ b/config.go @@ -3,6 +3,7 @@ package kvm import ( "encoding/json" "fmt" + "kvm/internal/usbgadget" "os" "sync" ) @@ -12,40 +13,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 UsbDevicesConfig struct { - AbsoluteMouse bool `json:"absolute_mouse"` - RelativeMouse bool `json:"relative_mouse"` - Keyboard bool `json:"keyboard"` - MassStorage bool `json:"mass_storage"` -} - 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"` - UsbConfig *UsbConfig `json:"usb_config"` - UsbDevices *UsbDevicesConfig `json:"usb_devices"` + 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" @@ -58,14 +45,15 @@ var defaultConfig = &Config{ DisplayMaxBrightness: 64, DisplayDimAfterSec: 120, // 2 minutes DisplayOffAfterSec: 1800, // 30 minutes - UsbConfig: &UsbConfig{ + TLSMode: "", + UsbConfig: &usbgadget.Config{ VendorId: "0x1d6b", //The Linux Foundation ProductId: "0x0104", //Multifunction Composite Gadget SerialNumber: "", Manufacturer: "JetKVM", Product: "USB Emulation Device", }, - UsbDevices: &UsbDevicesConfig{ + UsbDevices: &usbgadget.Devices{ AbsoluteMouse: true, RelativeMouse: true, Keyboard: true, 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/jsonrpc.go b/jsonrpc.go index d512362..c9ca5b3 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "kvm/internal/usbgadget" "log" "os" "os/exec" @@ -518,32 +519,26 @@ 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 + gadget.SetGadgetConfig(config.UsbConfig) return updateUsbRelatedConfig() } @@ -739,12 +734,12 @@ func rpcSetSerialSettings(settings SerialSettings) error { return nil } -func rpcGetUsbDevices() (UsbDevicesConfig, error) { +func rpcGetUsbDevices() (usbgadget.Devices, error) { return *config.UsbDevices, nil } func updateUsbRelatedConfig() error { - if err := UpdateGadgetConfig(); err != nil { + if err := gadget.UpdateGadgetConfig(); err != nil { return fmt.Errorf("failed to write gadget config: %w", err) } if err := SaveConfig(); err != nil { @@ -753,8 +748,9 @@ func updateUsbRelatedConfig() error { return nil } -func rpcSetUsbDevices(usbDevices UsbDevicesConfig) error { +func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { config.UsbDevices = &usbDevices + gadget.SetGadgetDevices(config.UsbDevices) return updateUsbRelatedConfig() } @@ -771,6 +767,7 @@ func rpcSetUsbDeviceState(device string, enabled bool) error { default: return fmt.Errorf("invalid device: %s", device) } + gadget.SetGadgetDevices(config.UsbDevices) return updateUsbRelatedConfig() } diff --git a/main.go b/main.go index e23e9c8..910f4e8 100644 --- a/main.go +++ b/main.go @@ -44,6 +44,8 @@ func Main() { } }() + initUsbGadget() + go func() { time.Sleep(15 * time.Minute) for { diff --git a/ui/src/components/UsbDeviceSetting.tsx b/ui/src/components/UsbDeviceSetting.tsx index 9509203..1c8812c 100644 --- a/ui/src/components/UsbDeviceSetting.tsx +++ b/ui/src/components/UsbDeviceSetting.tsx @@ -56,7 +56,6 @@ export function UsbDeviceSetting() { ); return; } - // setUsbConfigProduct(usbConfig.product); notifications.success( `USB Devices updated` ); diff --git a/usb.go b/usb.go index 0fdc906..1488183 100644 --- a/usb.go +++ b/usb.go @@ -1,480 +1,51 @@ package kvm import ( - "bytes" - "errors" - "fmt" + "kvm/internal/usbgadget" "log" - "os" - "os/exec" - "path" - "path/filepath" - "strings" - "sync" "time" - - gadget "github.com/openstadia/go-usb-gadget" ) -const configFSPath = "/sys/kernel/config" -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" +var gadget *usbgadget.UsbGadget -func (u *UsbDevicesConfig) isGadgetConfigItemEnabled(itemKey string) bool { - switch itemKey { - case "absolute_mouse": - return u.AbsoluteMouse - case "relative_mouse": - return u.RelativeMouse - case "keyboard": - return u.Keyboard - case "mass_storage_base": - return u.MassStorage - case "mass_storage_usb0": - return u.MassStorage - default: - return true - } -} +// initUsbGadget initializes the USB gadget. +// call it only after the config is loaded. +func initUsbGadget() { + gadget = usbgadget.NewUsbGadget( + "jetkvm", + config.UsbDevices, + config.UsbConfig, + &logger, + ) -type gadgetConfigItem struct { - device string - path []string - attrs gadgetAttributes - configAttrs gadgetAttributes - configPath string - reportDesc []byte -} - -type gadgetAttributes map[string]string - -var gadgetConfig = map[string]gadgetConfigItem{ - "base": { - 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": { - path: []string{"strings", "0x409"}, - attrs: gadgetAttributes{ - "serialnumber": GetDeviceID(), - "manufacturer": "JetKVM", - "product": "JetKVM USB Emulation Device", - }, - configAttrs: gadgetAttributes{ - "configuration": "Config 1: HID", - }, - }, - // keyboard HID - "keyboard": { - device: "hid.usb0", - path: []string{"functions", "hid.usb0"}, - configPath: path.Join(configC1Path, "hid.usb0"), - attrs: gadgetAttributes{ - "protocol": "1", - "subclass": "1", - "report_length": "8", - }, - reportDesc: KeyboardReportDesc, - }, - // mouse HID - "absolute_mouse": { - device: "hid.usb1", - path: []string{"functions", "hid.usb1"}, - configPath: path.Join(configC1Path, "hid.usb1"), - attrs: gadgetAttributes{ - "protocol": "2", - "subclass": "1", - "report_length": "6", - }, - reportDesc: CombinedAbsoluteMouseReportDesc, - }, - // relative mouse HID - "relative_mouse": { - device: "hid.usb2", - path: []string{"functions", "hid.usb2"}, - configPath: path.Join(configC1Path, "hid.usb2"), - attrs: gadgetAttributes{ - "protocol": "2", - "subclass": "1", - "report_length": "4", - }, - reportDesc: CombinedRelativeMouseReportDesc, - }, - // mass storage - "mass_storage_base": { - device: "mass_storage.usb0", - path: []string{"functions", "mass_storage.usb0"}, - configPath: path.Join(configC1Path, "mass_storage.usb0"), - attrs: gadgetAttributes{ - "stall": "1", - }, - }, - "mass_storage_usb0": { - path: []string{"functions", "mass_storage.usb0", "lun.0"}, - attrs: gadgetAttributes{ - "cdrom": "1", - "ro": "1", - "removable": "1", - "file": "\n", - "inquiry_string": "JetKVM Virtual Media", - }, - }, -} - -func mountConfigFS() error { - _, err := os.Stat(gadgetPath) - 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) + go func() { + for { + checkUSBState() + time.Sleep(500 * time.Millisecond) } - } else { - return fmt.Errorf("unable to access usb gadget path: %w", err) - } - return nil + }() } -func 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) { - logger.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 { - logger.Tracef("skipping writing to %s as it already has the correct content", filePath) - return nil - } - - logger.Tracef("writing to %s as it has different content %v %v", filePath, oldContent, content) - } - } - return os.WriteFile(filePath, content, permMode) -} - -func disableGadgetItemConfig(item gadgetConfigItem) error { - // remove symlink if exists - if item.configPath != "" { - if _, err := os.Lstat(item.configPath); os.IsNotExist(err) { - logger.Tracef("symlink %s does not exist", item.configPath) - } else { - err := os.Remove(item.configPath) - if err != nil { - return fmt.Errorf("failed to remove symlink %s: %w", item.configPath, err) - } - } - } - - return nil -} - -func writeGadgetItemConfig(item gadgetConfigItem) error { - // create directory for the item - gadgetItemPathArr := append([]string{kvmGadgetPath}, item.path...) - gadgetItemPath := filepath.Join(gadgetItemPathArr...) - err := os.MkdirAll(gadgetItemPath, 0755) - if err != nil { - return fmt.Errorf("failed to create path %s: %w", gadgetItemPath, err) - } - - if len(item.configAttrs) > 0 { - configItemPathArr := append([]string{configC1Path}, item.path...) - configItemPath := filepath.Join(configItemPathArr...) - err = os.MkdirAll(configItemPath, 0755) - if err != nil { - return fmt.Errorf("failed to create path %s: %w", config, err) - } - - err = writeGadgetAttrs(configItemPath, item.configAttrs) - if err != nil { - return fmt.Errorf("failed to write config attributes for %s: %w", configItemPath, err) - } - } - - if len(item.attrs) > 0 { - // write attributes for the item - err = 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 = writeIfDifferent(path.Join(gadgetItemPath, "report_desc"), item.reportDesc, 0644) - if err != nil { - return err - } - } - - // create symlink if configPath is set - if item.configPath != "" { - logger.Tracef("Creating symlink from %s to %s", item.configPath, gadgetItemPath) - - // check if the symlink already exists, if yes, check if it points to the correct path - if _, err := os.Lstat(item.configPath); err == nil { - linkPath, err := os.Readlink(item.configPath) - if err != nil || linkPath != gadgetItemPath { - err = os.Remove(item.configPath) - if err != nil { - return fmt.Errorf("failed to remove existing symlink %s: %w", item.configPath, err) - } - } - } else if !os.IsNotExist(err) { - return fmt.Errorf("failed to check if symlink exists: %w", err) - } - - err = os.Symlink(gadgetItemPath, item.configPath) - if err != nil { - return fmt.Errorf("failed to create symlink from %s to %s: %w", item.configPath, gadgetItemPath, 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 - } - - logger.Tracef("writing gadget config") - for key, item := range gadgetConfig { - // check if the item is enabled in the config - if !config.UsbDevices.isGadgetConfigItemEnabled(key) { - logger.Tracef("disabling gadget config: %s", key) - err = disableGadgetItemConfig(item) - if err != nil { - return err - } - continue - } - logger.Tracef("writing gadget config: %s", key) - err = writeGadgetItemConfig(item) - 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 { - keyboardLock.Lock() - 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 + 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 + return gadget.AbsMouseWheelReport(wheelY) } -// Helper function to get absolute value of float64 -func abs(x float64) float64 { - if x < 0 { - return -x - } - return x +func rpcRelMouseReport(mx, my int8, buttons uint8) error { + return gadget.RelMouseReport(mx, my, buttons) } 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() { @@ -487,102 +58,14 @@ func triggerUSBStateUpdate() { }() } -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..46bdd52 100644 --- a/usb_mass_storage.go +++ b/usb_mass_storage.go @@ -26,27 +26,33 @@ import ( 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 @@ -108,6 +114,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)