diff --git a/config.go b/config.go index 304d8f6..853081d 100644 --- a/config.go +++ b/config.go @@ -20,6 +20,13 @@ type UsbConfig struct { 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"` @@ -39,6 +46,7 @@ type Config struct { DisplayOffAfterSec int `json:"display_off_after_sec"` TLSMode string `json:"tls_mode"` UsbConfig *UsbConfig `json:"usb_config"` + UsbDevices *UsbDevicesConfig `json:"usb_devices"` } const configPath = "/userdata/kvm_config.json" @@ -59,6 +67,12 @@ var defaultConfig = &Config{ Manufacturer: "JetKVM", Product: "USB Emulation Device", }, + UsbDevices: &UsbDevicesConfig{ + AbsoluteMouse: true, + RelativeMouse: true, + Keyboard: true, + MassStorage: true, + }, } var ( @@ -97,6 +111,10 @@ func LoadConfig() { loadedConfig.UsbConfig = defaultConfig.UsbConfig } + if loadedConfig.UsbDevices == nil { + loadedConfig.UsbDevices = defaultConfig.UsbDevices + } + config = &loadedConfig } diff --git a/jsonrpc.go b/jsonrpc.go index e94a950..3021bd2 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -546,19 +546,7 @@ func rpcGetUsbConfig() (UsbConfig, error) { func rpcSetUsbConfig(usbConfig UsbConfig) 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 + return updateUsbRelatedConfig() } func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) { @@ -753,6 +741,41 @@ func rpcSetSerialSettings(settings SerialSettings) error { return nil } +func rpcGetUsbDevices() (UsbDevicesConfig, error) { + return *config.UsbDevices, nil +} + +func updateUsbRelatedConfig() error { + if err := 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 UsbDevicesConfig) error { + config.UsbDevices = &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) + } + return updateUsbRelatedConfig() +} + func rpcSetCloudUrl(apiUrl string, appUrl string) error { config.CloudURL = apiUrl config.CloudAppURL = appUrl @@ -823,5 +846,8 @@ 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"}}, } diff --git a/ui/src/components/UsbDeviceSetting.tsx b/ui/src/components/UsbDeviceSetting.tsx new file mode 100644 index 0000000..9509203 --- /dev/null +++ b/ui/src/components/UsbDeviceSetting.tsx @@ -0,0 +1,130 @@ +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; + } + // setUsbConfigProduct(usbConfig.product); + 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/usb.go b/usb.go index 6989ed4..df4f1e0 100644 --- a/usb.go +++ b/usb.go @@ -21,7 +21,25 @@ const gadgetPath = "/sys/kernel/config/usb_gadget" const kvmGadgetPath = "/sys/kernel/config/usb_gadget/jetkvm" const configC1Path = "/sys/kernel/config/usb_gadget/jetkvm/configs/c.1" +func (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 + } +} + type gadgetConfigItem struct { + device string path []string attrs gadgetAttributes configAttrs gadgetAttributes @@ -56,6 +74,7 @@ var gadgetConfig = map[string]gadgetConfigItem{ }, // keyboard HID "keyboard": { + device: "hid.usb0", path: []string{"functions", "hid.usb0"}, configPath: path.Join(configC1Path, "hid.usb0"), attrs: gadgetAttributes{ @@ -67,6 +86,7 @@ var gadgetConfig = map[string]gadgetConfigItem{ }, // mouse HID "absolute_mouse": { + device: "hid.usb1", path: []string{"functions", "hid.usb1"}, configPath: path.Join(configC1Path, "hid.usb1"), attrs: gadgetAttributes{ @@ -78,6 +98,7 @@ var gadgetConfig = map[string]gadgetConfigItem{ }, // relative mouse HID "relative_mouse": { + device: "hid.usb2", path: []string{"functions", "hid.usb2"}, configPath: path.Join(configC1Path, "hid.usb2"), attrs: gadgetAttributes{ @@ -89,13 +110,13 @@ var gadgetConfig = map[string]gadgetConfigItem{ }, // 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{ @@ -148,6 +169,22 @@ func writeIfDifferent(filePath string, content []byte, permMode os.FileMode) err 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...) @@ -289,6 +326,15 @@ func writeGadgetConfig() error { 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 {