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(skip_serializing_if = "Option::is_none", default)]
101 pub active_port: Option<u16>,
102 #[serde(skip_serializing_if = "Option::is_none", default)]
104 pub slug: Option<String>,
105 #[serde(skip_serializing_if = "Option::is_none", default)]
107 pub proxy: Option<bool>,
108 #[serde(default)]
109 pub auto_bump_port: bool,
110 #[serde(default)]
111 pub port_bump_attempts: u32,
112 #[serde(skip_serializing_if = "Vec::is_empty", default)]
113 pub depends: Vec<DaemonId>,
114 #[serde(skip_serializing_if = "Option::is_none", default)]
115 pub env: Option<IndexMap<String, String>>,
116 #[serde(skip_serializing_if = "Vec::is_empty", default)]
117 pub watch: Vec<String>,
118 #[serde(skip_serializing_if = "Option::is_none", default)]
119 pub watch_base_dir: Option<PathBuf>,
120 #[serde(skip_serializing_if = "Option::is_none", default)]
130 pub mise: Option<bool>,
131 #[serde(skip_serializing_if = "Option::is_none", default)]
133 pub memory_limit: Option<MemoryLimit>,
134 #[serde(skip_serializing_if = "Option::is_none", default)]
136 pub cpu_limit: Option<CpuLimit>,
137}
138
139#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
140pub struct RunOptions {
141 pub id: DaemonId,
142 pub cmd: Vec<String>,
143 pub force: bool,
144 pub shell_pid: Option<u32>,
145 pub dir: PathBuf,
146 pub autostop: bool,
147 pub cron_schedule: Option<String>,
148 pub cron_retrigger: Option<CronRetrigger>,
149 pub retry: u32,
150 pub retry_count: u32,
151 pub ready_delay: Option<u64>,
152 pub ready_output: Option<String>,
153 pub ready_http: Option<String>,
154 pub ready_port: Option<u16>,
155 pub ready_cmd: Option<String>,
156 pub expected_port: Vec<u16>,
157 pub auto_bump_port: bool,
158 pub port_bump_attempts: u32,
159 pub wait_ready: bool,
160 #[serde(skip_serializing_if = "Vec::is_empty", default)]
161 pub depends: Vec<DaemonId>,
162 #[serde(skip_serializing_if = "Option::is_none", default)]
163 pub env: Option<IndexMap<String, String>>,
164 #[serde(skip_serializing_if = "Vec::is_empty", default)]
165 pub watch: Vec<String>,
166 #[serde(skip_serializing_if = "Option::is_none", default)]
167 pub watch_base_dir: Option<PathBuf>,
168 #[serde(skip_serializing_if = "Option::is_none", default)]
173 pub mise: Option<bool>,
174 #[serde(skip_serializing_if = "Option::is_none", default)]
176 pub slug: Option<String>,
177 #[serde(skip_serializing_if = "Option::is_none", default)]
179 pub proxy: Option<bool>,
180 #[serde(skip_serializing_if = "Option::is_none", default)]
182 pub memory_limit: Option<MemoryLimit>,
183 #[serde(skip_serializing_if = "Option::is_none", default)]
185 pub cpu_limit: Option<CpuLimit>,
186}
187
188impl Default for Daemon {
189 fn default() -> Self {
190 Self {
191 id: DaemonId::default(),
192 title: None,
193 pid: None,
194 shell_pid: None,
195 status: DaemonStatus::default(),
196 dir: None,
197 cmd: None,
198 autostop: false,
199 cron_schedule: None,
200 cron_retrigger: None,
201 last_cron_triggered: None,
202 last_exit_success: None,
203 retry: 0,
204 retry_count: 0,
205 ready_delay: None,
206 ready_output: None,
207 ready_http: None,
208 ready_port: None,
209 ready_cmd: None,
210 expected_port: Vec::new(),
211 resolved_port: Vec::new(),
212 active_port: None,
213 slug: None,
214 proxy: None,
215 auto_bump_port: false,
216 port_bump_attempts: 10,
217 depends: Vec::new(),
218 env: None,
219 watch: Vec::new(),
220 watch_base_dir: None,
221 mise: None,
222 memory_limit: None,
223 cpu_limit: None,
224 }
225 }
226}
227
228impl Daemon {
229 pub fn to_run_options(&self, cmd: Vec<String>) -> RunOptions {
234 RunOptions {
235 id: self.id.clone(),
236 cmd,
237 force: false,
238 shell_pid: self.shell_pid,
239 dir: self.dir.clone().unwrap_or_else(|| crate::env::CWD.clone()),
240 autostop: self.autostop,
241 cron_schedule: self.cron_schedule.clone(),
242 cron_retrigger: self.cron_retrigger,
243 retry: self.retry,
244 retry_count: self.retry_count,
245 ready_delay: self.ready_delay,
246 ready_output: self.ready_output.clone(),
247 ready_http: self.ready_http.clone(),
248 ready_port: self.ready_port,
249 ready_cmd: self.ready_cmd.clone(),
250 expected_port: self.expected_port.clone(),
251 auto_bump_port: self.auto_bump_port,
252 port_bump_attempts: self.port_bump_attempts,
253 wait_ready: false,
254 depends: self.depends.clone(),
255 env: self.env.clone(),
256 watch: self.watch.clone(),
257 watch_base_dir: self.watch_base_dir.clone(),
258 mise: self.mise,
259 slug: self.slug.clone(),
260 proxy: self.proxy,
261 memory_limit: self.memory_limit,
262 cpu_limit: self.cpu_limit,
263 }
264 }
265}
266
267impl Default for RunOptions {
268 fn default() -> Self {
269 Self {
270 id: DaemonId::default(),
271 cmd: Vec::new(),
272 force: false,
273 shell_pid: None,
274 dir: crate::env::CWD.clone(),
275 autostop: false,
276 cron_schedule: None,
277 cron_retrigger: None,
278 retry: 0,
279 retry_count: 0,
280 ready_delay: None,
281 ready_output: None,
282 ready_http: None,
283 ready_port: None,
284 ready_cmd: None,
285 expected_port: Vec::new(),
286 auto_bump_port: false,
287 port_bump_attempts: 10,
288 wait_ready: false,
289 depends: Vec::new(),
290 env: None,
291 watch: Vec::new(),
292 watch_base_dir: None,
293 mise: None,
294 slug: None,
295 proxy: None,
296 memory_limit: None,
297 cpu_limit: None,
298 }
299 }
300}
301
302impl Display for Daemon {
303 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304 write!(f, "{}", self.id.qualified())
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn test_valid_daemon_ids() {
314 assert!(is_valid_daemon_id("myapp"));
316 assert!(is_valid_daemon_id("my-app"));
317 assert!(is_valid_daemon_id("my_app"));
318 assert!(is_valid_daemon_id("my.app"));
319 assert!(is_valid_daemon_id("MyApp123"));
320
321 assert!(is_valid_daemon_id("project/api"));
323 assert!(is_valid_daemon_id("global/web"));
324 assert!(is_valid_daemon_id("my-project/my-app"));
325 }
326
327 #[test]
328 fn test_invalid_daemon_ids() {
329 assert!(!is_valid_daemon_id(""));
331
332 assert!(!is_valid_daemon_id("a/b/c"));
334 assert!(!is_valid_daemon_id("../etc/passwd"));
335
336 assert!(!is_valid_daemon_id("/api"));
338 assert!(!is_valid_daemon_id("project/"));
339
340 assert!(!is_valid_daemon_id("foo\\bar"));
342
343 assert!(!is_valid_daemon_id(".."));
345 assert!(!is_valid_daemon_id("foo..bar"));
346
347 assert!(!is_valid_daemon_id("my--app"));
349 assert!(!is_valid_daemon_id("project--api"));
350 assert!(!is_valid_daemon_id("--app"));
351 assert!(!is_valid_daemon_id("app--"));
352
353 assert!(!is_valid_daemon_id("my app"));
355 assert!(!is_valid_daemon_id(" myapp"));
356 assert!(!is_valid_daemon_id("myapp "));
357
358 assert!(!is_valid_daemon_id("."));
360
361 assert!(!is_valid_daemon_id("my\x00app"));
363 assert!(!is_valid_daemon_id("my\napp"));
364 assert!(!is_valid_daemon_id("my\tapp"));
365
366 assert!(!is_valid_daemon_id("myäpp"));
368 assert!(!is_valid_daemon_id("приложение"));
369
370 assert!(!is_valid_daemon_id("app@host"));
372 assert!(!is_valid_daemon_id("app:8080"));
373 }
374}