@@ -3,8 +3,10 @@ |
| 3 | 3 | use std::collections::HashMap; |
| 4 | 4 | use std::fs; |
| 5 | 5 | use std::io; |
| 6 | +#[cfg(unix)] |
| 7 | +use std::os::unix::fs::PermissionsExt; |
| 6 | 8 | use std::path::Path; |
| 7 | | -use std::time::{Duration, Instant}; |
| 9 | +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; |
| 8 | 10 | |
| 9 | 11 | use crate::request::{RequestOwner, RequestRecord, RequestRegistry, RequestState}; |
| 10 | 12 | use crate::window::parse_optional_parent_window; |
@@ -45,7 +47,41 @@ pub fn persist_registry(path: &Path, registry: &RequestRegistry) -> io::Result<( |
| 45 | 47 | output.push_str(&format_record_line(&record)); |
| 46 | 48 | output.push('\n'); |
| 47 | 49 | } |
| 48 | | - fs::write(path, output) |
| 50 | + atomic_write(path, output.as_bytes()) |
| 51 | +} |
| 52 | + |
| 53 | +fn atomic_write(path: &Path, data: &[u8]) -> io::Result<()> { |
| 54 | + if let Some(parent) = path.parent() { |
| 55 | + fs::create_dir_all(parent)?; |
| 56 | + } |
| 57 | + |
| 58 | + let temp_path = unique_temp_path(path); |
| 59 | + let mut file = fs::OpenOptions::new() |
| 60 | + .create_new(true) |
| 61 | + .write(true) |
| 62 | + .open(&temp_path)?; |
| 63 | + #[cfg(unix)] |
| 64 | + { |
| 65 | + file.set_permissions(fs::Permissions::from_mode(0o600))?; |
| 66 | + } |
| 67 | + io::Write::write_all(&mut file, data)?; |
| 68 | + file.sync_all()?; |
| 69 | + drop(file); |
| 70 | + |
| 71 | + fs::rename(&temp_path, path)?; |
| 72 | + Ok(()) |
| 73 | +} |
| 74 | + |
| 75 | +fn unique_temp_path(path: &Path) -> std::path::PathBuf { |
| 76 | + let nanos = SystemTime::now() |
| 77 | + .duration_since(UNIX_EPOCH) |
| 78 | + .map_or(0, |duration| duration.as_nanos()); |
| 79 | + let parent = path.parent().unwrap_or_else(|| Path::new(".")); |
| 80 | + let file_name = path |
| 81 | + .file_name() |
| 82 | + .and_then(|name| name.to_str()) |
| 83 | + .unwrap_or("state"); |
| 84 | + parent.join(format!(".{file_name}.tmp-{}-{nanos}", std::process::id())) |
| 49 | 85 | } |
| 50 | 86 | |
| 51 | 87 | fn format_record_line(record: &RequestRecord) -> String { |
@@ -176,4 +212,36 @@ mod tests { |
| 176 | 212 | |
| 177 | 213 | let _ = fs::remove_file(path); |
| 178 | 214 | } |
| 215 | + |
| 216 | + #[test] |
| 217 | + fn persist_overwrites_previous_contents() { |
| 218 | + let path = unique_temp_file(); |
| 219 | + let mut first = RequestRegistry::new(Duration::from_secs(5)); |
| 220 | + first |
| 221 | + .begin_at( |
| 222 | + "req-first", |
| 223 | + RequestOwner::new(":1.2", None), |
| 224 | + None, |
| 225 | + Instant::now(), |
| 226 | + ) |
| 227 | + .expect("request should be created"); |
| 228 | + persist_registry(&path, &first).expect("first persist should succeed"); |
| 229 | + |
| 230 | + let mut second = RequestRegistry::new(Duration::from_secs(5)); |
| 231 | + second |
| 232 | + .begin_at( |
| 233 | + "req-second", |
| 234 | + RequestOwner::new(":1.3", Some("org.test.App".to_string())), |
| 235 | + None, |
| 236 | + Instant::now(), |
| 237 | + ) |
| 238 | + .expect("request should be created"); |
| 239 | + persist_registry(&path, &second).expect("second persist should succeed"); |
| 240 | + |
| 241 | + let loaded = load_registry(&path, Duration::from_secs(5)).expect("registry should load"); |
| 242 | + assert_eq!(loaded.state("req-first"), None); |
| 243 | + assert_eq!(loaded.state("req-second"), Some(RequestState::Pending)); |
| 244 | + |
| 245 | + let _ = fs::remove_file(path); |
| 246 | + } |
| 179 | 247 | } |