mirror of https://github.com/jetkvm/kvm.git
Fix: USB Gadgets update
This commit is contained in:
parent
44a35aa5c2
commit
bc53523fbb
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
if err != nil && !ignoreUnbindError {
|
||||
return err
|
||||
return rebindUsbWithTimeout(udc, ignoreUnbindError, 10*time.Second)
|
||||
}
|
||||
err = os.WriteFile(path.Join(dwc3Path, "bind"), []byte(udc), 0644)
|
||||
|
||||
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 fmt.Errorf("failed to unbind UDC: %w", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
|
@ -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")
|
||||
|
|
81
jsonrpc.go
81
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -88,6 +89,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 (
|
||||
<Container className="border-b border-b-slate-800/20 bg-white dark:border-b-slate-300/20 dark:bg-slate-900">
|
||||
<div
|
||||
|
@ -316,25 +321,32 @@ export default function Actionbar({
|
|||
/>
|
||||
</div>
|
||||
<Popover>
|
||||
<PopoverButton as={Fragment}>
|
||||
<PopoverButton as={Fragment} disabled={!isAudioEnabledInUsb}>
|
||||
<div title={!isAudioEnabledInUsb ? "Audio needs to be enabled in USB device settings" : undefined}>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Audio"
|
||||
disabled={!isAudioEnabledInUsb}
|
||||
LeadingIcon={({ className }) => (
|
||||
<div className="flex items-center">
|
||||
{isMuted ? (
|
||||
{!isAudioEnabledInUsb ? (
|
||||
<MdVolumeOff className={cx(className, "text-gray-400")} />
|
||||
) : isMuted ? (
|
||||
<MdVolumeOff className={cx(className, "text-red-500")} />
|
||||
) : (
|
||||
<MdVolumeUp className={cx(className, "text-green-500")} />
|
||||
)}
|
||||
<MdGraphicEq className={cx(className, "ml-1 text-blue-500")} />
|
||||
<MdGraphicEq className={cx(className, "ml-1", !isAudioEnabledInUsb ? "text-gray-400" : "text-blue-500")} />
|
||||
</div>
|
||||
)}
|
||||
onClick={() => {
|
||||
if (isAudioEnabledInUsb) {
|
||||
setDisableFocusTrap(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
anchor="bottom end"
|
||||
|
|
|
@ -22,6 +22,7 @@ export interface UsbDeviceConfig {
|
|||
absolute_mouse: boolean;
|
||||
relative_mouse: boolean;
|
||||
mass_storage: boolean;
|
||||
audio: boolean;
|
||||
}
|
||||
|
||||
const defaultUsbDeviceConfig: UsbDeviceConfig = {
|
||||
|
@ -29,17 +30,30 @@ const defaultUsbDeviceConfig: UsbDeviceConfig = {
|
|||
absolute_mouse: true,
|
||||
relative_mouse: true,
|
||||
mass_storage: true,
|
||||
audio: true,
|
||||
};
|
||||
|
||||
const usbPresets = [
|
||||
{
|
||||
label: "Keyboard, Mouse and Mass Storage",
|
||||
label: "Keyboard, Mouse, Mass Storage and Audio",
|
||||
value: "default",
|
||||
config: {
|
||||
keyboard: true,
|
||||
absolute_mouse: true,
|
||||
relative_mouse: true,
|
||||
mass_storage: true,
|
||||
audio: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Keyboard, Mouse and Mass Storage",
|
||||
value: "no_audio",
|
||||
config: {
|
||||
keyboard: true,
|
||||
absolute_mouse: true,
|
||||
relative_mouse: true,
|
||||
mass_storage: true,
|
||||
audio: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -50,6 +64,7 @@ const usbPresets = [
|
|||
absolute_mouse: false,
|
||||
relative_mouse: false,
|
||||
mass_storage: false,
|
||||
audio: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -217,6 +232,17 @@ export function UsbDeviceSetting() {
|
|||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Enable Audio Input/Output"
|
||||
description="Enable USB audio input and output devices"
|
||||
>
|
||||
<Checkbox
|
||||
checked={usbDeviceConfig.audio}
|
||||
onChange={onUsbConfigItemChange("audio")}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex gap-x-2">
|
||||
<Button
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { JsonRpcResponse, useJsonRpc } from "./useJsonRpc";
|
||||
|
||||
export interface UsbDeviceConfig {
|
||||
keyboard: boolean;
|
||||
absolute_mouse: boolean;
|
||||
relative_mouse: boolean;
|
||||
mass_storage: boolean;
|
||||
audio: boolean;
|
||||
}
|
||||
|
||||
export function useUsbDeviceConfig() {
|
||||
const { send } = useJsonRpc();
|
||||
const [usbDeviceConfig, setUsbDeviceConfig] = useState<UsbDeviceConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchUsbDeviceConfig = useCallback(() => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
send("getUsbDevices", {}, (resp: JsonRpcResponse) => {
|
||||
setLoading(false);
|
||||
|
||||
if ("error" in resp) {
|
||||
console.error("Failed to load USB devices:", resp.error);
|
||||
setError(resp.error.data || "Unknown error");
|
||||
setUsbDeviceConfig(null);
|
||||
} else {
|
||||
const config = resp.result as UsbDeviceConfig;
|
||||
setUsbDeviceConfig(config);
|
||||
setError(null);
|
||||
}
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsbDeviceConfig();
|
||||
}, [fetchUsbDeviceConfig]);
|
||||
|
||||
return {
|
||||
usbDeviceConfig,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchUsbDeviceConfig,
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue