blob: 735f0015c4ebd1449b7ce9af7ce915dfaaeef7f0 [file] [log] [blame]
// 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(())
}
}