1use std::{fmt, sync::Arc};
5
6use super::{ArmedControl, HostSwitchError, restore_host_controls};
7use crate::session::restore::{
8 PendingRestore, RestoreOutcome, RestorePlan, SessionFailure, rollback_start,
9};
10use crate::{ChannelRegistry, DeviceIoGate, SharedChannel, reprog_controls::ReprogControlsV4};
11
12#[must_use = "pending firmware restoration must be retained by the session manager"]
14pub enum HostSwitchSessionOutcome {
15 Restored {
17 requested_host: Option<u8>,
19 },
20 RestorePending {
22 requested_host: Option<u8>,
24 restore: PendingHostSwitchRestore,
26 },
27}
28
29impl HostSwitchSessionOutcome {
30 #[must_use]
32 pub fn into_parts(self) -> (Option<u8>, Option<PendingHostSwitchRestore>) {
33 match self {
34 Self::Restored { requested_host } => (requested_host, None),
35 Self::RestorePending {
36 requested_host,
37 restore,
38 } => (requested_host, Some(restore)),
39 }
40 }
41}
42
43pub struct HostSwitchRestorePlan {
50 feature_index: u8,
51 controls: Vec<ArmedControl>,
52}
53
54impl fmt::Debug for HostSwitchRestorePlan {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 f.debug_struct("HostSwitchRestorePlan")
57 .field("reporting_count", &self.controls.len())
58 .finish_non_exhaustive()
59 }
60}
61
62impl RestorePlan for HostSwitchRestorePlan {
63 async fn restore_on(&self, current: &SharedChannel) -> bool {
64 let controls = ReprogControlsV4::new(
65 Arc::clone(current.channel()),
66 current.device_index(),
67 self.feature_index,
68 );
69 restore_host_controls(&controls, &self.controls).await
70 }
71}
72
73pub type PendingHostSwitchRestore = PendingRestore<HostSwitchRestorePlan>;
75
76pub type HostSwitchRestoreOutcome = RestoreOutcome<HostSwitchRestorePlan>;
78
79pub type HostSwitchSessionFailure = SessionFailure<HostSwitchError, HostSwitchRestorePlan>;
81
82impl PendingHostSwitchRestore {
83 pub(super) fn new(
85 retired: &SharedChannel,
86 feature_index: u8,
87 controls: Vec<ArmedControl>,
88 ) -> Option<Self> {
89 (!controls.is_empty()).then(|| {
90 Self::owing(
91 retired,
92 HostSwitchRestorePlan {
93 feature_index,
94 controls,
95 },
96 )
97 })
98 }
99}
100
101pub(super) async fn rollback_host_switch_start(
105 error: HostSwitchError,
106 pending: Option<PendingHostSwitchRestore>,
107 registry: &ChannelRegistry,
108 device_io: &DeviceIoGate,
109) -> HostSwitchSessionFailure {
110 if device_io.allows_io() {
111 return rollback_start(error, pending, registry).await;
112 }
113 match pending {
114 Some(pending) => {
115 HostSwitchSessionFailure::with_pending(error, pending.allow_current_channel())
116 }
117 None => HostSwitchSessionFailure::clean(error),
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::super::{
124 HostSwitchStop, HostSwitchStopReason, monitor_host_switch, run_host_switch_session,
125 };
126 use super::*;
127 use crate::DeviceRoute;
128 use crate::backend::NodeId;
129 use crate::channel::scripted::{ScriptedRawHidChannel, feature_error, scripted_channel};
130 use crate::reprog_controls::{CidReporting, ControlId};
131 use crate::session::host_switch::{ArmedControl, ReportingMode};
132
133 fn capture_keyboard(request: &[u8], fail_second_control: bool) -> Option<Vec<u8>> {
134 let mut response = vec![0; 20];
135 response[0] = 0x11;
136 response[1..4].copy_from_slice(&request[1..4]);
137 match (request[2], request[3] >> 4) {
138 (0, 1) => response[4] = 4,
139 (0, 0) => response[4] = 0x22,
140 (0x22, 0) => response[4] = if fail_second_control { 2 } else { 1 },
141 (0x22, 1) => {
142 if request[4] == 1 {
143 return Some(feature_error(request, 0x08));
144 }
145 response[4..9].copy_from_slice(&[0, 0xd1, 0, 0, 0x20]);
147 }
148 (0x22, 2) => {
149 response[4..6].copy_from_slice(&request[4..6]);
150 response[6] = 0x54;
152 }
153 (0x22, 3) => return Some(request.to_vec()),
154 _ => return None,
155 }
156 Some(response)
157 }
158
159 #[tokio::test]
160 async fn failed_session_and_partial_arm_release_old_channels_and_restore_on_reconnect() {
161 for partial_arm in [false, true] {
162 let route = DeviceRoute::Direct {
163 vendor_id: 0x046d,
164 product_id: 0xb35b,
165 };
166 let node = NodeId::from("host-keyboard".to_owned());
167 let registry = ChannelRegistry::default();
168 let (raw, writes) = ScriptedRawHidChannel::with_dynamic_responder(move |request| {
169 if request[2] == 0x22 && request[3] >> 4 == 3 && request[6] & 1 == 0 {
170 Some(feature_error(request, 0x08))
171 } else {
172 capture_keyboard(request, partial_arm)
173 }
174 });
175 let channel = scripted_channel(raw).await;
176 let retired = Arc::downgrade(&channel);
177 registry.replace_node(node.clone(), [route.clone()], channel);
178 let (stop, stopped) = tokio::sync::oneshot::channel();
179 stop.send(HostSwitchStopReason::Graceful).unwrap();
180 let (_signal, gate) = crate::device_io_channel();
181
182 let outcome = tokio::time::timeout(
183 std::time::Duration::from_secs(2),
184 run_host_switch_session(route.clone(), stopped, ®istry, gate),
185 )
186 .await
187 .expect("failed restoration must return ownership instead of looping");
188 let pending = match outcome {
189 Ok(HostSwitchSessionOutcome::RestorePending {
190 requested_host,
191 restore,
192 }) => {
193 assert!(!partial_arm);
194 assert_eq!(requested_host, None);
195 restore
196 }
197 Err(failure) => {
198 assert!(partial_arm);
199 failure
200 .into_parts()
201 .1
202 .expect("partial arm rollback must retain firmware ownership")
203 }
204 Ok(HostSwitchSessionOutcome::Restored { .. }) => {
205 panic!("failed writes cannot be clean")
206 }
207 };
208 let reporting: Vec<_> = writes
209 .written_reports()
210 .into_iter()
211 .filter(|request| request[2] == 0x22 && request[3] >> 4 == 3)
212 .collect();
213 assert_eq!(
214 reporting.len(),
215 3,
216 "one arm, then two failed bounded restores"
217 );
218 assert_eq!(&reporting[0][4..7], &[0, 0xd1, 0x23]);
219 assert_eq!(&reporting[1][4..7], &[0, 0xd1, 0x32]);
220
221 registry.remove_node(&node);
222 assert!(
223 retired.upgrade().is_none(),
224 "pending ownership must release the dead channel"
225 );
226 let pending = match pending.retry(®istry).await {
227 HostSwitchRestoreOutcome::RestorePending(pending) => pending,
228 HostSwitchRestoreOutcome::Restored => panic!("absent route cannot be restored"),
229 };
230 let (raw, fresh) =
231 ScriptedRawHidChannel::with_responder(|request| Some(request.to_vec()));
232 registry.replace_node(node, [route], scripted_channel(raw).await);
233 assert!(matches!(
234 pending.retry(®istry).await,
235 HostSwitchRestoreOutcome::Restored
236 ));
237 let reporting = fresh.written_reports();
238 assert_eq!(reporting.len(), 1);
239 assert_eq!(&reporting[0][4..10], &[0, 0xd1, 0x32, 0, 0, 0]);
240 }
241 }
242
243 #[tokio::test]
244 async fn retirement_before_listener_subscription_is_observed_without_another_event() {
245 let route = DeviceRoute::Direct {
246 vendor_id: 0x046d,
247 product_id: 0xb35b,
248 };
249 let registry = ChannelRegistry::default();
250 let (raw, _) = ScriptedRawHidChannel::with_responder(|_| None);
251 let retired = SharedChannel::new(scripted_channel(raw).await, route);
252 let (_stop, stopped) = tokio::sync::oneshot::channel();
253 let (_presses, mut presses) = tokio::sync::mpsc::unbounded_channel();
254 let (_signal, gate) = crate::device_io_channel();
255 let result = tokio::time::timeout(
256 std::time::Duration::from_millis(100),
257 monitor_host_switch(stopped, &mut presses, ®istry, &retired, gate),
258 )
259 .await
260 .expect("a retired publication needs no further notification");
261 assert_eq!(result, HostSwitchStop::ChannelChanged);
262 }
263
264 #[tokio::test]
265 async fn monitor_names_why_it_stopped_on_a_current_channel() {
266 let route = DeviceRoute::Direct {
267 vendor_id: 0x046d,
268 product_id: 0xb35b,
269 };
270 let registry = ChannelRegistry::default();
271 let (raw, _) = ScriptedRawHidChannel::with_responder(|_| None);
272 registry.replace_node(
273 NodeId::from("keyboard-node".to_owned()),
274 [route.clone()],
275 scripted_channel(raw).await,
276 );
277 let current = registry
278 .lookup(&route)
279 .expect("the keyboard channel should be published");
280 let monitored = |stopped, mut presses| {
281 let (registry, current) = (registry.clone(), current.clone());
282 async move {
283 let (_signal, gate) = crate::device_io_channel();
284 tokio::time::timeout(
285 std::time::Duration::from_millis(100),
286 monitor_host_switch(stopped, &mut presses, ®istry, ¤t, gate),
287 )
288 .await
289 .expect("a ready stop source needs no further event")
290 }
291 };
292
293 for (reason, expected) in [
294 (
295 Some(HostSwitchStopReason::Graceful),
296 HostSwitchStop::Shutdown,
297 ),
298 (
299 Some(HostSwitchStopReason::DeviceLost),
300 HostSwitchStop::ChannelChanged,
301 ),
302 (None, HostSwitchStop::ChannelChanged),
304 ] {
305 let (stop, stopped) = tokio::sync::oneshot::channel();
306 match reason {
307 Some(reason) => stop.send(reason).unwrap(),
308 None => drop(stop),
309 }
310 let (_press, presses) = tokio::sync::mpsc::unbounded_channel();
311 assert_eq!(monitored(stopped, presses).await, expected, "{reason:?}");
312 }
313
314 let (_stop, stopped) = tokio::sync::oneshot::channel();
315 let (press, presses) = tokio::sync::mpsc::unbounded_channel();
316 press.send(2).unwrap();
317 assert_eq!(
318 monitored(stopped, presses).await,
319 HostSwitchStop::Pressed(2)
320 );
321 }
322
323 #[tokio::test]
324 async fn replaced_publication_during_successful_write_remains_pending() {
325 let route = DeviceRoute::Direct {
326 vendor_id: 0x046d,
327 product_id: 0xb35b,
328 };
329 let node = NodeId::from("keyboard-node".to_owned());
330 let registry = ChannelRegistry::default();
331 let (retired_raw, _) = ScriptedRawHidChannel::with_responder(|_| None);
332 let retired = SharedChannel::new(scripted_channel(retired_raw).await, route.clone());
333 let control = ArmedControl {
334 cid: 0x00d3,
335 host: 0,
336 mode: ReportingMode::Diverted,
337 original: CidReporting {
338 cid: ControlId(0x00d3),
339 diverted: false,
340 persistently_diverted: true,
341 force_raw_xy: true,
342 raw_xy: false,
343 remap: Some(ControlId(0x1234)),
344 analytics_key_events: false,
345 raw_wheel: true,
346 },
347 };
348 let pending = PendingHostSwitchRestore::new(&retired, 0x22, vec![control])
349 .expect("one armed control must require restoration");
350 let (winner_raw, winner_handle) =
351 ScriptedRawHidChannel::with_responder(|request| Some(request.to_vec()));
352 let winner = scripted_channel(winner_raw).await;
353 let replacement_registry = registry.clone();
354 let replacement_node = node.clone();
355 let replacement_route = route.clone();
356 let (superseded_raw, superseded_handle) =
357 ScriptedRawHidChannel::with_dynamic_responder(move |request| {
358 replacement_registry.replace_node(
359 replacement_node.clone(),
360 [replacement_route.clone()],
361 winner.clone(),
362 );
363 Some(request.to_vec())
364 });
365 registry.replace_node(node, [route], scripted_channel(superseded_raw).await);
366
367 let pending = match pending.retry(®istry).await {
368 HostSwitchRestoreOutcome::RestorePending(pending) => pending,
369 HostSwitchRestoreOutcome::Restored => panic!("superseded write counted as final"),
370 };
371 assert_eq!(superseded_handle.written_reports().len(), 1);
372 assert!(matches!(
373 pending.retry(®istry).await,
374 HostSwitchRestoreOutcome::Restored
375 ));
376 assert_eq!(winner_handle.written_reports().len(), 1);
377 }
378}