Skip to main content

isolated_swd/
updater.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4use anyhow::{Context, Error, anyhow};
5use fidl_fuchsia_paver::{BootManagerMarker, Configuration, PaverMarker, PaverProxy};
6use fidl_fuchsia_update_installer::{InstallerMarker, InstallerProxy, RebootControllerMarker};
7use fidl_fuchsia_update_installer_ext::options::{Initiator, Options};
8use fidl_fuchsia_update_installer_ext::{UpdateAttempt, start_update};
9
10use futures::prelude::*;
11
12pub const DEFAULT_UPDATE_PACKAGE_URL: &str = "fuchsia-pkg://fuchsia.com/update";
13
14pub struct Updater {
15    proxy: InstallerProxy,
16    paver_proxy: PaverProxy,
17}
18
19impl Updater {
20    pub fn new_with_proxies(proxy: InstallerProxy, paver_proxy: PaverProxy) -> Self {
21        Self { proxy, paver_proxy }
22    }
23
24    pub fn new() -> Result<Self, Error> {
25        Ok(Self::new_with_proxies(
26            fuchsia_component::client::connect_to_protocol::<InstallerMarker>()?,
27            fuchsia_component::client::connect_to_protocol::<PaverMarker>()?,
28        ))
29    }
30
31    pub async fn start_update(
32        &mut self,
33        update_package: Option<&http::Uri>,
34    ) -> Result<UpdateAttempt, Error> {
35        let update_package = match update_package {
36            Some(url) => url.to_owned(),
37            None => DEFAULT_UPDATE_PACKAGE_URL.parse().unwrap(),
38        };
39
40        let (reboot_controller, reboot_controller_server_end) =
41            fidl::endpoints::create_proxy::<RebootControllerMarker>();
42        let () = reboot_controller.detach().context("disabling automatic reboot")?;
43
44        start_update(
45            &update_package,
46            Options {
47                initiator: Initiator::User,
48                allow_attach_to_existing_attempt: false,
49                should_write_recovery: false,
50            },
51            &self.proxy,
52            Some(reboot_controller_server_end),
53        )
54        .await
55        .context("starting system update")
56    }
57
58    /// Perform an update, skipping the final reboot.
59    /// If `update_package` is Some, use the given package URL as the URL for the update package.
60    /// Otherwise, `system-updater` uses the default URL.
61    /// This will not install any images to the recovery partitions.
62    pub async fn install_update(
63        &mut self,
64        update_package: Option<&http::Uri>,
65    ) -> Result<(), Error> {
66        let attempt = self.start_update(update_package).await?;
67
68        let () = Self::monitor_update_attempt(attempt).await.context("monitoring installation")?;
69
70        let () = Self::activate_installed_slot(&self.paver_proxy)
71            .await
72            .context("activating installed slot")?;
73
74        Ok(())
75    }
76
77    async fn monitor_update_attempt(mut attempt: UpdateAttempt) -> Result<(), Error> {
78        while let Some(state) = attempt.try_next().await.context("fetching next update state")? {
79            log::info!("Install: {:?}", state);
80            if state.is_success() {
81                return Ok(());
82            } else if state.is_failure() {
83                return Err(anyhow!("update attempt failed in state {:?}", state));
84            }
85        }
86
87        Err(anyhow!("unexpected end of update attempt"))
88    }
89
90    async fn activate_installed_slot(paver: &PaverProxy) -> Result<(), Error> {
91        let (boot_manager, remote) = fidl::endpoints::create_proxy::<BootManagerMarker>();
92
93        paver.find_boot_manager(remote).context("finding boot manager")?;
94
95        let result = boot_manager.query_active_configuration().await;
96        if let Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. }) =
97            result
98        {
99            // board does not actually support ABR, so return.
100            log::info!("ABR not supported, not configuring slots.");
101            return Ok(());
102        }
103        let result = result?;
104        if result.is_ok() {
105            // active slot is valid - assume that system-updater handled this for us.
106            return Ok(());
107        }
108
109        // In recovery, the paver will return ZX_ERR_NOT_SUPPORTED to query_active_configuration(),
110        // even on devices which support ABR. Handle this manually in case it is actually
111        // supported.
112        zx::Status::ok(
113            boot_manager
114                .set_configuration_active(Configuration::A)
115                .await
116                .context("Sending set active configuration request")?,
117        )
118        .context("Setting A to active configuration")?;
119        Ok(())
120    }
121}
122
123#[cfg(test)]
124pub(crate) mod for_tests {
125    use super::*;
126    use crate::resolver::for_tests::{EMPTY_REPO_PATH, ResolverForTest};
127    use blobfs_ramdisk::BlobfsRamdisk;
128    use fidl_fuchsia_paver::PaverRequestStream;
129    use fuchsia_component_test::{
130        Capability, ChildOptions, DirectoryContents, RealmBuilder, RealmInstance, Ref, Route,
131    };
132    use fuchsia_merkle::Hash;
133    use fuchsia_pkg_testing::serve::ServedRepository;
134    use fuchsia_pkg_testing::{Package, RepositoryBuilder, SystemImageBuilder};
135    use mock_paver::{MockPaverService, MockPaverServiceBuilder, PaverEvent};
136    use std::collections::BTreeSet;
137    use std::sync::Arc;
138    use {fidl_fuchsia_metrics as fmetrics, fuchsia_async as fasync};
139
140    pub const TEST_REPO_URL: &str = "fuchsia-pkg://fuchsia.com";
141    pub struct UpdaterBuilder {
142        paver_builder: MockPaverServiceBuilder,
143        packages: Vec<Package>,
144        // The zbi and optional vbmeta contents.
145        fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
146        // The zbi and optional vbmeta contents of the recovery partition.
147        recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
148        repo_url: fuchsia_url::RepositoryUrl,
149    }
150
151    impl UpdaterBuilder {
152        /// Construct a new UpdateBuilder. Initially, this contains no images and an empty system
153        /// image package.
154        pub async fn new() -> UpdaterBuilder {
155            UpdaterBuilder {
156                paver_builder: MockPaverServiceBuilder::new(),
157                packages: vec![SystemImageBuilder::new().build().await],
158                fuchsia_image: None,
159                recovery_image: None,
160                repo_url: TEST_REPO_URL.parse().unwrap(),
161            }
162        }
163
164        /// Add a package to the update package this builder will generate.
165        pub fn add_package(mut self, package: Package) -> Self {
166            self.packages.push(package);
167            self
168        }
169
170        /// The zbi and optional vbmeta images to write.
171        pub fn fuchsia_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
172            assert_eq!(self.fuchsia_image, None);
173            self.fuchsia_image = Some((zbi, vbmeta));
174            self
175        }
176
177        /// The zbi and optional vbmeta images to write to the recovery partition.
178        pub fn recovery_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
179            assert_eq!(self.recovery_image, None);
180            self.recovery_image = Some((zbi, vbmeta));
181            self
182        }
183
184        /// Mutate the `MockPaverServiceBuilder` contained in this UpdaterBuilder.
185        pub fn paver<F>(mut self, f: F) -> Self
186        where
187            F: FnOnce(MockPaverServiceBuilder) -> MockPaverServiceBuilder,
188        {
189            self.paver_builder = f(self.paver_builder);
190            self
191        }
192
193        pub fn repo_url(mut self, url: &str) -> Self {
194            self.repo_url = url.parse().expect("Valid URL supplied to repo_url()");
195            self
196        }
197
198        fn serve_mock_paver(stream: PaverRequestStream, paver: Arc<MockPaverService>) {
199            let paver_clone = Arc::clone(&paver);
200            fasync::Task::spawn(
201                Arc::clone(&paver_clone)
202                    .run_paver_service(stream)
203                    .unwrap_or_else(|e| panic!("Failed to run mock paver: {e:?}")),
204            )
205            .detach();
206        }
207
208        async fn run_mock_paver(
209            handles: fuchsia_component_test::LocalComponentHandles,
210            paver: Arc<MockPaverService>,
211        ) -> Result<(), Error> {
212            let mut fs = fuchsia_component::server::ServiceFs::new();
213            fs.dir("svc")
214                .add_fidl_service(move |stream| Self::serve_mock_paver(stream, Arc::clone(&paver)));
215            fs.serve_connection(handles.outgoing_dir)?;
216            let () = fs.for_each_concurrent(None, |req| async move { req }).await;
217            Ok(())
218        }
219
220        /// Create an UpdateForTest from this UpdaterBuilder.
221        /// This will construct an update package containing all packages and images added to the
222        /// builder, create a repository containing the packages, and create a MockPaver.
223        pub async fn build(self) -> UpdaterForTest {
224            let mut update = fuchsia_pkg_testing::UpdatePackageBuilder::new(self.repo_url.clone())
225                .packages(
226                    self.packages
227                        .iter()
228                        .map(|p| {
229                            fuchsia_url::fuchsia_pkg::PinnedAbsolutePackageUrl::new(
230                                self.repo_url.clone(),
231                                p.name().clone(),
232                                None,
233                                *p.hash(),
234                            )
235                        })
236                        .collect::<Vec<_>>(),
237                );
238            if let Some((zbi, vbmeta)) = self.fuchsia_image {
239                update = update.fuchsia_image(zbi, vbmeta);
240            }
241            if let Some((zbi, vbmeta)) = self.recovery_image {
242                update = update.recovery_image(zbi, vbmeta);
243            }
244            let (update, images) = update.build().await;
245
246            // Do not include the images package, system-updater triggers GC after resolving it.
247            let expected_blobfs_contents = self
248                .packages
249                .iter()
250                .chain([update.as_package()])
251                .flat_map(|p| p.list_blobs())
252                .collect();
253
254            let repo = Arc::new(
255                self.packages
256                    .iter()
257                    .chain([update.as_package(), &images])
258                    .fold(
259                        RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH)
260                            .add_package(update.as_package()),
261                        |repo, package| repo.add_package(package),
262                    )
263                    .build()
264                    .await
265                    .expect("Building repo"),
266            );
267
268            let realm_builder = RealmBuilder::new().await.unwrap();
269            let blobfs = BlobfsRamdisk::start().await.context("starting blobfs").unwrap();
270
271            let served_repo = Arc::new(Arc::clone(&repo).server().start().unwrap());
272
273            let resolver_realm = ResolverForTest::realm_setup(
274                &realm_builder,
275                Arc::clone(&served_repo),
276                self.repo_url.clone(),
277                &blobfs,
278            )
279            .await
280            .unwrap();
281
282            let system_updater = realm_builder
283                .add_child("system-updater", "#meta/system-updater.cm", ChildOptions::new())
284                .await
285                .unwrap();
286
287            let service_reflector = realm_builder
288                .add_local_child(
289                    "system_updater_service_reflector",
290                    move |handles| {
291                        let mut fs = fuchsia_component::server::ServiceFs::new();
292                        // Not necessary for updates, but without this system-updater will wait 30
293                        // seconds trying to flush cobalt logs before logging an attempt error,
294                        // and the test is torn down before then, so the error is lost. Also
295                        // prevents spam of irrelevant error logs.
296                        fs.dir("svc").add_fidl_service(move |stream| {
297                            fasync::Task::spawn(
298                                Arc::new(mock_metrics::MockMetricEventLoggerFactory::new())
299                                    .run_logger_factory(stream),
300                            )
301                            .detach()
302                        });
303                        async move {
304                            fs.serve_connection(handles.outgoing_dir).unwrap();
305                            let () = fs.collect().await;
306                            Ok(())
307                        }
308                        .boxed()
309                    },
310                    ChildOptions::new(),
311                )
312                .await
313                .unwrap();
314
315            realm_builder
316                .add_route(
317                    Route::new()
318                        .capability(
319                            Capability::protocol::<fmetrics::MetricEventLoggerFactoryMarker>(),
320                        )
321                        .from(&service_reflector)
322                        .to(&system_updater),
323                )
324                .await
325                .unwrap();
326
327            // Set up paver and routes
328            let paver = Arc::new(self.paver_builder.build());
329            let paver_clone = Arc::clone(&paver);
330            let mock_paver = realm_builder
331                .add_local_child(
332                    "paver",
333                    move |handles| Box::pin(Self::run_mock_paver(handles, Arc::clone(&paver))),
334                    ChildOptions::new(),
335                )
336                .await
337                .unwrap();
338
339            realm_builder
340                .add_route(
341                    Route::new()
342                        .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
343                        .from(&mock_paver)
344                        .to(&system_updater),
345                )
346                .await
347                .unwrap();
348            realm_builder
349                .add_route(
350                    Route::new()
351                        .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
352                        .from(&mock_paver)
353                        .to(Ref::parent()),
354                )
355                .await
356                .unwrap();
357
358            // Set up build-info and routes
359            realm_builder
360                .read_only_directory(
361                    "build-info",
362                    vec![&system_updater],
363                    DirectoryContents::new().add_file("board", "test".as_bytes()),
364                )
365                .await
366                .unwrap();
367
368            // Set up pkg-resolver and pkg-cache routes
369            realm_builder
370                .add_route(
371                    Route::new()
372                        .capability(
373                            Capability::protocol_by_name("fuchsia.pkg.PackageResolver-ota")
374                                .as_("fuchsia.pkg.PackageResolver"),
375                        )
376                        .from(&resolver_realm.resolver)
377                        .to(&system_updater),
378                )
379                .await
380                .unwrap();
381
382            realm_builder
383                .add_route(
384                    Route::new()
385                        .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
386                        .capability(Capability::protocol_by_name("fuchsia.pkg.RetainedPackages"))
387                        .capability(Capability::protocol_by_name(
388                            "fuchsia.pkg.garbagecollector.Manager",
389                        ))
390                        .from(&resolver_realm.cache)
391                        .to(&system_updater),
392                )
393                .await
394                .unwrap();
395            realm_builder
396                .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
397                    name: "fuchsia.system-updater.ManifestPublicKeys".parse().unwrap(),
398                    value: Vec::<String>::new().into(),
399                }))
400                .await
401                .unwrap();
402            realm_builder
403                .add_route(
404                    Route::new()
405                        .capability(Capability::configuration(
406                            "fuchsia.system-updater.ManifestPublicKeys",
407                        ))
408                        .from(Ref::self_())
409                        .to(&system_updater),
410                )
411                .await
412                .unwrap();
413            realm_builder
414                .add_route(
415                    Route::new()
416                        .capability(Capability::configuration(
417                            "fuchsia.system-updater.VerifyExistingBlobs",
418                        ))
419                        .from(Ref::void())
420                        .to(&system_updater),
421                )
422                .await
423                .unwrap();
424
425            // Make sure the component under test can log.
426            realm_builder
427                .add_route(
428                    Route::new()
429                        .capability(Capability::protocol_by_name("fuchsia.logger.LogSink"))
430                        .from(Ref::parent())
431                        .to(&system_updater),
432                )
433                .await
434                .unwrap();
435
436            // Expose system_updater to the parent
437            realm_builder
438                .add_route(
439                    Route::new()
440                        .capability(Capability::protocol_by_name(
441                            "fuchsia.update.installer.Installer",
442                        ))
443                        .from(&system_updater)
444                        .to(Ref::parent()),
445                )
446                .await
447                .unwrap();
448
449            // Expose pkg-cache to these tests, for use by verify_packages
450            realm_builder
451                .add_route(
452                    Route::new()
453                        .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
454                        .from(&resolver_realm.cache)
455                        .to(Ref::parent()),
456                )
457                .await
458                .unwrap();
459
460            let realm_instance = realm_builder.build().await.unwrap();
461
462            let installer_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
463            let paver_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
464
465            let updater = Updater::new_with_proxies(installer_proxy, paver_proxy);
466
467            let resolver = ResolverForTest::new(&realm_instance, blobfs, Arc::clone(&served_repo))
468                .await
469                .unwrap();
470
471            UpdaterForTest {
472                served_repo,
473                paver: paver_clone,
474                expected_blobfs_contents,
475                update_merkle_root: *update.as_package().hash(),
476                repo_url: self.repo_url,
477                updater,
478                resolver,
479                realm_instance,
480            }
481        }
482
483        #[cfg(test)]
484        pub async fn build_and_run(self) -> UpdaterResult {
485            self.build().await.run().await
486        }
487    }
488
489    /// This wraps the `Updater` in order to reduce test boilerplate.
490    /// Should be constructed using `UpdaterBuilder`.
491    pub struct UpdaterForTest {
492        #[expect(dead_code)]
493        pub served_repo: Arc<ServedRepository>,
494        pub paver: Arc<MockPaverService>,
495        pub expected_blobfs_contents: BTreeSet<Hash>,
496        pub update_merkle_root: Hash,
497        #[expect(dead_code)]
498        pub repo_url: fuchsia_url::RepositoryUrl,
499        pub resolver: ResolverForTest,
500        pub updater: Updater,
501        pub realm_instance: RealmInstance,
502    }
503
504    impl UpdaterForTest {
505        /// Run the system update, returning an `UpdaterResult` containing information about the
506        /// result of the update.
507        pub async fn run(mut self) -> UpdaterResult {
508            let () = self.updater.install_update(None).await.expect("installing update");
509
510            UpdaterResult {
511                paver_events: self.paver.take_events(),
512                resolver: self.resolver,
513                expected_blobfs_contents: self.expected_blobfs_contents,
514                realm_instance: self.realm_instance,
515            }
516        }
517    }
518
519    /// Contains information about the state of the system after the updater was run.
520    pub struct UpdaterResult {
521        /// All paver events received by the MockPaver during the update.
522        pub paver_events: Vec<PaverEvent>,
523        /// The resolver used by the updater.
524        pub resolver: ResolverForTest,
525        /// All the blobs that should be in blobfs after the update.
526        pub expected_blobfs_contents: BTreeSet<Hash>,
527        // The RealmInstance used to run this update, for introspection into component states.
528        #[expect(dead_code)]
529        pub realm_instance: RealmInstance,
530    }
531
532    impl UpdaterResult {
533        /// Verify that all packages that should have been resolved by the update
534        /// were resolved.
535        pub async fn verify_packages(&self) {
536            // Verify directly against blobfs to avoid any trickery pkg-resolver or pkg-cache may
537            // engage in.
538            let actual_contents =
539                self.resolver.cache.blobfs.list_blobs().expect("Listing blobfs blobs");
540            assert_eq!(actual_contents, self.expected_blobfs_contents);
541        }
542    }
543}
544
545#[cfg(test)]
546pub mod tests {
547    use super::for_tests::UpdaterBuilder;
548    use super::*;
549    use fidl_fuchsia_paver::Asset;
550    use fuchsia_async as fasync;
551    use fuchsia_pkg_testing::PackageBuilder;
552    use mock_paver::PaverEvent;
553
554    #[fasync::run_singlethreaded(test)]
555    pub async fn test_updater() {
556        let data = "hello world!".as_bytes();
557        let test_package = PackageBuilder::new("test_package")
558            .add_resource_at("bin/hello", "this is a test".as_bytes())
559            .add_resource_at("data/file", "this is a file".as_bytes())
560            .add_resource_at("meta/test_package.cm", "{}".as_bytes())
561            .build()
562            .await
563            .expect("Building test_package");
564        let updater = UpdaterBuilder::new()
565            .await
566            .paver(|p| {
567                // Emulate ABR not being supported
568                p.boot_manager_close_with_epitaph(zx::Status::NOT_SUPPORTED)
569            })
570            .add_package(test_package)
571            .fuchsia_image(data.to_vec(), Some(data.to_vec()))
572            .recovery_image(data.to_vec(), Some(data.to_vec()));
573        let result = updater.build_and_run().await;
574
575        assert_eq!(
576            result.paver_events,
577            vec![
578                PaverEvent::WriteAsset {
579                    configuration: Configuration::A,
580                    asset: Asset::Kernel,
581                    payload: data.to_vec()
582                },
583                PaverEvent::WriteAsset {
584                    configuration: Configuration::B,
585                    asset: Asset::Kernel,
586                    payload: data.to_vec()
587                },
588                PaverEvent::WriteAsset {
589                    configuration: Configuration::A,
590                    asset: Asset::VerifiedBootMetadata,
591                    payload: data.to_vec()
592                },
593                PaverEvent::WriteAsset {
594                    configuration: Configuration::B,
595                    asset: Asset::VerifiedBootMetadata,
596                    payload: data.to_vec()
597                },
598                // isolated-swd does not write recovery even if an image is provided.
599                PaverEvent::DataSinkFlush,
600            ]
601        );
602
603        let () = result.verify_packages().await;
604    }
605}