superfluid_daemon/
store_id.rs1use std::path::Path;
4
5use crate::DaemonError;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
8pub struct StoreId(pub [u8; 16]);
9
10impl StoreId {
11 pub fn mint() -> StoreId {
12 let mut b = [0u8; 16];
13 let rc = unsafe { libc::getentropy(b.as_mut_ptr().cast(), b.len()) };
16 if rc != 0 {
17 static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
18 let t = std::time::SystemTime::now()
19 .duration_since(std::time::UNIX_EPOCH)
20 .map(|d| d.as_nanos())
21 .unwrap_or(0);
22 let seed = [
23 t.to_le_bytes().as_slice(),
24 &std::process::id().to_le_bytes(),
25 &N.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
26 .to_le_bytes(),
27 ]
28 .concat();
29 b = xxhash_rust::xxh3::xxh3_128(&seed).to_le_bytes();
30 }
31 StoreId(b)
32 }
33
34 pub fn to_hex(&self) -> String {
35 self.0.iter().map(|b| format!("{b:02x}")).collect()
36 }
37
38 pub fn from_hex(s: &str) -> Option<StoreId> {
39 let s = s.trim();
40 if s.len() != 32 || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
41 return None;
42 }
43 let mut b = [0u8; 16];
44 for (i, out) in b.iter_mut().enumerate() {
45 *out = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).ok()?;
46 }
47 Some(StoreId(b))
48 }
49
50 pub fn to_file_line(&self) -> String {
51 format!("{} {:016x}\n", self.to_hex(), Self::checksum(&self.0))
52 }
53
54 fn checksum(b: &[u8; 16]) -> u64 {
55 xxhash_rust::xxh3::xxh3_64_with_seed(b, 0x5354_4f52_4549_4421)
56 }
57
58 pub fn from_file_line(text: &str) -> Option<StoreId> {
59 let mut parts = text.split_whitespace();
60 let (id, sum) = (parts.next()?, parts.next()?);
61 if parts.next().is_some() {
62 return None;
63 }
64 let id = StoreId::from_hex(id)?;
65 let sum = u64::from_str_radix(sum, 16)
66 .ok()
67 .filter(|_| sum.len() == 16)?;
68 (Self::checksum(&id.0) == sum).then_some(id)
69 }
70
71 pub fn load_or_mint(path: &Path) -> Result<StoreId, DaemonError> {
72 let lock = StoreId::lock(path)?;
73 Self::open(path, false, &lock)
74 }
75
76 pub fn lock(path: &Path) -> Result<StoreIdLock, DaemonError> {
77 Ok(StoreIdLock::acquire(path, libc::LOCK_EX)?.expect("a blocking flock returns held"))
78 }
79
80 pub fn try_lock(path: &Path) -> Result<Option<StoreIdLock>, DaemonError> {
81 StoreIdLock::acquire(path, libc::LOCK_EX | libc::LOCK_NB)
82 }
83
84 pub fn open(path: &Path, fresh_wal: bool, _lock: &StoreIdLock) -> Result<StoreId, DaemonError> {
85 let dir = path
86 .parent()
87 .filter(|d| !d.as_os_str().is_empty())
88 .unwrap_or(Path::new("."));
89 let existing = Self::load(path)?;
90 if let (false, Some(id)) = (fresh_wal, existing) {
91 sync_dir(dir)?;
92 return Ok(id);
93 }
94 let id = StoreId::mint();
95 let name = path
96 .file_name()
97 .and_then(|n| n.to_str())
98 .unwrap_or("store-id");
99 let tmp = TempFile::write(dir, name, &id)?;
100 let id = if existing.is_some() {
101 std::fs::rename(&tmp.path, path)?;
102 id
103 } else {
104 match std::fs::hard_link(&tmp.path, path) {
105 Ok(()) => id,
106 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Self::load(path)?
107 .ok_or(DaemonError::Protocol(
108 "store id vanished while being published",
109 ))?,
110 Err(e) => return Err(e.into()),
111 }
112 };
113 drop(tmp);
114 sync_dir(dir)?;
115 Ok(id)
116 }
117
118 fn load(path: &Path) -> Result<Option<StoreId>, DaemonError> {
119 match std::fs::read_to_string(path) {
120 Ok(text) => StoreId::from_file_line(&text)
121 .map(Some)
122 .ok_or(DaemonError::Config(
123 "the session store's id file is corrupt; it only names the store's \
124 trace ids — delete it to mint a new one (exported trace ids change)",
125 )),
126 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
127 Err(e) => Err(e.into()),
128 }
129 }
130}
131
132pub struct StoreIdLock {
133 _file: std::fs::File,
134}
135
136impl StoreIdLock {
137 fn acquire(id_path: &Path, op: libc::c_int) -> Result<Option<StoreIdLock>, DaemonError> {
138 use std::os::fd::AsRawFd;
139 let mut name = id_path
140 .file_name()
141 .map(|n| n.to_os_string())
142 .unwrap_or_default();
143 name.push(".lock");
144 let file = std::fs::OpenOptions::new()
145 .read(true)
146 .write(true)
147 .create(true)
148 .truncate(false)
149 .open(id_path.with_file_name(name))?;
150 loop {
151 let rc = unsafe { libc::flock(file.as_raw_fd(), op) };
154 if rc == 0 {
155 return Ok(Some(StoreIdLock { _file: file }));
156 }
157 let err = std::io::Error::last_os_error();
158 match err.kind() {
159 std::io::ErrorKind::Interrupted => {}
160 std::io::ErrorKind::WouldBlock => return Ok(None),
161 _ => return Err(err.into()),
162 }
163 }
164 }
165}
166
167fn sync_dir(dir: &Path) -> Result<(), DaemonError> {
168 std::fs::File::open(dir)?.sync_all()?;
169 Ok(())
170}
171
172struct TempFile {
173 path: std::path::PathBuf,
174}
175
176impl TempFile {
177 fn write(dir: &Path, name: &str, id: &StoreId) -> Result<TempFile, DaemonError> {
178 use std::io::Write;
179 let path = dir.join(format!(
180 ".{name}.tmp.{}.{}",
181 std::process::id(),
182 id.to_hex()
183 ));
184 let mut f = std::fs::OpenOptions::new()
185 .write(true)
186 .create_new(true)
187 .open(&path)?;
188 let tmp = TempFile { path };
189 f.write_all(id.to_file_line().as_bytes())?;
190 f.sync_all()?;
191 Ok(tmp)
192 }
193}
194
195impl Drop for TempFile {
196 fn drop(&mut self) {
197 let _ = std::fs::remove_file(&self.path);
198 }
199}
200
201pub fn store_id_path(wal: &Path) -> std::path::PathBuf {
202 let mut name = wal
203 .file_name()
204 .map(|n| n.to_os_string())
205 .unwrap_or_default();
206 name.push(".store-id");
207 wal.with_file_name(name)
208}