1use commit::do_commit;
8use errors::MetadataError;
9use fidl_fuchsia_paver as fpaver;
10use fidl_fuchsia_update_verify::HealthVerificationProxy;
11use fuchsia_async::TimeoutExt as _;
12use fuchsia_inspect as finspect;
13use futures::channel::oneshot;
14use policy::PolicyEngine;
15use std::time::Instant;
16use zx::{EventPair, Peered};
17
18mod commit;
19mod configuration_without_recovery;
20mod errors;
21mod inspect;
22mod policy;
23
24pub async fn put_metadata_in_happy_state(
37 boot_manager: &fpaver::BootManagerProxy,
38 p_internal: &EventPair,
39 unblocker: oneshot::Sender<()>,
40 health_verification: &HealthVerificationProxy,
41 commit_timeout: zx::MonotonicDuration,
42 node: &finspect::Node,
43 commit_inspect: &CommitInspect,
44) -> Result<CommitResult, MetadataError> {
45 let start_time = Instant::now();
46 let res = put_metadata_in_happy_state_impl(
47 boot_manager,
48 p_internal,
49 unblocker,
50 health_verification,
51 commit_inspect,
52 )
53 .on_timeout(commit_timeout, || Err(MetadataError::Timeout))
54 .await;
55
56 match &res {
57 Ok(CommitResult::CommitNotNecessary) => (),
58 Ok(CommitResult::CommittedSystem) | Err(_) => {
59 inspect::write_to_inspect(node, res.as_ref().map(|_| ()), start_time.elapsed())
60 }
61 }
62
63 res
64}
65
66async fn put_metadata_in_happy_state_impl(
67 boot_manager: &fpaver::BootManagerProxy,
68 p_internal: &EventPair,
69 unblocker: oneshot::Sender<()>,
70 health_verification: &HealthVerificationProxy,
71 commit_inspect: &CommitInspect,
72) -> Result<CommitResult, MetadataError> {
73 let mut unblocker = Some(unblocker);
74 let commit_result = {
75 let engine = PolicyEngine::build(boot_manager).await.map_err(MetadataError::Policy)?;
76 if let Some((current_config, boot_attempts)) = engine.should_verify_and_commit() {
77 unblocker = unblock_fidl_server(unblocker)?;
80 let () =
81 zx::Status::ok(health_verification.query_health_checks().await.map_err(|e| {
82 MetadataError::HealthVerification(errors::HealthVerificationError::Fidl(e))
83 })?)
84 .map_err(|e| {
85 MetadataError::HealthVerification(errors::HealthVerificationError::Unhealthy(e))
86 })?;
87 let () =
88 do_commit(boot_manager, current_config).await.map_err(MetadataError::Commit)?;
89 let () = commit_inspect.record_boot_attempts(boot_attempts);
90 CommitResult::CommittedSystem
91 } else {
92 CommitResult::CommitNotNecessary
93 }
94 };
95
96 let () = p_internal
98 .signal_peer(zx::Signals::NONE, zx::Signals::USER_0)
99 .map_err(MetadataError::SignalPeer)?;
100
101 unblock_fidl_server(unblocker)?;
103
104 Ok(commit_result)
105}
106
107#[derive(Debug)]
108pub enum CommitResult {
109 CommittedSystem,
110 CommitNotNecessary,
111}
112
113impl CommitResult {
114 pub fn log_msg(&self) -> &'static str {
115 match self {
116 Self::CommittedSystem => "Committed system.",
117 Self::CommitNotNecessary => "Commit not necessary.",
118 }
119 }
120}
121
122pub struct CommitInspect(finspect::Node);
124
125impl CommitInspect {
126 pub fn new(node: finspect::Node) -> Self {
127 Self(node)
128 }
129
130 fn record_boot_attempts(&self, count: Option<u8>) {
131 match count {
132 Some(count) => self.0.record_uint("boot_attempts", count.into()),
133 None => self.0.record_uint("boot_attempts_missing", 0),
134 }
135 }
136}
137
138fn unblock_fidl_server(
139 unblocker: Option<oneshot::Sender<()>>,
140) -> Result<Option<oneshot::Sender<()>>, MetadataError> {
141 if let Some(sender) = unblocker {
142 let () = sender.send(()).map_err(|_| MetadataError::Unblock)?;
143 }
144 Ok(None)
145}
146
147#[cfg(test)]
150mod tests {
151 use super::errors::HealthVerificationError;
152 use super::*;
153 use assert_matches::assert_matches;
154 use configuration_without_recovery::ConfigurationWithoutRecovery;
155 use fasync::OnSignals;
156 use fuchsia_async as fasync;
157 use mock_health_verification::MockHealthVerificationService;
158 use mock_paver::{MockPaverServiceBuilder, PaverEvent, hooks as mphooks};
159 use std::sync::Arc;
160 use std::sync::atomic::{AtomicU32, Ordering};
161 use zx::Status;
162
163 fn health_verification_and_call_count(
164 status: Result<(), zx::Status>,
165 ) -> (HealthVerificationProxy, Arc<AtomicU32>) {
166 let call_count = Arc::new(AtomicU32::new(0));
167 let call_count_clone = Arc::clone(&call_count);
168 let verification = Arc::new(MockHealthVerificationService::new(move || {
169 call_count_clone.fetch_add(1, Ordering::SeqCst);
170 status
171 }));
172
173 let (health_verification, server) = verification.spawn_health_verification_service();
174 let () = server.detach();
175
176 (health_verification, call_count)
177 }
178
179 #[fuchsia::test]
182 async fn test_does_not_change_metadata_when_device_does_not_support_abr() {
183 let paver = Arc::new(
184 MockPaverServiceBuilder::new()
185 .boot_manager_close_with_epitaph(Status::NOT_SUPPORTED)
186 .build(),
187 );
188 let (p_internal, p_external) = EventPair::create();
189 let (unblocker, unblocker_recv) = oneshot::channel();
190 let (health_verification, health_verification_call_count) =
191 health_verification_and_call_count(Ok(()));
192
193 put_metadata_in_happy_state_impl(
194 &paver.spawn_boot_manager_service(),
195 &p_internal,
196 unblocker,
197 &health_verification,
198 &CommitInspect::new(finspect::Node::default()),
199 )
200 .await
201 .unwrap();
202
203 assert_eq!(paver.take_events(), vec![]);
204 assert_eq!(
205 p_external
206 .wait_one(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE_PAST)
207 .to_result(),
208 Ok(zx::Signals::USER_0)
209 );
210 assert_eq!(unblocker_recv.await, Ok(()));
211 assert_eq!(health_verification_call_count.load(Ordering::SeqCst), 0);
212 }
213
214 #[fuchsia::test]
217 async fn test_does_not_change_metadata_when_device_in_recovery() {
218 let paver = Arc::new(
219 MockPaverServiceBuilder::new()
220 .current_config(fpaver::Configuration::Recovery)
221 .insert_hook(mphooks::config_status(|_| Ok(fpaver::ConfigurationStatus::Healthy)))
222 .build(),
223 );
224 let (p_internal, p_external) = EventPair::create();
225 let (unblocker, unblocker_recv) = oneshot::channel();
226 let (health_verification, health_verification_call_count) =
227 health_verification_and_call_count(Ok(()));
228
229 put_metadata_in_happy_state_impl(
230 &paver.spawn_boot_manager_service(),
231 &p_internal,
232 unblocker,
233 &health_verification,
234 &CommitInspect::new(finspect::Node::default()),
235 )
236 .await
237 .unwrap();
238
239 assert_eq!(paver.take_events(), vec![PaverEvent::QueryCurrentConfiguration]);
240 assert_eq!(
241 p_external
242 .wait_one(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE_PAST)
243 .to_result(),
244 Ok(zx::Signals::USER_0)
245 );
246 assert_eq!(unblocker_recv.await, Ok(()));
247 assert_eq!(health_verification_call_count.load(Ordering::SeqCst), 0);
248 }
249
250 async fn test_does_not_change_metadata_when_current_is_healthy(
253 current_config: &ConfigurationWithoutRecovery,
254 ) {
255 let paver = Arc::new(
256 MockPaverServiceBuilder::new()
257 .current_config(current_config.into())
258 .insert_hook(mphooks::config_status_and_boot_attempts(|_| {
259 Ok((fpaver::ConfigurationStatus::Healthy, None))
260 }))
261 .build(),
262 );
263 let (p_internal, p_external) = EventPair::create();
264 let (unblocker, unblocker_recv) = oneshot::channel();
265 let (health_verification, health_verification_call_count) =
266 health_verification_and_call_count(Ok(()));
267
268 put_metadata_in_happy_state_impl(
269 &paver.spawn_boot_manager_service(),
270 &p_internal,
271 unblocker,
272 &health_verification,
273 &CommitInspect::new(finspect::Node::default()),
274 )
275 .await
276 .unwrap();
277
278 assert_eq!(
279 paver.take_events(),
280 vec![
281 PaverEvent::QueryCurrentConfiguration,
282 PaverEvent::QueryConfigurationStatusAndBootAttempts {
283 configuration: current_config.into()
284 }
285 ]
286 );
287 assert_eq!(
288 p_external
289 .wait_one(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE_PAST)
290 .to_result(),
291 Ok(zx::Signals::USER_0)
292 );
293 assert_eq!(unblocker_recv.await, Ok(()));
294 assert_eq!(health_verification_call_count.load(Ordering::SeqCst), 0);
295 }
296
297 #[fuchsia::test]
298 async fn test_does_not_change_metadata_when_current_is_healthy_a() {
299 test_does_not_change_metadata_when_current_is_healthy(&ConfigurationWithoutRecovery::A)
300 .await
301 }
302
303 #[fuchsia::test]
304 async fn test_does_not_change_metadata_when_current_is_healthy_b() {
305 test_does_not_change_metadata_when_current_is_healthy(&ConfigurationWithoutRecovery::B)
306 .await
307 }
308
309 async fn test_verifies_and_commits_when_current_is_pending(
311 current_config: &ConfigurationWithoutRecovery,
312 ) {
313 let paver = Arc::new(
314 MockPaverServiceBuilder::new()
315 .current_config(current_config.into())
316 .insert_hook(mphooks::config_status_and_boot_attempts(|_| {
317 Ok((fpaver::ConfigurationStatus::Pending, Some(1)))
318 }))
319 .build(),
320 );
321 let (p_internal, p_external) = EventPair::create();
322 let (unblocker, unblocker_recv) = oneshot::channel();
323 let (health_verification, health_verification_call_count) =
324 health_verification_and_call_count(Ok(()));
325
326 put_metadata_in_happy_state_impl(
327 &paver.spawn_boot_manager_service(),
328 &p_internal,
329 unblocker,
330 &health_verification,
331 &CommitInspect::new(finspect::Node::default()),
332 )
333 .await
334 .unwrap();
335
336 assert_eq!(
337 paver.take_events(),
338 vec![
339 PaverEvent::QueryCurrentConfiguration,
340 PaverEvent::QueryConfigurationStatusAndBootAttempts {
341 configuration: current_config.into()
342 },
343 PaverEvent::SetConfigurationHealthy { configuration: current_config.into() },
344 PaverEvent::SetConfigurationUnbootable {
345 configuration: current_config.to_alternate().into()
346 },
347 PaverEvent::BootManagerFlush,
348 ]
349 );
350 assert_eq!(OnSignals::new(&p_external, zx::Signals::USER_0).await, Ok(zx::Signals::USER_0));
351 assert_eq!(unblocker_recv.await, Ok(()));
352 assert_eq!(health_verification_call_count.load(Ordering::SeqCst), 1);
353 }
354
355 #[fuchsia::test]
356 async fn test_verifies_and_commits_when_current_is_pending_a() {
357 test_verifies_and_commits_when_current_is_pending(&ConfigurationWithoutRecovery::A).await
358 }
359
360 #[fuchsia::test]
361 async fn test_verifies_and_commits_when_current_is_pending_b() {
362 test_verifies_and_commits_when_current_is_pending(&ConfigurationWithoutRecovery::B).await
363 }
364
365 #[fuchsia::test]
367 async fn test_commits_when_failed_verification_ignored() {
368 let paver = Arc::new(
369 MockPaverServiceBuilder::new()
370 .current_config(fpaver::Configuration::Recovery)
371 .insert_hook(mphooks::config_status_and_boot_attempts(|_| {
372 Ok((fpaver::ConfigurationStatus::Pending, Some(1)))
373 }))
374 .build(),
375 );
376 let (p_internal, p_external) = EventPair::create();
377 let (unblocker, unblocker_recv) = oneshot::channel();
378 let (health_verification, health_verification_call_count) =
379 health_verification_and_call_count(Err(zx::Status::INTERNAL));
380
381 put_metadata_in_happy_state_impl(
382 &paver.spawn_boot_manager_service(),
383 &p_internal,
384 unblocker,
385 &health_verification,
386 &CommitInspect::new(finspect::Node::default()),
387 )
388 .await
389 .unwrap();
390
391 assert_eq!(paver.take_events(), vec![PaverEvent::QueryCurrentConfiguration,]);
392 assert_eq!(OnSignals::new(&p_external, zx::Signals::USER_0).await, Ok(zx::Signals::USER_0));
393 assert_eq!(unblocker_recv.await, Ok(()));
394 assert_eq!(health_verification_call_count.load(Ordering::SeqCst), 0);
395 }
396
397 async fn test_errors_when_failed_verification_not_ignored(
399 current_config: &ConfigurationWithoutRecovery,
400 ) {
401 let paver = Arc::new(
402 MockPaverServiceBuilder::new()
403 .current_config(current_config.into())
404 .insert_hook(mphooks::config_status_and_boot_attempts(|_| {
405 Ok((fpaver::ConfigurationStatus::Pending, Some(1)))
406 }))
407 .build(),
408 );
409 let (p_internal, p_external) = EventPair::create();
410 let (unblocker, unblocker_recv) = oneshot::channel();
411 let (health_verification, health_verification_call_count) =
412 health_verification_and_call_count(Err(zx::Status::INTERNAL));
413
414 let result = put_metadata_in_happy_state_impl(
415 &paver.spawn_boot_manager_service(),
416 &p_internal,
417 unblocker,
418 &health_verification,
419 &CommitInspect::new(finspect::Node::default()),
420 )
421 .await;
422
423 assert_matches!(
424 result,
425 Err(MetadataError::HealthVerification(HealthVerificationError::Unhealthy(
426 Status::INTERNAL
427 )))
428 );
429
430 assert_eq!(
431 paver.take_events(),
432 vec![
433 PaverEvent::QueryCurrentConfiguration,
434 PaverEvent::QueryConfigurationStatusAndBootAttempts {
435 configuration: current_config.into()
436 },
437 ]
438 );
439 assert_eq!(
440 p_external
441 .wait_one(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE_PAST)
442 .to_result(),
443 Err(zx::Status::TIMED_OUT)
444 );
445 assert_eq!(unblocker_recv.await, Ok(()));
446 assert_eq!(health_verification_call_count.load(Ordering::SeqCst), 1);
447 }
448
449 #[fuchsia::test]
450 async fn test_errors_when_failed_verification_not_ignored_a() {
451 test_errors_when_failed_verification_not_ignored(&ConfigurationWithoutRecovery::A).await
452 }
453
454 #[fuchsia::test]
455 async fn test_errors_when_failed_verification_not_ignored_b() {
456 test_errors_when_failed_verification_not_ignored(&ConfigurationWithoutRecovery::B).await
457 }
458
459 #[fuchsia::test]
460 async fn commit_inspect_handles_missing_count() {
461 let inspector = finspect::Inspector::default();
462 let commit_inspect = CommitInspect::new(inspector.root().create_child("commit"));
463
464 commit_inspect.record_boot_attempts(None);
465
466 diagnostics_assertions::assert_data_tree!(inspector, root: {
467 "commit": {
468 "boot_attempts_missing": 0u64
469 }
470 });
471 }
472}