Skip to main content

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