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 deferred: Mutex<DeferredState>,
52 deferred_signal: Condvar,
53 flusher: Mutex<Option<std::thread::JoinHandle<()>>>,
54}
55
56#[derive(Default)]
58struct DeferredState {
59 pending: Option<(Document, Instant)>,
61 queued: u64,
64 flushed: u64,
65 writing: bool,
67 closed: bool,
69 last_error: Option<String>,
72}
73
74#[derive(Clone)]
82pub struct LoaderFile {
83 inner: Arc<FileInner>,
84}
85
86impl std::fmt::Debug for LoaderFile {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 f.debug_struct("LoaderFile")
89 .field("path", &self.inner.path)
90 .field("format", &self.inner.format)
91 .finish_non_exhaustive()
92 }
93}
94
95impl LoaderFile {
96 pub fn open(path: impl Into<PathBuf>) -> Result<Self> {
100 let path = path.into();
101 let format = match path.extension().and_then(|ext| ext.to_str()) {
102 Some("yml" | "yaml") => FileFormat::Yaml,
103 Some("json") => FileFormat::Json,
104 _ => return Err(IncludeError::UnknownFormat { path }),
105 };
106 Ok(Self {
107 inner: Arc::new(FileInner {
108 path,
109 format,
110 suspend: Mutex::new(0),
111 deferred: Mutex::new(DeferredState::default()),
112 deferred_signal: Condvar::new(),
113 flusher: Mutex::new(None),
114 }),
115 })
116 }
117
118 pub fn path(&self) -> &Path {
120 &self.inner.path
121 }
122
123 pub fn format(&self) -> FileFormat {
125 self.inner.format
126 }
127
128 pub fn read(&self) -> Result<Document> {
132 let content = match fs::read_to_string(&self.inner.path) {
133 Ok(content) => content,
134 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
135 return Ok(Document::default());
136 }
137 Err(error) => return Err(error.into()),
138 };
139 if content.trim().is_empty() {
140 return Ok(Document::default());
141 }
142 match self.inner.format {
143 FileFormat::Yaml => {
144 let value =
145 serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content).map_err(|error| {
146 IncludeError::Parse {
147 format: "yaml",
148 source: Box::new(error),
149 }
150 })?;
151 if value.is_null() {
152 return Ok(Document::default());
153 }
154 serde_yaml_ng::from_value(value).map_err(|error| IncludeError::Parse {
155 format: "yaml",
156 source: Box::new(error),
157 })
158 }
159 FileFormat::Json => {
160 let value =
161 serde_json::from_str::<serde_json::Value>(&content).map_err(|error| {
162 IncludeError::Parse {
163 format: "json",
164 source: Box::new(error),
165 }
166 })?;
167 if value.is_null() {
168 return Ok(Document::default());
169 }
170 serde_json::from_value(value).map_err(|error| IncludeError::Parse {
171 format: "json",
172 source: Box::new(error),
173 })
174 }
175 }
176 }
177
178 pub fn write(&self, document: &Document) -> Result<()> {
181 write_document(&self.inner, document)
182 }
183
184 pub fn write_deferred(&self, document: Document, delay: Duration) {
193 {
194 let mut flusher = crate::lock(&self.inner.flusher);
195 if flusher.is_none() {
196 let weak = Arc::downgrade(&self.inner);
197 match std::thread::Builder::new()
198 .name(format!("cordis-flush-{}", self.inner.path.display()))
199 .spawn(move || flusher_loop(weak))
200 {
201 Ok(thread) => *flusher = Some(thread),
202 Err(_) => {
203 drop(flusher);
204 let _ = self.write(&document);
205 return;
206 }
207 }
208 }
209 }
210 {
211 let mut state = crate::lock(&self.inner.deferred);
212 state.pending = Some((document, Instant::now() + delay));
213 state.queued += 1;
214 }
215 self.inner.deferred_signal.notify_all();
216 }
217
218 pub fn flush_deferred(&self) {
222 let mut state = crate::lock(&self.inner.deferred);
223 let target = state.queued;
224 while state.flushed < target || state.writing || state.pending.is_some() {
225 let (guard, _) = self
226 .inner
227 .deferred_signal
228 .wait_timeout(state, Duration::from_millis(100))
229 .unwrap_or_else(|error| error.into_inner());
230 state = guard;
231 if state.flushed >= target && !state.writing && state.pending.is_none() {
232 return;
233 }
234 }
235 }
236
237 pub fn last_deferred_error(&self) -> Option<String> {
239 crate::lock(&self.inner.deferred).last_error.clone()
240 }
241
242 pub fn suspend(&self) -> FileSuspendGuard {
246 {
247 let mut suspend = lock(&self.inner.suspend);
248 *suspend += 1;
249 }
250 FileSuspendGuard { file: self.clone() }
251 }
252
253 pub fn is_suspended(&self) -> bool {
255 *lock(&self.inner.suspend) > 0
256 }
257}
258
259fn write_document(inner: &FileInner, document: &Document) -> Result<()> {
262 if *crate::lock(&inner.suspend) > 0 {
263 return Ok(());
264 }
265 if let Ok(metadata) = fs::metadata(&inner.path) {
266 if metadata.permissions().readonly() {
267 return Err(IncludeError::ReadOnly {
268 path: inner.path.clone(),
269 });
270 }
271 }
272 let content = match inner.format {
273 FileFormat::Yaml => {
274 serde_yaml_ng::to_string(document).map_err(|error| IncludeError::Parse {
275 format: "yaml",
276 source: Box::new(error),
277 })?
278 }
279 FileFormat::Json => {
280 let mut text =
281 serde_json::to_string_pretty(document).map_err(|error| IncludeError::Parse {
282 format: "json",
283 source: Box::new(error),
284 })?;
285 text.push('\n');
286 text
287 }
288 };
289 if let Some(parent) = inner.path.parent() {
290 if !parent.as_os_str().is_empty() {
291 fs::create_dir_all(parent)?;
292 }
293 }
294 let file_name = inner
295 .path
296 .file_name()
297 .map(|name| name.to_string_lossy().into_owned())
298 .unwrap_or_default();
299 let tmp = inner.path.with_file_name(format!("{file_name}.tmp"));
300 {
301 let mut file = fs::File::create(&tmp)?;
302 file.write_all(content.as_bytes())?;
303 file.sync_all()?;
304 }
305 fs::rename(&tmp, &inner.path)?;
306 Ok(())
307}
308
309fn flusher_loop(weak: Weak<FileInner>) {
312 const SUSPEND_RETRY: Duration = Duration::from_millis(50);
313 while let Some(inner) = weak.upgrade() {
314 let state = crate::lock(&inner.deferred);
315 if state.closed {
316 return;
317 }
318 let deadline = match state.pending.as_ref() {
319 Some((_, deadline)) => *deadline,
320 None => {
321 let waited = inner
322 .deferred_signal
323 .wait(state)
324 .unwrap_or_else(|error| error.into_inner());
325 drop(waited);
326 continue;
327 }
328 };
329 drop(state);
330 let now = Instant::now();
331 if now < deadline {
332 let state = crate::lock(&inner.deferred);
335 let (guard, _) = inner
336 .deferred_signal
337 .wait_timeout(state, deadline - now)
338 .unwrap_or_else(|error| error.into_inner());
339 drop(guard);
340 continue;
341 }
342 let mut state = crate::lock(&inner.deferred);
343 if state.closed {
344 return;
345 }
346 let Some((document, deadline)) = state.pending.take() else {
347 continue;
348 };
349 if Instant::now() < deadline {
350 state.pending = Some((document, deadline));
351 drop(state);
352 continue;
353 }
354 let queued_at_take = state.queued;
355 state.writing = true;
356 drop(state);
357
358 let suspended = *crate::lock(&inner.suspend) > 0;
359 if suspended {
360 let mut state = crate::lock(&inner.deferred);
361 state.pending = Some((document, Instant::now() + SUSPEND_RETRY));
362 state.writing = false;
363 drop(state);
364 inner.deferred_signal.notify_all();
365 continue;
366 }
367 let result = write_document(&inner, &document);
368 let mut state = crate::lock(&inner.deferred);
369 state.writing = false;
370 state.flushed = state.flushed.max(queued_at_take);
371 if let Err(error) = result {
372 state.last_error = Some(error.to_string());
373 }
374 drop(state);
375 inner.deferred_signal.notify_all();
376 }
377}
378
379impl Drop for FileInner {
380 fn drop(&mut self) {
381 {
382 let mut state = crate::lock(&self.deferred);
383 state.closed = true;
384 }
385 self.deferred_signal.notify_all();
386 if let Some(thread) = self
387 .flusher
388 .lock()
389 .unwrap_or_else(|error| error.into_inner())
390 .take()
391 {
392 let _ = thread.join();
393 }
394 if let Some((document, _)) = self
397 .deferred
398 .lock()
399 .unwrap_or_else(|error| error.into_inner())
400 .pending
401 .take()
402 {
403 let _ = write_document(self, &document);
404 }
405 }
406}
407
408#[derive(Debug)]
410pub struct FileSuspendGuard {
411 file: LoaderFile,
412}
413
414impl Drop for FileSuspendGuard {
415 fn drop(&mut self) {
416 let mut suspend = lock(&self.file.inner.suspend);
417 *suspend = suspend.saturating_sub(1);
418 }
419}