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> {
22 let mut paths = Vec::new();
23 paths.push(env::PITCHFORK_GLOBAL_CONFIG_SYSTEM.clone());
24 paths.push(env::PITCHFORK_GLOBAL_CONFIG_USER.clone());
25 paths.extend(xx::file::find_up_all(&env::CWD, &["pitchfork.toml"]));
26 paths
27 }
28
29 pub fn all_merged() -> PitchforkToml {
30 let mut pt = Self::default();
31 for p in Self::list_paths() {
32 match Self::read(&p) {
33 Ok(pt2) => pt.merge(pt2),
34 Err(e) => eprintln!("error reading {}: {}", p.display(), e),
35 }
36 }
37 pt
38 }
39}
40
41impl PitchforkToml {
42 pub fn new(path: PathBuf) -> Self {
43 Self {
44 daemons: Default::default(),
45 path: Some(path),
46 }
47 }
48
49 pub fn read<P: AsRef<Path>>(path: P) -> Result<Self> {
50 let path = path.as_ref();
51 if !path.exists() {
52 return Ok(Self::new(path.to_path_buf()));
53 }
54 let _lock = xx::fslock::get(path, false)
55 .wrap_err_with(|| format!("failed to acquire lock on {}", path.display()))?;
56 let raw = std::fs::read_to_string(path).map_err(|e| FileError::ReadError {
57 path: path.to_path_buf(),
58 source: e,
59 })?;
60 let mut pt: Self = toml::from_str(&raw)
61 .map_err(|e| ConfigParseError::from_toml_error(path, raw.clone(), e))?;
62 pt.path = Some(path.to_path_buf());
63 for (_id, d) in pt.daemons.iter_mut() {
64 d.path = pt.path.clone();
65 }
66 Ok(pt)
67 }
68
69 pub fn write(&self) -> Result<()> {
70 if let Some(path) = &self.path {
71 let _lock = xx::fslock::get(path, false)
72 .wrap_err_with(|| format!("failed to acquire lock on {}", path.display()))?;
73 let raw = toml::to_string(self).map_err(|e| FileError::SerializeError {
74 path: path.clone(),
75 source: e,
76 })?;
77 xx::file::write(path, &raw).map_err(|e| FileError::WriteError {
78 path: path.clone(),
79 details: Some(e.to_string()),
80 })?;
81 Ok(())
82 } else {
83 Err(FileError::NoPath.into())
84 }
85 }
86
87 pub fn merge(&mut self, pt: Self) {
88 for (id, d) in pt.daemons {
89 self.daemons.insert(id, d);
90 }
91 }
92}
93
94#[derive(Debug, serde::Serialize, serde::Deserialize, JsonSchema)]
96pub struct PitchforkTomlDaemon {
97 #[schemars(example = example_run_command())]
99 pub run: String,
100 #[serde(skip_serializing_if = "Vec::is_empty", default)]
102 pub auto: Vec<PitchforkTomlAuto>,
103 #[serde(skip_serializing_if = "Option::is_none", default)]
105 pub cron: Option<PitchforkTomlCron>,
106 #[serde(default)]
109 pub retry: Retry,
110 #[serde(skip_serializing_if = "Option::is_none", default)]
112 pub ready_delay: Option<u64>,
113 #[serde(skip_serializing_if = "Option::is_none", default)]
115 pub ready_output: Option<String>,
116 #[serde(skip_serializing_if = "Option::is_none", default)]
118 pub ready_http: Option<String>,
119 #[serde(skip_serializing_if = "Option::is_none", default)]
121 #[schemars(range(min = 1, max = 65535))]
122 pub ready_port: Option<u16>,
123 #[serde(skip_serializing_if = "Option::is_none", default)]
125 pub boot_start: Option<bool>,
126 #[serde(skip_serializing_if = "Vec::is_empty", default)]
128 pub depends: Vec<String>,
129 #[serde(skip_serializing_if = "Vec::is_empty", default)]
130 pub watch: Vec<String>,
131 #[serde(skip)]
132 #[schemars(skip)]
133 pub path: Option<PathBuf>,
134}
135
136fn example_run_command() -> &'static str {
137 "exec node server.js"
138}
139
140#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, JsonSchema)]
142pub struct PitchforkTomlCron {
143 #[schemars(example = example_cron_schedule())]
145 pub schedule: String,
146 #[serde(default = "default_retrigger")]
148 pub retrigger: CronRetrigger,
149}
150
151fn default_retrigger() -> CronRetrigger {
152 CronRetrigger::Finish
153}
154
155fn example_cron_schedule() -> &'static str {
156 "0 * * * *"
157}
158
159#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, JsonSchema)]
161#[serde(rename_all = "snake_case")]
162pub enum CronRetrigger {
163 Finish,
165 Always,
167 Success,
169 Fail,
171}
172
173#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, JsonSchema)]
175#[serde(rename_all = "snake_case")]
176pub enum PitchforkTomlAuto {
177 Start,
179 Stop,
181}
182
183#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)]
188pub struct Retry(pub u32);
189
190impl std::fmt::Display for Retry {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 if self.is_infinite() {
193 write!(f, "infinite")
194 } else {
195 write!(f, "{}", self.0)
196 }
197 }
198}
199
200impl Retry {
201 pub const INFINITE: Retry = Retry(u32::MAX);
202
203 pub fn count(&self) -> u32 {
204 self.0
205 }
206
207 pub fn is_infinite(&self) -> bool {
208 self.0 == u32::MAX
209 }
210}
211
212impl From<u32> for Retry {
213 fn from(n: u32) -> Self {
214 Retry(n)
215 }
216}
217
218impl From<bool> for Retry {
219 fn from(b: bool) -> Self {
220 if b { Retry::INFINITE } else { Retry(0) }
221 }
222}
223
224impl Serialize for Retry {
225 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
226 where
227 S: Serializer,
228 {
229 if self.is_infinite() {
231 serializer.serialize_bool(true)
232 } else {
233 serializer.serialize_u32(self.0)
234 }
235 }
236}
237
238impl<'de> Deserialize<'de> for Retry {
239 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
240 where
241 D: Deserializer<'de>,
242 {
243 use serde::de::{self, Visitor};
244
245 struct RetryVisitor;
246
247 impl Visitor<'_> for RetryVisitor {
248 type Value = Retry;
249
250 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
251 formatter.write_str("a boolean or non-negative integer")
252 }
253
254 fn visit_bool<E>(self, v: bool) -> std::result::Result<Self::Value, E>
255 where
256 E: de::Error,
257 {
258 Ok(Retry::from(v))
259 }
260
261 fn visit_i64<E>(self, v: i64) -> std::result::Result<Self::Value, E>
262 where
263 E: de::Error,
264 {
265 if v < 0 {
266 Err(de::Error::custom("retry count cannot be negative"))
267 } else if v > u32::MAX as i64 {
268 Ok(Retry::INFINITE)
269 } else {
270 Ok(Retry(v as u32))
271 }
272 }
273
274 fn visit_u64<E>(self, v: u64) -> std::result::Result<Self::Value, E>
275 where
276 E: de::Error,
277 {
278 if v > u32::MAX as u64 {
279 Ok(Retry::INFINITE)
280 } else {
281 Ok(Retry(v as u32))
282 }
283 }
284 }
285
286 deserializer.deserialize_any(RetryVisitor)
287 }
288}