diff --git a/internal/audio/supervisor.go b/internal/audio/supervisor.go index 8b4907f..ab65b4a 100644 --- a/internal/audio/supervisor.go +++ b/internal/audio/supervisor.go @@ -95,6 +95,14 @@ func (s *AudioServerSupervisor) Start() error { s.logger.Info().Msg("starting audio server supervisor") + // Recreate channels in case they were closed by a previous Stop() call + s.mutex.Lock() + s.processDone = make(chan struct{}) + s.stopChan = make(chan struct{}) + // Recreate context as well since it might have been cancelled + s.ctx, s.cancel = context.WithCancel(context.Background()) + s.mutex.Unlock() + // Start the supervision loop go s.supervisionLoop() diff --git a/internal/usbgadget/changeset_resolver.go b/internal/usbgadget/changeset_resolver.go index 67812e0..5d894a7 100644 --- a/internal/usbgadget/changeset_resolver.go +++ b/internal/usbgadget/changeset_resolver.go @@ -1,7 +1,9 @@ package usbgadget import ( + "context" "fmt" + "time" "github.com/rs/zerolog" "github.com/sourcegraph/tf-dag/dag" @@ -114,7 +116,20 @@ func (c *ChangeSetResolver) resolveChanges(initial bool) error { } func (c *ChangeSetResolver) applyChanges() error { + return c.applyChangesWithTimeout(30 * time.Second) +} + +func (c *ChangeSetResolver) applyChangesWithTimeout(timeout time.Duration) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + for _, change := range c.resolvedChanges { + select { + case <-ctx.Done(): + return fmt.Errorf("USB gadget reconfiguration timed out after %v: %w", timeout, ctx.Err()) + default: + } + change.ResetActionResolution() action := change.Action() actionStr := FileChangeResolvedActionString[action] @@ -126,7 +141,7 @@ func (c *ChangeSetResolver) applyChanges() error { l.Str("action", actionStr).Str("change", change.String()).Msg("applying change") - err := c.changeset.applyChange(change) + err := c.applyChangeWithTimeout(ctx, change) if err != nil { if change.IgnoreErrors { c.l.Warn().Str("change", change.String()).Err(err).Msg("ignoring error") @@ -139,6 +154,20 @@ func (c *ChangeSetResolver) applyChanges() error { return nil } +func (c *ChangeSetResolver) applyChangeWithTimeout(ctx context.Context, change *FileChange) error { + done := make(chan error, 1) + go func() { + done <- c.changeset.applyChange(change) + }() + + select { + case err := <-done: + return err + case <-ctx.Done(): + return fmt.Errorf("change application timed out for %s: %w", change.String(), ctx.Err()) + } +} + func (c *ChangeSetResolver) GetChanges() ([]*FileChange, error) { localChanges := c.changeset.Changes changesMap := make(map[string]*FileChange) diff --git a/internal/usbgadget/config.go b/internal/usbgadget/config.go index 3b98aca..ff802fc 100644 --- a/internal/usbgadget/config.go +++ b/internal/usbgadget/config.go @@ -213,11 +213,17 @@ func (u *UsbGadget) UpdateGadgetConfig() error { u.loadGadgetConfig() + // Close HID files before reconfiguration to prevent "file already closed" errors + u.CloseHidFiles() + err := u.configureUsbGadget(true) if err != nil { return u.logError("unable to update gadget config", err) } + // Reopen HID files after reconfiguration + u.PreOpenHidFiles() + return nil } diff --git a/internal/usbgadget/udc.go b/internal/usbgadget/udc.go index 4b7fbe3..3d8536d 100644 --- a/internal/usbgadget/udc.go +++ b/internal/usbgadget/udc.go @@ -1,10 +1,12 @@ package usbgadget import ( + "context" "fmt" "os" "path" "strings" + "time" ) func getUdcs() []string { @@ -26,17 +28,44 @@ func getUdcs() []string { } func rebindUsb(udc string, ignoreUnbindError bool) error { - err := os.WriteFile(path.Join(dwc3Path, "unbind"), []byte(udc), 0644) + return rebindUsbWithTimeout(udc, ignoreUnbindError, 10*time.Second) +} + +func rebindUsbWithTimeout(udc string, ignoreUnbindError bool, timeout time.Duration) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + // Unbind with timeout + err := writeFileWithTimeout(ctx, path.Join(dwc3Path, "unbind"), []byte(udc), 0644) if err != nil && !ignoreUnbindError { - return err + return fmt.Errorf("failed to unbind UDC: %w", err) } - err = os.WriteFile(path.Join(dwc3Path, "bind"), []byte(udc), 0644) + + // Small delay to allow unbind to complete + time.Sleep(100 * time.Millisecond) + + // Bind with timeout + err = writeFileWithTimeout(ctx, path.Join(dwc3Path, "bind"), []byte(udc), 0644) if err != nil { - return err + return fmt.Errorf("failed to bind UDC: %w", err) } return nil } +func writeFileWithTimeout(ctx context.Context, filename string, data []byte, perm os.FileMode) error { + done := make(chan error, 1) + go func() { + done <- os.WriteFile(filename, data, perm) + }() + + select { + case err := <-done: + return err + case <-ctx.Done(): + return fmt.Errorf("write operation timed out: %w", ctx.Err()) + } +} + func (u *UsbGadget) rebindUsb(ignoreUnbindError bool) error { u.log.Info().Str("udc", u.udc).Msg("rebinding USB gadget to UDC") return rebindUsb(u.udc, ignoreUnbindError) diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go index af078dc..ede6f52 100644 --- a/internal/usbgadget/usbgadget.go +++ b/internal/usbgadget/usbgadget.go @@ -95,8 +95,41 @@ func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger * return newUsbGadget(name, defaultGadgetConfig, enabledDevices, config, logger) } +// CloseHidFiles closes all open HID files +func (u *UsbGadget) CloseHidFiles() { + u.log.Debug().Msg("closing HID files") + + // Close keyboard HID file + if u.keyboardHidFile != nil { + if err := u.keyboardHidFile.Close(); err != nil { + u.log.Debug().Err(err).Msg("failed to close keyboard HID file") + } + u.keyboardHidFile = nil + } + + // Close absolute mouse HID file + if u.absMouseHidFile != nil { + if err := u.absMouseHidFile.Close(); err != nil { + u.log.Debug().Err(err).Msg("failed to close absolute mouse HID file") + } + u.absMouseHidFile = nil + } + + // Close relative mouse HID file + if u.relMouseHidFile != nil { + if err := u.relMouseHidFile.Close(); err != nil { + u.log.Debug().Err(err).Msg("failed to close relative mouse HID file") + } + u.relMouseHidFile = nil + } +} + // PreOpenHidFiles opens all HID files to reduce input latency func (u *UsbGadget) PreOpenHidFiles() { + // Add a small delay to allow USB gadget reconfiguration to complete + // This prevents "no such device or address" errors when trying to open HID files + time.Sleep(100 * time.Millisecond) + if u.enabledDevices.Keyboard { if err := u.openKeyboardHidFile(); err != nil { u.log.Debug().Err(err).Msg("failed to pre-open keyboard HID file") diff --git a/jsonrpc.go b/jsonrpc.go index ffe67ed..d592f2f 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -908,9 +908,58 @@ func updateUsbRelatedConfig() error { } func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { + // Check if audio state is changing + previousAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio + newAudioEnabled := usbDevices.Audio + + // Handle audio process management if state is changing + if previousAudioEnabled != newAudioEnabled { + if !newAudioEnabled && audioSupervisor != nil && audioSupervisor.IsRunning() { + // Stop audio processes when audio is disabled + logger.Info().Msg("stopping audio processes due to audio device being disabled") + if err := audioSupervisor.Stop(); err != nil { + logger.Error().Err(err).Msg("failed to stop audio supervisor") + } + // Wait for audio processes to fully stop before proceeding + for i := 0; i < 50; i++ { // Wait up to 5 seconds + if !audioSupervisor.IsRunning() { + break + } + time.Sleep(100 * time.Millisecond) + } + logger.Info().Msg("audio processes stopped, proceeding with USB gadget reconfiguration") + } else if newAudioEnabled && audioSupervisor != nil && !audioSupervisor.IsRunning() { + // Start audio processes when audio is enabled (after USB reconfiguration) + logger.Info().Msg("audio will be started after USB gadget reconfiguration") + } + } + config.UsbDevices = &usbDevices gadget.SetGadgetDevices(config.UsbDevices) - return updateUsbRelatedConfig() + + // Apply USB gadget configuration changes + err := updateUsbRelatedConfig() + if err != nil { + return err + } + + // Start audio processes after successful USB reconfiguration if needed + if previousAudioEnabled != newAudioEnabled && newAudioEnabled && audioSupervisor != nil { + // Ensure supervisor is fully stopped before starting + for i := 0; i < 50; i++ { // Wait up to 5 seconds + if !audioSupervisor.IsRunning() { + break + } + time.Sleep(100 * time.Millisecond) + } + logger.Info().Msg("starting audio processes after USB gadget reconfiguration") + if err := audioSupervisor.Start(); err != nil { + logger.Error().Err(err).Msg("failed to start audio supervisor") + // Don't return error here as USB reconfiguration was successful + } + } + + return nil } func rpcSetUsbDeviceState(device string, enabled bool) error { @@ -923,6 +972,36 @@ func rpcSetUsbDeviceState(device string, enabled bool) error { config.UsbDevices.Keyboard = enabled case "massStorage": config.UsbDevices.MassStorage = enabled + case "audio": + // Handle audio process management + if !enabled && audioSupervisor != nil && audioSupervisor.IsRunning() { + // Stop audio processes when audio is disabled + logger.Info().Msg("stopping audio processes due to audio device being disabled") + if err := audioSupervisor.Stop(); err != nil { + logger.Error().Err(err).Msg("failed to stop audio supervisor") + } + // Wait for audio processes to fully stop + for i := 0; i < 50; i++ { // Wait up to 5 seconds + if !audioSupervisor.IsRunning() { + break + } + time.Sleep(100 * time.Millisecond) + } + } else if enabled && audioSupervisor != nil { + // Ensure supervisor is fully stopped before starting + for i := 0; i < 50; i++ { // Wait up to 5 seconds + if !audioSupervisor.IsRunning() { + break + } + time.Sleep(100 * time.Millisecond) + } + // Start audio processes when audio is enabled + logger.Info().Msg("starting audio processes due to audio device being enabled") + if err := audioSupervisor.Start(); err != nil { + logger.Error().Err(err).Msg("failed to start audio supervisor") + } + } + config.UsbDevices.Audio = enabled default: return fmt.Errorf("invalid device: %s", device) } diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx index 97c9c91..11a7c6e 100644 --- a/ui/src/components/ActionBar.tsx +++ b/ui/src/components/ActionBar.tsx @@ -21,6 +21,7 @@ import ExtensionPopover from "@/components/popovers/ExtensionPopover"; import AudioControlPopover from "@/components/popovers/AudioControlPopover"; import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; import { useAudioEvents } from "@/hooks/useAudioEvents"; +import { useUsbDeviceConfig } from "@/hooks/useUsbDeviceConfig"; // Type for microphone error @@ -87,6 +88,10 @@ export default function Actionbar({ // Use WebSocket data exclusively - no polling fallback const isMuted = audioMuted ?? false; // Default to false if WebSocket data not available yet + + // Get USB device configuration to check if audio is enabled + const { usbDeviceConfig } = useUsbDeviceConfig(); + const isAudioEnabledInUsb = usbDeviceConfig?.audio ?? true; // Default to true while loading return ( @@ -316,25 +321,32 @@ export default function Actionbar({ /> - -