1use crate::error::{ConfigParseError, FileError};
2use crate::{Result, env};
3use indexmap::IndexMap;
4use miette::Context;
5use schemars::JsonSchema;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Default, serde::Serialize, serde::Deserialize, JsonSchema)]
11#[schemars(title = "Pitchfork Configuration")]
12pub struct PitchforkToml {
13 pub daemons: IndexMap<String, PitchforkTomlDaemon>,
15 #[serde(skip)]
16 #[schemars(skip)]
17 pub path: Option<PathBuf>,
18}
19
20impl PitchforkToml {
21 pub fn list_paths() -> Vec<PathBuf> {
24 Self::list_paths_from(&env::CWD)
25 }
26
27 pub fn list_paths_from(cwd: &Path) -> Vec<PathBuf> {
38 let mut paths = Vec::new();
39 paths.push(env::PITCHFORK_GLOBAL_CONFIG_SYSTEM.clone());
40 paths.push(env::PITCHFORK_GLOBAL_CONFIG_USER.clone());
41
42 let mut project_paths =
46 xx::file::find_up_all(cwd, &["pitchfork.local.toml", "pitchfork.toml"]);
47 project_paths.reverse();
48 paths.extend(project_paths);
49
50 paths
51 }
52
53 pub fn all_merged() -> PitchforkToml {
56 Self::all_merged_from(&env::CWD)
57 }
58
59 pub fn all_merged_from(cwd: &Path) -> PitchforkToml {
64 let mut pt = Self::default();
65 for p in Self::list_paths_from(cwd) {
66 match Self::read(&p) {
67 Ok(pt2) => pt.merge(pt2),
68 Err(e) => eprintln!("error reading {}: {}", p.display(), e),
69 }
70 }
71 pt
72 }
73}
74
75impl PitchforkToml {
76 pub fn new(path: PathBuf) -> Self {
77 Self {
78 daemons: Default::default(),
79 path: Some(path),
80 }
81 }
82
83 pub fn read<P: AsRef<Path>>(path: P) -> Result<Self> {
84 let path = path.as_ref();
85 if !path.exists() {
86 return Ok(Self::new(path.to_path_buf()));
87 }
88 let _lock = xx::fslock::get(path, false)
89 .wrap_err_with(|| format!("failed to acquire lock on {}", path.display()))?;
90 let raw = std::fs::read_to_string(path).map_err(|e| FileError::ReadError {
91 path: path.to_path_buf(),
92 source: e,
93 })?;
94 let mut pt: Self = toml::from_str(&raw)
95 .map_err(|e| ConfigParseError::from_toml_error(path, raw.clone(), e))?;
96 pt.path = Some(path.to_path_buf());
97 for (_id, d) in pt.daemons.iter_mut() {
98 d.path = pt.path.clone();
99 }
100 Ok(pt)
101 }
102
103 pub fn write(&self) -> Result<()> {
104 if let Some(path) = &self.path {
105 let _lock = xx::fslock::get(path, false)
106 .wrap_err_with(|| format!("failed to acquire lock on {}", path.display()))?;
107 let raw = toml::to_string(self).map_err(|e| FileError::SerializeError {
108 path: path.clone(),
109 source: e,
110 })?;
111 xx::file::write(path, &raw).map_err(|e| FileError::WriteError {
112 path: path.clone(),
113 details: Some(e.to_string()),
114 })?;
115 Ok(())
116 } else {
117 Err(FileError::NoPath.into())
118 }
119 }
120
121 pub fn merge(&mut self, pt: Self) {
122 for (id, d) in pt.daemons {
123 self.daemons.insert(id, d);
124 }
125 }
126}
127
128#[derive(Debug, serde::Serialize, serde::Deserialize, JsonSchema)]
130pub struct PitchforkTomlDaemon {
131 #[schemars(example = example_run_command())]
133 pub run: String,
134 #[serde(skip_serializing_if = "Vec::is_empty", default)]
136 pub auto: Vec<PitchforkTomlAuto>,
137 #[serde(skip_serializing_if = "Option::is_none", default)]
139 pub cron: Option<PitchforkTomlCron>,
140 #[serde(default)]
143 pub retry: Retry,
144 #[serde(skip_serializing_if = "Option::is_none", default)]
146 pub ready_delay: Option<u64>,
147 #[serde(skip_serializing_if = "Option::is_none", default)]
149 pub ready_output: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none", default)]
152 pub ready_http: Option<String>,
153 #[serde(skip_serializing_if = "Option::is_none", default)]
155 #[schemars(range(min = 1, max = 65535))]
156 pub ready_port: Option<u16>,
157 #[serde(skip_serializing_if = "Option::is_none", default)]
159 pub ready_cmd: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none", default)]
162 pub boot_start: Option<bool>,
163 #[serde(skip_serializing_if = "Vec::is_empty", default)]
165 pub depends: Vec<String>,
166 #[serde(skip_serializing_if = "Vec::is_empty", default)]
167 pub watch: Vec<String>,
168 #[serde(skip_serializing_if = "Option::is_none", default)]
170 pub dir: Option<String>,
171 #[serde(skip_serializing_if = "Option::is_none", default)]
173 pub env: Option<IndexMap<String, String>>,
174 #[serde(skip)]
175 #[schemars(skip)]
176 pub path: Option<PathBuf>,
177}
178
179fn example_run_command() -> &'static str {
180 "exec node server.js"
181}
182
183#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, JsonSchema)]
185pub struct PitchforkTomlCron {
186 #[schemars(example = example_cron_schedule())]
188 pub schedule: String,
189 #[serde(default = "default_retrigger")]
191 pub retrigger: CronRetrigger,
192}
193
194fn default_retrigger() -> CronRetrigger {
195 CronRetrigger::Finish
196}
197
198fn example_cron_schedule() -> &'static str {
199 "0 * * * *"
200}
201
202#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, JsonSchema)]
204#[serde(rename_all = "snake_case")]
205pub enum CronRetrigger {
206 Finish,
208 Always,
210 Success,
212 Fail,
214}
215
216#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, JsonSchema)]
218#[serde(rename_all = "snake_case")]
219pub enum PitchforkTomlAuto {
220 Start,
222 Stop,
224}
225
226#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)]
231pub struct Retry(pub u32);
232
233impl std::fmt::Display for Retry {
234 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235 if self.is_infinite() {
236 write!(f, "infinite")
237 } else {
238 write!(f, "{}", self.0)
239 }
240 }
241}
242
243impl Retry {
244 pub const INFINITE: Retry = Retry(u32::MAX);
245
246 pub fn count(&self) -> u32 {
247 self.0
248 }
249
250 pub fn is_infinite(&self) -> bool {
251 self.0 == u32::MAX
252 }
253}
254
255impl From<u32> for Retry {
256 fn from(n: u32) -> Self {
257 Retry(n)
258 }
259}
260
261impl From<bool> for Retry {
262 fn from(b: bool) -> Self {
263 if b { Retry::INFINITE } else { Retry(0) }
264 }
265}
266
267impl Serialize for Retry {
268 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
269 where
270 S: Serializer,
271 {
272 if self.is_infinite() {
274 serializer.serialize_bool(true)
275 } else {
276 serializer.serialize_u32(self.0)
277 }
278 }
279}
280
281impl<'de> Deserialize<'de> for Retry {
282 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
283 where
284 D: Deserializer<'de>,
285 {
286 use serde::de::{self, Visitor};
287
288 struct RetryVisitor;
289
290 impl Visitor<'_> for RetryVisitor {
291 type Value = Retry;
292
293 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
294 formatter.write_str("a boolean or non-negative integer")
295 }
296
297 fn visit_bool<E>(self, v: bool) -> std::result::Result<Self::Value, E>
298 where
299 E: de::Error,
300 {
301 Ok(Retry::from(v))
302 }
303
304 fn visit_i64<E>(self, v: i64) -> std::result::Result<Self::Value, E>
305 where
306 E: de::Error,
307 {
308 if v < 0 {
309 Err(de::Error::custom("retry count cannot be negative"))
310 } else if v > u32::MAX as i64 {
311 Ok(Retry::INFINITE)
312 } else {
313 Ok(Retry(v as u32))
314 }
315 }
316
317 fn visit_u64<E>(self, v: u64) -> std::result::Result<Self::Value, E>
318 where
319 E: de::Error,
320 {
321 if v > u32::MAX as u64 {
322 Ok(Retry::INFINITE)
323 } else {
324 Ok(Retry(v as u32))
325 }
326 }
327 }
328
329 deserializer.deserialize_any(RetryVisitor)
330 }
331}