persistence/
file_handler.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.
4
5use anyhow::{Context, Error, bail};
6use log::info;
7use persistence_config::Config;
8use serde::de::{self, Visitor};
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10use std::fs::{self, File};
11use std::io::ErrorKind;
12
13use crate::fetcher::PersistenceData;
14
15const CURRENT_DATA: &str = "/cache/current.json";
16const PREVIOUS_DATA: &str = "/cache/previous.json";
17
18#[derive(Clone, Debug, Serialize, Deserialize)]
19pub(crate) struct Timestamps {
20    // Warning: Persistence stores this information on disk across multiple
21    // reboots. These fields' serialization format should be treated as ABI and
22    // thus an avenue for breaking changes.
23    #[serde(serialize_with = "serialize_boot_time", deserialize_with = "deserialize_boot_time")]
24    pub last_sample_boot: zx::BootInstant,
25    #[serde(serialize_with = "serialize_utc_time", deserialize_with = "deserialize_utc_time")]
26    pub last_sample_utc: fuchsia_runtime::UtcInstant,
27}
28
29impl Timestamps {
30    pub fn merge(&mut self, other: Self) {
31        if self.last_sample_boot < other.last_sample_boot {
32            self.last_sample_boot = other.last_sample_boot;
33        }
34        if self.last_sample_utc < other.last_sample_utc {
35            self.last_sample_utc = other.last_sample_utc;
36        }
37    }
38}
39
40fn serialize_boot_time<S: Serializer>(
41    time: &zx::BootInstant,
42    serializer: S,
43) -> Result<S::Ok, S::Error> {
44    serializer.serialize_i64(time.into_nanos())
45}
46
47fn deserialize_boot_time<'de, D: Deserializer<'de>>(
48    deserializer: D,
49) -> Result<zx::BootInstant, D::Error> {
50    deserializer.deserialize_i64(TimeNanos).map(zx::BootInstant::from_nanos)
51}
52
53fn serialize_utc_time<S: Serializer>(
54    time: &fuchsia_runtime::UtcInstant,
55    serializer: S,
56) -> Result<S::Ok, S::Error> {
57    serializer.serialize_i64(time.into_nanos())
58}
59
60fn deserialize_utc_time<'de, D: Deserializer<'de>>(
61    deserializer: D,
62) -> Result<fuchsia_runtime::UtcInstant, D::Error> {
63    deserializer.deserialize_i64(TimeNanos).map(fuchsia_runtime::UtcInstant::from_nanos)
64}
65
66/// A visitor that deserializes times as represented by nanoseconds held in a 64-bit integer.
67struct TimeNanos;
68
69impl<'de> Visitor<'de> for TimeNanos {
70    type Value = i64;
71
72    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        formatter
74            .write_str("a 64-bit integer representing time in nanoseconds on an arbitrary timeline")
75    }
76
77    fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
78    where
79        E: de::Error,
80    {
81        i64::try_from(v).map_err(de::Error::custom)
82    }
83}
84
85// Forget persisted inspect data from two boots ago, except for tags with
86// persist_across_boot enabled.
87//
88// Persisted inspect data is held in both /cache/current and /cache/previous,
89// corresponding to the current and previous boot, respectively. When a boot
90// occurs, this function will move /cache/current to /cache/previous then copy
91// tags with persist_across_boot back into /cache/current.
92pub async fn forget_old_data(config: &Config) -> Result<(), Error> {
93    info!(
94        "Forgetting persisted inspect data from two boots ago, except for tags with persist_across_boot enabled"
95    );
96
97    match fs::remove_file(PREVIOUS_DATA) {
98        // Works as intended; cache was wiped or doesn't exist yet.
99        Err(e) if e.kind() == ErrorKind::NotFound => {}
100        // Unknown error
101        Err(e) => {
102            bail!("Failed to wipe previous data: {e}");
103        }
104        _ => {}
105    }
106
107    if let Err(e) = fs::rename(CURRENT_DATA, PREVIOUS_DATA) {
108        if e.kind() == ErrorKind::NotFound {
109            return Ok(());
110        }
111        bail!("Failed to swap current data with previous: {e}");
112    }
113
114    let mut data = previous_data().await?.context("Data not found; filesystem inconsistency")?;
115
116    remove_tags_without_persist_across_boot(&mut data, config)
117        .context("Failed to remove tags without persist_across_boot")?;
118
119    let file = File::create(CURRENT_DATA).context("Failed to open current data")?;
120    serde_json::to_writer(file, &data).context("Failed to write current data")
121}
122
123fn remove_tags_without_persist_across_boot(
124    data: &mut PersistenceData,
125    config: &Config,
126) -> Result<(), Error> {
127    let mut copied_count = 0;
128
129    for (service, service_data) in data.iter_mut() {
130        let tags_to_remove = config
131            .get(&service.clone())
132            .with_context(|| format!("Failed to find service \"{service}\" in config"))?
133            .iter()
134            .filter(|(_, config)| !config.persist_across_boot)
135            .map(|(tag, _)| tag);
136
137        for tag in tags_to_remove {
138            service_data.remove(tag);
139        }
140
141        copied_count += service_data.len();
142    }
143
144    info!("Persisted {copied_count} tags across boot");
145    Ok(())
146}
147
148async fn read_data(path: &str) -> Result<Option<PersistenceData>, Error> {
149    match fuchsia_fs::file::read_in_namespace(path).await {
150        Ok(bytes) => Ok(serde_json::from_slice(&bytes)
151            .with_context(|| format!("Failed to deserialize Persistence data from {path}"))?),
152        Err(e) if e.is_not_found_error() => Ok(None),
153        Err(e) => {
154            bail!("Failed to read Persistence data from \"{path}\": {e:?}")
155        }
156    }
157}
158
159pub(crate) async fn current_data() -> Result<Option<PersistenceData>, Error> {
160    read_data(CURRENT_DATA).await
161}
162
163pub(crate) async fn previous_data() -> Result<Option<PersistenceData>, Error> {
164    read_data(PREVIOUS_DATA).await
165}
166
167pub(crate) fn write_current_data(data: &PersistenceData) -> Result<(), Error> {
168    let file = File::create(CURRENT_DATA)
169        .context("Failed to open current Persistence data for writing")?;
170    serde_json::to_writer(file, data).context("Failed to serialize Persistence data")
171}
172
173#[cfg(test)]
174mod test {
175    use super::*;
176
177    fn make_timestamps(nanos: i64) -> Timestamps {
178        Timestamps {
179            last_sample_boot: zx::BootInstant::from_nanos(nanos),
180            last_sample_utc: fuchsia_runtime::UtcInstant::from_nanos(nanos),
181        }
182    }
183
184    #[fuchsia::test]
185    fn test_timestamps_merge() {
186        let mut timestamps_1 = make_timestamps(100);
187        let timestamps_2 = make_timestamps(200);
188
189        timestamps_1.merge(timestamps_2);
190
191        // timestamps_1 should now have the maximum of each field
192        assert_eq!(timestamps_1.last_sample_boot.into_nanos(), 200);
193        assert_eq!(timestamps_1.last_sample_utc.into_nanos(), 200);
194
195        let timestamps_3 = make_timestamps(50);
196        let mut timestamps_4 = make_timestamps(300);
197
198        timestamps_4.merge(timestamps_3);
199
200        // timestamps_4 should retain its original higher value
201        assert_eq!(timestamps_4.last_sample_boot.into_nanos(), 300);
202        assert_eq!(timestamps_4.last_sample_utc.into_nanos(), 300);
203    }
204}