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