From f0595fff405e0e396565804869b9f05883207507 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Wed, 5 Nov 2025 15:45:34 +0000 Subject: [PATCH 1/6] fix: await sleep needs to be called inside async function --- ui/src/routes/devices.$id.settings.advanced.tsx | 3 ++- ui/src/routes/devices.$id.settings.general.reboot.tsx | 3 ++- ui/src/routes/devices.$id.settings.general.update.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/src/routes/devices.$id.settings.advanced.tsx b/ui/src/routes/devices.$id.settings.advanced.tsx index dbc98a75..4c3c9e94 100644 --- a/ui/src/routes/devices.$id.settings.advanced.tsx +++ b/ui/src/routes/devices.$id.settings.advanced.tsx @@ -12,6 +12,7 @@ import { TextAreaWithLabel } from "@components/TextArea"; import { isOnDevice } from "@/main"; import notifications from "@/notifications"; import { m } from "@localizations/messages.js"; +import { sleep } from "@/utils"; export default function SettingsAdvancedRoute() { const { send } = useJsonRpc(); @@ -311,7 +312,7 @@ export default function SettingsAdvancedRoute() { size="SM" theme="light" text={m.advanced_reset_config_button()} - onClick={() => { + onClick={async () => { handleResetConfig(); // Add 2s delay between resetting the configuration and calling reload() to prevent reload from interrupting the RPC call to reset things. await sleep(2000); diff --git a/ui/src/routes/devices.$id.settings.general.reboot.tsx b/ui/src/routes/devices.$id.settings.general.reboot.tsx index 5a4474e6..fc0feeaa 100644 --- a/ui/src/routes/devices.$id.settings.general.reboot.tsx +++ b/ui/src/routes/devices.$id.settings.general.reboot.tsx @@ -4,12 +4,13 @@ import { useNavigate } from "react-router"; import { useJsonRpc } from "@hooks/useJsonRpc"; import { Button } from "@components/Button"; import { m } from "@localizations/messages.js"; +import { sleep } from "@/utils"; export default function SettingsGeneralRebootRoute() { const navigate = useNavigate(); const { send } = useJsonRpc(); - const onClose = useCallback(() => { + const onClose = useCallback(async () => { navigate(".."); // back to the devices.$id.settings page // Add 1s delay between navigation and calling reload() to prevent reload from interrupting the navigation. await sleep(1000); diff --git a/ui/src/routes/devices.$id.settings.general.update.tsx b/ui/src/routes/devices.$id.settings.general.update.tsx index 3a52eee2..285ce940 100644 --- a/ui/src/routes/devices.$id.settings.general.update.tsx +++ b/ui/src/routes/devices.$id.settings.general.update.tsx @@ -21,7 +21,7 @@ export default function SettingsGeneralUpdateRoute() { const { setModalView, otaState } = useUpdateStore(); const { send } = useJsonRpc(); - const onClose = useCallback(() => { + const onClose = useCallback(async () => { navigate(".."); // back to the devices.$id.settings page // Add 1s delay between navigation and calling reload() to prevent reload from interrupting the navigation. await sleep(1000); From dafebacdfd306c924827aeab48b3e5519fa0bc89 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Wed, 5 Nov 2025 15:43:10 +0000 Subject: [PATCH 2/6] feat: reset USB gadget if needed --- internal/usbgadget/config.go | 38 ++++++++++++++++++++++++------------ jsonrpc.go | 10 +++++----- usb_mass_storage.go | 2 +- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/internal/usbgadget/config.go b/internal/usbgadget/config.go index 6d1bd391..8cbdf081 100644 --- a/internal/usbgadget/config.go +++ b/internal/usbgadget/config.go @@ -177,7 +177,7 @@ func (u *UsbGadget) Init() error { u.udc = udcs[0] - err := u.configureUsbGadget(false) + err := u.configureUsbGadget(false, true) if err != nil { return u.logError("unable to initialize USB stack", err) } @@ -185,13 +185,13 @@ func (u *UsbGadget) Init() error { return nil } -func (u *UsbGadget) UpdateGadgetConfig() error { +func (u *UsbGadget) UpdateGadgetConfig(resetUsbIfNeeded bool) error { u.configLock.Lock() defer u.configLock.Unlock() u.loadGadgetConfig() - err := u.configureUsbGadget(true) + err := u.configureUsbGadget(true, resetUsbIfNeeded) if err != nil { return u.logError("unable to update gadget config", err) } @@ -199,14 +199,28 @@ func (u *UsbGadget) UpdateGadgetConfig() error { return nil } -func (u *UsbGadget) configureUsbGadget(resetUsb bool) error { - return u.WithTransaction(func() error { - u.tx.MountConfigFS() - u.tx.CreateConfigPath() - u.tx.WriteGadgetConfig() - if resetUsb { - u.tx.RebindUsb(true) +func (u *UsbGadget) configureUsbGadget(resetUsb bool, resetUsbIfNeeded bool) error { + f := func(resetUsbBefore bool, resetUsbAfter bool) func() error { + return func() error { + if resetUsbBefore { + u.tx.RebindUsb(true) + } + u.tx.MountConfigFS() + u.tx.CreateConfigPath() + u.tx.WriteGadgetConfig() + if resetUsbAfter { + u.tx.RebindUsb(true) + } + return nil } - return nil - }) + } + + // initial attempt to configure the gadget + err := u.WithTransaction(f(false, resetUsb)) + if err != nil && !resetUsbIfNeeded { + return err + } + + // if the initial attempt failed, try to configure the gadget again with the resetUsb flag + return u.WithTransaction(f(true, resetUsb)) } diff --git a/jsonrpc.go b/jsonrpc.go index 5ed90a7a..4d8695c2 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -678,7 +678,7 @@ func rpcSetUsbConfig(usbConfig usbgadget.Config) error { LoadConfig() config.UsbConfig = &usbConfig gadget.SetGadgetConfig(config.UsbConfig) - return updateUsbRelatedConfig() + return updateUsbRelatedConfig(false) } func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) { @@ -890,8 +890,8 @@ func rpcGetUsbDevices() (usbgadget.Devices, error) { return *config.UsbDevices, nil } -func updateUsbRelatedConfig() error { - if err := gadget.UpdateGadgetConfig(); err != nil { +func updateUsbRelatedConfig(resetUsbIfNeeded bool) error { + if err := gadget.UpdateGadgetConfig(resetUsbIfNeeded); err != nil { return fmt.Errorf("failed to write gadget config: %w", err) } if err := SaveConfig(); err != nil { @@ -903,7 +903,7 @@ func updateUsbRelatedConfig() error { func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { config.UsbDevices = &usbDevices gadget.SetGadgetDevices(config.UsbDevices) - return updateUsbRelatedConfig() + return updateUsbRelatedConfig(false) } func rpcSetUsbDeviceState(device string, enabled bool) error { @@ -920,7 +920,7 @@ func rpcSetUsbDeviceState(device string, enabled bool) error { return fmt.Errorf("invalid device: %s", device) } gadget.SetGadgetDevices(config.UsbDevices) - return updateUsbRelatedConfig() + return updateUsbRelatedConfig(false) } func rpcSetCloudUrl(apiUrl string, appUrl string) error { diff --git a/usb_mass_storage.go b/usb_mass_storage.go index 0f1f4b93..9a112e3a 100644 --- a/usb_mass_storage.go +++ b/usb_mass_storage.go @@ -66,7 +66,7 @@ func setMassStorageMode(cdrom bool) error { return nil } - return gadget.UpdateGadgetConfig() + return gadget.UpdateGadgetConfig(true) } func mountImage(imagePath string) error { From a2eb32b9377058513169d9d2fbbe58f7a8ffd642 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Tue, 11 Nov 2025 11:26:49 +0000 Subject: [PATCH 3/6] chore(usb-reset): use usbGadget.overrideGadgetConfig instead of writePath --- usb_mass_storage.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/usb_mass_storage.go b/usb_mass_storage.go index 9a112e3a..d1ea954c 100644 --- a/usb_mass_storage.go +++ b/usb_mass_storage.go @@ -22,10 +22,6 @@ import ( "github.com/jetkvm/kvm/resource" ) -func writeFile(path string, data string) error { - return os.WriteFile(path, []byte(data), 0644) -} - func getMassStorageImage() (string, error) { massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0") if err != nil { @@ -40,14 +36,14 @@ func getMassStorageImage() (string, error) { } func setMassStorageImage(imagePath string) error { - massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0") - if err != nil { - return fmt.Errorf("failed to get mass storage path: %w", err) + if err, _ := gadget.OverrideGadgetConfig("mass_storage_lun0", "file", imagePath); err != nil { + return fmt.Errorf("failed to set 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) + if err := gadget.UpdateGadgetConfig(true); err != nil { + return fmt.Errorf("failed to update gadget config: %w", err) } + return nil } From 65119ff0407c9fc467ee5e3ff2bfa6cde666e2c2 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Tue, 11 Nov 2025 11:48:03 +0000 Subject: [PATCH 4/6] refactor(usb-reset): use UsbResetMode enum instead of bool --- internal/usbgadget/config.go | 39 +++++++++++++++++++++++++----------- jsonrpc.go | 10 ++++----- usb_mass_storage.go | 5 +++-- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/internal/usbgadget/config.go b/internal/usbgadget/config.go index 8cbdf081..8333a365 100644 --- a/internal/usbgadget/config.go +++ b/internal/usbgadget/config.go @@ -24,6 +24,14 @@ type gadgetConfigItemWithKey struct { type orderedGadgetConfigItems []gadgetConfigItemWithKey +type UsbResetMode uint8 + +const ( + UsbResetNever UsbResetMode = 0 // never reset the usb gadget + UsbResetAlways UsbResetMode = 1 // always reset the usb gadget + UsbResetOnDemand UsbResetMode = 2 // reset the usb gadget when needed +) + var defaultGadgetConfig = map[string]gadgetConfigItem{ "base": { order: 0, @@ -177,7 +185,7 @@ func (u *UsbGadget) Init() error { u.udc = udcs[0] - err := u.configureUsbGadget(false, true) + err := u.configureUsbGadget(UsbResetAlways) if err != nil { return u.logError("unable to initialize USB stack", err) } @@ -185,13 +193,13 @@ func (u *UsbGadget) Init() error { return nil } -func (u *UsbGadget) UpdateGadgetConfig(resetUsbIfNeeded bool) error { +func (u *UsbGadget) UpdateGadgetConfig(resetUsbMode UsbResetMode) error { u.configLock.Lock() defer u.configLock.Unlock() u.loadGadgetConfig() - err := u.configureUsbGadget(true, resetUsbIfNeeded) + err := u.configureUsbGadget(resetUsbMode) if err != nil { return u.logError("unable to update gadget config", err) } @@ -199,12 +207,9 @@ func (u *UsbGadget) UpdateGadgetConfig(resetUsbIfNeeded bool) error { return nil } -func (u *UsbGadget) configureUsbGadget(resetUsb bool, resetUsbIfNeeded bool) error { - f := func(resetUsbBefore bool, resetUsbAfter bool) func() error { +func (u *UsbGadget) configureUsbGadget(resetUsbMode UsbResetMode) error { + f := func(resetUsbAfter bool) func() error { return func() error { - if resetUsbBefore { - u.tx.RebindUsb(true) - } u.tx.MountConfigFS() u.tx.CreateConfigPath() u.tx.WriteGadgetConfig() @@ -216,11 +221,21 @@ func (u *UsbGadget) configureUsbGadget(resetUsb bool, resetUsbIfNeeded bool) err } // initial attempt to configure the gadget - err := u.WithTransaction(f(false, resetUsb)) - if err != nil && !resetUsbIfNeeded { - return err + err := u.WithTransaction(f(resetUsbMode == UsbResetAlways)) + if err == nil { + return nil } // if the initial attempt failed, try to configure the gadget again with the resetUsb flag - return u.WithTransaction(f(true, resetUsb)) + if resetUsbMode == UsbResetOnDemand { + u.log.Warn().Err(err).Msg("initial attempt to configure the gadget failed, resetting USB gadget then trying again") + // NOTES: do not use the RebindUsb method here because it will block the transaction + if err := u.rebindUsb(true); err != nil { + u.log.Warn().Err(err).Msg("failed to reset USB gadget, skipping second attempt") + return err + } + return u.WithTransaction(f(true)) + } + + return err } diff --git a/jsonrpc.go b/jsonrpc.go index 4d8695c2..68e580a3 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -678,7 +678,7 @@ func rpcSetUsbConfig(usbConfig usbgadget.Config) error { LoadConfig() config.UsbConfig = &usbConfig gadget.SetGadgetConfig(config.UsbConfig) - return updateUsbRelatedConfig(false) + return updateUsbRelatedConfig(usbgadget.UsbResetAlways) } func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) { @@ -890,8 +890,8 @@ func rpcGetUsbDevices() (usbgadget.Devices, error) { return *config.UsbDevices, nil } -func updateUsbRelatedConfig(resetUsbIfNeeded bool) error { - if err := gadget.UpdateGadgetConfig(resetUsbIfNeeded); err != nil { +func updateUsbRelatedConfig(resetUsbMode usbgadget.UsbResetMode) error { + if err := gadget.UpdateGadgetConfig(resetUsbMode); err != nil { return fmt.Errorf("failed to write gadget config: %w", err) } if err := SaveConfig(); err != nil { @@ -903,7 +903,7 @@ func updateUsbRelatedConfig(resetUsbIfNeeded bool) error { func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { config.UsbDevices = &usbDevices gadget.SetGadgetDevices(config.UsbDevices) - return updateUsbRelatedConfig(false) + return updateUsbRelatedConfig(usbgadget.UsbResetOnDemand) } func rpcSetUsbDeviceState(device string, enabled bool) error { @@ -920,7 +920,7 @@ func rpcSetUsbDeviceState(device string, enabled bool) error { return fmt.Errorf("invalid device: %s", device) } gadget.SetGadgetDevices(config.UsbDevices) - return updateUsbRelatedConfig(false) + return updateUsbRelatedConfig(usbgadget.UsbResetAlways) } func rpcSetCloudUrl(apiUrl string, appUrl string) error { diff --git a/usb_mass_storage.go b/usb_mass_storage.go index d1ea954c..2e200ea0 100644 --- a/usb_mass_storage.go +++ b/usb_mass_storage.go @@ -19,6 +19,7 @@ import ( "github.com/pion/webrtc/v4" "github.com/psanford/httpreadat" + "github.com/jetkvm/kvm/internal/usbgadget" "github.com/jetkvm/kvm/resource" ) @@ -40,7 +41,7 @@ func setMassStorageImage(imagePath string) error { return fmt.Errorf("failed to set mass storage path: %w", err) } - if err := gadget.UpdateGadgetConfig(true); err != nil { + if err := gadget.UpdateGadgetConfig(usbgadget.UsbResetOnDemand); err != nil { return fmt.Errorf("failed to update gadget config: %w", err) } @@ -62,7 +63,7 @@ func setMassStorageMode(cdrom bool) error { return nil } - return gadget.UpdateGadgetConfig(true) + return gadget.UpdateGadgetConfig(usbgadget.UsbResetOnDemand) } func mountImage(imagePath string) error { From 3e2786ef178267b2d8d2f3529d3eb88fd13949c0 Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Tue, 11 Nov 2025 13:16:48 +0100 Subject: [PATCH 5/6] Update usb_mass_storage.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- usb_mass_storage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usb_mass_storage.go b/usb_mass_storage.go index 2e200ea0..10d4db58 100644 --- a/usb_mass_storage.go +++ b/usb_mass_storage.go @@ -37,7 +37,7 @@ func getMassStorageImage() (string, error) { } func setMassStorageImage(imagePath string) error { - if err, _ := gadget.OverrideGadgetConfig("mass_storage_lun0", "file", imagePath); err != nil { + if err := gadget.OverrideGadgetConfig("mass_storage_lun0", "file", imagePath); err != nil { return fmt.Errorf("failed to set mass storage path: %w", err) } From cab8ea4fd35758c7e300a2f82c87b24f77020b28 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Tue, 11 Nov 2025 12:18:59 +0000 Subject: [PATCH 6/6] fix golangci-lint errors --- usb_mass_storage.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/usb_mass_storage.go b/usb_mass_storage.go index 10d4db58..85921153 100644 --- a/usb_mass_storage.go +++ b/usb_mass_storage.go @@ -37,7 +37,8 @@ func getMassStorageImage() (string, error) { } func setMassStorageImage(imagePath string) error { - if err := gadget.OverrideGadgetConfig("mass_storage_lun0", "file", imagePath); err != nil { + err, _ := gadget.OverrideGadgetConfig("mass_storage_lun0", "file", imagePath) + if err != nil { return fmt.Errorf("failed to set mass storage path: %w", err) }