1use std::fmt;
7use std::fs::{self, File, OpenOptions};
8use std::io::{self, Write};
9use std::path::{Path, PathBuf};
10use std::time::{SystemTime, UNIX_EPOCH};
11
12use serde::{Deserialize, Serialize};
13
14use crate::blob_store::FullKey;
15use crate::fs_lock;
16use crate::root_cache::{self, ReadMarker};
17
18pub const PIN_TTL_MS: u64 = 30 * 60 * 1_000;
20pub const PIN_RENEW_INTERVAL_MS: u64 = PIN_TTL_MS / 3;
22
23#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
24pub struct PinOwner {
25 pub pid: u32,
26 pub start_time: u64,
27}
28
29#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
30pub struct PinMetadata {
31 pub family: String,
32 pub view: String,
33 pub generation: String,
34 pub owner: PinOwner,
35 pub created_at: u64,
36 pub renewed_at: u64,
37}
38
39#[derive(Debug)]
40pub enum PinError {
41 Io(io::Error),
42 Serialize(serde_json::Error),
43 InvalidGeneration(String),
44}
45
46impl fmt::Display for PinError {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 Self::Io(error) => write!(f, "pin I/O error: {error}"),
50 Self::Serialize(error) => write!(f, "pin serialization error: {error}"),
51 Self::InvalidGeneration(generation) => {
52 write!(f, "invalid pin generation `{generation}`")
53 }
54 }
55 }
56}
57
58impl std::error::Error for PinError {
59 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
60 match self {
61 Self::Io(error) => Some(error),
62 Self::Serialize(error) => Some(error),
63 Self::InvalidGeneration(_) => None,
64 }
65 }
66}
67
68impl From<io::Error> for PinError {
69 fn from(error: io::Error) -> Self {
70 Self::Io(error)
71 }
72}
73
74impl From<serde_json::Error> for PinError {
75 fn from(error: serde_json::Error) -> Self {
76 Self::Serialize(error)
77 }
78}
79
80#[derive(Debug)]
82pub struct AssemblyPin {
83 keys_path: PathBuf,
84 metadata_path: PathBuf,
85 metadata: PinMetadata,
86 released: bool,
87}
88
89impl AssemblyPin {
90 pub fn create(
93 view_dir: &Path,
94 family: impl Into<String>,
95 view: impl Into<String>,
96 generation: impl Into<String>,
97 keys: &[FullKey],
98 ) -> Result<Self, PinError> {
99 let family = family.into();
100 let view = view.into();
101 let generation = generation.into();
102 validate_generation(&generation)?;
103 let pins_dir = view_dir.join("pins");
104 fs::create_dir_all(&pins_dir)?;
105
106 let keys_path = pins_dir.join(format!("{generation}.keys"));
107 write_keys(&keys_path, keys)?;
108 let now = now_ms();
109 let metadata = PinMetadata {
110 family,
111 view,
112 generation: generation.clone(),
113 owner: PinOwner {
114 pid: std::process::id(),
115 start_time: root_cache::process_start_time_ms(std::process::id()).unwrap_or(now),
116 },
117 created_at: now,
118 renewed_at: now,
119 };
120 let metadata_path = pins_dir.join(format!("{generation}.json"));
121 write_metadata(&metadata_path, &metadata)?;
122 Ok(Self {
123 keys_path,
124 metadata_path,
125 metadata,
126 released: false,
127 })
128 }
129
130 pub fn metadata(&self) -> &PinMetadata {
131 &self.metadata
132 }
133
134 pub fn keys_path(&self) -> &Path {
135 &self.keys_path
136 }
137
138 pub fn put<T>(&mut self, put: impl FnOnce() -> Result<T, PinError>) -> Result<T, PinError> {
141 self.renew_if_due()?;
142 put()
143 }
144
145 pub fn renew_if_due(&mut self) -> Result<(), PinError> {
146 let now = now_ms();
147 if now.saturating_sub(self.metadata.renewed_at) >= PIN_RENEW_INTERVAL_MS {
148 self.metadata.renewed_at = now;
149 write_metadata(&self.metadata_path, &self.metadata)?;
150 }
151 Ok(())
152 }
153
154 pub fn release(&mut self) {
156 if self.released {
157 return;
158 }
159 let _ = fs::remove_file(&self.metadata_path);
160 let _ = fs::remove_file(&self.keys_path);
161 fs_lock::sync_parent(&self.metadata_path);
162 self.released = true;
163 }
164}
165
166impl Drop for AssemblyPin {
167 fn drop(&mut self) {
168 self.release();
169 }
170}
171
172#[derive(Debug)]
175pub struct QueryPin {
176 marker: ReadMarker,
177}
178
179impl QueryPin {
180 pub fn acquire(view_dir: &Path, generation: &str) -> Result<Self, PinError> {
181 Ok(Self {
182 marker: ReadMarker::create(view_dir, generation)?,
183 })
184 }
185
186 pub fn touch_if_due(&self) -> Result<(), PinError> {
187 self.marker.touch_if_due()?;
188 Ok(())
189 }
190
191 pub fn path(&self) -> &Path {
192 self.marker.path()
193 }
194}
195
196pub(crate) fn pin_paths(view_dir: &Path, generation: &str) -> (PathBuf, PathBuf) {
197 let pins_dir = view_dir.join("pins");
198 (
199 pins_dir.join(format!("{generation}.json")),
200 pins_dir.join(format!("{generation}.keys")),
201 )
202}
203
204pub(crate) fn read_keys(path: &Path) -> Result<Vec<[u8; 32]>, PinError> {
205 let contents = fs::read_to_string(path)?;
206 contents.lines().map(parse_hex_key).collect()
207}
208
209pub(crate) fn owner_is_live(owner: &PinOwner) -> bool {
210 root_cache::process_start_time_ms(owner.pid)
211 .map(|actual| actual == owner.start_time)
212 .unwrap_or_else(|| crate::fs_lock::process_alive(owner.pid))
213}
214
215pub(crate) fn now_ms() -> u64 {
216 SystemTime::now()
217 .duration_since(UNIX_EPOCH)
218 .unwrap_or_default()
219 .as_millis() as u64
220}
221
222fn validate_generation(generation: &str) -> Result<(), PinError> {
223 if generation.is_empty()
224 || generation == "."
225 || generation == ".."
226 || generation.contains(['/', '\\'])
227 {
228 return Err(PinError::InvalidGeneration(generation.to_owned()));
229 }
230 Ok(())
231}
232
233fn write_keys(path: &Path, keys: &[FullKey]) -> Result<(), PinError> {
234 let mut encoded = keys.iter().map(FullKey::to_hex).collect::<Vec<_>>();
235 encoded.sort_unstable();
236 encoded.dedup();
237 let mut file = create_private(path)?;
238 for key in encoded {
239 writeln!(file, "{key}")?;
240 }
241 file.sync_all()?;
242 drop(file);
243 fs_lock::sync_parent(path);
244 Ok(())
245}
246
247fn write_metadata(path: &Path, metadata: &PinMetadata) -> Result<(), PinError> {
248 let temporary = path.with_extension(format!("json.tmp.{}.{}", std::process::id(), now_ms()));
249 let result = (|| {
250 let mut file = create_private(&temporary)?;
251 serde_json::to_writer(&mut file, metadata)?;
252 file.write_all(b"\n")?;
253 file.sync_all()?;
254 drop(file);
255 fs_lock::rename_over(&temporary, path)?;
256 fs_lock::sync_parent(path);
257 Ok(())
258 })();
259 if result.is_err() {
260 let _ = fs::remove_file(&temporary);
261 }
262 result
263}
264
265fn create_private(path: &Path) -> io::Result<File> {
266 #[cfg(unix)]
267 {
268 use std::os::unix::fs::OpenOptionsExt;
269 return OpenOptions::new()
270 .write(true)
271 .create_new(true)
272 .mode(0o600)
273 .open(path);
274 }
275 #[cfg(not(unix))]
276 OpenOptions::new().write(true).create_new(true).open(path)
277}
278
279fn parse_hex_key(value: &str) -> Result<[u8; 32], PinError> {
280 if value.len() != 64 {
281 return Err(PinError::InvalidGeneration(value.to_owned()));
282 }
283 let mut key = [0; 32];
284 for (index, byte) in key.iter_mut().enumerate() {
285 *byte = u8::from_str_radix(&value[index * 2..index * 2 + 2], 16)
286 .map_err(|_| PinError::InvalidGeneration(value.to_owned()))?;
287 }
288 Ok(key)
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn failed_renewal_stops_the_next_put() {
297 let view = tempfile::tempdir().expect("create view");
298 let mut pin = AssemblyPin::create(view.path(), "family", "view", "generation", &[])
299 .expect("create pin");
300 pin.metadata.renewed_at = now_ms().saturating_sub(PIN_RENEW_INTERVAL_MS);
301 fs::remove_file(&pin.metadata_path).expect("remove pin metadata");
302 fs::create_dir(&pin.metadata_path).expect("make renewal destination invalid");
303
304 let mut put_called = false;
305 let result = pin.put(|| {
306 put_called = true;
307 Ok(())
308 });
309 assert!(result.is_err());
310 assert!(!put_called, "a failed renewal must stop the put");
311 }
312}