| // 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 as ErrorKind; |
| use anyhow::{Context as _, Error}; |
| use fidl_fuchsia_sys::{LauncherMarker, LauncherProxy}; |
| use fuchsia_async::futures::{future::BoxFuture, FutureExt}; |
| use fuchsia_component::client::{connect_to_service, launch}; |
| use fuchsia_merkle::Hash; |
| use fuchsia_syslog::{fx_log_info, fx_log_warn}; |
| 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<(), anyhow::Error> { |
| let launcher = |
| connect_to_service::<LauncherMarker>().map_err(|_| ErrorKind::ConnectToLauncher)?; |
| let mut component_runner = RealComponentRunner { launcher_proxy: launcher }; |
| apply_system_update_impl( |
| current_system_image, |
| latest_system_image, |
| &mut component_runner, |
| initiator, |
| &RealTimeSource, |
| &RealFileSystem, |
| ) |
| .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() |
| } |
| } |
| |
| trait FileSystem { |
| fn read_to_string(&self, path: &str) -> std::io::Result<String>; |
| } |
| |
| struct RealFileSystem; |
| |
| impl FileSystem for RealFileSystem { |
| fn read_to_string(&self, path: &str) -> std::io::Result<String> { |
| std::fs::read_to_string(path) |
| } |
| } |
| |
| // TODO(fxb/22779): Undo this once we do this for all base packages by default. |
| fn get_system_updater_resource_url( |
| file_system: &impl FileSystem, |
| ) -> Result<String, crate::errors::Error> { |
| // Attempt to find pinned version. |
| let file = file_system |
| .read_to_string("/system/data/static_packages") |
| .map_err(|_| ErrorKind::ReadStaticPackages)?; |
| |
| for line in file.lines() { |
| let line = line.trim(); |
| // Line format to parse: `<NAME>/<VERSION>=<MERKLE>`. |
| let parts: Vec<_> = line.split("=").collect(); |
| if parts.len() != 2 { |
| fx_log_warn!("invalid line in static manifest: {}", line); |
| continue; |
| } |
| let name_version = parts[0]; |
| let merkle = parts[1]; |
| |
| if merkle.len() != 64 { |
| fx_log_warn!("invalid merkleroot in static manifest: {}", line); |
| continue; |
| } |
| |
| let parts: Vec<_> = name_version.split("/").collect(); |
| if parts.len() != 2 { |
| fx_log_warn!("invalid name/version pair in static manifest: {}", line); |
| continue; |
| } |
| let name = parts[0]; |
| |
| if name != "amber" { |
| continue; |
| } |
| |
| return Ok(format!( |
| "fuchsia-pkg://fuchsia.com/amber?hash={}#meta/system_updater.cmx", |
| merkle |
| )); |
| } |
| fx_log_warn!("Unable to find 'amber' in static manifest"); |
| |
| // Backup is to just use unpinned version. |
| Ok(SYSTEM_UPDATER_RESOURCE_URL.to_string()) |
| } |
| |
| async fn apply_system_update_impl<'a>( |
| current_system_image: Hash, |
| latest_system_image: Hash, |
| component_runner: &'a mut impl ComponentRunner, |
| initiator: Initiator, |
| time_source: &'a impl TimeSource, |
| file_system: &'a impl FileSystem, |
| ) -> Result<(), anyhow::Error> { |
| fx_log_info!("starting system_updater"); |
| let fut = component_runner.run_until_exit( |
| get_system_updater_resource_url(file_system)?, |
| Some( |
| vec![ |
| "--initiator", |
| &format!("{}", initiator), |
| "--start", |
| &format!("{}", time_source.get_nanos()), |
| "--source", |
| &format!("{}", current_system_image), |
| "--target", |
| &format!("{}", latest_system_image), |
| ] |
| .iter() |
| .map(|s| s.to_string()) |
| .collect(), |
| ), |
| ); |
| fut.await?; |
| Err(ErrorKind::SystemUpdaterFinished)? |
| } |
| |
| #[cfg(test)] |
| mod test_apply_system_update_impl { |
| use super::*; |
| use fuchsia_async::{self as fasync, futures::future}; |
| use proptest::prelude::*; |
| |
| const ACTIVE_SYSTEM_IMAGE_MERKLE: [u8; 32] = [0u8; 32]; |
| const NEW_SYSTEM_IMAGE_MERKLE: [u8; 32] = [1u8; 32]; |
| |
| 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 |
| } |
| } |
| |
| const HAS_AMBER: &str = |
| "amber/0=6b8f5baf0eff6379701cedd3a86ab0fde5dfd8d73c6cf488926b2c94cdf63af0 \n\ |
| pkgfs/0=1d3c71e2124dc84263a56559ab72bccc840679fe95c91efe0b1a49b2bc0d9f62 "; |
| |
| const NO_AMBER: &str = |
| "pkgfs/0=1d3c71e2124dc84263a56559ab72bccc840679fe95c91efe0b1a49b2bc0d9f62 "; |
| |
| struct FakeFileSystem { |
| has_amber: bool, |
| } |
| |
| impl FileSystem for FakeFileSystem { |
| fn read_to_string(&self, path: &str) -> std::io::Result<String> { |
| if path != "/system/data/static_packages" { |
| return Err(std::io::Error::new(std::io::ErrorKind::NotFound, "invalid path")); |
| } |
| if self.has_amber { |
| Ok(HAS_AMBER.to_string()) |
| } else { |
| Ok(NO_AMBER.to_string()) |
| } |
| } |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_trigger_pkgfs_gc_if_update_available() { |
| let mut component_runner = DoNothingComponentRunner; |
| let time_source = FakeTimeSource { now: 0 }; |
| let filesystem = FakeFileSystem { has_amber: true }; |
| |
| let result = apply_system_update_impl( |
| ACTIVE_SYSTEM_IMAGE_MERKLE.into(), |
| NEW_SYSTEM_IMAGE_MERKLE.into(), |
| &mut component_runner, |
| Initiator::Manual, |
| &time_source, |
| &filesystem, |
| ) |
| .await; |
| |
| assert_eq!( |
| result.unwrap_err().downcast::<ErrorKind>().unwrap(), |
| ErrorKind::SystemUpdaterFinished |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_launch_system_updater_if_update_available() { |
| let mut component_runner = WasCalledComponentRunner { was_called: false }; |
| let time_source = FakeTimeSource { now: 0 }; |
| let filesystem = FakeFileSystem { has_amber: true }; |
| |
| let result = apply_system_update_impl( |
| ACTIVE_SYSTEM_IMAGE_MERKLE.into(), |
| NEW_SYSTEM_IMAGE_MERKLE.into(), |
| &mut component_runner, |
| Initiator::Manual, |
| &time_source, |
| &filesystem, |
| ) |
| .await; |
| |
| assert_eq!( |
| result.unwrap_err().downcast::<ErrorKind>().unwrap(), |
| ErrorKind::SystemUpdaterFinished |
| ); |
| assert!(component_runner.was_called); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_launch_system_updater_even_if_gc_fails() { |
| let mut component_runner = WasCalledComponentRunner { was_called: false }; |
| let time_source = FakeTimeSource { now: 0 }; |
| let filesystem = FakeFileSystem { has_amber: true }; |
| |
| let result = apply_system_update_impl( |
| ACTIVE_SYSTEM_IMAGE_MERKLE.into(), |
| NEW_SYSTEM_IMAGE_MERKLE.into(), |
| &mut component_runner, |
| Initiator::Manual, |
| &time_source, |
| &filesystem, |
| ) |
| .await; |
| |
| assert_eq!( |
| result.unwrap_err().downcast::<ErrorKind>().unwrap(), |
| ErrorKind::SystemUpdaterFinished |
| ); |
| assert!(component_runner.was_called); |
| } |
| |
| #[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() |
| } |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_launch_system_updater_url_obtained_from_static_packages() { |
| let mut component_runner = ArgumentCapturingComponentRunner { captured_args: vec![] }; |
| let time_source = FakeTimeSource { now: 0 }; |
| let filesystem = FakeFileSystem { has_amber: true }; |
| |
| let result = apply_system_update_impl( |
| ACTIVE_SYSTEM_IMAGE_MERKLE.into(), |
| NEW_SYSTEM_IMAGE_MERKLE.into(), |
| &mut component_runner, |
| Initiator::Manual, |
| &time_source, |
| &filesystem, |
| ) |
| .await; |
| |
| let expected_url = "fuchsia-pkg://fuchsia.com/amber?hash=\ |
| 6b8f5baf0eff6379701cedd3a86ab0fde5dfd8d73c6cf488926b2c94cdf63af0\ |
| #meta/system_updater.cmx" |
| .to_string(); |
| |
| assert_eq!( |
| result.unwrap_err().downcast::<ErrorKind>().unwrap(), |
| ErrorKind::SystemUpdaterFinished |
| ); |
| assert_eq!( |
| component_runner.captured_args, |
| vec![Args { |
| url: expected_url, |
| arguments: Some( |
| vec![ |
| "--initiator", |
| &format!("{}", Initiator::Manual), |
| "--start", |
| &format!("{}", 0), |
| "--source", |
| &format!("{}", std::iter::repeat('0').take(64).collect::<String>()), |
| "--target", |
| &format!("{}", std::iter::repeat("01").take(32).collect::<String>()), |
| ] |
| .iter() |
| .map(|s| s.to_string()) |
| .collect() |
| ) |
| }] |
| ); |
| } |
| |
| 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); |
| |
| let mut component_runner = ArgumentCapturingComponentRunner { captured_args: vec![] }; |
| let time_source = FakeTimeSource { now: start_time }; |
| let filesystem = FakeFileSystem { has_amber: false }; |
| |
| 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"), |
| &mut component_runner, |
| initiator, |
| &time_source, |
| &filesystem, |
| )); |
| |
| prop_assert!(result.is_err()); |
| prop_assert_eq!(result.unwrap_err().downcast::<ErrorKind>().unwrap(), ErrorKind::SystemUpdaterFinished); |
| prop_assert_eq!( |
| component_runner.captured_args, |
| vec![Args { |
| url: SYSTEM_UPDATER_RESOURCE_URL.to_string(), |
| arguments: Some(vec![ |
| "--initiator", |
| &format!("{}",initiator), |
| "--start", |
| &format!("{}",start_time), |
| "--source", |
| &source_merkle.to_lowercase(), |
| "--target", |
| &target_merkle.to_lowercase() |
| ] |
| .iter() |
| .map(|s| s.to_string()) |
| .collect()) |
| }] |
| ); |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod test_real_service_connector { |
| use super::*; |
| use fuchsia_async as fasync; |
| 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(e) if e.is_closed() |
| ); |
| } |
| } |
| |
| #[cfg(test)] |
| mod test_real_component_runner { |
| use super::*; |
| use fuchsia_async as fasync; |
| |
| 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").downcast::<ErrorKind>().unwrap(), |
| ErrorKind::SystemUpdaterFailed |
| ); |
| } |
| } |