blob: 1c99cb68de1bae2d24b6513878815c10f3aabb3c [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::{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);
}
}