[client] usbredir: gracefully disconnect USB devices

Wait for the usbredir disconnect acknowledgement before closing SPICE.

Block replug while removal is pending and bound the shutdown wait.
This commit is contained in:
Geoffrey McRae
2026-08-10 05:01:55 +10:00
parent 2ca2553086
commit 73b15e4723
3 changed files with 54 additions and 9 deletions

View File

@@ -224,6 +224,7 @@ static bool tickTimerFn(void * unused)
#define FRAME_TIMING_RECORD_COUNT 1024 #define FRAME_TIMING_RECORD_COUNT 1024
#define FRAME_TIMING_PUBLISH_BATCH_SIZE 32 #define FRAME_TIMING_PUBLISH_BATCH_SIZE 32
#define FRAME_TIMING_PRESENT_TIMEOUT_NS 500000000ULL #define FRAME_TIMING_PRESENT_TIMEOUT_NS 500000000ULL
#define USB_REDIR_DISCONNECT_TIMEOUT_US 500000ULL
enum FrameTimingReady enum FrameTimingReady
{ {
@@ -1811,6 +1812,7 @@ int spiceThread(void * arg)
}; };
bool connected = false; bool connected = false;
PSStatus status = PS_STATUS_SHUTDOWN;
if (!purespice_connect(&config)) if (!purespice_connect(&config))
{ {
DEBUG_ERROR("Failed to connect to spice server"); DEBUG_ERROR("Failed to connect to spice server");
@@ -1818,6 +1820,7 @@ int spiceThread(void * arg)
goto end; goto end;
} }
connected = true; connected = true;
status = PS_STATUS_RUN;
int processTimeout = 100; int processTimeout = 100;
#if ENABLE_USB_AUDIO #if ENABLE_USB_AUDIO
@@ -1837,7 +1840,6 @@ int spiceThread(void * arg)
DEBUG_WARN("Failed to process USB audio redirection"); DEBUG_WARN("Failed to process USB audio redirection");
#endif #endif
PSStatus status;
if ((status = purespice_process(processTimeout)) != PS_STATUS_RUN) if ((status = purespice_process(processTimeout)) != PS_STATUS_RUN)
{ {
if (status != PS_STATUS_SHUTDOWN) if (status != PS_STATUS_SHUTDOWN)
@@ -1856,8 +1858,27 @@ end:
lgaSpice_setAvailable(false); lgaSpice_setAvailable(false);
#endif #endif
#if ENABLE_USB_AUDIO #if ENABLE_USB_AUDIO
if (connected && usbRedir && !lgUsbRedir_process(usbRedir)) if (connected && status == PS_STATUS_RUN && usbRedir)
{
if (!lgUsbRedir_process(usbRedir))
DEBUG_WARN("Failed to disconnect USB audio device"); DEBUG_WARN("Failed to disconnect USB audio device");
else
{
const uint64_t deadline =
microtime() + USB_REDIR_DISCONNECT_TIMEOUT_US;
while (lgUsbRedir_disconnectPending(usbRedir) &&
microtime() < deadline)
{
status = purespice_process(10);
if (status != PS_STATUS_RUN)
break;
}
if (status == PS_STATUS_RUN &&
lgUsbRedir_disconnectPending(usbRedir))
DEBUG_WARN("Timed out disconnecting USB audio device");
}
}
#endif #endif
if (connected) if (connected)
purespice_disconnect(); purespice_disconnect();

View File

@@ -48,6 +48,7 @@ struct LG_USBRedir
atomic_bool desiredPlugged; atomic_bool desiredPlugged;
atomic_bool available; atomic_bool available;
bool plugged; bool plugged;
bool disconnectPending;
}; };
static void setAvailable(LG_USBRedir * usbredir, bool available) static void setAvailable(LG_USBRedir * usbredir, bool available)
@@ -118,6 +119,12 @@ static void helloUSBRedir(void * opaque,
setAvailable(usbredir, true); setAvailable(usbredir, true);
} }
static void deviceDisconnectAck(void * opaque)
{
LG_USBRedir * usbredir = opaque;
usbredir->disconnectPending = false;
}
static void unplugDevice(LG_USBRedir * usbredir) static void unplugDevice(LG_USBRedir * usbredir)
{ {
if (!usbredir->plugged) if (!usbredir->plugged)
@@ -131,6 +138,7 @@ static void destroyParser(LG_USBRedir * usbredir)
{ {
setAvailable(usbredir, false); setAvailable(usbredir, false);
unplugDevice(usbredir); unplugDevice(usbredir);
usbredir->disconnectPending = false;
if (!usbredir->parser) if (!usbredir->parser)
return; return;
@@ -192,8 +200,11 @@ static bool createParser(LG_USBRedir * usbredir)
usbredir->parser->read_func = readUSBRedir; usbredir->parser->read_func = readUSBRedir;
usbredir->parser->write_func = writeUSBRedir; usbredir->parser->write_func = writeUSBRedir;
usbredir->parser->hello_func = helloUSBRedir; usbredir->parser->hello_func = helloUSBRedir;
usbredir->parser->device_disconnect_ack_func = deviceDisconnectAck;
uint32_t caps[USB_REDIR_CAPS_SIZE] = { 0 }; uint32_t caps[USB_REDIR_CAPS_SIZE] = { 0 };
usbredirparser_caps_set_cap(caps,
usb_redir_cap_device_disconnect_ack);
usbredirparser_caps_set_cap(caps, usbredirparser_caps_set_cap(caps,
usb_redir_cap_connect_device_version); usb_redir_cap_connect_device_version);
usbredirparser_caps_set_cap(caps, usbredirparser_caps_set_cap(caps,
@@ -252,12 +263,20 @@ bool lgUsbRedir_available(const LG_USBRedir * usbredir)
return atomic_load_explicit(&usbredir->available, memory_order_acquire); return atomic_load_explicit(&usbredir->available, memory_order_acquire);
} }
bool lgUsbRedir_disconnectPending(const LG_USBRedir * usbredir)
{
return usbredir->disconnectPending;
}
bool lgUsbRedir_process(LG_USBRedir * usbredir) bool lgUsbRedir_process(LG_USBRedir * usbredir)
{ {
if (!usbredir->parser || if (!usbredir->parser ||
!atomic_load_explicit(&usbredir->available, memory_order_acquire)) !atomic_load_explicit(&usbredir->available, memory_order_acquire))
return flushUSBRedir(usbredir); return flushUSBRedir(usbredir);
if (usbredir->disconnectPending)
return flushUSBRedir(usbredir);
const bool desired = atomic_load_explicit(&usbredir->desiredPlugged, const bool desired = atomic_load_explicit(&usbredir->desiredPlugged,
memory_order_acquire); memory_order_acquire);
if (desired != usbredir->plugged) if (desired != usbredir->plugged)
@@ -271,6 +290,8 @@ bool lgUsbRedir_process(LG_USBRedir * usbredir)
else else
{ {
unplugDevice(usbredir); unplugDevice(usbredir);
usbredir->disconnectPending = usbredirparser_peer_has_cap(
usbredir->parser, usb_redir_cap_device_disconnect_ack);
usbredirparser_send_device_disconnect(usbredir->parser); usbredirparser_send_device_disconnect(usbredir->parser);
} }
} }

View File

@@ -57,6 +57,9 @@ void lgUsbRedir_destroy(LG_USBRedir * usbredir);
* lgUsbRedir_process on the PureSpice processing thread. */ * lgUsbRedir_process on the PureSpice processing thread. */
void lgUsbRedir_setPlugged(LG_USBRedir * usbredir, bool plugged); void lgUsbRedir_setPlugged(LG_USBRedir * usbredir, bool plugged);
bool lgUsbRedir_available(const LG_USBRedir * usbredir); bool lgUsbRedir_available(const LG_USBRedir * usbredir);
/* True after a device disconnect has been sent and until the guest has
* completed the detach. This must be queried on the processing thread. */
bool lgUsbRedir_disconnectPending(const LG_USBRedir * usbredir);
/* Apply pending device state and flush parser output. This must be called on /* Apply pending device state and flush parser output. This must be called on
* the PureSpice processing thread. */ * the PureSpice processing thread. */