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