1use fuchsia_sync::Mutex;
6use futures::StreamExt;
7use log::{debug, info, warn};
8use std::sync::{Arc, LazyLock};
9use zx;
10
11use fidl::endpoints::create_request_stream;
12use fidl_fuchsia_ui_input::MediaButtonsEvent;
13use {
14 fidl_fuchsia_feedback as ffeedback, fidl_fuchsia_hardware_power_statecontrol as fpower,
15 fidl_fuchsia_ui_policy as fuipolicy,
16};
17
18static MAX_PRESS_INTERVAL_NS: LazyLock<i64> = LazyLock::new(|| 500 * 1_000_000); const REQUIRED_PRESS_COUNT: u32 = 5;
20
21pub trait Clock: Send + Sync {
22 fn now_ns(&self) -> i64;
23}
24
25pub struct BootClock;
26
27impl Clock for BootClock {
28 fn now_ns(&self) -> i64 {
29 zx::BootInstant::get().into_nanos()
30 }
31}
32
33#[derive(Debug)]
34struct ButtonPressState {
35 count: u32,
36 last_press_time_ns: i64,
37 power_was_pressed: bool,
38 crash_report_in_progress: bool,
39 #[cfg(test)]
40 action_triggered_count: u32,
41}
42
43impl ButtonPressState {
44 fn new() -> Self {
45 Self {
46 count: 0,
47 last_press_time_ns: 0,
48 power_was_pressed: false,
49 crash_report_in_progress: false,
50 #[cfg(test)]
51 action_triggered_count: 0,
52 }
53 }
54}
55
56pub struct DebugState<C: Clock> {
57 debug_enabled: bool,
59 button_press_state: Mutex<ButtonPressState>,
60 clock: C,
61 crash_reporter_source: Option<ffeedback::CrashReporterProxy>,
62 power_statecontrol_admin_source: Option<fpower::AdminProxy>,
63}
64
65pub type DebugManager = DebugState<BootClock>;
67
68impl DebugState<BootClock> {
69 pub fn new(debug_enabled: bool) -> Self {
70 Self {
71 debug_enabled,
72 button_press_state: Mutex::new(ButtonPressState::new()),
73 clock: BootClock,
74 crash_reporter_source: fuchsia_component::client::connect_to_protocol::<
75 ffeedback::CrashReporterMarker,
76 >()
77 .map_err(|e| warn!("Failed to connect to CrashReporter: {e}"))
78 .ok(),
79 power_statecontrol_admin_source: fuchsia_component::client::connect_to_protocol::<
80 fpower::AdminMarker,
81 >()
82 .map_err(|e| warn!("Failed to connect to PowerStateControlAdmin: {e}"))
83 .ok(),
84 }
85 }
86}
87
88impl<C: Clock + 'static> DebugState<C> {
89 #[cfg(test)]
90 fn new_for_test(
91 debug_enabled: bool,
92 clock: C,
93 crash_reporter_proxy: Option<ffeedback::CrashReporterProxy>,
94 power_statecontrol_admin_proxy: Option<fpower::AdminProxy>,
95 ) -> Self {
96 Self {
97 debug_enabled,
98 button_press_state: Mutex::new(ButtonPressState::new()),
99 clock,
100 crash_reporter_source: crash_reporter_proxy,
101 power_statecontrol_admin_source: power_statecontrol_admin_proxy,
102 }
103 }
104
105 pub fn start_media_buttons_listener(self: Arc<Self>) {
106 if !self.debug_enabled {
107 info!("Debug mode not enabled, skipping media button listener registration.");
108 return;
109 }
110
111 info!("Registering for media button events to enable 5-button press for debug.");
112 fuchsia_async::Task::spawn(async move {
113 match fuchsia_component::client::connect_to_protocol::<
114 fuipolicy::DeviceListenerRegistryMarker,
115 >() {
116 Ok(device_listener_registry) => {
117 let (client_end, mut stream) =
118 create_request_stream::<fuipolicy::MediaButtonsListenerMarker>();
119 if let Err(e) = device_listener_registry.register_listener(client_end).await {
120 warn!("Failed to register media buttons listener: {e:?}");
121 return;
122 }
123 while let Some(Ok(request)) = stream.next().await {
124 if let fuipolicy::MediaButtonsListenerRequest::OnEvent {
125 event,
126 responder,
127 } = request
128 {
129 if self.process_button_event(&event) {
130 info!("Detected 5 function button presses in a row. Filing crash report.");
131 if self.file_crash_report().await {
132 if !self.reboot_device().await {
133 self.button_press_state.lock().crash_report_in_progress =
134 false;
135 }
136 } else {
137 self.button_press_state.lock().crash_report_in_progress = false;
138 }
139 }
140 if let Err(e) = responder.send() {
141 warn!("Failed to send response for media buttons event: {e:?}");
142 }
143 }
144 }
145 }
146 Err(e) => {
147 warn!("Failed to connect to fuchsia.ui.policy.DeviceListenerRegistry: {e:?}");
148 }
149 }
150 })
151 .detach();
152 }
153
154 fn process_button_event(&self, event: &MediaButtonsEvent) -> bool {
155 let mut state = self.button_press_state.lock();
156 if state.crash_report_in_progress {
157 info!("Crash report in progress, ignoring button event.");
158 return false;
159 }
160 if let Some(power_is_pressed) = event.power
161 && (power_is_pressed || state.power_was_pressed)
162 {
163 if state.count >= 3 {
164 info!(
165 "Detected overlapping POWER button activity; resetting FUNCTION button counter."
166 );
167 } else {
168 debug!(
169 "Detected overlapping POWER button activity; resetting FUNCTION button counter."
170 );
171 }
172 state.power_was_pressed = power_is_pressed;
173 state.count = 0;
174 return false;
175 }
176
177 if event.function != Some(true) {
178 return false;
180 }
181
182 let now_ns = self.clock.now_ns();
184
185 if now_ns - state.last_press_time_ns > *MAX_PRESS_INTERVAL_NS {
186 state.count = 1;
187 } else {
188 state.count += 1;
189 }
190
191 state.last_press_time_ns = now_ns;
192
193 if state.count >= 3 {
194 info!(
195 "Identified {:?} side button presses in a row. {:?} in a row will trigger a crash report and reboot sequence.",
196 state.count, REQUIRED_PRESS_COUNT
197 );
198 }
199
200 if state.count >= REQUIRED_PRESS_COUNT {
201 state.count = 0;
202 state.crash_report_in_progress = true;
203 #[cfg(test)]
204 {
205 state.action_triggered_count += 1;
206 }
207 return true;
208 }
209 false
210 }
211
212 pub fn stop(&self) {
213 if !self.debug_enabled {
214 return;
215 }
216 info!("Resetting debug button press state.");
217 *self.button_press_state.lock() = ButtonPressState::new();
218 }
219
220 #[cfg(test)]
221 pub(crate) fn get_button_press_state_count(&self) -> u32 {
222 self.button_press_state.lock().count
223 }
224
225 #[cfg(test)]
226 pub(crate) fn set_button_press_state_count(&self, count: u32) {
227 self.button_press_state.lock().count = count;
228 }
229
230 async fn file_crash_report(&self) -> bool {
231 if let Some(reporter) = &self.crash_reporter_source {
232 let report = ffeedback::CrashReport {
233 program_name: Some("session_manager".to_string()),
234 crash_signature: Some("fuchsia-user-SOS-device-stuck".to_string()),
235 is_fatal: Some(false),
236 ..Default::default()
237 };
238 match reporter.file_report(report).await {
239 Ok(Ok(_)) => {
240 info!("Successfully filed crash report.");
241 true
242 }
243 Ok(Err(e)) => {
244 warn!("Failed to file crash report: {e:?}");
245 false
246 }
247 Err(e) => {
248 warn!("Failed to file crash report: {e:?}");
249 false
250 }
251 }
252 } else {
253 warn!("Failed to get fuchsia.feedback.CrashReporter");
254 false
255 }
256 }
257
258 async fn reboot_device(&self) -> bool {
259 info!("Rebooting device due to 5-button press.");
260 if let Some(proxy) = self.power_statecontrol_admin_source.clone() {
261 let options = fpower::ShutdownOptions {
262 action: Some(fpower::ShutdownAction::Reboot),
263 reasons: Some(vec![fpower::ShutdownReason::UserRequestDeviceStuck]),
264 ..Default::default()
265 };
266 if let Err(e) = proxy.shutdown(&options).await {
267 warn!("Failed to reboot device: {e:?}");
268 false
269 } else {
270 true
271 }
272 } else {
273 warn!("Failed to connect to fuchsia.hardware.power.statecontrol.Admin");
274 false
275 }
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use fidl::endpoints::create_proxy_and_stream;
283 use fidl_fuchsia_ui_input as fui_input;
284 use futures::{TryStreamExt, join};
285
286 struct FakeClock {
287 now: Mutex<i64>,
288 }
289
290 impl FakeClock {
291 fn new() -> Self {
292 Self { now: Mutex::new(0) }
293 }
294
295 fn advance_ns(&self, duration_ns: i64) {
296 *self.now.lock() += duration_ns;
297 }
298 }
299
300 impl Clock for FakeClock {
301 fn now_ns(&self) -> i64 {
302 *self.now.lock()
303 }
304 }
305
306 impl<T: Clock> Clock for Arc<T> {
310 fn now_ns(&self) -> i64 {
311 self.as_ref().now_ns()
312 }
313 }
314
315 #[fuchsia::test]
316 fn test_successful_press_sequence() {
317 let clock = Arc::new(FakeClock::new());
318 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
319
320 let press_event =
321 fui_input::MediaButtonsEvent { function: Some(true), ..Default::default() };
322
323 for i in 1..REQUIRED_PRESS_COUNT {
325 assert!(
326 !debug_state.process_button_event(&press_event),
327 "Incorrectly triggered action after {i} presses"
328 );
329 let state = debug_state.button_press_state.lock();
330 assert_eq!(
331 state.action_triggered_count, 0,
332 "Incorrectly incremented trigger count after {i} presses. State: {state:?}"
333 );
334 }
335
336 assert!(
337 debug_state.process_button_event(&press_event),
338 "The final press should trigger the sequence."
339 );
340 let state = debug_state.button_press_state.lock();
341 assert_eq!(state.action_triggered_count, 1, "State: {state:?}");
342 }
343
344 #[fuchsia::test]
345 fn test_counter_resets_after_successful_sequence() {
346 let clock = Arc::new(FakeClock::new());
347 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
348
349 let press_event =
350 fui_input::MediaButtonsEvent { function: Some(true), ..Default::default() };
351
352 for _ in 1..REQUIRED_PRESS_COUNT {
354 assert!(!debug_state.process_button_event(&press_event));
355 let state = debug_state.button_press_state.lock();
356 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
357 }
358 assert!(debug_state.process_button_event(&press_event));
359 {
360 let mut state = debug_state.button_press_state.lock();
361 assert_eq!(state.action_triggered_count, 1, "State: {state:?}");
362 assert!(state.crash_report_in_progress);
363 state.crash_report_in_progress = false;
365 }
366
367 assert!(
368 !debug_state.process_button_event(&press_event),
369 "The next press should start a new sequence."
370 );
371 let state = debug_state.button_press_state.lock();
372 assert_eq!(state.count, 1, "State: {state:?}");
373 assert_eq!(state.action_triggered_count, 1, "State: {state:?}");
374 }
375
376 #[fuchsia::test]
377 fn test_ignores_presses_while_crash_report_in_progress() {
378 let clock = Arc::new(FakeClock::new());
379 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
380
381 let press_event =
382 fui_input::MediaButtonsEvent { function: Some(true), ..Default::default() };
383
384 for _ in 1..REQUIRED_PRESS_COUNT {
386 assert!(!debug_state.process_button_event(&press_event));
387 }
388 assert!(debug_state.process_button_event(&press_event));
389 {
390 let state = debug_state.button_press_state.lock();
391 assert!(state.crash_report_in_progress);
392 assert_eq!(state.action_triggered_count, 1);
393 }
394
395 assert!(!debug_state.process_button_event(&press_event));
397 {
398 let state = debug_state.button_press_state.lock();
399 assert!(state.crash_report_in_progress);
400 assert_eq!(state.action_triggered_count, 1);
401 assert_eq!(state.count, 0);
402 }
403
404 debug_state.button_press_state.lock().crash_report_in_progress = false;
406
407 assert!(!debug_state.process_button_event(&press_event));
409 let state = debug_state.button_press_state.lock();
410 assert_eq!(state.count, 1);
411 }
412
413 #[fuchsia::test]
414 fn test_counter_resets_after_timeout() {
415 let clock = Arc::new(FakeClock::new());
416 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
417
418 let press_event =
419 fui_input::MediaButtonsEvent { function: Some(true), ..Default::default() };
420
421 for _ in 1..REQUIRED_PRESS_COUNT - 1 {
423 assert!(!debug_state.process_button_event(&press_event));
424 let state = debug_state.button_press_state.lock();
425 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
426 }
427 clock.advance_ns(*MAX_PRESS_INTERVAL_NS + 1);
429
430 assert!(
431 !debug_state.process_button_event(&press_event),
432 "This press should reset the counter to 1, not increment it."
433 );
434 let state = debug_state.button_press_state.lock();
435 assert_eq!(state.count, 1, "State: {state:?}");
436 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
437 }
438
439 #[fuchsia::test]
440 fn test_power_button_press_resets_counter() {
441 let clock = Arc::new(FakeClock::new());
442 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
443
444 let press_event =
445 fui_input::MediaButtonsEvent { function: Some(true), ..Default::default() };
446 let power_press_event =
447 fui_input::MediaButtonsEvent { power: Some(true), ..Default::default() };
448
449 assert!(
450 !debug_state.process_button_event(&press_event),
451 "first function press should not trigger action"
452 );
453 {
454 let state = debug_state.button_press_state.lock();
455 assert_eq!(state.count, 1, "should have count of 1. State: {state:?}");
456 }
457 assert!(
458 !debug_state.process_button_event(&power_press_event),
459 "power press should not trigger action"
460 );
461 {
462 let state = debug_state.button_press_state.lock();
463 assert_eq!(
464 state.count, 0,
465 "A non-function-button press should reset the counter. State: {state:?}"
466 );
467 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
468 }
469
470 assert!(
471 !debug_state.process_button_event(&press_event),
472 "The next press should start a new sequence."
473 );
474 let state = debug_state.button_press_state.lock();
475 assert_eq!(state.count, 1, "State: {state:?}");
476 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
477 }
478
479 #[fuchsia::test]
480 fn test_power_button_release_resets_counter() {
481 let clock = Arc::new(FakeClock::new());
482 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
483
484 let function_press_event =
485 fui_input::MediaButtonsEvent { function: Some(true), ..Default::default() };
486 let function_release_event =
487 fui_input::MediaButtonsEvent { function: Some(false), ..Default::default() };
488 let power_press_event =
489 fui_input::MediaButtonsEvent { power: Some(true), ..Default::default() };
490 let power_release_event =
491 fui_input::MediaButtonsEvent { power: Some(false), ..Default::default() };
492
493 assert!(
494 !debug_state.process_button_event(&power_press_event),
495 "power press should not trigger action"
496 );
497 {
498 let state = debug_state.button_press_state.lock();
499 assert_eq!(state.count, 0, "count should be 0 after power press. State: {state:?}");
500 }
501 assert!(
502 !debug_state.process_button_event(&function_press_event),
503 "function press should not trigger action"
504 );
505 {
506 let state = debug_state.button_press_state.lock();
507 assert_eq!(state.count, 1, "count should be 1 after function press. State: {state:?}");
508 }
509 assert!(
510 !debug_state.process_button_event(&power_release_event),
511 "A non-function-button release should reset the counter."
512 );
513 {
514 let state = debug_state.button_press_state.lock();
515 assert_eq!(state.count, 0, "count should be 0 after power release. State: {state:?}");
516 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
517 }
518 assert!(
519 !debug_state.process_button_event(&function_release_event),
520 "function release should not trigger action"
521 );
522 {
523 let state = debug_state.button_press_state.lock();
524 assert_eq!(
525 state.count, 0,
526 "count should be 0 after function release. State: {state:?}"
527 );
528 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
529 }
530
531 assert!(
532 !debug_state.process_button_event(&function_press_event),
533 "The next press should start a new sequence."
534 );
535 let state = debug_state.button_press_state.lock();
536 assert_eq!(state.count, 1, "State: {state:?}");
537 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
538 }
539
540 #[fuchsia::test]
541 fn test_ignores_function_button_releases() {
542 let clock = Arc::new(FakeClock::new());
543 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
544
545 let press_event =
546 fui_input::MediaButtonsEvent { function: Some(true), ..Default::default() };
547 let function_release_event =
548 fui_input::MediaButtonsEvent { function: Some(false), ..Default::default() };
549
550 assert!(
551 !debug_state.process_button_event(&press_event),
552 "first function press should not trigger action"
553 );
554 {
555 let state = debug_state.button_press_state.lock();
556 assert_eq!(state.count, 1, "count should be 1. State: {state:?}");
557 }
558 assert!(
559 !debug_state.process_button_event(&function_release_event),
560 "A button release should be ignored and not affect the counter."
561 );
562 {
563 let state = debug_state.button_press_state.lock();
564 assert_eq!(state.count, 1, "count should still be 1. State: {state:?}");
565 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
566 }
567 assert!(
568 !debug_state.process_button_event(&press_event),
569 "second function press should not trigger action"
570 );
571 let state = debug_state.button_press_state.lock();
572 assert_eq!(state.count, 2, "count should be 2. State: {state:?}");
573 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
574 }
575
576 async fn run_report_server(mut stream: ffeedback::CrashReporterRequestStream) {
577 let request =
578 stream.try_next().await.expect("failed to read from stream").expect("stream is empty");
579 match request {
580 ffeedback::CrashReporterRequest::FileReport { report, responder } => {
581 assert_eq!(
582 report,
583 ffeedback::CrashReport {
584 program_name: Some("session_manager".to_string()),
585 crash_signature: Some("fuchsia-user-SOS-device-stuck".to_string()),
586 is_fatal: Some(false),
587 ..Default::default()
588 }
589 );
590 responder
591 .send(Ok(&ffeedback::FileReportResults::default()))
592 .expect("failed to send response");
593 }
594 }
595 }
596
597 #[fuchsia::test]
598 async fn test_file_crash_report() {
599 let clock = Arc::new(FakeClock::new());
600 let (crash_reporter_proxy, stream) =
601 create_proxy_and_stream::<ffeedback::CrashReporterMarker>();
602 let (power_statecontrol_admin_proxy, _) = create_proxy_and_stream::<fpower::AdminMarker>();
603
604 let server_fut = run_report_server(stream);
605
606 let debug_state = DebugState::new_for_test(
608 true,
609 clock.clone(),
610 Some(crash_reporter_proxy),
611 Some(power_statecontrol_admin_proxy),
612 );
613 let client_fut = async {
614 assert!(debug_state.file_crash_report().await);
615 };
616
617 join!(client_fut, server_fut);
618 }
619
620 async fn run_reboot_server(mut stream: fpower::AdminRequestStream) {
621 let request =
622 stream.try_next().await.expect("failed to read from stream").expect("stream is empty");
623 match request {
624 fpower::AdminRequest::Shutdown { options, responder } => {
625 assert_eq!(
626 options,
627 fpower::ShutdownOptions {
628 action: Some(fpower::ShutdownAction::Reboot),
629 reasons: Some(vec![fpower::ShutdownReason::UserRequestDeviceStuck]),
630 ..Default::default()
631 }
632 );
633 responder.send(Ok(())).expect("failed to send response");
634 }
635 _ => panic!("unexpected request"),
636 }
637 }
638
639 #[fuchsia::test]
640 async fn test_reboot_device() {
641 let clock = Arc::new(FakeClock::new());
642 let (crash_reporter_proxy, _) = create_proxy_and_stream::<ffeedback::CrashReporterMarker>();
643 let (power_statecontrol_admin_proxy, stream) =
644 create_proxy_and_stream::<fpower::AdminMarker>();
645
646 let server_fut = run_reboot_server(stream);
647
648 let debug_state = DebugState::new_for_test(
650 true,
651 clock.clone(),
652 Some(crash_reporter_proxy),
653 Some(power_statecontrol_admin_proxy),
654 );
655 let reboot_fut = debug_state.reboot_device();
656
657 let (reboot_succeeded, ()) = join!(reboot_fut, server_fut);
659 assert!(reboot_succeeded);
660 }
661}