| // Copyright 2021 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 crate::error::PowerManagerError; |
| use crate::message::{Message, MessageReturn}; |
| use crate::node::Node; |
| use crate::shutdown_request::ShutdownRequest; |
| use crate::utils::connect_to_driver; |
| use anyhow::{format_err, Error}; |
| use async_trait::async_trait; |
| use fidl_fuchsia_hardware_input::{DeviceMarker as LidMarker, DeviceProxy as LidProxy}; |
| use fuchsia_async::OnSignals; |
| use fuchsia_fs::{open_directory_in_namespace, OpenFlags}; |
| use fuchsia_inspect::{self as inspect, NumericProperty, Property}; |
| use fuchsia_inspect_contrib::{inspect_log, nodes::BoundedListNode}; |
| use fuchsia_vfs_watcher as vfs; |
| use fuchsia_zircon as zx; |
| use futures::{ |
| future::{FutureExt, LocalBoxFuture}, |
| stream::FuturesUnordered, |
| TryStreamExt, |
| }; |
| use log::*; |
| use serde_derive::Deserialize; |
| use serde_json as json; |
| use std::{ |
| cell::RefCell, |
| collections::HashMap, |
| path::{Path, PathBuf}, |
| rc::Rc, |
| }; |
| |
| /// Node: LidShutdown |
| /// |
| /// Summary: Responds to lid closed events from devices with a lid sensor by waiting for a report |
| /// using the input FIDL protocol. |
| /// |
| /// Handles Messages: N/A |
| /// |
| /// Sends Messages: |
| /// - SystemShutdown |
| /// |
| /// FIDL dependencies: |
| /// - fuchsia.hardware.input: the node uses this protocol to wait on reports from the |
| /// lid device |
| |
| /// The lid sensor is not a real HID device however this descriptor is defined in the lid driver |
| /// and will be used to send lid HID reports for any ACPI lid sensor. |
| const HID_LID_DESCRIPTOR: [u8; 9] = [ |
| 0x05, 0x01, // Usage Page (Generic Desktop) |
| 0x09, 0x80, // Usage (System Control) |
| 0xA1, 0x01, // Collection (Application) |
| 0x0A, 0xFF, 0x01, // Usage (0x01FF, unique to lid reports) |
| ]; |
| |
| // Lid closed HID report |
| const LID_CLOSED: u8 = 0x0; |
| |
| static INPUT_DEVICES_DIRECTORY: &str = "/dev/class/input"; |
| |
| pub struct LidShutdownBuilder<'a> { |
| proxy: Option<LidProxy>, |
| lid_report_event: Option<zx::Event>, |
| system_shutdown_node: Rc<dyn Node>, |
| inspect_root: Option<&'a inspect::Node>, |
| } |
| |
| impl<'a> LidShutdownBuilder<'a> { |
| pub fn new(system_shutdown_node: Rc<dyn Node>) -> Self { |
| LidShutdownBuilder { |
| proxy: None, |
| lid_report_event: None, |
| system_shutdown_node, |
| inspect_root: None, |
| } |
| } |
| |
| #[cfg(test)] |
| pub fn new_with_event_and_proxy( |
| proxy: LidProxy, |
| lid_report_event: zx::Event, |
| system_shutdown_node: Rc<dyn Node>, |
| ) -> Self { |
| Self { |
| proxy: Some(proxy), |
| lid_report_event: Some(lid_report_event), |
| system_shutdown_node, |
| inspect_root: None, |
| } |
| } |
| |
| pub fn new_from_json(json_data: json::Value, nodes: &HashMap<String, Rc<dyn Node>>) -> Self { |
| #[derive(Deserialize)] |
| struct Dependencies { |
| system_shutdown_node: String, |
| } |
| |
| #[derive(Deserialize)] |
| struct JsonData { |
| dependencies: Dependencies, |
| } |
| |
| let data: JsonData = json::from_value(json_data).unwrap(); |
| Self::new(nodes[&data.dependencies.system_shutdown_node].clone()) |
| } |
| |
| #[cfg(test)] |
| pub fn with_inspect_root(mut self, root: &'a inspect::Node) -> Self { |
| self.inspect_root = Some(root); |
| self |
| } |
| |
| pub async fn build<'b>( |
| self, |
| futures_out: &FuturesUnordered<LocalBoxFuture<'b, ()>>, |
| ) -> Result<Rc<LidShutdown>, Error> { |
| // In tests use the default proxy. |
| let proxy = match self.proxy { |
| Some(proxy) => proxy, |
| None => Self::find_lid_sensor().await?, |
| }; |
| |
| // In tests use the default event. |
| let report_event = match self.lid_report_event { |
| Some(report_event) => report_event, |
| None => match proxy.get_reports_event().await { |
| Ok((_, report_event)) => report_event, |
| Err(_e) => return Err(format_err!("Could not get report event.")), |
| }, |
| }; |
| |
| // In tests use the default inspect root node |
| let inspect_root = self.inspect_root.unwrap_or(inspect::component::inspector().root()); |
| |
| let node = Rc::new(LidShutdown { |
| proxy, |
| report_event, |
| system_shutdown_node: self.system_shutdown_node, |
| inspect: InspectData::new(inspect_root, "LidShutdown".to_string()), |
| }); |
| |
| futures_out.push(node.clone().watch_lid()); |
| Ok(node) |
| } |
| |
| /// Checks all the input devices until the lid sensor is found. |
| async fn find_lid_sensor() -> Result<LidProxy, Error> { |
| let dir_proxy = |
| open_directory_in_namespace(INPUT_DEVICES_DIRECTORY, OpenFlags::RIGHT_READABLE)?; |
| |
| let mut watcher = vfs::Watcher::new(dir_proxy).await?; |
| |
| while let Some(msg) = watcher.try_next().await? { |
| match msg.event { |
| vfs::WatchEvent::EXISTING | vfs::WatchEvent::ADD_FILE => { |
| match Self::open_sensor(&msg.filename).await { |
| Ok(device) => return Ok(device), |
| _ => (), |
| } |
| } |
| _ => (), |
| } |
| } |
| |
| Err(format_err!("No lid device found")) |
| } |
| |
| /// Opens the sensor's device file. Returns the device if the correct HID |
| /// report descriptor is found. |
| async fn open_sensor(filename: &PathBuf) -> Result<LidProxy, Error> { |
| let path = Path::new(INPUT_DEVICES_DIRECTORY).join(filename); |
| let device = connect_to_driver::<LidMarker>(&String::from( |
| path.to_str().ok_or(format_err!("Could not read path {:?}", path))?, |
| )) |
| .await?; |
| if let Ok(device_descriptor) = device.get_report_desc().await { |
| if device_descriptor.len() < HID_LID_DESCRIPTOR.len() { |
| return Err(format_err!("Short HID header")); |
| } |
| let device_header = &device_descriptor[0..HID_LID_DESCRIPTOR.len()]; |
| if device_header == HID_LID_DESCRIPTOR { |
| return Ok(device); |
| } else { |
| return Err(format_err!("Device is not lid sensor")); |
| } |
| } |
| Err(format_err!("Could not get device HID report descriptor")) |
| } |
| } |
| |
| pub struct LidShutdown { |
| proxy: LidProxy, |
| |
| /// Event that will signal |USER_0| when a report is in the lid device's report FIFO. |
| report_event: zx::Event, |
| |
| /// Node to provide the system shutdown functionality via the SystemShutdown message. |
| system_shutdown_node: Rc<dyn Node>, |
| |
| /// A struct for managing Component Inspection data |
| inspect: InspectData, |
| } |
| |
| impl LidShutdown { |
| fn watch_lid<'a>(self: Rc<Self>) -> LocalBoxFuture<'a, ()> { |
| async move { |
| loop { |
| self.watch_lid_inner().await; |
| } |
| } |
| .boxed_local() |
| } |
| |
| /// Watches the lid device for reports. |
| async fn watch_lid_inner(&self) { |
| match OnSignals::new(&self.report_event, zx::Signals::USER_0).await { |
| Err(e) => error!("Could not wait for lid event: {:?}", e), |
| _ => match self.check_report().await { |
| Ok(()) => (), |
| Err(e) => { |
| self.inspect.read_errors.add(1); |
| self.inspect.last_read_error.set(format!("{}", e).as_str()); |
| error!("Failed to read lid report: {}", e) |
| } |
| }, |
| }; |
| } |
| |
| /// Reads the report from the lid sensor and sends shutdown signal if lid is closed. |
| async fn check_report(&self) -> Result<(), Error> { |
| let (status, report, _time) = self.proxy.read_report().await?; |
| let status = zx::Status::from_raw(status); |
| if status != zx::Status::OK { |
| return Err(format_err!("Error reading report {}", status)); |
| } |
| if report.len() != 1 { |
| return Err(format_err!("Expected single byte report, found {:?}", report)); |
| } |
| self.inspect.log_lid_report(format!("{:?}", report)); |
| let report = report[0]; |
| |
| if report == LID_CLOSED { |
| info!("Lid closed. Shutting down..."); |
| self.send_message( |
| &self.system_shutdown_node, |
| &Message::SystemShutdown(ShutdownRequest::PowerOff), |
| ) |
| .await |
| .map_err(|e| format_err!("Failed to shut down the system: {:?}", e))?; |
| } |
| Ok(()) |
| } |
| } |
| |
| #[async_trait(?Send)] |
| impl Node for LidShutdown { |
| fn name(&self) -> String { |
| "LidShutdown".to_string() |
| } |
| |
| async fn handle_message(&self, _msg: &Message) -> Result<MessageReturn, PowerManagerError> { |
| Err(PowerManagerError::Unsupported) |
| } |
| } |
| |
| struct InspectData { |
| lid_reports: RefCell<BoundedListNode>, |
| read_errors: inspect::UintProperty, |
| last_read_error: inspect::StringProperty, |
| } |
| |
| impl InspectData { |
| /// Number of inspect samples to store in the `lid_reports` BoundedListNode. |
| // Store the last 60 lid reports |
| const NUM_INSPECT_LID_REPORTS: usize = 60; |
| |
| fn new(parent: &inspect::Node, name: String) -> Self { |
| // Create a local root node and properties |
| let root = parent.create_child(name); |
| let lid_reports = RefCell::new(BoundedListNode::new( |
| root.create_child("lid_reports"), |
| Self::NUM_INSPECT_LID_REPORTS, |
| )); |
| let read_errors = root.create_uint("read_lid_report_error_count", 0); |
| let last_read_error = root.create_string("last_read_error", ""); |
| |
| // Pass ownership of the new node to the parent node, otherwise it'll be dropped |
| parent.record(root); |
| |
| InspectData { lid_reports, read_errors, last_read_error } |
| } |
| |
| fn log_lid_report(&self, lid_report: String) { |
| inspect_log!(self.lid_reports.borrow_mut(), lid_report: lid_report); |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use crate::test::mock_node::{create_dummy_node, MessageMatcher, MockNodeMaker}; |
| use crate::{msg_eq, msg_ok_return}; |
| use fidl_fuchsia_hardware_input as finput; |
| use fuchsia_async as fasync; |
| use fuchsia_inspect::testing::TreeAssertion; |
| use fuchsia_zircon::{AsHandleRef, HandleBased}; |
| use inspect::assert_data_tree; |
| |
| const LID_OPEN: u8 = 0x1; |
| |
| /// Spawns a new task that acts as a fake device driver for testing purposes. The driver only |
| /// handles requests for ReadReport - trying to send any other requests to it is a bug. |
| /// Each ReadReport responds with the |lid_report| specified. |
| fn setup_fake_driver(lid_report: u8) -> LidProxy { |
| let (proxy, mut stream) = fidl::endpoints::create_proxy_and_stream::<LidMarker>().unwrap(); |
| fasync::Task::local(async move { |
| while let Ok(req) = stream.try_next().await { |
| match req { |
| Some(finput::DeviceRequest::ReadReport { responder }) => { |
| let _ = responder.send(zx::Status::OK.into_raw(), &[lid_report], 0 as i64); |
| } |
| _ => assert!(false), |
| } |
| } |
| }) |
| .detach(); |
| |
| proxy |
| } |
| |
| /// Tests that well-formed configuration JSON does not panic the `new_from_json` function. |
| #[fasync::run_singlethreaded(test)] |
| async fn test_new_from_json() { |
| let json_data = json::json!({ |
| "type": "LidShutdown", |
| "name": "lid_shutdown", |
| "dependencies": { |
| "system_shutdown_node": "shutdown", |
| }, |
| }); |
| |
| let mut nodes: HashMap<String, Rc<dyn Node>> = HashMap::new(); |
| nodes.insert("shutdown".to_string(), create_dummy_node()); |
| let _ = LidShutdownBuilder::new_from_json(json_data, &nodes); |
| } |
| |
| /// Tests that when the node receives a signal on its |report_event|, it checks for a lid |
| /// report and, on reception of a lid closed report, it triggers a system shutdown. |
| #[fasync::run_singlethreaded(test)] |
| async fn test_triggered_shutdown() { |
| let mut mock_maker = MockNodeMaker::new(); |
| let shutdown_node = mock_maker.make( |
| "Shutdown", |
| vec![( |
| msg_eq!(SystemShutdown(ShutdownRequest::PowerOff)), |
| msg_ok_return!(SystemShutdown), |
| )], |
| ); |
| |
| let event = zx::Event::create().unwrap(); |
| let node = LidShutdownBuilder::new_with_event_and_proxy( |
| setup_fake_driver(LID_CLOSED), |
| event.duplicate_handle(zx::Rights::BASIC).unwrap(), |
| shutdown_node, |
| ) |
| .build(&FuturesUnordered::new()) |
| .await |
| .unwrap(); |
| |
| event |
| .signal_handle(zx::Signals::NONE, zx::Signals::USER_0) |
| .expect("Failed to signal event"); |
| |
| node.watch_lid_inner().await; |
| |
| // When mock_maker goes out of scope, it verifies the ShutdownNode received the shutdown |
| // request. |
| } |
| |
| /// Tests that when the node receives a signal on its |report_event|, it checks for a lid |
| /// report and, on reception of a lid open report, it does NOT trigger a system shutdown. |
| #[fasync::run_singlethreaded(test)] |
| async fn test_event_handling() { |
| let mut mock_maker = MockNodeMaker::new(); |
| let shutdown_node = mock_maker.make( |
| "Shutdown", |
| vec![], // the shutdown node is not expected to receive any messages |
| ); |
| |
| let event = zx::Event::create().unwrap(); |
| let node = LidShutdownBuilder::new_with_event_and_proxy( |
| setup_fake_driver(LID_OPEN), |
| event.duplicate_handle(zx::Rights::BASIC).unwrap(), |
| shutdown_node, |
| ) |
| .build(&FuturesUnordered::new()) |
| .await |
| .unwrap(); |
| |
| event |
| .signal_handle(zx::Signals::NONE, zx::Signals::USER_0) |
| .expect("Failed to signal event"); |
| |
| node.watch_lid_inner().await; |
| |
| // When mock_maker will verify that the ShutdownNode receives no messages until it goes |
| // out of scope. |
| } |
| |
| /// Tests that an unsupported message is handled gracefully and an error is returned. |
| #[fasync::run_singlethreaded(test)] |
| async fn test_unsupported_msg() { |
| let mut mock_maker = MockNodeMaker::new(); |
| let shutdown_node = mock_maker.make("Shutdown", vec![]); |
| |
| let node_futures = FuturesUnordered::new(); |
| let node = LidShutdownBuilder::new_with_event_and_proxy( |
| setup_fake_driver(LID_CLOSED), |
| zx::Event::create().unwrap(), |
| shutdown_node, |
| ) |
| .build(&node_futures) |
| .await |
| .unwrap(); |
| match node.handle_message(&Message::GetCpuLoads).await { |
| Err(PowerManagerError::Unsupported) => {} |
| e => panic!("Unexpected return value: {:?}", e), |
| } |
| } |
| |
| #[test] |
| fn test_loop_is_not_blocked() { |
| let mut executor = fasync::TestExecutor::new().unwrap(); |
| let mut mock_maker = MockNodeMaker::new(); |
| let shutdown_node = mock_maker.make("Shutdown", vec![]); |
| let node_futures = FuturesUnordered::new(); |
| let node = executor |
| .run_singlethreaded( |
| LidShutdownBuilder::new_with_event_and_proxy( |
| setup_fake_driver(LID_OPEN), |
| zx::Event::create().unwrap(), |
| shutdown_node, |
| ) |
| .build(&node_futures), |
| ) |
| .unwrap(); |
| |
| let mut future = node.watch_lid(); |
| let _ = executor.run_until_stalled(&mut future); |
| // If watch_lid() blocks waiting for the event, it will block forever (because the event |
| // will never come), meaning that this test will time out. Otherwise run_until_stalled() |
| // will return immediately and the test will pass. |
| } |
| |
| /// Tests for the presence and correctness of dynamically-added inspect data |
| #[fasync::run_singlethreaded(test)] |
| async fn test_inspect_data() { |
| let lid_state = LID_OPEN; |
| let inspector = inspect::Inspector::new(); |
| let mut mock_maker = MockNodeMaker::new(); |
| let shutdown_node = mock_maker.make("Shutdown", vec![]); |
| let event = zx::Event::create().unwrap(); |
| |
| let node_futures = FuturesUnordered::new(); |
| let node = LidShutdownBuilder::new_with_event_and_proxy( |
| setup_fake_driver(lid_state), |
| event.duplicate_handle(zx::Rights::BASIC).unwrap(), |
| shutdown_node, |
| ) |
| .with_inspect_root(inspector.root()) |
| .build(&node_futures) |
| .await |
| .unwrap(); |
| |
| // The node will read the current temperature and log the sample into Inspect. Read enough |
| // samples to test that the correct number of samples are logged and older ones are dropped. |
| for _ in 0..InspectData::NUM_INSPECT_LID_REPORTS + 10 { |
| event |
| .signal_handle(zx::Signals::NONE, zx::Signals::USER_0) |
| .expect("Failed to signal event"); |
| node.watch_lid_inner().await; |
| } |
| |
| let mut root = TreeAssertion::new("LidShutdown", false); |
| let mut lid_reports = TreeAssertion::new("lid_reports", true); |
| |
| // Since we read 10 more samples than our limit allows, the first 10 should be dropped. So |
| // test that the sample numbering starts at 10 and continues for the expected number of |
| // samples. |
| for i in 10..InspectData::NUM_INSPECT_LID_REPORTS + 10 { |
| let mut sample_child = TreeAssertion::new(&i.to_string(), true); |
| sample_child |
| .add_property_assertion("lid_report", Box::new(format!("{:?}", [lid_state]))); |
| sample_child.add_property_assertion("@time", Box::new(inspect::testing::AnyProperty)); |
| lid_reports.add_child_assertion(sample_child); |
| } |
| |
| root.add_child_assertion(lid_reports); |
| assert_data_tree!(inspector, root: { root, }); |
| } |
| } |