1use anyhow::Error;
8use argh::FromArgs;
9use diagnostics_reader::{ArchiveReader, InspectArchiveReader};
10use fidl::endpoints::Proxy;
11use fidl_fuchsia_diagnostics as fdiagnostics;
12use fidl_fuchsia_diagnostics_persistence as fpersistence;
13use fidl_fuchsia_hardware_power_statecontrol as fpower;
14use fidl_fuchsia_io as fio;
15use fidl_fuchsia_power_battery as fbattery;
16use fidl_fuchsia_update as fupdate;
17use fuchsia_async as fasync;
18use fuchsia_component::client::{connect_to_protocol, connect_to_protocol_at_path};
19use fuchsia_component::server::ServiceFs;
20use fuchsia_inspect::health::Reporter;
21use futures::lock::Mutex;
22use futures::{FutureExt, StreamExt, TryStreamExt};
23use log::*;
24use persistence_build_config::Config;
25use serde::{Deserialize, Serialize};
26use std::path::{Path, PathBuf};
27use std::sync::Arc;
28use zx::{BootInstant, MonotonicDuration, MonotonicInstant};
29
30pub const PROGRAM_NAME: &str = "persistence";
32
33const BATTERY_TRIGGER_HYSTERESIS_PERCENT: f32 = 5.0;
35
36#[derive(FromArgs, Debug, PartialEq)]
38#[argh(subcommand, name = "persistence")]
39pub struct CommandLine {}
40
41#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
42pub struct Metadata {
43 pub monotonic_timestamp: i64,
44 pub boot_timestamp: i64,
45 pub utc_timestamp_ns: Option<i64>,
46}
47
48enum IncomingService {
49 PreviousBootDataProvider(fpersistence::PreviousBootDataProviderRequestStream),
50}
51
52pub async fn main(_args: CommandLine) -> Result<(), Error> {
53 info!("Persistence starting up");
54 let scope = fasync::Scope::new();
55
56 let _inspect_controller = inspect_runtime::publish(
57 fuchsia_inspect::component::inspector(),
58 inspect_runtime::PublishOptions::default().custom_scope(scope.clone()),
59 );
60
61 fuchsia_inspect::component::health().set_starting_up();
62 let config = Config::take_from_startup_handle();
63
64 let cache_dir = Path::new("/cache");
65 if let Err(e) = rotate_active_to_previous_boot(cache_dir) {
66 warn!(e:?; "Error rotating active data to previous boot");
67 }
68
69 let (ready_tx, ready_rx) = futures::channel::oneshot::channel::<()>();
70 let ready = ready_rx.shared();
71
72 let mut fs = ServiceFs::new();
73 fs.dir("svc").add_fidl_service(IncomingService::PreviousBootDataProvider);
74 fs.take_and_serve_directory_handle()?;
75
76 let cache_dir_buf = cache_dir.to_path_buf();
77 let ready_for_handler = ready.clone();
78 scope.spawn(async move {
79 fs.for_each_concurrent(None, |IncomingService::PreviousBootDataProvider(stream)| {
80 let cache_dir = cache_dir_buf.clone();
81 let ready = ready_for_handler.clone();
82 async move {
83 if let Err(e) = handle_previous_boot_data_provider(stream, &cache_dir, ready).await
84 {
85 warn!(e:?; "Error handling PreviousBootDataProvider request stream");
86 }
87 }
88 })
89 .await;
90 });
91
92 if config.skip_update_check {
93 info!("Skipping the update check, publishing previous boot data");
94 let _ = ready_tx.send(());
95 } else {
96 scope.spawn(async move {
97 if let Err(e) = wait_for_update().await {
98 warn!(e:?; "Will not publish previous boot data");
99 } else {
100 let _ = ready_tx.send(());
101 }
102 });
103 }
104
105 let proxy = connect_to_protocol_at_path::<fdiagnostics::ArchiveAccessorMarker>(
106 "/svc/fuchsia.diagnostics.ArchiveAccessor.previous_boot",
107 )?;
108
109 let mut reader = ArchiveReader::inspect();
110 reader.with_archive(proxy);
111 let collector = Arc::new(Mutex::new(SnapshotCollector::new(reader, cache_dir.to_path_buf())));
112
113 let period = MonotonicDuration::from_seconds(config.persistence_period_seconds);
114 let collector_periodic = collector.clone();
115 scope.spawn(async move {
116 if let Err(e) = collector_periodic.lock().await.collect_active_snapshot().await {
117 error!(e:?; "Error collecting initial active inspect snapshot");
118 }
119 let mut interval = fasync::Interval::new(period);
120 while let Some(()) = interval.next().await {
121 if let Err(e) = collector_periodic.lock().await.collect_active_snapshot().await {
122 error!(e:?; "Error collecting active inspect snapshot");
123 }
124 }
125 });
126
127 if let Ok(battery_manager) = connect_to_protocol::<fbattery::BatteryManagerMarker>() {
128 let threshold = config.low_battery_threshold_percent as f32;
129 let collector_battery = collector.clone();
130 scope.spawn(async move {
131 if let Err(e) =
132 listen_for_low_battery(battery_manager, threshold, collector_battery).await
133 {
134 warn!(e:?; "BatteryManager watcher task terminated");
135 }
136 });
137 }
138
139 if config.enable_shutdown_snapshot {
140 if let Ok(shutdown_watcher_register) =
141 connect_to_protocol::<fpower::ShutdownWatcherRegisterMarker>()
142 {
143 let collector_shutdown = collector.clone();
144 scope.spawn(async move {
145 if let Err(e) =
146 listen_for_shutdown(shutdown_watcher_register, collector_shutdown).await
147 {
148 warn!(e:?; "Shutdown watcher task terminated");
149 }
150 });
151 }
152 } else {
153 info!("Shutdown snapshot trigger disabled by configuration");
154 }
155
156 fuchsia_inspect::component::health().set_ok();
157 scope.await;
158
159 Ok(())
160}
161
162async fn listen_for_low_battery(
163 battery_manager: fbattery::BatteryManagerProxy,
164 threshold_percent: f32,
165 collector: Arc<Mutex<SnapshotCollector>>,
166) -> Result<(), Error> {
167 let (watcher_client, mut request_stream) =
168 fidl::endpoints::create_request_stream::<fbattery::BatteryInfoWatcherMarker>();
169 battery_manager.watch(watcher_client)?;
170
171 let mut triggered = false;
172
173 while let Some(request) = request_stream.try_next().await? {
174 match request {
175 fbattery::BatteryInfoWatcherRequest::OnChangeBatteryInfo {
176 info,
177 wake_lease: _,
178 responder,
179 } => {
180 let level_percent = info.level_percent.unwrap_or(100.0);
181 let level_status = info.level_status.unwrap_or(fbattery::LevelStatus::Unknown);
182
183 let is_low = level_percent <= threshold_percent
184 || level_status == fbattery::LevelStatus::Low
185 || level_status == fbattery::LevelStatus::Critical;
186
187 if is_low {
188 if !triggered {
189 info!(
190 level_percent,
191 level_status:?;
192 "Low battery threshold reached, collecting active snapshot"
193 );
194 if let Err(e) = collector.lock().await.collect_active_snapshot().await {
195 error!(e:?; "Error collecting active snapshot on low battery");
196 }
197 triggered = true;
198 }
199 } else if level_percent > threshold_percent + BATTERY_TRIGGER_HYSTERESIS_PERCENT {
200 triggered = false;
201 }
202
203 responder.send()?;
204 }
205 }
206 }
207 Ok(())
208}
209
210async fn listen_for_shutdown(
211 shutdown_watcher_register: fpower::ShutdownWatcherRegisterProxy,
212 collector: Arc<Mutex<SnapshotCollector>>,
213) -> Result<(), Error> {
214 let (watcher_client, mut request_stream) =
215 fidl::endpoints::create_request_stream::<fpower::ShutdownWatcherMarker>();
216 shutdown_watcher_register.register_watcher(watcher_client).await?;
217
218 while let Some(request) = request_stream.try_next().await? {
219 match request {
220 fpower::ShutdownWatcherRequest::OnShutdown { options: _, responder } => {
221 info!("Shutdown initiated, collecting active snapshot");
222 if let Err(e) = collector.lock().await.collect_active_snapshot().await {
223 error!(e:?; "Error collecting active snapshot on shutdown");
224 }
225 let _ = responder.send();
226 break;
227 }
228 fpower::ShutdownWatcherRequest::_UnknownMethod { .. } => {}
229 }
230 }
231 Ok(())
232}
233
234fn rotate_active_to_previous_boot(cache_dir: &Path) -> Result<(), Error> {
235 let active_dir = cache_dir.join("active");
236 let previous_boot_dir = cache_dir.join("previous_boot");
237
238 std::fs::create_dir_all(&active_dir)?;
239 std::fs::create_dir_all(&previous_boot_dir)?;
240
241 let active_file = active_dir.join("active.json");
242 let active_meta = active_dir.join("metadata.json");
243
244 info!(
245 active_exists = active_file.exists(),
246 meta_exists = active_meta.exists();
247 "Rotating active data to previous boot"
248 );
249
250 for entry in std::fs::read_dir(&previous_boot_dir)? {
252 let entry = entry?;
253 let path = entry.path();
254 if path.is_file() {
255 let _ = std::fs::remove_file(path);
256 }
257 }
258
259 if active_file.exists() {
260 std::fs::rename(&active_file, previous_boot_dir.join("active.json"))?;
261 }
262 if active_meta.exists() {
263 std::fs::rename(&active_meta, previous_boot_dir.join("metadata.json"))?;
264 }
265
266 for entry in std::fs::read_dir(&active_dir)? {
268 let entry = entry?;
269 let path = entry.path();
270 if path.is_file() {
271 let _ = std::fs::remove_file(path);
272 }
273 }
274
275 Ok(())
276}
277
278fn maybe_get_utc_timestamp_from_clock<R: zx::Timeline, O: zx::Timeline>(
279 clock: Option<&zx::Clock<R, O>>,
280) -> Option<i64> {
281 let clock = clock?;
282 clock.read().ok().map(|time| time.into_nanos())
283}
284
285pub fn maybe_get_utc_timestamp() -> Option<i64> {
286 let clock = fuchsia_runtime::duplicate_utc_clock_handle(zx::Rights::SAME_RIGHTS).ok();
287 maybe_get_utc_timestamp_from_clock(clock.as_ref())
288}
289
290struct SnapshotCollector {
291 reader: InspectArchiveReader,
292 cache_dir: PathBuf,
293}
294
295impl SnapshotCollector {
296 fn new(reader: InspectArchiveReader, cache_dir: PathBuf) -> Self {
297 Self { reader, cache_dir }
298 }
299
300 async fn collect_active_snapshot(&mut self) -> Result<(), Error> {
301 info!("Collecting active snapshot...");
302 let inspect_data = match self.reader.snapshot().await {
303 Ok(v) => v,
304 Err(e) => {
305 error!(e:?; "ArchiveReader snapshot failed");
306 return Err(e.into());
307 }
308 };
309 let active_dir = self.cache_dir.join("active");
310 std::fs::create_dir_all(&active_dir)?;
311
312 let active_json_data = serde_json::to_vec(&inspect_data)?;
313 let tmp_file = active_dir.join("active.json.tmp");
314 let final_file = active_dir.join("active.json");
315 std::fs::write(&tmp_file, active_json_data)?;
316 std::fs::rename(&tmp_file, &final_file)?;
317
318 let metadata = Metadata {
319 monotonic_timestamp: MonotonicInstant::get().into_nanos(),
320 boot_timestamp: BootInstant::get().into_nanos(),
321 utc_timestamp_ns: maybe_get_utc_timestamp(),
322 };
323
324 let meta_json_data = serde_json::to_vec(&metadata)?;
325 let tmp_meta = active_dir.join("metadata.json.tmp");
326 let final_meta = active_dir.join("metadata.json");
327 std::fs::write(&tmp_meta, meta_json_data)?;
328 std::fs::rename(&tmp_meta, &final_meta)?;
329
330 Ok(())
331 }
332}
333
334async fn handle_previous_boot_data_provider(
335 mut stream: fpersistence::PreviousBootDataProviderRequestStream,
336 cache_dir: &Path,
337 ready: futures::future::Shared<futures::channel::oneshot::Receiver<()>>,
338) -> Result<(), Error> {
339 while let Some(request) = stream.try_next().await? {
340 match request {
341 fpersistence::PreviousBootDataProviderRequest::WatchPreviousBootData {
342 options: _,
343 responder,
344 } => {
345 info!("Received WatchPreviousBootData request, awaiting ready signal...");
346 if ready.clone().await.is_err() {
347 warn!("Update check failed or cancelled; will not serve previous boot data");
348 return Ok(());
349 }
350 info!("Ready signal received in WatchPreviousBootData handler");
351 let previous_boot_dir = cache_dir.join("previous_boot");
352 let active_file = previous_boot_dir.join("active.json");
353 let meta_file = previous_boot_dir.join("metadata.json");
354
355 if !active_file.exists() || !meta_file.exists() {
356 responder.send(fpersistence::PreviousBootData::default())?;
357 continue;
358 }
359
360 let meta_bytes = std::fs::read(&meta_file)?;
361 let metadata: Metadata = serde_json::from_slice(&meta_bytes)?;
362
363 let active_path_str =
364 active_file.to_str().ok_or_else(|| anyhow::anyhow!("Invalid path"))?;
365 let file_proxy =
366 fuchsia_fs::file::open_in_namespace(active_path_str, fio::PERM_READABLE)?;
367 let client_end = file_proxy
368 .into_client_end()
369 .map_err(|_| anyhow::anyhow!("Failed to convert to client end"))?;
370
371 let data = fpersistence::PreviousBootData {
372 monotonic_timestamp: Some(MonotonicInstant::from_nanos(
373 metadata.monotonic_timestamp,
374 )),
375 boot_timestamp: Some(BootInstant::from_nanos(metadata.boot_timestamp)),
376 utc_timestamp_ns: metadata.utc_timestamp_ns,
377 inspect: Some(client_end),
378 ..Default::default()
379 };
380
381 responder.send(data)?;
382 }
383 fpersistence::PreviousBootDataProviderRequest::_UnknownMethod { .. } => {}
384 }
385 }
386 Ok(())
387}
388
389async fn wait_for_update() -> Result<(), Error> {
390 info!("Waiting for post-boot update check...");
391 let (notifier_client, mut notifier_request_stream) =
392 fidl::endpoints::create_request_stream::<fupdate::NotifierMarker>();
393 let proxy = connect_to_protocol::<fupdate::ListenerMarker>()?;
394 proxy.notify_on_first_update_check(fupdate::ListenerNotifyOnFirstUpdateCheckRequest {
395 notifier: Some(notifier_client),
396 ..Default::default()
397 })?;
398
399 match notifier_request_stream.try_next().await {
400 Ok(Some(fupdate::NotifierRequest::Notify { control_handle: _ })) => {}
401 Ok(None) => {
402 return Err(anyhow::anyhow!("Did not receive update notification; not publishing"));
403 }
404 Err(e) => {
405 return Err(anyhow::anyhow!(
406 "Error waiting for update notification; not publishing: {e}"
407 ));
408 }
409 }
410
411 info!("...Update check has completed; publishing previous boot data");
413 Ok(())
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use tempfile::TempDir;
420
421 #[test]
422 fn test_metadata_serde() {
423 let meta = Metadata {
424 monotonic_timestamp: 1000,
425 boot_timestamp: 2000,
426 utc_timestamp_ns: Some(3000),
427 };
428
429 let serialized = serde_json::to_vec(&meta).unwrap();
430 let deserialized: Metadata = serde_json::from_slice(&serialized).unwrap();
431 assert_eq!(meta, deserialized);
432 }
433
434 #[test]
435 fn test_metadata_serde_without_utc() {
436 let meta =
437 Metadata { monotonic_timestamp: 1000, boot_timestamp: 2000, utc_timestamp_ns: None };
438
439 let serialized = serde_json::to_vec(&meta).unwrap();
440 let deserialized: Metadata = serde_json::from_slice(&serialized).unwrap();
441 assert_eq!(meta, deserialized);
442 }
443
444 #[test]
445 fn test_maybe_get_utc_timestamp_when_utc_not_available() {
446 assert_eq!(
448 maybe_get_utc_timestamp_from_clock::<zx::MonotonicTimeline, zx::SyntheticTimeline>(
449 None
450 ),
451 None
452 );
453
454 let clock = zx::SyntheticClock::create(zx::ClockOpts::AUTO_START, None)
456 .expect("failed to create clock");
457 let no_read_clock = clock.duplicate_handle(zx::Rights::NONE).expect("duplicate handle");
458 assert_eq!(maybe_get_utc_timestamp_from_clock(Some(&no_read_clock)), None);
459
460 assert!(maybe_get_utc_timestamp_from_clock(Some(&clock)).is_some());
462 }
463
464 #[test]
465 fn test_rotation() {
466 let _ = std::fs::create_dir_all("/cache");
467 let temp_dir = TempDir::new_in("/cache").unwrap();
468 let cache_dir = temp_dir.path();
469 let active_dir = cache_dir.join("active");
470 let previous_boot_dir = cache_dir.join("previous_boot");
471
472 std::fs::create_dir_all(&active_dir).unwrap();
473 std::fs::write(active_dir.join("active.json"), b"active_data").unwrap();
474 std::fs::write(active_dir.join("metadata.json"), b"meta_data").unwrap();
475
476 rotate_active_to_previous_boot(cache_dir).unwrap();
477
478 assert!(!active_dir.join("active.json").exists());
479 assert!(!active_dir.join("metadata.json").exists());
480 assert_eq!(std::fs::read(previous_boot_dir.join("active.json")).unwrap(), b"active_data");
481 assert_eq!(std::fs::read(previous_boot_dir.join("metadata.json")).unwrap(), b"meta_data");
482 }
483
484 #[test]
485 fn test_rotation_cleans_non_clean_active_dir() {
486 let _ = std::fs::create_dir_all("/cache");
487 let temp_dir = TempDir::new_in("/cache").unwrap();
488 let cache_dir = temp_dir.path();
489 let active_dir = cache_dir.join("active");
490 let previous_boot_dir = cache_dir.join("previous_boot");
491
492 std::fs::create_dir_all(&active_dir).unwrap();
493 std::fs::write(active_dir.join("active.json"), b"active_data").unwrap();
494 std::fs::write(active_dir.join("metadata.json"), b"meta_data").unwrap();
495 std::fs::write(active_dir.join("active.json.tmp"), b"tmp_active_data").unwrap();
497 std::fs::write(active_dir.join("metadata.json.tmp"), b"tmp_meta_data").unwrap();
498 std::fs::write(active_dir.join("stray.txt"), b"stray_data").unwrap();
499
500 rotate_active_to_previous_boot(cache_dir).unwrap();
501
502 assert_eq!(std::fs::read(previous_boot_dir.join("active.json")).unwrap(), b"active_data");
504 assert_eq!(std::fs::read(previous_boot_dir.join("metadata.json")).unwrap(), b"meta_data");
505 assert!(!previous_boot_dir.join("active.json.tmp").exists());
507 assert!(!previous_boot_dir.join("metadata.json.tmp").exists());
508 assert!(!previous_boot_dir.join("stray.txt").exists());
509
510 assert_eq!(std::fs::read_dir(&active_dir).unwrap().count(), 0);
512 }
513
514 #[test]
515 fn test_rotation_clears_stale_previous_boot_when_active_empty() {
516 let _ = std::fs::create_dir_all("/cache");
517 let temp_dir = TempDir::new_in("/cache").unwrap();
518 let cache_dir = temp_dir.path();
519 let active_dir = cache_dir.join("active");
520 let previous_boot_dir = cache_dir.join("previous_boot");
521
522 std::fs::create_dir_all(&active_dir).unwrap();
523 std::fs::create_dir_all(&previous_boot_dir).unwrap();
524
525 std::fs::write(previous_boot_dir.join("active.json"), b"stale_active_data").unwrap();
527 std::fs::write(previous_boot_dir.join("metadata.json"), b"stale_meta_data").unwrap();
528
529 rotate_active_to_previous_boot(cache_dir).unwrap();
531
532 assert!(!previous_boot_dir.join("active.json").exists());
534 assert!(!previous_boot_dir.join("metadata.json").exists());
535 }
536
537 #[fasync::run_singlethreaded(test)]
538 async fn test_handle_previous_boot_data_provider_missing_files() {
539 let _ = std::fs::create_dir_all("/cache");
540 let temp_dir = TempDir::new_in("/cache").unwrap();
541 let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<
542 fpersistence::PreviousBootDataProviderMarker,
543 >();
544
545 let (ready_tx, ready_rx) = futures::channel::oneshot::channel();
546 let _ = ready_tx.send(());
547 let ready = ready_rx.shared();
548
549 let scope = fasync::Scope::new();
550 let cache_dir = temp_dir.path().to_path_buf();
551 scope.spawn(async move {
552 let _ = handle_previous_boot_data_provider(stream, &cache_dir, ready).await;
553 });
554
555 let data = proxy.watch_previous_boot_data(&Default::default()).await.unwrap();
556 assert!(data.inspect.is_none());
557 assert!(data.monotonic_timestamp.is_none());
558 assert!(data.boot_timestamp.is_none());
559 assert!(data.utc_timestamp_ns.is_none());
560 assert_eq!(data, fpersistence::PreviousBootData::default());
561 }
562
563 #[fasync::run_singlethreaded(test)]
564 async fn test_handle_previous_boot_data_provider_valid_data() {
565 let _ = std::fs::create_dir_all("/cache");
566 let temp_dir = TempDir::new_in("/cache").unwrap();
567 let previous_boot_dir = temp_dir.path().join("previous_boot");
568 std::fs::create_dir_all(&previous_boot_dir).unwrap();
569
570 let meta = Metadata {
571 monotonic_timestamp: 12345,
572 boot_timestamp: 67890,
573 utc_timestamp_ns: Some(100000),
574 };
575 let meta_bytes = serde_json::to_vec(&meta).unwrap();
576 std::fs::write(previous_boot_dir.join("metadata.json"), meta_bytes).unwrap();
577 std::fs::write(previous_boot_dir.join("active.json"), b"valid_inspect_json").unwrap();
578
579 let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<
580 fpersistence::PreviousBootDataProviderMarker,
581 >();
582
583 let (ready_tx, ready_rx) = futures::channel::oneshot::channel();
584 let _ = ready_tx.send(());
585 let ready = ready_rx.shared();
586
587 let scope = fasync::Scope::new();
588 let cache_dir = temp_dir.path().to_path_buf();
589 scope.spawn(async move {
590 let _ = handle_previous_boot_data_provider(stream, &cache_dir, ready).await;
591 });
592
593 let data = proxy.watch_previous_boot_data(&Default::default()).await.unwrap();
594 assert_eq!(data.monotonic_timestamp, Some(MonotonicInstant::from_nanos(12345)));
595 assert_eq!(data.boot_timestamp, Some(BootInstant::from_nanos(67890)));
596 assert_eq!(data.utc_timestamp_ns, Some(100000));
597 assert!(data.inspect.is_some());
598 }
599
600 #[fasync::run_singlethreaded(test)]
601 async fn test_handle_previous_boot_data_provider_utc_not_available() {
602 let _ = std::fs::create_dir_all("/cache");
603 let temp_dir = TempDir::new_in("/cache").unwrap();
604 let previous_boot_dir = temp_dir.path().join("previous_boot");
605 std::fs::create_dir_all(&previous_boot_dir).unwrap();
606
607 let meta =
608 Metadata { monotonic_timestamp: 12345, boot_timestamp: 67890, utc_timestamp_ns: None };
609 let meta_bytes = serde_json::to_vec(&meta).unwrap();
610 std::fs::write(previous_boot_dir.join("metadata.json"), meta_bytes).unwrap();
611 std::fs::write(previous_boot_dir.join("active.json"), b"valid_inspect_json").unwrap();
612
613 let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<
614 fpersistence::PreviousBootDataProviderMarker,
615 >();
616
617 let (ready_tx, ready_rx) = futures::channel::oneshot::channel();
618 let _ = ready_tx.send(());
619 let ready = ready_rx.shared();
620
621 let scope = fasync::Scope::new();
622 let cache_dir = temp_dir.path().to_path_buf();
623 scope.spawn(async move {
624 let _ = handle_previous_boot_data_provider(stream, &cache_dir, ready).await;
625 });
626
627 let data = proxy.watch_previous_boot_data(&Default::default()).await.unwrap();
628 assert_eq!(data.monotonic_timestamp, Some(MonotonicInstant::from_nanos(12345)));
629 assert_eq!(data.boot_timestamp, Some(BootInstant::from_nanos(67890)));
630 assert_eq!(data.utc_timestamp_ns, None);
631 assert!(data.inspect.is_some());
632 }
633
634 #[fasync::run_singlethreaded(test)]
635 async fn test_wait_for_update_no_listener() {
636 let res = wait_for_update().await;
637 assert!(res.is_err());
638 }
639
640 #[fasync::run_singlethreaded(test)]
641 async fn test_handle_previous_boot_data_provider_canceled_ready() {
642 let _ = std::fs::create_dir_all("/cache");
643 let temp_dir = TempDir::new_in("/cache").unwrap();
644 let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<
645 fpersistence::PreviousBootDataProviderMarker,
646 >();
647
648 let (ready_tx, ready_rx) = futures::channel::oneshot::channel::<()>();
649 drop(ready_tx);
650 let ready = ready_rx.shared();
651
652 let scope = fasync::Scope::new();
653 let cache_dir = temp_dir.path().to_path_buf();
654 scope.spawn(async move {
655 let _ = handle_previous_boot_data_provider(stream, &cache_dir, ready).await;
656 });
657
658 let res = proxy.watch_previous_boot_data(&Default::default()).await;
659 assert!(res.is_err());
660 }
661}