1use crate::daemon_id::DaemonId;
2use crate::daemon_status::DaemonStatus;
3use crate::pitchfork_toml::{CpuLimit, CronRetrigger, MemoryLimit};
4use indexmap::IndexMap;
5use std::fmt::Display;
6use std::path::PathBuf;
7
8pub fn is_valid_daemon_id(id: &str) -> bool {
27 if id.contains('/') {
28 DaemonId::parse(id).is_ok()
29 } else {
30 DaemonId::try_new("global", id).is_ok()
31 }
32}
33
34pub fn daemon_id_to_path(id: &str) -> String {
43 id.replace('/', "--")
44}
45
46pub fn daemon_log_path(id: &str) -> std::path::PathBuf {
53 let safe_id = daemon_id_to_path(id);
54 crate::env::PITCHFORK_LOGS_DIR
55 .join(&safe_id)
56 .join(format!("{safe_id}.log"))
57}
58
59#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
60pub struct Daemon {
61 pub id: DaemonId,
62 pub title: Option<String>,
63 pub pid: Option<u32>,
64 pub shell_pid: Option<u32>,
65 pub status: DaemonStatus,
66 pub dir: Option<PathBuf>,
67 #[serde(skip_serializing_if = "Option::is_none", default)]
68 pub cmd: Option<Vec<String>>,
69 pub autostop: bool,
70 #[serde(skip_serializing_if = "Option::is_none", default)]
71 pub cron_schedule: Option<String>,
72 #[serde(skip_serializing_if = "Option::is_none", default)]
73 pub cron_retrigger: Option<CronRetrigger>,
74 #[serde(skip_serializing_if = "Option::is_none", default)]
75 pub last_cron_triggered: Option<chrono::DateTime<chrono::Local>>,
76 #[serde(skip_serializing_if = "Option::is_none", default)]
77 pub last_exit_success: Option<bool>,
78 #[serde(default)]
79 pub retry: u32,
80 #[serde(default)]
81 pub retry_count: u32,
82 #[serde(skip_serializing_if = "Option::is_none", default)]
83 pub ready_delay: Option<u64>,
84 #[serde(skip_serializing_if = "Option::is_none", default)]
85 pub ready_output: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none", default)]
87 pub ready_http: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none", default)]
89 pub ready_port: Option<u16>,
90 #[serde(skip_serializing_if = "Option::is_none", default)]
91 pub ready_cmd: Option<String>,
92 #[serde(skip_serializing_if = "Vec::is_empty", default)]
94 pub expected_port: Vec<u16>,
95 #[serde(skip_serializing_if = "Vec::is_empty", default)]
97 pub resolved_port: Vec<u16>,
98 #[serde(default)]
99 pub auto_bump_port: bool,
100 #[serde(default)]
101 pub port_bump_attempts: u32,
102 #[serde(skip_serializing_if = "Vec::is_empty", default)]
103 pub depends: Vec<DaemonId>,
104 #[serde(skip_serializing_if = "Option::is_none", default)]
105 pub env: Option<IndexMap<String, String>>,
106 #[serde(skip_serializing_if = "Vec::is_empty", default)]
107 pub watch: Vec<String>,
108 #[serde(skip_serializing_if = "Option::is_none", default)]
109 pub watch_base_dir: Option<PathBuf>,
110 #[serde(default)]
111 pub mise: bool,
112 #[serde(skip_serializing_if = "Option::is_none", default)]
114 pub memory_limit: Option<MemoryLimit>,
115 #[serde(skip_serializing_if = "Option::is_none", default)]
117 pub cpu_limit: Option<CpuLimit>,
118}
119
120#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
121pub struct RunOptions {
122 pub id: DaemonId,
123 pub cmd: Vec<String>,
124 pub force: bool,
125 pub shell_pid: Option<u32>,
126 pub dir: PathBuf,
127 pub autostop: bool,
128 pub cron_schedule: Option<String>,
129 pub cron_retrigger: Option<CronRetrigger>,
130 pub retry: u32,
131 pub retry_count: u32,
132 pub ready_delay: Option<u64>,
133 pub ready_output: Option<String>,
134 pub ready_http: Option<String>,
135 pub ready_port: Option<u16>,
136 pub ready_cmd: Option<String>,
137 pub expected_port: Vec<u16>,
138 pub auto_bump_port: bool,
139 pub port_bump_attempts: u32,
140 pub wait_ready: bool,
141 #[serde(skip_serializing_if = "Vec::is_empty", default)]
142 pub depends: Vec<DaemonId>,
143 #[serde(skip_serializing_if = "Option::is_none", default)]
144 pub env: Option<IndexMap<String, String>>,
145 #[serde(skip_serializing_if = "Vec::is_empty", default)]
146 pub watch: Vec<String>,
147 #[serde(skip_serializing_if = "Option::is_none", default)]
148 pub watch_base_dir: Option<PathBuf>,
149 #[serde(default)]
150 pub mise: bool,
151 #[serde(skip_serializing_if = "Option::is_none", default)]
153 pub memory_limit: Option<MemoryLimit>,
154 #[serde(skip_serializing_if = "Option::is_none", default)]
156 pub cpu_limit: Option<CpuLimit>,
157}
158
159impl Default for Daemon {
160 fn default() -> Self {
161 Self {
162 id: DaemonId::default(),
163 title: None,
164 pid: None,
165 shell_pid: None,
166 status: DaemonStatus::default(),
167 dir: None,
168 cmd: None,
169 autostop: false,
170 cron_schedule: None,
171 cron_retrigger: None,
172 last_cron_triggered: None,
173 last_exit_success: None,
174 retry: 0,
175 retry_count: 0,
176 ready_delay: None,
177 ready_output: None,
178 ready_http: None,
179 ready_port: None,
180 ready_cmd: None,
181 expected_port: Vec::new(),
182 resolved_port: Vec::new(),
183 auto_bump_port: false,
184 port_bump_attempts: 10,
185 depends: Vec::new(),
186 env: None,
187 watch: Vec::new(),
188 watch_base_dir: None,
189 mise: false,
190 memory_limit: None,
191 cpu_limit: None,
192 }
193 }
194}
195
196impl Daemon {
197 pub fn to_run_options(&self, cmd: Vec<String>) -> RunOptions {
202 RunOptions {
203 id: self.id.clone(),
204 cmd,
205 force: false,
206 shell_pid: self.shell_pid,
207 dir: self.dir.clone().unwrap_or_else(|| crate::env::CWD.clone()),
208 autostop: self.autostop,
209 cron_schedule: self.cron_schedule.clone(),
210 cron_retrigger: self.cron_retrigger,
211 retry: self.retry,
212 retry_count: self.retry_count,
213 ready_delay: self.ready_delay,
214 ready_output: self.ready_output.clone(),
215 ready_http: self.ready_http.clone(),
216 ready_port: self.ready_port,
217 ready_cmd: self.ready_cmd.clone(),
218 expected_port: self.expected_port.clone(),
219 auto_bump_port: self.auto_bump_port,
220 port_bump_attempts: self.port_bump_attempts,
221 wait_ready: false,
222 depends: self.depends.clone(),
223 env: self.env.clone(),
224 watch: self.watch.clone(),
225 watch_base_dir: self.watch_base_dir.clone(),
226 mise: self.mise,
227 memory_limit: self.memory_limit,
228 cpu_limit: self.cpu_limit,
229 }
230 }
231}
232
233impl Default for RunOptions {
234 fn default() -> Self {
235 Self {
236 id: DaemonId::default(),
237 cmd: Vec::new(),
238 force: false,
239 shell_pid: None,
240 dir: crate::env::CWD.clone(),
241 autostop: false,
242 cron_schedule: None,
243 cron_retrigger: None,
244 retry: 0,
245 retry_count: 0,
246 ready_delay: None,
247 ready_output: None,
248 ready_http: None,
249 ready_port: None,
250 ready_cmd: None,
251 expected_port: Vec::new(),
252 auto_bump_port: false,
253 port_bump_attempts: 10,
254 wait_ready: false,
255 depends: Vec::new(),
256 env: None,
257 watch: Vec::new(),
258 watch_base_dir: None,
259 mise: false,
260 memory_limit: None,
261 cpu_limit: None,
262 }
263 }
264}
265
266impl Display for Daemon {
267 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268 write!(f, "{}", self.id.qualified())
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_valid_daemon_ids() {
278 assert!(is_valid_daemon_id("myapp"));
280 assert!(is_valid_daemon_id("my-app"));
281 assert!(is_valid_daemon_id("my_app"));
282 assert!(is_valid_daemon_id("my.app"));
283 assert!(is_valid_daemon_id("MyApp123"));
284
285 assert!(is_valid_daemon_id("project/api"));
287 assert!(is_valid_daemon_id("global/web"));
288 assert!(is_valid_daemon_id("my-project/my-app"));
289 }
290
291 #[test]
292 fn test_invalid_daemon_ids() {
293 assert!(!is_valid_daemon_id(""));
295
296 assert!(!is_valid_daemon_id("a/b/c"));
298 assert!(!is_valid_daemon_id("../etc/passwd"));
299
300 assert!(!is_valid_daemon_id("/api"));
302 assert!(!is_valid_daemon_id("project/"));
303
304 assert!(!is_valid_daemon_id("foo\\bar"));
306
307 assert!(!is_valid_daemon_id(".."));
309 assert!(!is_valid_daemon_id("foo..bar"));
310
311 assert!(!is_valid_daemon_id("my--app"));
313 assert!(!is_valid_daemon_id("project--api"));
314 assert!(!is_valid_daemon_id("--app"));
315 assert!(!is_valid_daemon_id("app--"));
316
317 assert!(!is_valid_daemon_id("my app"));
319 assert!(!is_valid_daemon_id(" myapp"));
320 assert!(!is_valid_daemon_id("myapp "));
321
322 assert!(!is_valid_daemon_id("."));
324
325 assert!(!is_valid_daemon_id("my\x00app"));
327 assert!(!is_valid_daemon_id("my\napp"));
328 assert!(!is_valid_daemon_id("my\tapp"));
329
330 assert!(!is_valid_daemon_id("myäpp"));
332 assert!(!is_valid_daemon_id("приложение"));
333
334 assert!(!is_valid_daemon_id("app@host"));
336 assert!(!is_valid_daemon_id("app:8080"));
337 }
338}