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 value =
150 serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content).map_err(|error| {
151 IncludeError::Parse {
152 format: "yaml",
153 source: Box::new(error),
154 }
155 })?;
156 if value.is_null() {
157 return Ok(Document::default());
158 }
159 serde_yaml_ng::from_value(value).map_err(|error| IncludeError::Parse {
160 format: "yaml",
161 source: Box::new(error),
162 })
163 }
164 FileFormat::Json => {
165 let value =
166 serde_json::from_str::<serde_json::Value>(&content).map_err(|error| {
167 IncludeError::Parse {
168 format: "json",
169 source: Box::new(error),
170 }
171 })?;
172 if value.is_null() {
173 return Ok(Document::default());
174 }
175 serde_json::from_value(value).map_err(|error| IncludeError::Parse {
176 format: "json",
177 source: Box::new(error),
178 })
179 }
180 }
181 }
182
183 pub fn write(&self, document: &Document) -> Result<()> {
186 write_document(&self.inner, document)
187 }
188
189 pub fn write_deferred(&self, document: Document, delay: Duration) {
198 {
199 let mut flusher = crate::lock(&self.inner.flusher);
200 if flusher.is_none() {
201 let weak = Arc::downgrade(&self.inner);
202 match std::thread::Builder::new()
203 .name(format!("cordis-flush-{}", self.inner.path.display()))
204 .spawn(move || flusher_loop(weak))
205 {
206 Ok(thread) => *flusher = Some(thread),
207 Err(_) => {
208 drop(flusher);
209 let _ = self.write(&document);
210 return;
211 }
212 }
213 }
214 }
215 {
216 let mut state = crate::lock(&self.inner.deferred);
217 state.pending = Some((document, Instant::now() + delay));
218 state.queued += 1;
219 }
220 self.inner.deferred_signal.notify_all();
221 }
222
223 pub fn flush_deferred(&self) {
227 let mut state = crate::lock(&self.inner.deferred);
228 let target = state.queued;
229 while state.flushed < target || state.writing || state.pending.is_some() {
230 let (guard, _) = self
231 .inner
232 .deferred_signal
233 .wait_timeout(state, Duration::from_millis(100))
234 .unwrap_or_else(|error| error.into_inner());
235 state = guard;
236 if state.flushed >= target && !state.writing && state.pending.is_none() {
237 return;
238 }
239 }
240 }
241
242 pub fn last_deferred_error(&self) -> Option<String> {
244 crate::lock(&self.inner.deferred).last_error.clone()
245 }
246
247 pub fn suspend(&self) -> FileSuspendGuard {
251 {
252 let mut suspend = lock(&self.inner.suspend);
253 *suspend += 1;
254 }
255 FileSuspendGuard { file: self.clone() }
256 }
257
258 pub fn is_suspended(&self) -> bool {
260 *lock(&self.inner.suspend) > 0
261 }
262}
263
264fn write_document(inner: &FileInner, document: &Document) -> Result<()> {
272 let _write = crate::lock(&inner.write_lock);
273 if *crate::lock(&inner.suspend) > 0 {
274 return Ok(());
275 }
276 if let Ok(metadata) = fs::metadata(&inner.path) {
277 if metadata.permissions().readonly() {
278 return Err(IncludeError::ReadOnly {
279 path: inner.path.clone(),
280 });
281 }
282 }
283 let content = match inner.format {
284 FileFormat::Yaml => {
285 serde_yaml_ng::to_string(document).map_err(|error| IncludeError::Parse {
286 format: "yaml",
287 source: Box::new(error),
288 })?
289 }
290 FileFormat::Json => {
291 let mut text =
292 serde_json::to_string_pretty(document).map_err(|error| IncludeError::Parse {
293 format: "json",
294 source: Box::new(error),
295 })?;
296 text.push('\n');
297 text
298 }
299 };
300 if let Some(parent) = inner.path.parent() {
301 if !parent.as_os_str().is_empty() {
302 fs::create_dir_all(parent)?;
303 }
304 }
305 let file_name = inner
306 .path
307 .file_name()
308 .map(|name| name.to_string_lossy().into_owned())
309 .unwrap_or_default();
310 let tmp = inner.path.with_file_name(format!("{file_name}.tmp"));
311 {
312 let mut file = fs::File::create(&tmp)?;
313 file.write_all(content.as_bytes())?;
314 file.sync_all()?;
315 }
316 fs::rename(&tmp, &inner.path)?;
317 Ok(())
318}
319
320fn flusher_loop(weak: Weak<FileInner>) {
323 const SUSPEND_RETRY: Duration = Duration::from_millis(50);
324 while let Some(inner) = weak.upgrade() {
325 let state = crate::lock(&inner.deferred);
326 if state.closed {
327 return;
328 }
329 let deadline = match state.pending.as_ref() {
330 Some((_, deadline)) => *deadline,
331 None => {
332 let waited = inner
333 .deferred_signal
334 .wait(state)
335 .unwrap_or_else(|error| error.into_inner());
336 drop(waited);
337 continue;
338 }
339 };
340 drop(state);
341 let now = Instant::now();
342 if now < deadline {
343 let state = crate::lock(&inner.deferred);
346 let (guard, _) = inner
347 .deferred_signal
348 .wait_timeout(state, deadline - now)
349 .unwrap_or_else(|error| error.into_inner());
350 drop(guard);
351 continue;
352 }
353 let mut state = crate::lock(&inner.deferred);
354 if state.closed {
355 return;
356 }
357 let Some((document, deadline)) = state.pending.take() else {
358 continue;
359 };
360 if Instant::now() < deadline {
361 state.pending = Some((document, deadline));
362 drop(state);
363 continue;
364 }
365 let queued_at_take = state.queued;
366 state.writing = true;
367 drop(state);
368
369 let suspended = *crate::lock(&inner.suspend) > 0;
370 if suspended {
371 let mut state = crate::lock(&inner.deferred);
372 state.pending = Some((document, Instant::now() + SUSPEND_RETRY));
373 state.writing = false;
374 drop(state);
375 inner.deferred_signal.notify_all();
376 continue;
377 }
378 let result = write_document(&inner, &document);
379 let mut state = crate::lock(&inner.deferred);
380 state.writing = false;
381 state.flushed = state.flushed.max(queued_at_take);
382 if let Err(error) = result {
383 state.last_error = Some(error.to_string());
384 }
385 drop(state);
386 inner.deferred_signal.notify_all();
387 }
388}
389
390impl Drop for FileInner {
391 fn drop(&mut self) {
392 {
393 let mut state = crate::lock(&self.deferred);
394 state.closed = true;
395 }
396 self.deferred_signal.notify_all();
397 if let Some(thread) = self
398 .flusher
399 .lock()
400 .unwrap_or_else(|error| error.into_inner())
401 .take()
402 {
403 let _ = thread.join();
404 }
405 if let Some((document, _)) = self
408 .deferred
409 .lock()
410 .unwrap_or_else(|error| error.into_inner())
411 .pending
412 .take()
413 {
414 let _ = write_document(self, &document);
415 }
416 }
417}
418
419#[derive(Debug)]
421pub struct FileSuspendGuard {
422 file: LoaderFile,
423}
424
425impl Drop for FileSuspendGuard {
426 fn drop(&mut self) {
427 let mut suspend = lock(&self.file.inner.suspend);
428 *suspend = suspend.saturating_sub(1);
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435 use std::time::{SystemTime, UNIX_EPOCH};
436
437 fn temp_file(stem: &str) -> LoaderFile {
438 let path = std::env::temp_dir().join(format!(
439 "cordis-include-write-test-{stem}-{}-{}.yml",
440 std::process::id(),
441 randomish()
442 ));
443 let _ = std::fs::remove_file(&path);
444 LoaderFile::open(path).unwrap()
445 }
446
447 fn randomish() -> u64 {
448 SystemTime::now()
449 .duration_since(UNIX_EPOCH)
450 .map(|elapsed| elapsed.as_nanos() as u64)
451 .unwrap_or(0)
452 }
453
454 fn padded_document(tag: u64) -> Document {
455 let filler = "x".repeat(4096);
457 let config = Node::String(format!("{tag}-{filler}"));
458 Document::with_entries(vec![
459 EntryOptions::new("w").with_id("w").with_config(config),
460 ])
461 }
462
463 #[test]
468 fn concurrent_writers_never_produce_a_torn_file() {
469 for _ in 0..25 {
470 let file = temp_file("race");
471 let writers: Vec<_> = (0..4_u64)
472 .map(|tag| {
473 let file = file.clone();
474 std::thread::spawn(move || {
475 let document = padded_document(tag);
476 for _ in 0..5 {
477 file.write(&document).unwrap();
478 }
479 })
480 })
481 .collect();
482 for writer in writers {
483 writer.join().unwrap();
484 }
485 let document = file.read().unwrap();
486 let config = document.entries[0].config.clone().unwrap();
487 let Node::String(text) = &config else {
488 panic!("config is not the string one writer wrote: {config:?}");
489 };
490 let tag: u64 = text.split('-').next().unwrap().parse().unwrap();
491 let expected = padded_document(tag);
492 assert_eq!(document, expected, "torn or mixed content survived");
493 let _ = std::fs::remove_file(file.path());
494 }
495 }
496
497 #[test]
500 fn concurrent_deferred_and_sync_writes_stay_parseable() {
501 for _ in 0..25 {
502 let file = temp_file("deferred-race");
503 let deferred = std::thread::spawn({
504 let file = file.clone();
505 move || {
506 for tag in 0..20_u64 {
507 file.write_deferred(padded_document(tag), Duration::from_millis(1));
508 }
509 file.flush_deferred();
510 }
511 });
512 for tag in 100..120_u64 {
513 file.write(&padded_document(tag)).unwrap();
514 }
515 deferred.join().unwrap();
516 let document = file.read().unwrap();
517 let config = document.entries[0].config.clone().unwrap();
518 let Node::String(text) = &config else {
519 panic!("config is not the string one writer wrote: {config:?}");
520 };
521 let tag: u64 = text.split('-').next().unwrap().parse().unwrap();
522 assert_eq!(document, padded_document(tag));
523 let _ = std::fs::remove_file(file.path());
524 }
525 }
526}