| // 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::{anyhow, Error}, |
| async_trait::async_trait, |
| chrono::{prelude::*, LocalResult}, |
| fdio::service_connect, |
| fidl::endpoints::create_proxy, |
| fidl_fuchsia_hardware_rtc as frtc, |
| fuchsia_async::{self as fasync, TimeoutExt}, |
| fuchsia_zircon::{self as zx, DurationNum}, |
| futures::TryFutureExt, |
| lazy_static::lazy_static, |
| std::{fs, path::PathBuf}, |
| thiserror::Error, |
| tracing::error, |
| }; |
| #[cfg(test)] |
| use {parking_lot::Mutex, std::sync::Arc}; |
| |
| lazy_static! { |
| /// The absolute path at which RTC devices are exposed. |
| static ref RTC_PATH: PathBuf = PathBuf::from("/dev/class/rtc/"); |
| } |
| |
| /// Time to wait before declaring a FIDL call to be failed. |
| const FIDL_TIMEOUT: zx::Duration = zx::Duration::from_seconds(2); |
| |
| // The minimum error at which to begin an async wait for top of second while setting RTC. |
| const WAIT_THRESHOLD: zx::Duration = zx::Duration::from_millis(1); |
| |
| const NANOS_PER_SECOND: i64 = 1_000_000_000; |
| |
| #[derive(Error, Debug)] |
| pub enum RtcCreationError { |
| #[error("Could not find any RTC devices")] |
| NoDevices, |
| #[error("Found {0} RTC devices, more than the 1 we expected")] |
| MultipleDevices(usize), |
| #[error("Could not connect to RTC device: {0}")] |
| ConnectionFailed(Error), |
| } |
| |
| /// Interface to interact with a real-time clock. Note that the RTC hardware interface is limited |
| /// to a resolution of one second; times returned by the RTC will always be a whole number of |
| /// seconds and times sent to the RTC will discard any fractional second. |
| #[async_trait] |
| pub trait Rtc: Send + Sync { |
| /// Returns the current time reported by the realtime clock. |
| async fn get(&self) -> Result<zx::Time, Error>; |
| /// Sets the time of the realtime clock to `value`. |
| async fn set(&self, value: zx::Time) -> Result<(), Error>; |
| } |
| |
| /// An implementation of the `Rtc` trait that connects to an RTC device in /dev/class/rtc. |
| pub struct RtcImpl { |
| /// A proxy for the client end of a connection to the device. |
| proxy: frtc::DeviceProxy, |
| } |
| |
| impl RtcImpl { |
| /// Returns a new `RtcImpl` connected to the only available RTC device. Returns an Error if no |
| /// devices were found, multiple devices were found, or the connection failed. |
| pub fn only_device() -> Result<RtcImpl, RtcCreationError> { |
| let mut iterator = fs::read_dir(&*RTC_PATH).map_err(|_| RtcCreationError::NoDevices)?; |
| match iterator.next() { |
| Some(Ok(first_entry)) => match iterator.count() { |
| 0 => RtcImpl::new(first_entry.path()), |
| additional_devices => { |
| Err(RtcCreationError::MultipleDevices(additional_devices + 1)) |
| } |
| }, |
| Some(Err(err)) => { |
| Err(RtcCreationError::ConnectionFailed(anyhow!("Failed to read entry: {}", err))) |
| } |
| None => Err(RtcCreationError::NoDevices), |
| } |
| } |
| |
| /// Returns a new `RtcImpl` connected to the device at the supplied path. |
| pub fn new(path_buf: PathBuf) -> Result<RtcImpl, RtcCreationError> { |
| let path_str = path_buf |
| .to_str() |
| .ok_or(RtcCreationError::ConnectionFailed(anyhow!("Non unicode path")))?; |
| let (proxy, server) = create_proxy::<frtc::DeviceMarker>().map_err(|err| { |
| RtcCreationError::ConnectionFailed(anyhow!("Failed to create proxy: {}", err)) |
| })?; |
| service_connect(&path_str, server.into_channel()).map_err(|err| { |
| RtcCreationError::ConnectionFailed(anyhow!("Failed to connect to device: {}", err)) |
| })?; |
| Ok(RtcImpl { proxy }) |
| } |
| } |
| |
| fn fidl_time_to_zx_time(fidl_time: frtc::Time) -> Result<zx::Time, Error> { |
| let chrono = Utc |
| .ymd_opt(fidl_time.year as i32, fidl_time.month as u32, fidl_time.day as u32) |
| .and_hms_opt(fidl_time.hours as u32, fidl_time.minutes as u32, fidl_time.seconds as u32); |
| match chrono { |
| LocalResult::Single(t) => Ok(zx::Time::from_nanos(t.timestamp_nanos())), |
| _ => Err(anyhow!("Invalid RTC time: {:?}", fidl_time)), |
| } |
| } |
| |
| fn zx_time_to_fidl_time(zx_time: zx::Time) -> frtc::Time { |
| let nanos = zx::Time::into_nanos(zx_time); |
| let chrono = Utc.timestamp(nanos / NANOS_PER_SECOND, 0); |
| frtc::Time { |
| year: chrono.year() as u16, |
| month: chrono.month() as u8, |
| day: chrono.day() as u8, |
| hours: chrono.hour() as u8, |
| minutes: chrono.minute() as u8, |
| seconds: chrono.second() as u8, |
| } |
| } |
| |
| #[async_trait] |
| impl Rtc for RtcImpl { |
| async fn get(&self) -> Result<zx::Time, Error> { |
| self.proxy |
| .get() |
| .map_err(|err| anyhow!("FIDL error: {}", err)) |
| .on_timeout(zx::Time::after(FIDL_TIMEOUT), || Err(anyhow!("FIDL timeout on get"))) |
| .await |
| .and_then(fidl_time_to_zx_time) |
| } |
| |
| async fn set(&self, value: zx::Time) -> Result<(), Error> { |
| let fractional_second = zx::Duration::from_nanos(value.into_nanos() % NANOS_PER_SECOND); |
| // The RTC API only accepts integer seconds but we really need higher accuracy, particularly |
| // for the kernel clock set by the RTC driver... |
| let mut fidl_time = if fractional_second < WAIT_THRESHOLD { |
| // ...if we are being asked to set a time at or near the bottom of the second, truncate |
| // the time and set the RTC immediately... |
| zx_time_to_fidl_time(value) |
| } else { |
| // ...otherwise, wait until the top of the current second than set the RTC using the |
| // following second. |
| fasync::Timer::new(fasync::Time::after(1.second() - fractional_second)).await; |
| zx_time_to_fidl_time(value + 1.second()) |
| }; |
| let status = self |
| .proxy |
| .set(&mut fidl_time) |
| .map_err(|err| anyhow!("FIDL error: {}", err)) |
| .on_timeout(zx::Time::after(FIDL_TIMEOUT), || Err(anyhow!("FIDL timeout on set"))) |
| .await?; |
| zx::Status::ok(status).map_err(|stat| anyhow!("Bad status on set: {:?}", stat)) |
| } |
| } |
| |
| /// A Fake implementation of the Rtc trait for use in testing. The fake always returns a fixed |
| /// value set during construction and remembers the last value it was told to set (shared across |
| /// all clones of the `FakeRtc`). |
| #[cfg(test)] |
| #[derive(Clone)] |
| pub struct FakeRtc { |
| /// The response used for get requests. |
| value: Result<zx::Time, String>, |
| /// The most recent value received in a set request. |
| last_set: Arc<Mutex<Option<zx::Time>>>, |
| } |
| |
| #[cfg(test)] |
| impl FakeRtc { |
| /// Returns a new `FakeRtc` that always returns the supplied time. |
| pub fn valid(time: zx::Time) -> FakeRtc { |
| FakeRtc { value: Ok(time), last_set: Arc::new(Mutex::new(None)) } |
| } |
| |
| /// Returns a new `FakeRtc` that always returns the supplied error message. |
| pub fn invalid(error: String) -> FakeRtc { |
| FakeRtc { value: Err(error), last_set: Arc::new(Mutex::new(None)) } |
| } |
| |
| /// Returns the last time set on this clock, or none if the clock has never been set. |
| pub fn last_set(&self) -> Option<zx::Time> { |
| self.last_set.lock().map(|time| time.clone()) |
| } |
| } |
| |
| #[cfg(test)] |
| #[async_trait] |
| impl Rtc for FakeRtc { |
| async fn get(&self) -> Result<zx::Time, Error> { |
| self.value.as_ref().map(|time| time.clone()).map_err(|msg| Error::msg(msg.clone())) |
| } |
| |
| async fn set(&self, value: zx::Time) -> Result<(), Error> { |
| let mut last_set = self.last_set.lock(); |
| last_set.replace(value); |
| Ok(()) |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use { |
| super::*, |
| fidl::endpoints::create_proxy_and_stream, |
| fuchsia_async as fasync, |
| futures::StreamExt, |
| test_util::{assert_gt, assert_lt}, |
| }; |
| |
| const TEST_FIDL_TIME: frtc::Time = |
| frtc::Time { year: 2020, month: 8, day: 14, hours: 0, minutes: 0, seconds: 0 }; |
| const INVALID_FIDL_TIME_1: frtc::Time = |
| frtc::Time { year: 2020, month: 14, day: 0, hours: 0, minutes: 0, seconds: 0 }; |
| const INVALID_FIDL_TIME_2: frtc::Time = |
| frtc::Time { year: 2020, month: 8, day: 14, hours: 99, minutes: 99, seconds: 99 }; |
| const TEST_OFFSET: zx::Duration = zx::Duration::from_millis(250); |
| const TEST_ZX_TIME: zx::Time = zx::Time::from_nanos(1_597_363_200_000_000_000); |
| const DIFFERENT_ZX_TIME: zx::Time = zx::Time::from_nanos(1_597_999_999_000_000_000); |
| |
| #[fuchsia::test] |
| fn time_conversion() { |
| let to_fidl = zx_time_to_fidl_time(TEST_ZX_TIME); |
| assert_eq!(to_fidl, TEST_FIDL_TIME); |
| // Times should be truncated to the previous second |
| let to_fidl_2 = zx_time_to_fidl_time(TEST_ZX_TIME + 999.millis()); |
| assert_eq!(to_fidl_2, TEST_FIDL_TIME); |
| |
| let to_zx = fidl_time_to_zx_time(TEST_FIDL_TIME).unwrap(); |
| assert_eq!(to_zx, TEST_ZX_TIME); |
| |
| assert_eq!(fidl_time_to_zx_time(INVALID_FIDL_TIME_1).is_err(), true); |
| assert_eq!(fidl_time_to_zx_time(INVALID_FIDL_TIME_2).is_err(), true); |
| } |
| |
| #[fuchsia::test] |
| async fn rtc_impl_get_valid() { |
| let (proxy, mut stream) = create_proxy_and_stream::<frtc::DeviceMarker>().unwrap(); |
| |
| let rtc_impl = RtcImpl { proxy }; |
| let _responder = fasync::Task::spawn(async move { |
| if let Some(Ok(frtc::DeviceRequest::Get { responder })) = stream.next().await { |
| let mut fidl_time = TEST_FIDL_TIME; |
| responder.send(&mut fidl_time).expect("Failed response"); |
| } |
| }); |
| assert_eq!(rtc_impl.get().await.unwrap(), TEST_ZX_TIME); |
| } |
| |
| #[fuchsia::test] |
| async fn rtc_impl_get_invalid() { |
| let (proxy, mut stream) = create_proxy_and_stream::<frtc::DeviceMarker>().unwrap(); |
| |
| let rtc_impl = RtcImpl { proxy }; |
| let _responder = fasync::Task::spawn(async move { |
| if let Some(Ok(frtc::DeviceRequest::Get { responder })) = stream.next().await { |
| let mut fidl_time = INVALID_FIDL_TIME_1; |
| responder.send(&mut fidl_time).expect("Failed response"); |
| } |
| }); |
| assert_eq!(rtc_impl.get().await.is_err(), true); |
| } |
| |
| #[fuchsia::test] |
| async fn rtc_impl_set_whole_second() { |
| let (proxy, mut stream) = create_proxy_and_stream::<frtc::DeviceMarker>().unwrap(); |
| |
| let rtc_impl = RtcImpl { proxy }; |
| let _responder = fasync::Task::spawn(async move { |
| if let Some(Ok(frtc::DeviceRequest::Set { rtc, responder })) = stream.next().await { |
| let status = match rtc { |
| TEST_FIDL_TIME => zx::Status::OK, |
| _ => zx::Status::INVALID_ARGS, |
| }; |
| responder.send(status.into_raw()).expect("Failed response"); |
| } |
| }); |
| let before = zx::Time::get_monotonic(); |
| assert!(rtc_impl.set(TEST_ZX_TIME).await.is_ok()); |
| let span = zx::Time::get_monotonic() - before; |
| // Setting an integer second should not require any delay and therefore should complete |
| // very fast - well under a millisecond typically. |
| assert_lt!(span, 50.millis()); |
| } |
| |
| #[fuchsia::test] |
| async fn rtc_impl_set_partial_second() { |
| let (proxy, mut stream) = create_proxy_and_stream::<frtc::DeviceMarker>().unwrap(); |
| |
| let rtc_impl = RtcImpl { proxy }; |
| let _responder = fasync::Task::spawn(async move { |
| if let Some(Ok(frtc::DeviceRequest::Set { rtc, responder })) = stream.next().await { |
| let status = match rtc { |
| TEST_FIDL_TIME => zx::Status::OK, |
| _ => zx::Status::INVALID_ARGS, |
| }; |
| responder.send(status.into_raw()).expect("Failed response"); |
| } |
| }); |
| let before = zx::Time::get_monotonic(); |
| assert!(rtc_impl.set(TEST_ZX_TIME - TEST_OFFSET).await.is_ok()); |
| let span = zx::Time::get_monotonic() - before; |
| // Setting a fractional second should cause a delay until the top of second before calling |
| // the FIDL interface. We only verify half the expected time has passed to allow for some |
| // slack in the timer calculation. |
| assert_gt!(span, TEST_OFFSET / 2); |
| } |
| |
| #[fuchsia::test(allow_stalls = false)] |
| async fn valid_fake() { |
| let fake = FakeRtc::valid(TEST_ZX_TIME); |
| assert_eq!(fake.get().await.unwrap(), TEST_ZX_TIME); |
| assert_eq!(fake.last_set(), None); |
| |
| // Set a new time, this should be recorded but get should still return the original time. |
| assert!(fake.set(DIFFERENT_ZX_TIME).await.is_ok()); |
| assert_eq!(fake.last_set(), Some(DIFFERENT_ZX_TIME)); |
| assert_eq!(fake.get().await.unwrap(), TEST_ZX_TIME); |
| } |
| |
| #[fuchsia::test(allow_stalls = false)] |
| async fn invalid_fake() { |
| let message = "I'm designed to fail".to_string(); |
| let fake = FakeRtc::invalid(message.clone()); |
| assert_eq!(&fake.get().await.unwrap_err().to_string(), &message); |
| assert_eq!(fake.last_set(), None); |
| |
| // Setting a new time should still succeed and be recorded but it won't make get valid. |
| assert!(fake.set(DIFFERENT_ZX_TIME).await.is_ok()); |
| assert_eq!(fake.last_set(), Some(DIFFERENT_ZX_TIME)); |
| assert_eq!(&fake.get().await.unwrap_err().to_string(), &message); |
| } |
| } |