blob: 28ff321eadce5ee6d06efa1cf7b136389c46f301 [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::{Context, Error},
fidl_fuchsia_update::{CommitStatusProviderMarker, CommitStatusProviderProxy},
fuchsia_async as fasync,
fuchsia_component::client::connect_to_service,
fuchsia_zircon as zx,
futures::{future::FusedFuture, prelude::*},
std::time::Duration,
};
const WARNING_DURATION: Duration = Duration::from_secs(30);
/// Connects to the FIDL service, waits for the commit, and prints updates to stdout.
pub async fn handle_wait_for_commit() -> Result<(), Error> {
let proxy = connect_to_service::<CommitStatusProviderMarker>()
.context("while connecting to fuchsia.update/CommitStatusProvider")?;
handle_wait_for_commit_impl(&proxy, Printer).await
}
/// The set of events associated with the `wait-for-commit` path.
#[derive(Debug, PartialEq)]
enum CommitEvent {
Begin,
Warning,
End,
}
/// An observer of `update wait-for-commit`.
trait CommitObserver {
fn on_event(&self, event: CommitEvent);
}
/// A `CommitObserver` that forwards the events to stdout.
struct Printer;
impl CommitObserver for Printer {
fn on_event(&self, event: CommitEvent) {
let text = match event {
CommitEvent::Begin => "Waiting for commit.",
CommitEvent::Warning => {
"It's been 30 seconds. Something is probably wrong. Consider \
running `update revert` to fall back to the previous slot."
}
CommitEvent::End => "Committed!",
};
println!("{}", text);
}
}
/// Waits for the system to commit (e.g. when the EventPair observes a signal).
async fn wait_for_commit(proxy: &CommitStatusProviderProxy) -> Result<(), Error> {
let p = proxy.is_current_system_committed().await.context("while obtaining EventPair")?;
fasync::OnSignals::new(&p, zx::Signals::USER_0)
.await
.context("while waiting for the commit")?;
Ok(())
}
/// Waits for the commit and sends updates to the observer. This is abstracted from the regular
/// `handle_wait_for_commit` fn so we can test events without having to wait the `WARNING_DURATION`.
/// The [testability rubric](https://fuchsia.dev/fuchsia-src/concepts/testing/testability_rubric)
/// exempts logs from testing, but in this case we test them anyway because of the additional layer
/// of complexity that the warning timeout introduces.
async fn handle_wait_for_commit_impl(
proxy: &CommitStatusProviderProxy,
observer: impl CommitObserver,
) -> Result<(), Error> {
let () = observer.on_event(CommitEvent::Begin);
let commit_fut = wait_for_commit(&proxy).fuse();
futures::pin_mut!(commit_fut);
let mut timer_fut = fasync::Timer::new(WARNING_DURATION).fuse();
// Send a warning after the WARNING_DURATION.
let () = futures::select! {
commit_res = commit_fut => commit_res?,
_ = timer_fut => observer.on_event(CommitEvent::Warning),
};
// If we timed out on WARNING_DURATION, try again.
if !commit_fut.is_terminated() {
let () = commit_fut.await.context("while calling wait_for_commit second")?;
}
let () = observer.on_event(CommitEvent::End);
Ok(())
}
#[cfg(test)]
mod tests {
use {
super::*,
fidl_fuchsia_update::CommitStatusProviderRequest,
fuchsia_zircon::{DurationNum, EventPair, HandleBased, Peered},
futures::{pin_mut, task::Poll},
parking_lot::Mutex,
};
struct TestObserver {
events: Mutex<Vec<CommitEvent>>,
}
impl TestObserver {
fn new() -> Self {
Self { events: Mutex::new(vec![]) }
}
fn assert_events(&self, expected_events: &[CommitEvent]) {
assert_eq!(self.events.lock().as_slice(), expected_events);
}
}
impl CommitObserver for &TestObserver {
fn on_event(&self, event: CommitEvent) {
self.events.lock().push(event);
}
}
#[test]
fn test_wait_for_commit() {
let mut executor = fasync::Executor::new_with_fake_time().unwrap();
let (proxy, mut stream) =
fidl::endpoints::create_proxy_and_stream::<CommitStatusProviderMarker>().unwrap();
let (p, p_stream) = EventPair::create().unwrap();
fasync::Task::spawn(async move {
while let Some(req) = stream.try_next().await.unwrap() {
let CommitStatusProviderRequest::IsCurrentSystemCommitted { responder } = req;
let pair = p_stream.duplicate_handle(zx::Rights::BASIC).unwrap();
let () = responder.send(pair).unwrap();
}
})
.detach();
let observer = TestObserver::new();
let fut = handle_wait_for_commit_impl(&proxy, &observer);
pin_mut!(fut);
// Begin the `wait_for_commit`.
match executor.run_until_stalled(&mut fut) {
Poll::Ready(res) => panic!("future unexpectedly completed with: {:?}", res),
Poll::Pending => (),
};
observer.assert_events(&[CommitEvent::Begin]);
// We should observe no new events when both the system is not committed and we are within
// the warning duration.
executor
.set_fake_time(fasync::Time::after((WARNING_DURATION - Duration::from_secs(1)).into()));
assert!(!executor.wake_expired_timers());
match executor.run_until_stalled(&mut fut) {
Poll::Ready(res) => panic!("future unexpectedly completed with: {:?}", res),
Poll::Pending => (),
};
observer.assert_events(&[CommitEvent::Begin]);
// Once we hit the warning duration, we should get a warning event.
executor.set_fake_time(fasync::Time::after(1.seconds()));
assert!(executor.wake_expired_timers());
match executor.run_until_stalled(&mut fut) {
Poll::Ready(res) => panic!("future unexpectedly completed with: {:?}", res),
Poll::Pending => (),
};
observer.assert_events(&[CommitEvent::Begin, CommitEvent::Warning]);
// Once we get the commit signal, the future should complete.
let () = p.signal_peer(zx::Signals::NONE, zx::Signals::USER_0).unwrap();
match executor.run_until_stalled(&mut fut) {
Poll::Ready(res) => res.unwrap(),
Poll::Pending => panic!("future unexpectedly pending"),
};
observer.assert_events(&[CommitEvent::Begin, CommitEvent::Warning, CommitEvent::End]);
}
}