| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use anyhow::{bail, Context, Error}; |
| use carnelian::input::consumer_control::Phase; |
| use fidl_fuchsia_input_report::ConsumerControlButton; |
| use fidl_fuchsia_paver::{BootManagerMarker, BootManagerProxy, Configuration, PaverMarker}; |
| use fidl_fuchsia_recovery::FactoryResetMarker; |
| use fuchsia_component::client::connect_to_protocol; |
| |
| #[derive(Debug, PartialEq, Copy, Clone)] |
| pub enum FactoryResetState { |
| Waiting, |
| AwaitingPolicy(usize), |
| StartCountdown, |
| CancelCountdown, |
| ExecuteReset, |
| AwaitingReset, |
| } |
| |
| #[derive(Debug, PartialEq, Copy, Clone)] |
| pub enum ResetEvent { |
| ButtonPress(ConsumerControlButton, Phase), |
| AwaitPolicyResult(usize, bool), |
| CountdownFinished, |
| CountdownCancelled, |
| } |
| |
| pub struct FactoryResetStateMachine { |
| volume_up_phase: Phase, |
| volume_down_phase: Phase, |
| state: FactoryResetState, |
| last_policy_check_id: usize, |
| } |
| |
| impl FactoryResetStateMachine { |
| pub fn new() -> FactoryResetStateMachine { |
| FactoryResetStateMachine { |
| volume_down_phase: Phase::Up, |
| volume_up_phase: Phase::Up, |
| state: FactoryResetState::Waiting, |
| last_policy_check_id: 0, |
| } |
| } |
| |
| pub fn is_counting_down(&self) -> bool { |
| self.state == FactoryResetState::StartCountdown |
| } |
| |
| fn update_button_state(&mut self, button: ConsumerControlButton, phase: Phase) { |
| match button { |
| ConsumerControlButton::VolumeUp => self.volume_up_phase = phase, |
| ConsumerControlButton::VolumeDown => self.volume_down_phase = phase, |
| _ => panic!("Invalid button provided {:?}", button), |
| }; |
| } |
| |
| fn check_buttons_pressed(&self) -> bool { |
| match (self.volume_up_phase, self.volume_down_phase) { |
| (Phase::Down, Phase::Down) => true, |
| _ => false, |
| } |
| } |
| |
| /// Updates the state machine's button state on button presses and returns |
| /// the new state and a boolean indicating if that new state is changed from |
| /// the previous state. |
| pub fn handle_event(&mut self, event: ResetEvent) -> FactoryResetState { |
| if let ResetEvent::ButtonPress(button, phase) = event { |
| // Handle all volume button events here to make sure we don't miss anything in default matches |
| self.update_button_state(button, phase); |
| } |
| let new_state = match self.state { |
| FactoryResetState::Waiting => match event { |
| ResetEvent::ButtonPress(_, _) => { |
| if self.check_buttons_pressed() { |
| self.last_policy_check_id += 1; |
| FactoryResetState::AwaitingPolicy(self.last_policy_check_id) |
| } else { |
| FactoryResetState::Waiting |
| } |
| } |
| ResetEvent::CountdownFinished => { |
| panic!("Not expecting timer updates when in waiting state") |
| } |
| ResetEvent::CountdownCancelled | ResetEvent::AwaitPolicyResult(_, _) => { |
| FactoryResetState::Waiting |
| } |
| }, |
| FactoryResetState::AwaitingPolicy(check_id) => match event { |
| ResetEvent::ButtonPress(_, _) => { |
| if !self.check_buttons_pressed() { |
| FactoryResetState::Waiting |
| } else { |
| FactoryResetState::AwaitingPolicy(check_id) |
| } |
| } |
| ResetEvent::AwaitPolicyResult(check_id, fdr_enabled) |
| if check_id == self.last_policy_check_id => |
| { |
| if fdr_enabled { |
| println!("recovery: start reset countdown"); |
| FactoryResetState::StartCountdown |
| } else { |
| FactoryResetState::Waiting |
| } |
| } |
| _ => FactoryResetState::Waiting, |
| }, |
| FactoryResetState::StartCountdown => match event { |
| ResetEvent::ButtonPress(_, _) => { |
| if self.check_buttons_pressed() { |
| panic!( |
| "Not expecting both buttons to be pressed while in StartCountdown state" |
| ); |
| } else { |
| println!("recovery: cancel reset countdown"); |
| FactoryResetState::CancelCountdown |
| } |
| } |
| ResetEvent::CountdownCancelled => { |
| panic!( |
| "Not expecting CountdownCancelled here, expecting input event to \ |
| move to CancelCountdown state first." |
| ); |
| } |
| ResetEvent::CountdownFinished => { |
| println!("recovery: execute factory reset"); |
| FactoryResetState::ExecuteReset |
| } |
| ResetEvent::AwaitPolicyResult(_, _) => FactoryResetState::StartCountdown, |
| }, |
| FactoryResetState::CancelCountdown => match event { |
| ResetEvent::CountdownCancelled => FactoryResetState::Waiting, |
| ResetEvent::AwaitPolicyResult(_, _) | ResetEvent::ButtonPress(_, _) => { |
| FactoryResetState::CancelCountdown |
| } |
| _ => panic!("Only expecting CountdownCancelled event in CancelCountdown state."), |
| }, |
| FactoryResetState::ExecuteReset => match event { |
| ResetEvent::AwaitPolicyResult(_, _) => FactoryResetState::AwaitingReset, |
| // Subsequent button presses should not trigger additional reset calls. |
| ResetEvent::ButtonPress(_, _) => FactoryResetState::AwaitingReset, |
| _ => { |
| panic!("Not expecting countdown events while in ExecuteReset state") |
| } |
| }, |
| FactoryResetState::AwaitingReset => match event { |
| ResetEvent::AwaitPolicyResult(_, _) => FactoryResetState::AwaitingReset, |
| ResetEvent::ButtonPress(_, _) => FactoryResetState::AwaitingReset, |
| _ => panic!("Not expecting countdown events while in ExecuteReset state"), |
| }, |
| }; |
| |
| self.state = new_state; |
| new_state |
| } |
| |
| #[cfg(test)] |
| pub fn get_state(&self) -> FactoryResetState { |
| return self.state; |
| } |
| } |
| |
| fn get_other_slot_config(config: Configuration) -> Configuration { |
| match config { |
| Configuration::A => Configuration::B, |
| Configuration::B => Configuration::A, |
| // A/B recovery is not supported currently. |
| Configuration::Recovery => Configuration::Recovery, |
| } |
| } |
| |
| /// Uses fuchsia.paver FIDL's BootManager API to set the active slot using the following strategy: |
| /// |
| /// 1. Query for slot-last-set-active |
| /// 2. Set the last inactive slot (opposite of the result from 1) to active to reset retry count |
| /// 3. Set the result from slot-last-set-active to active |
| /// |
| /// This aims to recover from cases where both slots A and B are marked unbootable. |
| /// Requires fuchsia.paver.Paver protocol capability. |
| pub async fn reset_active_slot() -> Result<(), Error> { |
| let paver_proxy = connect_to_protocol::<PaverMarker>().context("failed to connect to paver")?; |
| let (boot_manager, server) = fidl::endpoints::create_proxy::<BootManagerMarker>()?; |
| |
| paver_proxy.find_boot_manager(server).context("failed to find boot manager")?; |
| |
| reset_active_slot_with_proxy(boot_manager).await |
| } |
| |
| async fn reset_active_slot_with_proxy(boot_manager: BootManagerProxy) -> Result<(), Error> { |
| let last_active_config = match boot_manager.query_configuration_last_set_active().await { |
| Ok(Ok(config)) => config, |
| Ok(Err(err)) => bail!("Failure status querying last active config: {:?}", err), |
| Err(err) => bail!("Error querying last active configuration: {:?}", err), |
| }; |
| |
| if last_active_config == Configuration::Recovery { |
| eprintln!("Last active config is recovery: no information to decide which other config to set active. Leaving as is."); |
| return Ok(()); |
| } |
| |
| // Set inactive config, then last active config to reset the boot attempt retry counters. |
| let inactive_config = get_other_slot_config(last_active_config); |
| boot_manager |
| .set_configuration_active(inactive_config) |
| .await |
| .context("failed to set inactive config")?; |
| boot_manager |
| .set_configuration_active(last_active_config) |
| .await |
| .context("failed to set last active config")?; |
| |
| Ok(()) |
| } |
| |
| pub async fn execute_reset() -> Result<(), Error> { |
| let factory_reset_service = connect_to_protocol::<FactoryResetMarker>(); |
| let proxy = match factory_reset_service { |
| Ok(marker) => marker.clone(), |
| Err(error) => { |
| bail!("Could not connect to factory_reset_service: {:?}", error); |
| } |
| }; |
| |
| println!("recovery: Executing factory reset command"); |
| |
| match proxy.reset().await { |
| Ok(_) => {} |
| Err(error) => { |
| bail!("Error executing factory reset command : {:?}", error); |
| } |
| }; |
| Ok(()) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use fidl_fuchsia_paver::BootManagerRequest; |
| use fuchsia_async as fasync; |
| use fuchsia_zircon as zx; |
| use futures::{channel::mpsc, StreamExt, TryStreamExt}; |
| |
| // A mock BootManager service that stores an initial last_active_config and reports the queries |
| // it receives to mpsc::Receivers for the test to listen and consume. |
| fn create_mock_boot_manager( |
| last_active_config: Configuration, |
| ) -> Result< |
| (BootManagerProxy, mpsc::Receiver<Configuration>, mpsc::Receiver<Configuration>), |
| Error, |
| > { |
| let (mut query_last_active_sender, query_last_active_receiver) = mpsc::channel(10); |
| let (mut set_active_sender, set_active_receiver) = mpsc::channel(10); |
| let (proxy, mut request_stream) = |
| fidl::endpoints::create_proxy_and_stream::<BootManagerMarker>()?; |
| |
| fasync::Task::local(async move { |
| while let Some(request) = |
| request_stream.try_next().await.expect("failed to read mock request") |
| { |
| match request { |
| BootManagerRequest::QueryConfigurationLastSetActive { responder } => { |
| query_last_active_sender.start_send(last_active_config.clone()).unwrap(); |
| responder.send(Ok(last_active_config)).unwrap(); |
| } |
| BootManagerRequest::SetConfigurationActive { configuration, responder } => { |
| set_active_sender.start_send(configuration.clone()).unwrap(); |
| responder.send(zx::Status::OK.into_raw()).unwrap(); |
| } |
| _ => { |
| panic!("Unexpected request sent to mock boot manager"); |
| } |
| } |
| } |
| }) |
| .detach(); |
| |
| Ok((proxy, query_last_active_receiver, set_active_receiver)) |
| } |
| |
| #[fuchsia::test] |
| async fn test_set_last_config_a() { |
| let last_active_config = Configuration::A; |
| let (boot_manager_proxy, mut query_last_active_listener, mut set_active_listener) = |
| create_mock_boot_manager(last_active_config).unwrap(); |
| |
| let _res = reset_active_slot_with_proxy(boot_manager_proxy).await.unwrap(); |
| |
| // Read in order the requests sent to boot manager: |
| // 1. Reading the last active config. |
| // 2. Setting the last inactive config as active. |
| // 3. Setting the last active config as active. |
| |
| assert_eq!(Configuration::A, query_last_active_listener.next().await.unwrap()); |
| assert_eq!(Configuration::B, set_active_listener.next().await.unwrap()); |
| assert_eq!(Configuration::A, set_active_listener.next().await.unwrap()); |
| } |
| |
| #[fuchsia::test] |
| async fn test_set_last_config_b() { |
| let last_active_config = Configuration::B; |
| let (boot_manager_proxy, mut query_last_active_listener, mut set_active_listener) = |
| create_mock_boot_manager(last_active_config).unwrap(); |
| |
| let _res = reset_active_slot_with_proxy(boot_manager_proxy).await.unwrap(); |
| |
| // Read in order the requests sent to boot manager: |
| // 1. Reading the last active config. |
| // 2. Setting the last inactive config as active. |
| // 3. Setting the last active config as active. |
| |
| assert_eq!(Configuration::B, query_last_active_listener.next().await.unwrap()); |
| assert_eq!(Configuration::A, set_active_listener.next().await.unwrap()); |
| assert_eq!(Configuration::B, set_active_listener.next().await.unwrap()); |
| } |
| |
| #[fuchsia::test] |
| async fn test_set_last_config_recovery() { |
| let last_active_config = Configuration::Recovery; |
| let (boot_manager_proxy, mut query_last_active_listener, set_active_listener) = |
| create_mock_boot_manager(last_active_config).unwrap(); |
| |
| let _res = reset_active_slot_with_proxy(boot_manager_proxy).await; |
| |
| // There should be no attempts to set active slot if the last active slot is recovery. |
| assert_eq!(Configuration::Recovery, query_last_active_listener.next().await.unwrap()); |
| assert_eq!(0, set_active_listener.count().await); |
| } |
| |
| #[test] |
| fn test_reset_complete() -> std::result::Result<(), anyhow::Error> { |
| let mut state_machine = FactoryResetStateMachine::new(); |
| let state = state_machine.get_state(); |
| assert_eq!(state, FactoryResetState::Waiting); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeUp, Phase::Down)); |
| assert_eq!(state, FactoryResetState::Waiting); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeDown, Phase::Down)); |
| assert_eq!(state, FactoryResetState::AwaitingPolicy(1)); |
| let state = state_machine.handle_event(ResetEvent::AwaitPolicyResult(1, true)); |
| assert_eq!(state, FactoryResetState::StartCountdown); |
| let state = state_machine.handle_event(ResetEvent::CountdownFinished); |
| assert_eq!(state, FactoryResetState::ExecuteReset); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_reset_complete_reverse() -> std::result::Result<(), anyhow::Error> { |
| let mut state_machine = FactoryResetStateMachine::new(); |
| let state = state_machine.get_state(); |
| assert_eq!(state, FactoryResetState::Waiting); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeDown, Phase::Down)); |
| assert_eq!(state, FactoryResetState::Waiting); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeUp, Phase::Down)); |
| assert_eq!(state, FactoryResetState::AwaitingPolicy(1)); |
| let state = state_machine.handle_event(ResetEvent::AwaitPolicyResult(1, true)); |
| assert_eq!(state, FactoryResetState::StartCountdown); |
| let state = state_machine.handle_event(ResetEvent::CountdownFinished); |
| assert_eq!(state, FactoryResetState::ExecuteReset); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_reset_cancelled() -> std::result::Result<(), anyhow::Error> { |
| test_reset_cancelled_button(ConsumerControlButton::VolumeUp); |
| test_reset_cancelled_button(ConsumerControlButton::VolumeDown); |
| Ok(()) |
| } |
| |
| fn test_reset_cancelled_button(button: ConsumerControlButton) { |
| let mut state_machine = FactoryResetStateMachine::new(); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeUp, Phase::Down)); |
| assert_eq!(state, FactoryResetState::Waiting); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeDown, Phase::Down)); |
| assert_eq!(state, FactoryResetState::AwaitingPolicy(1)); |
| let state = state_machine.handle_event(ResetEvent::AwaitPolicyResult(1, true)); |
| assert_eq!(state, FactoryResetState::StartCountdown); |
| let state = state_machine.handle_event(ResetEvent::ButtonPress(button, Phase::Up)); |
| assert_eq!(state, FactoryResetState::CancelCountdown); |
| let state = state_machine.handle_event(ResetEvent::CountdownCancelled); |
| assert_eq!(state, FactoryResetState::Waiting); |
| let state = state_machine.handle_event(ResetEvent::ButtonPress(button, Phase::Down)); |
| assert_eq!(state, FactoryResetState::AwaitingPolicy(2)); |
| let state = state_machine.handle_event(ResetEvent::AwaitPolicyResult(2, true)); |
| assert_eq!(state, FactoryResetState::StartCountdown); |
| } |
| |
| #[test] |
| #[should_panic] |
| fn test_early_complete_countdown() { |
| let mut state_machine = FactoryResetStateMachine::new(); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeUp, Phase::Down)); |
| assert_eq!(state, FactoryResetState::Waiting); |
| let _state = state_machine.handle_event(ResetEvent::CountdownFinished); |
| } |
| |
| #[test] |
| #[should_panic] |
| fn test_cancelled_countdown_not_complete() { |
| let mut state_machine = FactoryResetStateMachine::new(); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeUp, Phase::Down)); |
| assert_eq!(state, FactoryResetState::Waiting); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeDown, Phase::Down)); |
| assert_eq!(state, FactoryResetState::AwaitingPolicy(1)); |
| let state = state_machine.handle_event(ResetEvent::AwaitPolicyResult(1, true)); |
| assert_eq!(state, FactoryResetState::StartCountdown); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeDown, Phase::Up)); |
| assert_eq!(state, FactoryResetState::CancelCountdown); |
| let _state = state_machine.handle_event(ResetEvent::CountdownFinished); |
| } |
| |
| #[test] |
| fn test_cancelled_countdown_with_extra_button_press() { |
| let mut state_machine = FactoryResetStateMachine::new(); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeUp, Phase::Down)); |
| assert_eq!(state, FactoryResetState::Waiting); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeDown, Phase::Down)); |
| assert_eq!(state, FactoryResetState::AwaitingPolicy(1)); |
| let state = state_machine.handle_event(ResetEvent::AwaitPolicyResult(1, true)); |
| assert_eq!(state, FactoryResetState::StartCountdown); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeDown, Phase::Up)); |
| assert_eq!(state, FactoryResetState::CancelCountdown); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeUp, Phase::Up)); |
| assert_eq!(state, FactoryResetState::CancelCountdown); |
| } |
| |
| #[test] |
| fn test_reset_complete_button_released() -> std::result::Result<(), anyhow::Error> { |
| let mut state_machine = FactoryResetStateMachine::new(); |
| let state = state_machine.get_state(); |
| assert_eq!(state, FactoryResetState::Waiting); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeDown, Phase::Down)); |
| assert_eq!(state, FactoryResetState::Waiting); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeUp, Phase::Down)); |
| assert_eq!(state, FactoryResetState::AwaitingPolicy(1)); |
| let state = state_machine.handle_event(ResetEvent::AwaitPolicyResult(1, true)); |
| assert_eq!(state, FactoryResetState::StartCountdown); |
| let state = state_machine.handle_event(ResetEvent::CountdownFinished); |
| assert_eq!(state, FactoryResetState::ExecuteReset); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeDown, Phase::Up)); |
| assert_eq!(state, FactoryResetState::AwaitingReset); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_reset_complete_multiple_button_presses() -> std::result::Result<(), anyhow::Error> { |
| // Multiple button presses should leave the state machine in AwaitingReset. |
| let mut state_machine = FactoryResetStateMachine::new(); |
| let state = state_machine.get_state(); |
| assert_eq!(state, FactoryResetState::Waiting); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeDown, Phase::Down)); |
| assert_eq!(state, FactoryResetState::Waiting); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeUp, Phase::Down)); |
| assert_eq!(state, FactoryResetState::AwaitingPolicy(1)); |
| let state = state_machine.handle_event(ResetEvent::AwaitPolicyResult(1, true)); |
| assert_eq!(state, FactoryResetState::StartCountdown); |
| let state = state_machine.handle_event(ResetEvent::CountdownFinished); |
| assert_eq!(state, FactoryResetState::ExecuteReset); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeDown, Phase::Up)); |
| assert_eq!(state, FactoryResetState::AwaitingReset); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeDown, Phase::Down)); |
| assert_eq!(state, FactoryResetState::AwaitingReset); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeUp, Phase::Up)); |
| assert_eq!(state, FactoryResetState::AwaitingReset); |
| let state = state_machine |
| .handle_event(ResetEvent::ButtonPress(ConsumerControlButton::VolumeUp, Phase::Down)); |
| assert_eq!(state, FactoryResetState::AwaitingReset); |
| Ok(()) |
| } |
| } |