Skip to main content

session_manager_lib/
debug.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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); // 500ms in nanoseconds
19const 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    /// Whether the system supports 5-button press to debug.
58    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
65/// The concrete `DebugState` used in production.
66pub 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            // Function button could have been released. Ignore it.
179            return false;
180        }
181
182        // At this point, we have a pure function press event.
183        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    // By implementing Clock for Arc<T>, we can pass a clone of the Arc to the DebugState,
307    // while the test retains ownership of the original Arc. This allows the test to call
308    // `advance_ns` on the FakeClock.
309    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        // Press the button REQUIRED_PRESS_COUNT - 1 times, which should not trigger the debug state.
324        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        // Test that the counter resets after a successful sequence.
353        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            // Manually reset for test purposes.
364            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        // Trigger the crash report.
385        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        // Try to press again, it should be ignored.
396        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        // Manually reset the flag.
405        debug_state.button_press_state.lock().crash_report_in_progress = false;
406
407        // The next press should start a new sequence.
408        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        // Test that the counter resets after a timeout.
422        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        // Wait for longer than the max interval.
428        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        // File the crash report.
607        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        // Reboot the device.
649        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        // Wait for the server to have received the call.
658        let (reboot_succeeded, ()) = join!(reboot_fut, server_fut);
659        assert!(reboot_succeeded);
660    }
661}