persistence/
file_handler.rs1use 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 #[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
66struct 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
85pub 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 Err(e) if e.kind() == ErrorKind::NotFound => {}
100 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 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 assert_eq!(timestamps_4.last_sample_boot.into_nanos(), 300);
202 assert_eq!(timestamps_4.last_sample_utc.into_nanos(), 300);
203 }
204}