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_feedback as ffeedback;
13use fidl_fuchsia_hardware_power_statecontrol as fpower;
14use fidl_fuchsia_ui_input::MediaButtonsEvent;
15use fidl_fuchsia_ui_policy as fuipolicy;
16
17static MAX_PRESS_INTERVAL_NS: LazyLock<i64> = LazyLock::new(|| 500 * 1_000_000); // 500ms in nanoseconds
18const REQUIRED_PRESS_COUNT: u32 = 5;
19
20pub trait Clock: Send + Sync {
21    fn now_ns(&self) -> i64;
22}
23
24pub struct BootClock;
25
26impl Clock for BootClock {
27    fn now_ns(&self) -> i64 {
28        zx::BootInstant::get().into_nanos()
29    }
30}
31
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33struct CurrentButtons {
34    power_pressed: bool,
35    volume_up_pressed: bool,
36    volume_down_pressed: bool,
37}
38
39impl CurrentButtons {
40    fn both_volume_pressed(self) -> bool {
41        self.volume_up_pressed && self.volume_down_pressed
42    }
43}
44
45#[derive(Debug)]
46struct ButtonPressState {
47    count: u32,
48    last_press_time_ns: i64,
49    buttons: CurrentButtons,
50    crash_report_in_progress: bool,
51    #[cfg(test)]
52    action_triggered_count: u32,
53}
54
55impl ButtonPressState {
56    fn new() -> Self {
57        Self {
58            count: 0,
59            last_press_time_ns: 0,
60            buttons: CurrentButtons::default(),
61            crash_report_in_progress: false,
62            #[cfg(test)]
63            action_triggered_count: 0,
64        }
65    }
66}
67
68pub struct DebugState<C: Clock> {
69    /// Whether the system supports 5-button press to debug.
70    debug_enabled: bool,
71    button_press_state: Mutex<ButtonPressState>,
72    clock: C,
73    crash_reporter_source: Option<ffeedback::CrashReporterProxy>,
74    power_statecontrol_admin_source: Option<fpower::AdminProxy>,
75    listener_task: Mutex<Option<fuchsia_async::Task<()>>>,
76}
77
78/// The concrete `DebugState` used in production.
79pub type DebugManager = DebugState<BootClock>;
80
81impl DebugState<BootClock> {
82    pub fn new(debug_enabled: bool) -> Self {
83        Self {
84            debug_enabled,
85            button_press_state: Mutex::new(ButtonPressState::new()),
86            clock: BootClock,
87            crash_reporter_source: fuchsia_component::client::connect_to_protocol::<
88                ffeedback::CrashReporterMarker,
89            >()
90            .map_err(|e| warn!("Failed to connect to CrashReporter: {e}"))
91            .ok(),
92            power_statecontrol_admin_source: fuchsia_component::client::connect_to_protocol::<
93                fpower::AdminMarker,
94            >()
95            .map_err(|e| warn!("Failed to connect to PowerStateControlAdmin: {e}"))
96            .ok(),
97            listener_task: Mutex::new(None),
98        }
99    }
100}
101
102impl<C: Clock + 'static> DebugState<C> {
103    #[cfg(test)]
104    fn new_for_test(
105        debug_enabled: bool,
106        clock: C,
107        crash_reporter_proxy: Option<ffeedback::CrashReporterProxy>,
108        power_statecontrol_admin_proxy: Option<fpower::AdminProxy>,
109    ) -> Self {
110        Self {
111            debug_enabled,
112            button_press_state: Mutex::new(ButtonPressState::new()),
113            clock,
114            crash_reporter_source: crash_reporter_proxy,
115            power_statecontrol_admin_source: power_statecontrol_admin_proxy,
116            listener_task: Mutex::new(None),
117        }
118    }
119
120    pub fn start_media_buttons_listener(self: Arc<Self>) {
121        if !self.debug_enabled {
122            info!("Debug mode not enabled, skipping media button listener registration.");
123            return;
124        }
125
126        let mut task_lock = self.listener_task.lock();
127        if task_lock.is_some() {
128            // Already listening.
129            return;
130        }
131
132        info!("Registering for media button events to enable 5-button press for debug.");
133        let weak_this = Arc::downgrade(&self);
134        let task = fuchsia_async::Task::spawn(async move {
135            match fuchsia_component::client::connect_to_protocol::<
136                fuipolicy::DeviceListenerRegistryMarker,
137            >() {
138                Ok(device_listener_registry) => {
139                    let (client_end, mut stream) =
140                        create_request_stream::<fuipolicy::MediaButtonsListenerMarker>();
141                    if let Err(e) = device_listener_registry.register_listener(client_end).await {
142                        warn!("Failed to register media buttons listener: {e:?}");
143                        return;
144                    }
145                    while let Some(Ok(request)) = stream.next().await {
146                        if let fuipolicy::MediaButtonsListenerRequest::OnEvent {
147                            event,
148                            responder,
149                        } = request
150                        {
151                            if let Some(this) = weak_this.upgrade() {
152                                if this.process_button_event(&event) {
153                                    info!(
154                                        "Detected 5 volume up+down button presses in a row. Filing crash report."
155                                    );
156                                    if this.file_crash_report().await {
157                                        if !this.reboot_device().await {
158                                            this.button_press_state
159                                                .lock()
160                                                .crash_report_in_progress = false;
161                                        }
162                                    } else {
163                                        this.button_press_state.lock().crash_report_in_progress =
164                                            false;
165                                    }
166                                }
167                            } else {
168                                // DebugState was dropped, exit the task.
169                                break;
170                            }
171                            if let Err(e) = responder.send() {
172                                warn!("Failed to send response for media buttons event: {e:?}");
173                            }
174                        }
175                    }
176                }
177                Err(e) => {
178                    warn!("Failed to connect to fuchsia.ui.policy.DeviceListenerRegistry: {e:?}");
179                }
180            }
181        });
182        *task_lock = Some(task);
183    }
184
185    fn process_button_event(&self, event: &MediaButtonsEvent) -> bool {
186        let mut state = self.button_press_state.lock();
187        // Update volume button states from discrete fields. We deliberately ignore
188        // the legacy `volume: int8` field here because simultaneous button presses
189        // cancel out in that representation.
190        let was_both_pressed = state.buttons.both_volume_pressed();
191
192        if let Some(v_up) = event.volume_up {
193            state.buttons.volume_up_pressed = v_up;
194        }
195        if let Some(v_down) = event.volume_down {
196            state.buttons.volume_down_pressed = v_down;
197        }
198
199        let both_volume_pressed = state.buttons.both_volume_pressed();
200        let is_new_press = both_volume_pressed && !was_both_pressed;
201
202        if let Some(power_is_pressed) = event.power
203            && (power_is_pressed || state.buttons.power_pressed)
204        {
205            if state.count >= 3 {
206                info!(
207                    "Detected overlapping POWER button activity; resetting VOLUME button counter."
208                );
209            } else {
210                debug!(
211                    "Detected overlapping POWER button activity; resetting VOLUME button counter."
212                );
213            }
214            state.buttons.power_pressed = power_is_pressed;
215            state.count = 0;
216            return false;
217        }
218
219        if state.crash_report_in_progress {
220            info!("Crash report in progress, ignoring button event.");
221            return false;
222        }
223
224        if !is_new_press {
225            return false;
226        }
227
228        // Rising edge of simultaneous press: both buttons are pressed at the same time.
229        let now_ns = self.clock.now_ns();
230
231        if now_ns - state.last_press_time_ns > *MAX_PRESS_INTERVAL_NS {
232            state.count = 1;
233        } else {
234            state.count += 1;
235        }
236
237        state.last_press_time_ns = now_ns;
238
239        if state.count >= 3 {
240            info!(
241                "Identified {:?} volume up+down button presses in a row. {:?} in a row will trigger a crash report and reboot sequence.",
242                state.count, REQUIRED_PRESS_COUNT
243            );
244        }
245
246        if state.count >= REQUIRED_PRESS_COUNT {
247            state.count = 0;
248            state.crash_report_in_progress = true;
249            #[cfg(test)]
250            {
251                state.action_triggered_count += 1;
252            }
253            return true;
254        }
255        false
256    }
257
258    pub fn stop(&self) {
259        if !self.debug_enabled {
260            return;
261        }
262        info!("Resetting debug button press state.");
263        *self.button_press_state.lock() = ButtonPressState::new();
264    }
265
266    #[cfg(test)]
267    pub(crate) fn get_button_press_state_count(&self) -> u32 {
268        self.button_press_state.lock().count
269    }
270
271    #[cfg(test)]
272    pub(crate) fn set_button_press_state_count(&self, count: u32) {
273        self.button_press_state.lock().count = count;
274    }
275
276    async fn file_crash_report(&self) -> bool {
277        if let Some(reporter) = &self.crash_reporter_source {
278            let report = ffeedback::CrashReport {
279                program_name: Some("session_manager".to_string()),
280                crash_signature: Some("fuchsia-user-SOS-device-stuck".to_string()),
281                is_fatal: Some(false),
282                ..Default::default()
283            };
284            match reporter.file_report(report).await {
285                Ok(Ok(_)) => {
286                    info!("Successfully filed crash report.");
287                    true
288                }
289                Ok(Err(e)) => {
290                    warn!("Failed to file crash report: {e:?}");
291                    false
292                }
293                Err(e) => {
294                    warn!("Failed to file crash report: {e:?}");
295                    false
296                }
297            }
298        } else {
299            warn!("Failed to get fuchsia.feedback.CrashReporter");
300            false
301        }
302    }
303
304    async fn reboot_device(&self) -> bool {
305        info!("Rebooting device due to 5-button press.");
306        if let Some(proxy) = self.power_statecontrol_admin_source.clone() {
307            let options = fpower::ShutdownOptions {
308                action: Some(fpower::ShutdownAction::Reboot),
309                reasons: Some(vec![fpower::ShutdownReason::UserRequestDeviceStuck]),
310                ..Default::default()
311            };
312            if let Err(e) = proxy.shutdown(&options).await {
313                warn!("Failed to reboot device: {e:?}");
314                false
315            } else {
316                true
317            }
318        } else {
319            warn!("Failed to connect to fuchsia.hardware.power.statecontrol.Admin");
320            false
321        }
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use fidl::endpoints::create_proxy_and_stream;
329    use fidl_fuchsia_ui_input as fui_input;
330    use futures::{TryStreamExt, join};
331
332    struct FakeClock {
333        now: Mutex<i64>,
334    }
335
336    impl FakeClock {
337        fn new() -> Self {
338            Self { now: Mutex::new(0) }
339        }
340
341        fn advance_ns(&self, duration_ns: i64) {
342            *self.now.lock() += duration_ns;
343        }
344    }
345
346    impl Clock for FakeClock {
347        fn now_ns(&self) -> i64 {
348            *self.now.lock()
349        }
350    }
351
352    // By implementing Clock for Arc<T>, we can pass a clone of the Arc to the DebugState,
353    // while the test retains ownership of the original Arc. This allows the test to call
354    // `advance_ns` on the FakeClock.
355    impl<T: Clock> Clock for Arc<T> {
356        fn now_ns(&self) -> i64 {
357            self.as_ref().now_ns()
358        }
359    }
360
361    fn make_both_press_event() -> fui_input::MediaButtonsEvent {
362        fui_input::MediaButtonsEvent {
363            volume_up: Some(true),
364            volume_down: Some(true),
365            ..Default::default()
366        }
367    }
368
369    fn make_release_event() -> fui_input::MediaButtonsEvent {
370        fui_input::MediaButtonsEvent {
371            volume_up: Some(false),
372            volume_down: Some(false),
373            ..Default::default()
374        }
375    }
376
377    #[fuchsia::test]
378    fn test_successful_simultaneous_press_sequence() {
379        let clock = Arc::new(FakeClock::new());
380        let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
381
382        let press_event = make_both_press_event();
383        let release_event = make_release_event();
384
385        // Press the buttons REQUIRED_PRESS_COUNT - 1 times, which should not trigger the debug state.
386        for i in 1..REQUIRED_PRESS_COUNT {
387            assert!(
388                !debug_state.process_button_event(&press_event),
389                "Incorrectly triggered action after {i} presses"
390            );
391            let state = debug_state.button_press_state.lock();
392            assert_eq!(state.count, i);
393            assert_eq!(
394                state.action_triggered_count, 0,
395                "Incorrectly incremented trigger count after {i} presses. State: {state:?}"
396            );
397            drop(state);
398
399            assert!(!debug_state.process_button_event(&release_event));
400        }
401
402        assert!(
403            debug_state.process_button_event(&press_event),
404            "The final press should trigger the sequence."
405        );
406        let state = debug_state.button_press_state.lock();
407        assert_eq!(state.action_triggered_count, 1, "State: {state:?}");
408    }
409
410    #[fuchsia::test]
411    fn test_staggered_press_triggers() {
412        let clock = Arc::new(FakeClock::new());
413        let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
414
415        let vol_up_event =
416            fui_input::MediaButtonsEvent { volume_up: Some(true), ..Default::default() };
417        let vol_down_event =
418            fui_input::MediaButtonsEvent { volume_down: Some(true), ..Default::default() };
419        let release_event = make_release_event();
420
421        for i in 1..REQUIRED_PRESS_COUNT {
422            assert!(!debug_state.process_button_event(&vol_up_event));
423            assert!(!debug_state.process_button_event(&vol_down_event));
424            let state = debug_state.button_press_state.lock();
425            assert_eq!(state.count, i);
426            drop(state);
427
428            assert!(!debug_state.process_button_event(&release_event));
429        }
430
431        assert!(!debug_state.process_button_event(&vol_up_event));
432        assert!(debug_state.process_button_event(&vol_down_event));
433        let state = debug_state.button_press_state.lock();
434        assert_eq!(state.action_triggered_count, 1);
435    }
436
437    #[fuchsia::test]
438    fn test_holding_both_buttons_does_not_repeat_count() {
439        let clock = Arc::new(FakeClock::new());
440        let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
441
442        let press_event = make_both_press_event();
443
444        // Initial press
445        assert!(!debug_state.process_button_event(&press_event));
446        assert_eq!(debug_state.button_press_state.lock().count, 1);
447
448        // Repeated events with both still pressed (e.g. from repeat or other metadata changes)
449        assert!(!debug_state.process_button_event(&press_event));
450        assert_eq!(debug_state.button_press_state.lock().count, 1);
451        assert!(!debug_state.process_button_event(&press_event));
452        assert_eq!(debug_state.button_press_state.lock().count, 1);
453    }
454
455    #[fuchsia::test]
456    fn test_counter_resets_after_successful_sequence() {
457        let clock = Arc::new(FakeClock::new());
458        let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
459
460        let press_event = make_both_press_event();
461        let release_event = make_release_event();
462
463        // Test that the counter resets after a successful sequence.
464        for _ in 1..REQUIRED_PRESS_COUNT {
465            assert!(!debug_state.process_button_event(&press_event));
466            let state = debug_state.button_press_state.lock();
467            assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
468            drop(state);
469            assert!(!debug_state.process_button_event(&release_event));
470        }
471        assert!(debug_state.process_button_event(&press_event));
472        {
473            let mut state = debug_state.button_press_state.lock();
474            assert_eq!(state.action_triggered_count, 1, "State: {state:?}");
475            assert!(state.crash_report_in_progress);
476            // Manually reset for test purposes.
477            state.crash_report_in_progress = false;
478        }
479
480        assert!(!debug_state.process_button_event(&release_event));
481        assert!(
482            !debug_state.process_button_event(&press_event),
483            "The next press should start a new sequence."
484        );
485        let state = debug_state.button_press_state.lock();
486        assert_eq!(state.count, 1, "State: {state:?}");
487        assert_eq!(state.action_triggered_count, 1, "State: {state:?}");
488    }
489
490    #[fuchsia::test]
491    fn test_ignores_presses_while_crash_report_in_progress() {
492        let clock = Arc::new(FakeClock::new());
493        let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
494
495        let press_event = make_both_press_event();
496        let release_event = make_release_event();
497
498        // Trigger the crash report.
499        for _ in 1..REQUIRED_PRESS_COUNT {
500            assert!(!debug_state.process_button_event(&press_event));
501            assert!(!debug_state.process_button_event(&release_event));
502        }
503        assert!(debug_state.process_button_event(&press_event));
504        {
505            let state = debug_state.button_press_state.lock();
506            assert!(state.crash_report_in_progress);
507            assert_eq!(state.action_triggered_count, 1);
508        }
509
510        // Try to press again, it should be ignored.
511        assert!(!debug_state.process_button_event(&release_event));
512        assert!(!debug_state.process_button_event(&press_event));
513        assert!(!debug_state.process_button_event(&release_event));
514        {
515            let state = debug_state.button_press_state.lock();
516            assert!(state.crash_report_in_progress);
517            assert_eq!(state.action_triggered_count, 1);
518            assert_eq!(state.count, 0);
519        }
520
521        // Manually reset the flag.
522        debug_state.button_press_state.lock().crash_report_in_progress = false;
523
524        // The next press should start a new sequence.
525        assert!(!debug_state.process_button_event(&press_event));
526        let state = debug_state.button_press_state.lock();
527        assert_eq!(state.count, 1);
528    }
529
530    #[fuchsia::test]
531    fn test_counter_resets_after_timeout() {
532        let clock = Arc::new(FakeClock::new());
533        let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
534
535        let press_event = make_both_press_event();
536        let release_event = make_release_event();
537
538        // Test that the counter resets after a timeout.
539        for _ in 1..REQUIRED_PRESS_COUNT - 1 {
540            assert!(!debug_state.process_button_event(&press_event));
541            assert!(!debug_state.process_button_event(&release_event));
542        }
543        // Wait for longer than the max interval.
544        clock.advance_ns(*MAX_PRESS_INTERVAL_NS + 1);
545
546        assert!(
547            !debug_state.process_button_event(&press_event),
548            "This press should reset the counter to 1, not increment it."
549        );
550        let state = debug_state.button_press_state.lock();
551        assert_eq!(state.count, 1, "State: {state:?}");
552        assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
553    }
554
555    #[fuchsia::test]
556    fn test_power_button_press_resets_counter() {
557        let clock = Arc::new(FakeClock::new());
558        let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
559
560        let press_event = make_both_press_event();
561        let release_event = make_release_event();
562        let power_press_event =
563            fui_input::MediaButtonsEvent { power: Some(true), ..Default::default() };
564
565        assert!(
566            !debug_state.process_button_event(&press_event),
567            "first volume press should not trigger action"
568        );
569        assert!(!debug_state.process_button_event(&release_event));
570        {
571            let state = debug_state.button_press_state.lock();
572            assert_eq!(state.count, 1, "should have count of 1. State: {state:?}");
573        }
574        assert!(
575            !debug_state.process_button_event(&power_press_event),
576            "power press should not trigger action"
577        );
578        {
579            let state = debug_state.button_press_state.lock();
580            assert_eq!(state.count, 0, "A power press should reset the counter. State: {state:?}");
581            assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
582        }
583
584        assert!(
585            !debug_state.process_button_event(&press_event),
586            "The next press should start a new sequence."
587        );
588        let state = debug_state.button_press_state.lock();
589        assert_eq!(state.count, 1, "State: {state:?}");
590        assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
591    }
592
593    #[fuchsia::test]
594    fn test_power_button_release_resets_counter() {
595        let clock = Arc::new(FakeClock::new());
596        let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
597
598        let vol_press_event = make_both_press_event();
599        let vol_release_event = make_release_event();
600        let power_press_event =
601            fui_input::MediaButtonsEvent { power: Some(true), ..Default::default() };
602        let power_release_event =
603            fui_input::MediaButtonsEvent { power: Some(false), ..Default::default() };
604
605        assert!(
606            !debug_state.process_button_event(&power_press_event),
607            "power press should not trigger action"
608        );
609        {
610            let state = debug_state.button_press_state.lock();
611            assert_eq!(state.count, 0, "count should be 0 after power press. State: {state:?}");
612        }
613        assert!(
614            !debug_state.process_button_event(&vol_press_event),
615            "volume press should not trigger action"
616        );
617        assert!(!debug_state.process_button_event(&vol_release_event));
618        {
619            let state = debug_state.button_press_state.lock();
620            assert_eq!(state.count, 1, "count should be 1 after volume press. State: {state:?}");
621        }
622        assert!(
623            !debug_state.process_button_event(&power_release_event),
624            "A power button release should reset the counter."
625        );
626        {
627            let state = debug_state.button_press_state.lock();
628            assert_eq!(state.count, 0, "count should be 0 after power release. State: {state:?}");
629            assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
630        }
631        assert!(
632            !debug_state.process_button_event(&vol_press_event),
633            "The next press should start a new sequence."
634        );
635        let state = debug_state.button_press_state.lock();
636        assert_eq!(state.count, 1, "State: {state:?}");
637        assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
638    }
639
640    #[fuchsia::test]
641    fn test_single_button_press_does_not_increment() {
642        let clock = Arc::new(FakeClock::new());
643        let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
644
645        let vol_up_event =
646            fui_input::MediaButtonsEvent { volume_up: Some(true), ..Default::default() };
647        let vol_down_event =
648            fui_input::MediaButtonsEvent { volume_down: Some(true), ..Default::default() };
649        let release_event = make_release_event();
650
651        // REQUIRED_PRESS_COUNT presses of Volume Up only
652        for _ in 0..REQUIRED_PRESS_COUNT {
653            assert!(!debug_state.process_button_event(&vol_up_event));
654            assert!(!debug_state.process_button_event(&release_event));
655        }
656        assert_eq!(debug_state.button_press_state.lock().count, 0);
657
658        // REQUIRED_PRESS_COUNT presses of Volume Down only
659        for _ in 0..REQUIRED_PRESS_COUNT {
660            assert!(!debug_state.process_button_event(&vol_down_event));
661            assert!(!debug_state.process_button_event(&release_event));
662        }
663        assert_eq!(debug_state.button_press_state.lock().count, 0);
664    }
665
666    async fn run_report_server(mut stream: ffeedback::CrashReporterRequestStream) {
667        let request =
668            stream.try_next().await.expect("failed to read from stream").expect("stream is empty");
669        match request {
670            ffeedback::CrashReporterRequest::FileReport { report, responder } => {
671                assert_eq!(
672                    report,
673                    ffeedback::CrashReport {
674                        program_name: Some("session_manager".to_string()),
675                        crash_signature: Some("fuchsia-user-SOS-device-stuck".to_string()),
676                        is_fatal: Some(false),
677                        ..Default::default()
678                    }
679                );
680                responder
681                    .send(Ok(&ffeedback::FileReportResults::default()))
682                    .expect("failed to send response");
683            }
684        }
685    }
686
687    #[fuchsia::test]
688    async fn test_file_crash_report() {
689        let clock = Arc::new(FakeClock::new());
690        let (crash_reporter_proxy, stream) =
691            create_proxy_and_stream::<ffeedback::CrashReporterMarker>();
692        let (power_statecontrol_admin_proxy, _) = create_proxy_and_stream::<fpower::AdminMarker>();
693
694        let server_fut = run_report_server(stream);
695
696        // File the crash report.
697        let debug_state = DebugState::new_for_test(
698            true,
699            clock.clone(),
700            Some(crash_reporter_proxy),
701            Some(power_statecontrol_admin_proxy),
702        );
703        let client_fut = async {
704            assert!(debug_state.file_crash_report().await);
705        };
706
707        join!(client_fut, server_fut);
708    }
709
710    async fn run_reboot_server(mut stream: fpower::AdminRequestStream) {
711        let request =
712            stream.try_next().await.expect("failed to read from stream").expect("stream is empty");
713        match request {
714            fpower::AdminRequest::Shutdown { options, responder } => {
715                assert_eq!(
716                    options,
717                    fpower::ShutdownOptions {
718                        action: Some(fpower::ShutdownAction::Reboot),
719                        reasons: Some(vec![fpower::ShutdownReason::UserRequestDeviceStuck]),
720                        ..Default::default()
721                    }
722                );
723                responder.send(Ok(())).expect("failed to send response");
724            }
725            _ => panic!("unexpected request"),
726        }
727    }
728
729    #[fuchsia::test]
730    async fn test_reboot_device() {
731        let clock = Arc::new(FakeClock::new());
732        let (crash_reporter_proxy, _) = create_proxy_and_stream::<ffeedback::CrashReporterMarker>();
733        let (power_statecontrol_admin_proxy, stream) =
734            create_proxy_and_stream::<fpower::AdminMarker>();
735
736        let server_fut = run_reboot_server(stream);
737
738        // Reboot the device.
739        let debug_state = DebugState::new_for_test(
740            true,
741            clock.clone(),
742            Some(crash_reporter_proxy),
743            Some(power_statecontrol_admin_proxy),
744        );
745        let reboot_fut = debug_state.reboot_device();
746
747        // Wait for the server to have received the call.
748        let (reboot_succeeded, ()) = join!(reboot_fut, server_fut);
749        assert!(reboot_succeeded);
750    }
751}