1use crate::error::{IncludeError, Result};
4use crate::lock;
5use crate::node::Node;
6use crate::options::EntryOptions;
7use indexmap::IndexMap;
8use serde::{Deserialize, Serialize};
9use std::fs;
10use std::io::Write;
11use std::path::{Path, PathBuf};
12use std::sync::{Arc, Condvar, Mutex, Weak};
13use std::time::{Duration, Instant};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum FileFormat {
18 Yaml,
20 Json,
22}
23
24#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
27pub struct Document {
28 #[serde(default, skip_serializing_if = "Vec::is_empty")]
30 pub entries: Vec<EntryOptions>,
31 #[serde(flatten, default, skip_serializing_if = "IndexMap::is_empty")]
33 pub extra: IndexMap<String, Node>,
34}
35
36impl Document {
37 pub fn with_entries(entries: Vec<EntryOptions>) -> Self {
39 Self {
40 entries,
41 extra: IndexMap::new(),
42 }
43 }
44}
45
46struct FileInner {
48 path: PathBuf,
49 format: FileFormat,
50 suspend: Mutex<usize>,
51 write_lock: Mutex<()>,
55 deferred: Mutex<DeferredState>,
56 deferred_signal: Condvar,
57 flusher: Mutex<Option<std::thread::JoinHandle<()>>>,
58}
59
60#[derive(Default)]
62struct DeferredState {
63 pending: Option<(Document, Instant)>,
65 queued: u64,
68 flushed: u64,
69 writing: bool,
71 closed: bool,
73 last_error: Option<String>,
76}
77
78#[derive(Clone)]
86pub struct LoaderFile {
87 inner: Arc<FileInner>,
88}
89
90impl std::fmt::Debug for LoaderFile {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.debug_struct("LoaderFile")
93 .field("path", &self.inner.path)
94 .field("format", &self.inner.format)
95 .finish_non_exhaustive()
96 }
97}
98
99impl LoaderFile {
100 pub fn open(path: impl Into<PathBuf>) -> Result<Self> {
104 let path = path.into();
105 let format = match path.extension().and_then(|ext| ext.to_str()) {
106 Some("yml" | "yaml") => FileFormat::Yaml,
107 Some("json") => FileFormat::Json,
108 _ => return Err(IncludeError::UnknownFormat { path }),
109 };
110 Ok(Self {
111 inner: Arc::new(FileInner {
112 path,
113 format,
114 suspend: Mutex::new(0),
115 write_lock: Mutex::new(()),
116 deferred: Mutex::new(DeferredState::default()),
117 deferred_signal: Condvar::new(),
118 flusher: Mutex::new(None),
119 }),
120 })
121 }
122
123 pub fn path(&self) -> &Path {
125 &self.inner.path
126 }
127
128 pub fn format(&self) -> FileFormat {
130 self.inner.format
131 }
132
133 pub fn read(&self) -> Result<Document> {
137 let content = match fs::read_to_string(&self.inner.path) {
138 Ok(content) => content,
139 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
140 return Ok(Document::default());
141 }
142 Err(error) => return Err(error.into()),
143 };
144 if content.trim().is_empty() {
145 return Ok(Document::default());
146 }
147 match self.inner.format {
148 FileFormat::Yaml => {
149 let node = crate::yaml::parse_node(&content)?;
150 if node.is_null() {
151 return Ok(Document::default());
152 }
153 crate::yaml::document_from_node(node)
154 }
155 FileFormat::Json => {
156 let value =
157 serde_json::from_str::<serde_json::Value>(&content).map_err(|error| {
158 IncludeError::Parse {
159 format: "json",
160 source: Box::new(error),
161 }
162 })?;
163 if value.is_null() {
164 return Ok(Document::default());
165 }
166 serde_json::from_value(value).map_err(|error| IncludeError::Parse {
167 format: "json",
168 source: Box::new(error),
169 })
170 }
171 }
172 }
173
174 pub fn write(&self, document: &Document) -> Result<()> {
177 write_document(&self.inner, document)
178 }
179
180 pub fn write_deferred(&self, document: Document, delay: Duration) {
189 {
190 let mut flusher = crate::lock(&self.inner.flusher);
191 if flusher.is_none() {
192 let weak = Arc::downgrade(&self.inner);
193 match std::thread::Builder::new()
194 .name(format!("cordis-flush-{}", self.inner.path.display()))
195 .spawn(move || flusher_loop(weak))
196 {
197 Ok(thread) => *flusher = Some(thread),
198 Err(_) => {
199 drop(flusher);
200 let _ = self.write(&document);
201 return;
202 }
203 }
204 }
205 }
206 {
207 let mut state = crate::lock(&self.inner.deferred);
208 state.pending = Some((document, Instant::now() + delay));
209 state.queued += 1;
210 }
211 self.inner.deferred_signal.notify_all();
212 }
213
214 pub fn flush_deferred(&self) {
218 let mut state = crate::lock(&self.inner.deferred);
219 let target = state.queued;
220 while state.flushed < target || state.writing || state.pending.is_some() {
221 let (guard, _) = self
222 .inner
223 .deferred_signal
224 .wait_timeout(state, Duration::from_millis(100))
225 .unwrap_or_else(|error| error.into_inner());
226 state = guard;
227 if state.flushed >= target && !state.writing && state.pending.is_none() {
228 return;
229 }
230 }
231 }
232
233 pub fn last_deferred_error(&self) -> Option<String> {
235 crate::lock(&self.inner.deferred).last_error.clone()
236 }
237
238 pub fn suspend(&self) -> FileSuspendGuard {
242 {
243 let mut suspend = lock(&self.inner.suspend);
244 *suspend += 1;
245 }
246 FileSuspendGuard { file: self.clone() }
247 }
248
249 pub fn is_suspended(&self) -> bool {
251 *lock(&self.inner.suspend) > 0
252 }
253}
254
255fn write_document(inner: &FileInner, document: &Document) -> Result<()> {
264 let _write = crate::lock(&inner.write_lock);
265 if *crate::lock(&inner.suspend) > 0 {
266 return Ok(());
267 }
268 if let Ok(metadata) = fs::metadata(&inner.path) {
269 if metadata.permissions().readonly() {
270 return Err(IncludeError::ReadOnly {
271 path: inner.path.clone(),
272 });
273 }
274 }
275 let content = match inner.format {
276 FileFormat::Yaml => crate::yaml::emit_document(document),
277 FileFormat::Json => {
278 let mut text =
279 serde_json::to_string_pretty(document).map_err(|error| IncludeError::Parse {
280 format: "json",
281 source: Box::new(error),
282 })?;
283 text.push('\n');
284 text
285 }
286 };
287 if let Some(parent) = inner.path.parent() {
288 if !parent.as_os_str().is_empty() {
289 fs::create_dir_all(parent)?;
290 }
291 }
292 let file_name = inner
293 .path
294 .file_name()
295 .map(|name| name.to_string_lossy().into_owned())
296 .unwrap_or_default();
297 let tmp = inner.path.with_file_name(format!("{file_name}.tmp"));
298 {
299 let mut file = fs::File::create(&tmp)?;
300 file.write_all(content.as_bytes())?;
301 file.sync_all()?;
302 }
303 fs::rename(&tmp, &inner.path)?;
304 #[cfg(unix)]
309 {
310 if let Some(parent) = inner.path.parent() {
311 if let Ok(dir) = fs::File::open(parent) {
312 let _ = dir.sync_all();
313 }
314 }
315 }
316 Ok(())
317}
318
319fn flusher_loop(weak: Weak<FileInner>) {
322 const SUSPEND_RETRY: Duration = Duration::from_millis(50);
323 while let Some(inner) = weak.upgrade() {
324 let state = crate::lock(&inner.deferred);
325 if state.closed {
326 return;
327 }
328 let deadline = match state.pending.as_ref() {
329 Some((_, deadline)) => *deadline,
330 None => {
331 let waited = inner
332 .deferred_signal
333 .wait(state)
334 .unwrap_or_else(|error| error.into_inner());
335 drop(waited);
336 continue;
337 }
338 };
339 drop(state);
340 let now = Instant::now();
341 if now < deadline {
342 let state = crate::lock(&inner.deferred);
345 let (guard, _) = inner
346 .deferred_signal
347 .wait_timeout(state, deadline - now)
348 .unwrap_or_else(|error| error.into_inner());
349 drop(guard);
350 continue;
351 }
352 let mut state = crate::lock(&inner.deferred);
353 if state.closed {
354 return;
355 }
356 let Some((document, deadline)) = state.pending.take() else {
357 continue;
358 };
359 if Instant::now() < deadline {
360 state.pending = Some((document, deadline));
361 drop(state);
362 continue;
363 }
364 let queued_at_take = state.queued;
365 state.writing = true;
366 drop(state);
367
368 let suspended = *crate::lock(&inner.suspend) > 0;
369 if suspended {
370 let mut state = crate::lock(&inner.deferred);
371 state.pending = Some((document, Instant::now() + SUSPEND_RETRY));
372 state.writing = false;
373 drop(state);
374 inner.deferred_signal.notify_all();
375 continue;
376 }
377 let result = write_document(&inner, &document);
378 let mut state = crate::lock(&inner.deferred);
379 state.writing = false;
380 state.flushed = state.flushed.max(queued_at_take);
381 if let Err(error) = result {
382 state.last_error = Some(error.to_string());
383 }
384 drop(state);
385 inner.deferred_signal.notify_all();
386 }
387}
388
389impl Drop for FileInner {
390 fn drop(&mut self) {
391 {
392 let mut state = crate::lock(&self.deferred);
393 state.closed = true;
394 }
395 self.deferred_signal.notify_all();
396 if let Some(thread) = self
397 .flusher
398 .lock()
399 .unwrap_or_else(|error| error.into_inner())
400 .take()
401 {
402 let _ = thread.join();
403 }
404 if let Some((document, _)) = self
407 .deferred
408 .lock()
409 .unwrap_or_else(|error| error.into_inner())
410 .pending
411 .take()
412 {
413 let _ = write_document(self, &document);
414 }
415 }
416}
417
418#[derive(Debug)]
420pub struct FileSuspendGuard {
421 file: LoaderFile,
422}
423
424impl Drop for FileSuspendGuard {
425 fn drop(&mut self) {
426 let mut suspend = lock(&self.file.inner.suspend);
427 *suspend = suspend.saturating_sub(1);
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434 use std::time::{SystemTime, UNIX_EPOCH};
435
436 fn temp_file(stem: &str) -> LoaderFile {
437 let path = std::env::temp_dir().join(format!(
438 "cordis-include-write-test-{stem}-{}-{}.yml",
439 std::process::id(),
440 randomish()
441 ));
442 let _ = std::fs::remove_file(&path);
443 LoaderFile::open(path).unwrap()
444 }
445
446 fn randomish() -> u64 {
447 SystemTime::now()
448 .duration_since(UNIX_EPOCH)
449 .map(|elapsed| elapsed.as_nanos() as u64)
450 .unwrap_or(0)
451 }
452
453 fn padded_document(tag: u64) -> Document {
454 let filler = "x".repeat(4096);
456 let config = Node::String(format!("{tag}-{filler}"));
457 Document::with_entries(vec![
458 EntryOptions::new("w").with_id("w").with_config(config),
459 ])
460 }
461
462 #[test]
467 fn concurrent_writers_never_produce_a_torn_file() {
468 for _ in 0..25 {
469 let file = temp_file("race");
470 let writers: Vec<_> = (0..4_u64)
471 .map(|tag| {
472 let file = file.clone();
473 std::thread::spawn(move || {
474 let document = padded_document(tag);
475 for _ in 0..5 {
476 file.write(&document).unwrap();
477 }
478 })
479 })
480 .collect();
481 for writer in writers {
482 writer.join().unwrap();
483 }
484 let document = file.read().unwrap();
485 let config = document.entries[0].config.clone().unwrap();
486 let Node::String(text) = &config else {
487 panic!("config is not the string one writer wrote: {config:?}");
488 };
489 let tag: u64 = text.split('-').next().unwrap().parse().unwrap();
490 let expected = padded_document(tag);
491 assert_eq!(document, expected, "torn or mixed content survived");
492 let _ = std::fs::remove_file(file.path());
493 }
494 }
495
496 #[test]
499 fn concurrent_deferred_and_sync_writes_stay_parseable() {
500 for _ in 0..25 {
501 let file = temp_file("deferred-race");
502 let deferred = std::thread::spawn({
503 let file = file.clone();
504 move || {
505 for tag in 0..20_u64 {
506 file.write_deferred(padded_document(tag), Duration::from_millis(1));
507 }
508 file.flush_deferred();
509 }
510 });
511 for tag in 100..120_u64 {
512 file.write(&padded_document(tag)).unwrap();
513 }
514 deferred.join().unwrap();
515 let document = file.read().unwrap();
516 let config = document.entries[0].config.clone().unwrap();
517 let Node::String(text) = &config else {
518 panic!("config is not the string one writer wrote: {config:?}");
519 };
520 let tag: u64 = text.split('-').next().unwrap().parse().unwrap();
521 assert_eq!(document, padded_document(tag));
522 let _ = std::fs::remove_file(file.path());
523 }
524 }
525}