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<()> {
263 let _write = crate::lock(&inner.write_lock);
264 if *crate::lock(&inner.suspend) > 0 {
265 return Ok(());
266 }
267 if let Ok(metadata) = fs::metadata(&inner.path) {
268 if metadata.permissions().readonly() {
269 return Err(IncludeError::ReadOnly {
270 path: inner.path.clone(),
271 });
272 }
273 }
274 let content = match inner.format {
275 FileFormat::Yaml => crate::yaml::emit_document(document),
276 FileFormat::Json => {
277 let mut text =
278 serde_json::to_string_pretty(document).map_err(|error| IncludeError::Parse {
279 format: "json",
280 source: Box::new(error),
281 })?;
282 text.push('\n');
283 text
284 }
285 };
286 if let Some(parent) = inner.path.parent() {
287 if !parent.as_os_str().is_empty() {
288 fs::create_dir_all(parent)?;
289 }
290 }
291 let file_name = inner
292 .path
293 .file_name()
294 .map(|name| name.to_string_lossy().into_owned())
295 .unwrap_or_default();
296 let tmp = inner.path.with_file_name(format!("{file_name}.tmp"));
297 {
298 let mut file = fs::File::create(&tmp)?;
299 file.write_all(content.as_bytes())?;
300 file.sync_all()?;
301 }
302 fs::rename(&tmp, &inner.path)?;
303 Ok(())
304}
305
306fn flusher_loop(weak: Weak<FileInner>) {
309 const SUSPEND_RETRY: Duration = Duration::from_millis(50);
310 while let Some(inner) = weak.upgrade() {
311 let state = crate::lock(&inner.deferred);
312 if state.closed {
313 return;
314 }
315 let deadline = match state.pending.as_ref() {
316 Some((_, deadline)) => *deadline,
317 None => {
318 let waited = inner
319 .deferred_signal
320 .wait(state)
321 .unwrap_or_else(|error| error.into_inner());
322 drop(waited);
323 continue;
324 }
325 };
326 drop(state);
327 let now = Instant::now();
328 if now < deadline {
329 let state = crate::lock(&inner.deferred);
332 let (guard, _) = inner
333 .deferred_signal
334 .wait_timeout(state, deadline - now)
335 .unwrap_or_else(|error| error.into_inner());
336 drop(guard);
337 continue;
338 }
339 let mut state = crate::lock(&inner.deferred);
340 if state.closed {
341 return;
342 }
343 let Some((document, deadline)) = state.pending.take() else {
344 continue;
345 };
346 if Instant::now() < deadline {
347 state.pending = Some((document, deadline));
348 drop(state);
349 continue;
350 }
351 let queued_at_take = state.queued;
352 state.writing = true;
353 drop(state);
354
355 let suspended = *crate::lock(&inner.suspend) > 0;
356 if suspended {
357 let mut state = crate::lock(&inner.deferred);
358 state.pending = Some((document, Instant::now() + SUSPEND_RETRY));
359 state.writing = false;
360 drop(state);
361 inner.deferred_signal.notify_all();
362 continue;
363 }
364 let result = write_document(&inner, &document);
365 let mut state = crate::lock(&inner.deferred);
366 state.writing = false;
367 state.flushed = state.flushed.max(queued_at_take);
368 if let Err(error) = result {
369 state.last_error = Some(error.to_string());
370 }
371 drop(state);
372 inner.deferred_signal.notify_all();
373 }
374}
375
376impl Drop for FileInner {
377 fn drop(&mut self) {
378 {
379 let mut state = crate::lock(&self.deferred);
380 state.closed = true;
381 }
382 self.deferred_signal.notify_all();
383 if let Some(thread) = self
384 .flusher
385 .lock()
386 .unwrap_or_else(|error| error.into_inner())
387 .take()
388 {
389 let _ = thread.join();
390 }
391 if let Some((document, _)) = self
394 .deferred
395 .lock()
396 .unwrap_or_else(|error| error.into_inner())
397 .pending
398 .take()
399 {
400 let _ = write_document(self, &document);
401 }
402 }
403}
404
405#[derive(Debug)]
407pub struct FileSuspendGuard {
408 file: LoaderFile,
409}
410
411impl Drop for FileSuspendGuard {
412 fn drop(&mut self) {
413 let mut suspend = lock(&self.file.inner.suspend);
414 *suspend = suspend.saturating_sub(1);
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use std::time::{SystemTime, UNIX_EPOCH};
422
423 fn temp_file(stem: &str) -> LoaderFile {
424 let path = std::env::temp_dir().join(format!(
425 "cordis-include-write-test-{stem}-{}-{}.yml",
426 std::process::id(),
427 randomish()
428 ));
429 let _ = std::fs::remove_file(&path);
430 LoaderFile::open(path).unwrap()
431 }
432
433 fn randomish() -> u64 {
434 SystemTime::now()
435 .duration_since(UNIX_EPOCH)
436 .map(|elapsed| elapsed.as_nanos() as u64)
437 .unwrap_or(0)
438 }
439
440 fn padded_document(tag: u64) -> Document {
441 let filler = "x".repeat(4096);
443 let config = Node::String(format!("{tag}-{filler}"));
444 Document::with_entries(vec![
445 EntryOptions::new("w").with_id("w").with_config(config),
446 ])
447 }
448
449 #[test]
454 fn concurrent_writers_never_produce_a_torn_file() {
455 for _ in 0..25 {
456 let file = temp_file("race");
457 let writers: Vec<_> = (0..4_u64)
458 .map(|tag| {
459 let file = file.clone();
460 std::thread::spawn(move || {
461 let document = padded_document(tag);
462 for _ in 0..5 {
463 file.write(&document).unwrap();
464 }
465 })
466 })
467 .collect();
468 for writer in writers {
469 writer.join().unwrap();
470 }
471 let document = file.read().unwrap();
472 let config = document.entries[0].config.clone().unwrap();
473 let Node::String(text) = &config else {
474 panic!("config is not the string one writer wrote: {config:?}");
475 };
476 let tag: u64 = text.split('-').next().unwrap().parse().unwrap();
477 let expected = padded_document(tag);
478 assert_eq!(document, expected, "torn or mixed content survived");
479 let _ = std::fs::remove_file(file.path());
480 }
481 }
482
483 #[test]
486 fn concurrent_deferred_and_sync_writes_stay_parseable() {
487 for _ in 0..25 {
488 let file = temp_file("deferred-race");
489 let deferred = std::thread::spawn({
490 let file = file.clone();
491 move || {
492 for tag in 0..20_u64 {
493 file.write_deferred(padded_document(tag), Duration::from_millis(1));
494 }
495 file.flush_deferred();
496 }
497 });
498 for tag in 100..120_u64 {
499 file.write(&padded_document(tag)).unwrap();
500 }
501 deferred.join().unwrap();
502 let document = file.read().unwrap();
503 let config = document.entries[0].config.clone().unwrap();
504 let Node::String(text) = &config else {
505 panic!("config is not the string one writer wrote: {config:?}");
506 };
507 let tag: u64 = text.split('-').next().unwrap().parse().unwrap();
508 assert_eq!(document, padded_document(tag));
509 let _ = std::fs::remove_file(file.path());
510 }
511 }
512}