Phase 2: Wire usb_tx and channel events¶
Parent plan: PLAN-usb-ui.md
Goal¶
Wire the USB command channel sender (usb_tx) from
RyllApp through to UsbredirChannel, following the
same pattern as input_tx/input_rx. Add new
ChannelEvent variants for device disconnect and
connection failure. Update process_events() to handle
all USB-related events including usbredir channel
disconnect.
After this phase:
RyllAppholds ausb_txsender that can sendUsbCommandmessages to theUsbredirChannel.- The
_usb_txthat was created and dropped inrun_connection()is gone — the channel is created inRyllApp::new()and threaded through. ChannelEvent::UsbDeviceDisconnectedandChannelEvent::UsbConnectFailed(String)exist and are handled inprocess_events().- Usbredir channel disconnect resets all USB UI state.
- Headless mode handles the new events too.
No UI changes in this phase — just plumbing.
Detailed steps¶
Step 1: Add new ChannelEvent variants¶
In src/channels/mod.rs, add two variants to
ChannelEvent:
/// A USB device was disconnected
UsbDeviceDisconnected,
/// A USB device connection attempt failed
UsbConnectFailed(String),
Remove the #[allow(dead_code)] from
UsbDevicesChanged while we're here — it will be used
in phase 4.
Step 2: Send UsbDeviceDisconnected from the channel handler¶
In src/channels/usbredir.rs, update
disconnect_device() to send the event after
disconnecting:
async fn disconnect_device(&mut self) -> Result<()> {
if self.backend.is_some() {
info!("usbredir: disconnecting device");
self.stop_all_interrupt_polls();
self.send_usbredir(msg_type::DEVICE_DISCONNECT, 0, &[])
.await?;
self.backend = None;
self.event_tx
.send(ChannelEvent::UsbDeviceDisconnected)
.await
.ok();
}
Ok(())
}
Step 3: Add new fields to RyllApp¶
In src/app.rs, add three fields to struct RyllApp:
// USB command sender (None if no usbredir channel)
usb_tx: Option<mpsc::Sender<UsbCommand>>,
// USB channel readiness and operation state
usb_channel_ready: bool,
usb_connecting: bool,
// USB error message from failed operations
usb_error_message: Option<String>,
Import UsbCommand in app.rs (add to the existing
use crate::channels::{ ... } block).
Initialise all four fields in RyllApp::new():
Step 4: Create usb channel in RyllApp::new() (GUI mode)¶
In RyllApp::new(), next to the existing channel
creation at line 214-215:
let (event_tx, event_rx) = mpsc::channel(EVENT_CHANNEL_SIZE);
let (input_tx, input_rx) = mpsc::channel(INPUT_CHANNEL_SIZE);
Add:
Then pass usb_rx into the run_connection() call at
line 248. This means adding it to the closure's move
captures and to the function call.
The std::thread::spawn closure currently captures
input_rx by move. Add usb_rx the same way.
Step 5: Add usb_rx parameter to run_connection()¶
Change the signature of run_connection() from:
async fn run_connection(
config: Config,
event_tx: mpsc::Sender<ChannelEvent>,
input_rx: mpsc::Receiver<InputEvent>,
virtual_disks: Vec<VirtualDiskConfig>,
capture: Option<Arc<CaptureSession>>,
byte_counter: Arc<ByteCounter>,
traffic: Arc<TrafficBuffers>,
snapshots: ChannelSnapshots,
) -> Result<()> {
to:
async fn run_connection(
config: Config,
event_tx: mpsc::Sender<ChannelEvent>,
input_rx: mpsc::Receiver<InputEvent>,
usb_rx: mpsc::Receiver<UsbCommand>,
virtual_disks: Vec<VirtualDiskConfig>,
capture: Option<Arc<CaptureSession>>,
byte_counter: Arc<ByteCounter>,
traffic: Arc<TrafficBuffers>,
snapshots: ChannelSnapshots,
) -> Result<()> {
Step 6: Use the passed-in usb_rx in run_connection()¶
In run_connection(), in the ChannelType::Usbredir
arm (line 1453-1467), replace:
with just using the usb_rx parameter directly. Since
usb_rx is moved into UsbredirChannel::new(), and
run_connection only handles one usbredir channel, this
works. If the server advertises multiple usbredir
channels, only the first gets the usb_rx — subsequent
ones would need their own channels, but that's out of
scope (the code already only processes one due to the
loop structure).
However, there's a subtlety: usb_rx is a parameter
that can only be moved once. The channel loop iterates
over potentially multiple channels. We need to wrap it
in an Option and take() it:
At the top of the channel connection loop (after the
let mut handles line), add:
Then in the ChannelType::Usbredir arm:
ChannelType::Usbredir => {
if let Some(usb_rx) = usb_rx.take() {
let stream = client
.connect_channel(
session_id,
channel_type,
channel_id,
)
.await?;
let mut channel = UsbredirChannel::new(
stream,
event_tx.clone(),
usb_rx,
virtual_disks.clone(),
capture.clone(),
byte_counter.clone(),
);
handles.push(tokio::spawn(
async move { channel.run().await },
));
} else {
info!(
"Skipping additional usbredir channel \
(id={}): only one supported",
channel_id
);
}
}
This pattern mirrors how input_rx is handled — it's
moved once and then the loop breaks (line 1449-1450).
For usbredir we don't break the loop (other channel
types may follow), but we skip subsequent usbredir
channels with a log message.
Step 7: Wire usb_rx in headless mode¶
In run_headless() (line 1489), create the USB channel
alongside the existing channels:
let (event_tx, mut event_rx) = mpsc::channel(EVENT_CHANNEL_SIZE);
let (input_tx, input_rx) = mpsc::channel(INPUT_CHANNEL_SIZE);
let (_usb_tx, usb_rx) = mpsc::channel(16);
In headless mode, no UI sends USB commands, so the
sender is intentionally unused. The _usb_tx prefix
suppresses the warning. Pass usb_rx to
run_connection().
Step 8: Update process_events() in RyllApp¶
Update the event handling in process_events() (around
line 427-441) to handle all USB events:
ChannelEvent::UsbChannelReady => {
info!("app: USB redirection channel connected");
self.usb_channel_ready = true;
}
ChannelEvent::UsbDeviceConnected(desc) => {
info!("app: USB device connected: {}", desc);
self.usb_device_description = Some(desc);
self.usb_connecting = false;
}
ChannelEvent::UsbDeviceDisconnected => {
info!("app: USB device disconnected");
self.usb_device_description = None;
self.usb_connecting = false;
}
ChannelEvent::UsbConnectFailed(err) => {
error!("app: USB connect failed: {}", err);
self.usb_connecting = false;
self.usb_error_message = Some(err);
}
ChannelEvent::Disconnected(channel) => {
info!(
"app: channel {} disconnected",
channel.name(),
);
if channel == ChannelType::Main {
self.connected = false;
}
if channel == ChannelType::Usbredir {
self.usb_channel_ready = false;
self.usb_device_description = None;
self.usb_connecting = false;
}
}
The key change to the existing Disconnected handler
is adding the ChannelType::Usbredir check to reset
usb_channel_ready, usb_device_description, and
usb_connecting.
Step 9: Update headless event loop¶
In run_headless() (around line 1561-1571), add
handling for the new events:
ChannelEvent::UsbDeviceConnected(desc) => {
info!("headless: USB device connected: {}", desc);
}
ChannelEvent::UsbDeviceDisconnected => {
info!("headless: USB device disconnected");
}
ChannelEvent::UsbConnectFailed(err) => {
error!("headless: USB connect failed: {}", err);
}
Step 10: Remove dead_code allow from UsbCommand¶
In src/channels/mod.rs, the UsbCommand enum has
#[allow(dead_code)]. Since usb_tx is now stored on
RyllApp (not dropped), the variants will be used in
phase 5. However, they won't actually be sent until
phase 5, so the allow(dead_code) may still be needed
to pass clippy. Check after building — if clippy
complains, keep the allow; if not, remove it.
Files changed¶
| File | Change |
|---|---|
src/channels/mod.rs |
Add UsbDeviceDisconnected and UsbConnectFailed(String) to ChannelEvent; remove #[allow(dead_code)] from UsbDevicesChanged if clippy allows |
src/channels/usbredir.rs |
Send UsbDeviceDisconnected event in disconnect_device() |
src/app.rs |
Add usb_tx, usb_channel_ready, usb_connecting, usb_error_message fields to RyllApp; create USB channel in new(); pass usb_rx to run_connection(); update process_events() for all USB events including usbredir channel disconnect; add usb_rx parameter to run_connection() and use it via Option::take(); update run_headless() to create and pass USB channel; handle new events in headless loop |
What is NOT in scope¶
- Any UI changes (no buttons, no panel).
- Changes to
UsbCommandvariants (that's phase 5). - Sending any
UsbCommandmessages (that's phase 5). UsbConnectFailedis not yet sent by the channel handler — the variant is defined and handled inprocess_events(), but the handler code that sends it comes in phase 5 whenUsbCommandis redesigned.
Testing¶
make build— compiles without errors.make test— all existing tests pass (77 tests).pre-commit run --all-files— rustfmt and clippy clean.- No new unit tests needed for this phase — it's pure plumbing. The correctness is verified by building (type-checked by the compiler) and by the existing test suite still passing.
- Manual verification: run ryll with
--usb-diskand confirm auto-connect still works (the auto-connect path in the hello handler is unchanged).
Back brief¶
This phase threads the USB command channel through the
same path as the input channel: created in
RyllApp::new(), usb_tx stored on the app, usb_rx
passed through run_connection() into
UsbredirChannel. It adds two new event variants
(UsbDeviceDisconnected, UsbConnectFailed) and
updates process_events() to handle them plus the
usbredir channel disconnect case that was previously
missing. The auto-connect flow is unaffected — it calls
connect_device() directly without going through
UsbCommand. Headless mode creates the channel but
drops the sender since no UI drives it.