blob: 2d0d51ed4e5165e00e96de643ca951a76febc5a0 [file] [log] [blame]
// Copyright 2019 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::errors::{Error, ErrorKind};
use failure::ResultExt;
use fidl_fuchsia_io;
use fidl_fuchsia_sys::{LauncherMarker, LauncherProxy};
use fuchsia_async::{
self as fasync,
futures::{future::BoxFuture, FutureExt},
};
use fuchsia_component::client::{connect_to_service, launch};
use fuchsia_merkle::Hash;
use fuchsia_syslog::{fx_log_err, fx_log_info};
use fuchsia_zircon as zx;
#[cfg(test)]
use proptest_derive::Arbitrary;
const SYSTEM_UPDATER_RESOURCE_URL: &str = "fuchsia-pkg://fuchsia.com/amber#meta/system_updater.cmx";
#[derive(Debug, Clone, Copy)]
#[cfg_attr(test, derive(Arbitrary))]
pub enum Initiator {
Manual,
Automatic,
}
impl std::fmt::Display for Initiator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Initiator::Manual => write!(f, "manual"),
Initiator::Automatic => write!(f, "automatic"),
}
}
}
// On success, system will reboot before this function returns
pub async fn apply_system_update(
current_system_image: Hash,
latest_system_image: Hash,
initiator: Initiator,
) -> Result<(), Error> {
let launcher = connect_to_service::<LauncherMarker>().context(ErrorKind::ConnectToLauncher)?;
let mut component_runner = RealComponentRunner { launcher_proxy: launcher };
apply_system_update_impl(
current_system_image,
latest_system_image,
&RealServiceConnector,
&mut component_runner,
initiator,
&RealTimeSource,
)
.await
}
// For mocking
trait ServiceConnector {
fn service_connect(&self, service_path: &str, channel: zx::Channel) -> Result<(), zx::Status>;
}
struct RealServiceConnector;
impl ServiceConnector for RealServiceConnector {
fn service_connect(&self, service_path: &str, channel: zx::Channel) -> Result<(), zx::Status> {
fdio::service_connect(service_path, channel)
}
}
// For mocking
trait ComponentRunner {
fn run_until_exit(
&mut self,
url: String,
arguments: Option<Vec<String>>,
) -> BoxFuture<'_, Result<(), Error>>;
}
struct RealComponentRunner {
launcher_proxy: LauncherProxy,
}
impl ComponentRunner for RealComponentRunner {
fn run_until_exit(
&mut self,
url: String,
arguments: Option<Vec<String>>,
) -> BoxFuture<'_, Result<(), Error>> {
let app_res = launch(&self.launcher_proxy, url, arguments);
async move {
let mut app = app_res.context(ErrorKind::LaunchSystemUpdater)?;
let exit_status = app.wait().await.context(ErrorKind::WaitForSystemUpdater)?;
exit_status.ok().context(ErrorKind::SystemUpdaterFailed)?;
Ok(())
}
.boxed()
}
}
// For mocking
trait TimeSource {
fn get_nanos(&self) -> i64;
}
struct RealTimeSource;
impl TimeSource for RealTimeSource {
fn get_nanos(&self) -> i64 {
zx::Time::get(zx::ClockId::UTC).into_nanos()
}
}
async fn apply_system_update_impl<'a>(
current_system_image: Hash,
latest_system_image: Hash,
service_connector: &'a impl ServiceConnector,
component_runner: &'a mut impl ComponentRunner,
initiator: Initiator,
time_source: &'a impl TimeSource,
) -> Result<(), Error> {
if let Err(err) = pkgfs_gc(service_connector).await {
fx_log_err!("failed to garbage collect pkgfs, will still attempt system update: {}", err);
}
fx_log_info!("starting system_updater");
component_runner
.run_until_exit(
SYSTEM_UPDATER_RESOURCE_URL.to_string(),
Some(vec![
format!("-initiator={}", initiator),
format!("-start={}", time_source.get_nanos()),
format!("-source={}", current_system_image),
format!("-target={}", latest_system_image),
]),
)
.await?;
Err(ErrorKind::SystemUpdaterFinished)?
}
async fn pkgfs_gc(service_connector: &impl ServiceConnector) -> Result<(), Error> {
fx_log_info!("triggering pkgfs GC");
let (dir_end, dir_server_end) =
fidl::endpoints::create_endpoints::<fidl_fuchsia_io::DirectoryMarker>()
.context(ErrorKind::PkgfsGc)?;
service_connector
.service_connect("/pkgfs/ctl", dir_server_end.into_channel())
.context(ErrorKind::PkgfsGc)?;
let dir_proxy = fidl_fuchsia_io::DirectoryProxy::new(
fasync::Channel::from_channel(dir_end.into_channel()).context(ErrorKind::PkgfsGc)?,
);
let status = dir_proxy.unlink("garbage").await.context(ErrorKind::PkgfsGc)?;
zx::Status::ok(status).context(ErrorKind::PkgfsGc)?;
Ok(())
}
#[cfg(test)]
mod test_apply_system_update_impl {
use super::*;
use fuchsia_async::futures::future;
use matches::assert_matches;
use proptest::prelude::*;
use std::fs;
const ACTIVE_SYSTEM_IMAGE_MERKLE: [u8; 32] = [0u8; 32];
const NEW_SYSTEM_IMAGE_MERKLE: [u8; 32] = [1u8; 32];
struct TempDirServiceConnector {
temp_dir: tempfile::TempDir,
}
impl TempDirServiceConnector {
fn new() -> TempDirServiceConnector {
TempDirServiceConnector { temp_dir: tempfile::tempdir().expect("create temp dir") }
}
fn new_with_pkgfs_garbage() -> TempDirServiceConnector {
let service_connector = Self::new();
let pkgfs = service_connector.temp_dir.path().join("pkgfs");
fs::create_dir(&pkgfs).expect("create pkgfs dir");
fs::create_dir(pkgfs.join("ctl")).expect("create pkgfs/ctl dir");
fs::File::create(pkgfs.join("ctl/garbage")).expect("create garbage file");
service_connector
}
}
impl TempDirServiceConnector {
fn has_garbage_file(&self) -> bool {
self.temp_dir.path().join("pkgfs/ctl/garbage").exists()
}
}
impl ServiceConnector for TempDirServiceConnector {
fn service_connect(
&self,
service_path: &str,
channel: zx::Channel,
) -> Result<(), zx::Status> {
fdio::service_connect(
self.temp_dir.path().join(&service_path[1..]).to_str().expect("paths are utf8"),
channel,
)
}
}
struct DoNothingComponentRunner;
impl ComponentRunner for DoNothingComponentRunner {
fn run_until_exit(
&mut self,
_url: String,
_arguments: Option<Vec<String>>,
) -> BoxFuture<'_, Result<(), Error>> {
future::ok(()).boxed()
}
}
struct WasCalledComponentRunner {
was_called: bool,
}
impl ComponentRunner for WasCalledComponentRunner {
fn run_until_exit(
&mut self,
_url: String,
_arguments: Option<Vec<String>>,
) -> BoxFuture<'_, Result<(), Error>> {
self.was_called = true;
future::ok(()).boxed()
}
}
struct FakeTimeSource {
now: i64,
}
impl TimeSource for FakeTimeSource {
fn get_nanos(&self) -> i64 {
self.now
}
}
#[fasync::run_singlethreaded(test)]
async fn test_trigger_pkgfs_gc_if_update_available() {
let service_connector = TempDirServiceConnector::new_with_pkgfs_garbage();
let mut component_runner = DoNothingComponentRunner;
let time_source = FakeTimeSource { now: 0 };
assert!(service_connector.has_garbage_file());
let result = apply_system_update_impl(
ACTIVE_SYSTEM_IMAGE_MERKLE.into(),
NEW_SYSTEM_IMAGE_MERKLE.into(),
&service_connector,
&mut component_runner,
Initiator::Manual,
&time_source,
)
.await;
assert_matches!(result.map_err(|e| e.kind()), Err(ErrorKind::SystemUpdaterFinished));
assert!(!service_connector.has_garbage_file());
}
#[fasync::run_singlethreaded(test)]
async fn test_launch_system_updater_if_update_available() {
let service_connector = TempDirServiceConnector::new();
let mut component_runner = WasCalledComponentRunner { was_called: false };
let time_source = FakeTimeSource { now: 0 };
let result = apply_system_update_impl(
ACTIVE_SYSTEM_IMAGE_MERKLE.into(),
NEW_SYSTEM_IMAGE_MERKLE.into(),
&service_connector,
&mut component_runner,
Initiator::Manual,
&time_source,
)
.await;
assert_matches!(result.map_err(|e| e.kind()), Err(ErrorKind::SystemUpdaterFinished));
assert!(component_runner.was_called);
}
#[fasync::run_singlethreaded(test)]
async fn test_launch_system_updater_even_if_gc_fails() {
let service_connector = TempDirServiceConnector::new();
let mut component_runner = WasCalledComponentRunner { was_called: false };
let time_source = FakeTimeSource { now: 0 };
let result = apply_system_update_impl(
ACTIVE_SYSTEM_IMAGE_MERKLE.into(),
NEW_SYSTEM_IMAGE_MERKLE.into(),
&service_connector,
&mut component_runner,
Initiator::Manual,
&time_source,
)
.await;
assert_matches!(result.map_err(|e| e.kind()), Err(ErrorKind::SystemUpdaterFinished));
assert!(component_runner.was_called);
}
proptest! {
#[test]
fn test_values_passed_through_to_component_launcher(
initiator: Initiator,
start_time in proptest::num::i64::ANY,
source_merkle in "[A-Fa-f0-9]{64}",
target_merkle in "[A-Fa-f0-9]{64}")
{
prop_assume!(source_merkle != target_merkle);
#[derive(Debug, PartialEq, Eq)]
struct Args {
url: String,
arguments: Option<Vec<String>>,
}
struct ArgumentCapturingComponentRunner {
captured_args: Vec<Args>,
}
impl ComponentRunner for ArgumentCapturingComponentRunner {
fn run_until_exit(
&mut self,
url: String,
arguments: Option<Vec<String>>,
) -> BoxFuture<'_, Result<(), Error>> {
self.captured_args.push(Args { url, arguments });
future::ok(()).boxed()
}
}
let service_connector = TempDirServiceConnector::new();
let mut component_runner = ArgumentCapturingComponentRunner { captured_args: vec![] };
let time_source = FakeTimeSource { now: start_time };
let mut executor =
fasync::Executor::new().expect("create executor in test");
let result = executor.run_singlethreaded(apply_system_update_impl(
source_merkle.parse().expect("source merkle string literal"),
target_merkle.parse().expect("target merkle string literal"),
&service_connector,
&mut component_runner,
initiator,
&time_source,
));
prop_assert!(result.is_err());
prop_assert_eq!(
result.err().unwrap().kind(),
ErrorKind::SystemUpdaterFinished
);
prop_assert_eq!(
component_runner.captured_args,
vec![Args {
url: SYSTEM_UPDATER_RESOURCE_URL.to_string(),
arguments: Some(vec![
format!("-initiator={}", initiator),
format!("-start={}", start_time),
format!("-source={}", source_merkle.to_lowercase()),
format!("-target={}", target_merkle.to_lowercase()),
])
}]
);
}
}
}
#[cfg(test)]
mod test_real_service_connector {
use super::*;
use matches::assert_matches;
use std::fs;
#[fasync::run_singlethreaded(test)]
async fn test_connect_to_directory_and_unlink_file() {
let dir = tempfile::tempdir().expect("create temp dir");
let file_name = "the-file";
let file_path = dir.path().join(file_name);
fs::File::create(&file_path).expect("create file");
let (dir_end, dir_server_end) =
fidl::endpoints::create_endpoints::<fidl_fuchsia_io::DirectoryMarker>()
.expect("create endpoints");
RealServiceConnector
.service_connect(
dir.path().to_str().expect("paths are utf8"),
dir_server_end.into_channel(),
)
.expect("service_connect");
let dir_proxy = fidl_fuchsia_io::DirectoryProxy::new(
fasync::Channel::from_channel(dir_end.into_channel()).expect("create async channel"),
);
assert!(file_path.exists());
let status = dir_proxy.unlink(file_name).await.expect("unlink the file fidl");
zx::Status::ok(status).expect("unlink the file");
assert!(!file_path.exists());
}
#[fasync::run_singlethreaded(test)]
async fn test_connect_to_missing_directory_errors() {
let dir = tempfile::tempdir().expect("create temp dir");
let (dir_end, dir_server_end) =
fidl::endpoints::create_endpoints::<fidl_fuchsia_io::DirectoryMarker>()
.expect("create endpoints");
RealServiceConnector
.service_connect(
dir.path().join("non-existent-directory").to_str().expect("paths are utf8"),
dir_server_end.into_channel(),
)
.expect("service_connect");
let dir_proxy = fidl_fuchsia_io::DirectoryProxy::new(
fasync::Channel::from_channel(dir_end.into_channel()).expect("create async channel"),
);
let read_dirents_res = dir_proxy
.read_dirents(1000 /*size shouldn't matter, as this should immediately fail*/)
.await;
assert_matches!(
read_dirents_res,
Err(fidl::Error::ClientRead(zx::Status::PEER_CLOSED))
| Err(fidl::Error::ClientWrite(zx::Status::PEER_CLOSED))
);
}
}
#[cfg(test)]
mod test_real_component_runner {
use super::*;
const TEST_SHELL_COMMAND_RESOURCE_URL: &str =
"fuchsia-pkg://fuchsia.com/system-update-checker-tests/0#meta/test-shell-command.cmx";
#[fasync::run_singlethreaded(test)]
async fn test_run_a_component_that_exits_0() {
let launcher_proxy = connect_to_service::<LauncherMarker>().expect("connect to launcher");
let mut runner = RealComponentRunner { launcher_proxy };
let run_res = runner
.run_until_exit(
TEST_SHELL_COMMAND_RESOURCE_URL.to_string(),
Some(vec!["!".to_string()]),
)
.await;
assert!(run_res.is_ok(), "{:?}", run_res.err().unwrap());
}
#[fasync::run_singlethreaded(test)]
async fn test_run_a_component_that_exits_1() {
let launcher_proxy = connect_to_service::<LauncherMarker>().expect("connect to launcher");
let mut runner = RealComponentRunner { launcher_proxy };
let run_res =
runner.run_until_exit(TEST_SHELL_COMMAND_RESOURCE_URL.to_string(), Some(vec![])).await;
assert_eq!(run_res.err().expect("run should fail").kind(), ErrorKind::SystemUpdaterFailed);
}
}