1use crate::daemon_id::DaemonId;
2use crate::daemon_status::DaemonStatus;
3use crate::pitchfork_toml::{CpuLimit, CronRetrigger, MemoryLimit, WatchMode};
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(default)]
119 pub watch_mode: WatchMode,
120 #[serde(skip_serializing_if = "Option::is_none", default)]
121 pub watch_base_dir: Option<PathBuf>,
122 #[serde(skip_serializing_if = "Option::is_none", default)]
132 pub mise: Option<bool>,
133 #[serde(skip_serializing_if = "Option::is_none", default)]
135 pub user: Option<String>,
136 #[serde(skip_serializing_if = "Option::is_none", default)]
138 pub memory_limit: Option<MemoryLimit>,
139 #[serde(skip_serializing_if = "Option::is_none", default)]
141 pub cpu_limit: Option<CpuLimit>,
142}
143
144#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
145pub struct RunOptions {
146 pub id: DaemonId,
147 pub cmd: Vec<String>,
148 pub force: bool,
149 pub shell_pid: Option<u32>,
150 pub dir: PathBuf,
151 pub autostop: bool,
152 pub cron_schedule: Option<String>,
153 pub cron_retrigger: Option<CronRetrigger>,
154 pub retry: u32,
155 pub retry_count: u32,
156 pub ready_delay: Option<u64>,
157 pub ready_output: Option<String>,
158 pub ready_http: Option<String>,
159 pub ready_port: Option<u16>,
160 pub ready_cmd: Option<String>,
161 pub expected_port: Vec<u16>,
162 pub auto_bump_port: bool,
163 pub port_bump_attempts: u32,
164 pub wait_ready: bool,
165 #[serde(skip_serializing_if = "Vec::is_empty", default)]
166 pub depends: Vec<DaemonId>,
167 #[serde(skip_serializing_if = "Option::is_none", default)]
168 pub env: Option<IndexMap<String, String>>,
169 #[serde(skip_serializing_if = "Vec::is_empty", default)]
170 pub watch: Vec<String>,
171 #[serde(default)]
172 pub watch_mode: WatchMode,
173 #[serde(skip_serializing_if = "Option::is_none", default)]
174 pub watch_base_dir: Option<PathBuf>,
175 #[serde(skip_serializing_if = "Option::is_none", default)]
180 pub mise: Option<bool>,
181 #[serde(skip_serializing_if = "Option::is_none", default)]
183 pub slug: Option<String>,
184 #[serde(skip_serializing_if = "Option::is_none", default)]
186 pub proxy: Option<bool>,
187 #[serde(skip_serializing_if = "Option::is_none", default)]
189 pub user: Option<String>,
190 #[serde(skip_serializing_if = "Option::is_none", default)]
192 pub memory_limit: Option<MemoryLimit>,
193 #[serde(skip_serializing_if = "Option::is_none", default)]
195 pub cpu_limit: Option<CpuLimit>,
196}
197
198impl Default for Daemon {
199 fn default() -> Self {
200 Self {
201 id: DaemonId::default(),
202 title: None,
203 pid: None,
204 shell_pid: None,
205 status: DaemonStatus::default(),
206 dir: None,
207 cmd: None,
208 autostop: false,
209 cron_schedule: None,
210 cron_retrigger: None,
211 last_cron_triggered: None,
212 last_exit_success: None,
213 retry: 0,
214 retry_count: 0,
215 ready_delay: None,
216 ready_output: None,
217 ready_http: None,
218 ready_port: None,
219 ready_cmd: None,
220 expected_port: Vec::new(),
221 resolved_port: Vec::new(),
222 active_port: None,
223 slug: None,
224 proxy: None,
225 auto_bump_port: false,
226 port_bump_attempts: 10,
227 depends: Vec::new(),
228 env: None,
229 watch: Vec::new(),
230 watch_mode: WatchMode::default(),
231 watch_base_dir: None,
232 mise: None,
233 user: None,
234 memory_limit: None,
235 cpu_limit: None,
236 }
237 }
238}
239
240impl Daemon {
241 pub fn to_run_options(&self, cmd: Vec<String>) -> RunOptions {
246 RunOptions {
247 id: self.id.clone(),
248 cmd,
249 force: false,
250 shell_pid: self.shell_pid,
251 dir: self.dir.clone().unwrap_or_else(|| crate::env::CWD.clone()),
252 autostop: self.autostop,
253 cron_schedule: self.cron_schedule.clone(),
254 cron_retrigger: self.cron_retrigger,
255 retry: self.retry,
256 retry_count: self.retry_count,
257 ready_delay: self.ready_delay,
258 ready_output: self.ready_output.clone(),
259 ready_http: self.ready_http.clone(),
260 ready_port: self.ready_port,
261 ready_cmd: self.ready_cmd.clone(),
262 expected_port: self.expected_port.clone(),
263 auto_bump_port: self.auto_bump_port,
264 port_bump_attempts: self.port_bump_attempts,
265 wait_ready: false,
266 depends: self.depends.clone(),
267 env: self.env.clone(),
268 watch: self.watch.clone(),
269 watch_mode: self.watch_mode,
270 watch_base_dir: self.watch_base_dir.clone(),
271 mise: self.mise,
272 slug: self.slug.clone(),
273 proxy: self.proxy,
274 user: self.user.clone(),
275 memory_limit: self.memory_limit,
276 cpu_limit: self.cpu_limit,
277 }
278 }
279}
280
281impl Default for RunOptions {
282 fn default() -> Self {
283 Self {
284 id: DaemonId::default(),
285 cmd: Vec::new(),
286 force: false,
287 shell_pid: None,
288 dir: crate::env::CWD.clone(),
289 autostop: false,
290 cron_schedule: None,
291 cron_retrigger: None,
292 retry: 0,
293 retry_count: 0,
294 ready_delay: None,
295 ready_output: None,
296 ready_http: None,
297 ready_port: None,
298 ready_cmd: None,
299 expected_port: Vec::new(),
300 auto_bump_port: false,
301 port_bump_attempts: 10,
302 wait_ready: false,
303 depends: Vec::new(),
304 env: None,
305 watch: Vec::new(),
306 watch_mode: WatchMode::default(),
307 watch_base_dir: None,
308 mise: None,
309 slug: None,
310 proxy: None,
311 user: None,
312 memory_limit: None,
313 cpu_limit: None,
314 }
315 }
316}
317
318impl Display for Daemon {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 write!(f, "{}", self.id.qualified())
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn test_valid_daemon_ids() {
330 assert!(is_valid_daemon_id("myapp"));
332 assert!(is_valid_daemon_id("my-app"));
333 assert!(is_valid_daemon_id("my_app"));
334 assert!(is_valid_daemon_id("my.app"));
335 assert!(is_valid_daemon_id("MyApp123"));
336
337 assert!(is_valid_daemon_id("project/api"));
339 assert!(is_valid_daemon_id("global/web"));
340 assert!(is_valid_daemon_id("my-project/my-app"));
341 }
342
343 #[test]
344 fn test_invalid_daemon_ids() {
345 assert!(!is_valid_daemon_id(""));
347
348 assert!(!is_valid_daemon_id("a/b/c"));
350 assert!(!is_valid_daemon_id("../etc/passwd"));
351
352 assert!(!is_valid_daemon_id("/api"));
354 assert!(!is_valid_daemon_id("project/"));
355
356 assert!(!is_valid_daemon_id("foo\\bar"));
358
359 assert!(!is_valid_daemon_id(".."));
361 assert!(!is_valid_daemon_id("foo..bar"));
362
363 assert!(!is_valid_daemon_id("my--app"));
365 assert!(!is_valid_daemon_id("project--api"));
366 assert!(!is_valid_daemon_id("--app"));
367 assert!(!is_valid_daemon_id("app--"));
368
369 assert!(!is_valid_daemon_id("my app"));
371 assert!(!is_valid_daemon_id(" myapp"));
372 assert!(!is_valid_daemon_id("myapp "));
373
374 assert!(!is_valid_daemon_id("."));
376
377 assert!(!is_valid_daemon_id("my\x00app"));
379 assert!(!is_valid_daemon_id("my\napp"));
380 assert!(!is_valid_daemon_id("my\tapp"));
381
382 assert!(!is_valid_daemon_id("myäpp"));
384 assert!(!is_valid_daemon_id("приложение"));
385
386 assert!(!is_valid_daemon_id("app@host"));
388 assert!(!is_valid_daemon_id("app:8080"));
389 }
390}