1use crate::config_types::OneshotWait;
2use crate::daemon_id::DaemonId;
3use crate::daemon_status::DaemonStatus;
4use crate::pitchfork_toml::{
5 CpuLimit, CronRetrigger, Dir, HealthCmd, HealthHttp, HealthPort, MemoryLimit, PortConfig,
6 ReadyCmd, ReadyHttp, ReadyOutput, ReadyPort, Retry, StopConfig, WatchMode,
7};
8use indexmap::IndexMap;
9use std::fmt::Display;
10use std::path::PathBuf;
11
12pub fn is_valid_daemon_id(id: &str) -> bool {
31 if id.contains('/') {
32 DaemonId::parse(id).is_ok()
33 } else {
34 DaemonId::try_new("global", id).is_ok()
35 }
36}
37
38#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
39pub struct Daemon {
40 pub id: DaemonId,
41 pub title: Option<String>,
42 pub pid: Option<u32>,
43 #[serde(skip_serializing_if = "Option::is_none", default)]
48 pub start_time: Option<u64>,
49 #[serde(skip_serializing_if = "Option::is_none", default)]
55 pub boot_time: Option<u64>,
56 pub shell_pid: Option<u32>,
57 pub status: DaemonStatus,
58 pub dir: Option<PathBuf>,
59 #[serde(skip_serializing_if = "Option::is_none", default)]
60 pub cmd: Option<Vec<String>>,
61 #[serde(skip_serializing_if = "Option::is_none", default)]
63 pub run: Option<String>,
64 pub autostop: bool,
65 #[serde(skip_serializing_if = "Option::is_none", default)]
66 pub cron_schedule: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none", default)]
68 pub cron_retrigger: Option<CronRetrigger>,
69 #[serde(skip_serializing_if = "Option::is_none", default)]
70 pub cron_immediate: Option<bool>,
71 #[serde(skip_serializing_if = "Option::is_none", default)]
72 pub last_cron_triggered: Option<chrono::DateTime<chrono::Local>>,
73 #[serde(skip_serializing_if = "Option::is_none", default)]
74 pub last_exit_success: Option<bool>,
75 #[serde(default)]
76 pub retry: Retry,
77 #[serde(default)]
78 pub retry_count: u32,
79 #[serde(skip_serializing_if = "Option::is_none", default)]
80 pub ready_delay: Option<u64>,
81 #[serde(skip_serializing_if = "Option::is_none", default)]
82 pub ready_output: Option<ReadyOutput>,
83 #[serde(skip_serializing_if = "Option::is_none", default)]
84 pub ready_http: Option<ReadyHttp>,
85 #[serde(skip_serializing_if = "Option::is_none", default)]
86 pub ready_port: Option<ReadyPort>,
87 #[serde(skip_serializing_if = "Option::is_none", default)]
88 pub ready_cmd: Option<ReadyCmd>,
89 #[serde(skip_serializing_if = "Option::is_none", default)]
90 pub health_cmd: Option<HealthCmd>,
91 #[serde(skip_serializing_if = "Option::is_none", default)]
92 pub health_http: Option<HealthHttp>,
93 #[serde(skip_serializing_if = "Option::is_none", default)]
94 pub health_port: Option<HealthPort>,
95 #[serde(skip_serializing_if = "Option::is_none", default)]
97 pub port: Option<PortConfig>,
98 #[serde(skip_serializing_if = "Vec::is_empty", default)]
100 pub resolved_port: Vec<u16>,
101 #[serde(skip_serializing_if = "Option::is_none", default)]
104 pub active_port: Option<u16>,
105 #[serde(skip_serializing_if = "Option::is_none", default)]
107 pub slug: Option<String>,
108 #[serde(skip_serializing_if = "Option::is_none", default)]
110 pub proxy: Option<bool>,
111 #[serde(skip_serializing_if = "Vec::is_empty", default)]
112 pub depends: Vec<DaemonId>,
113 #[serde(skip_serializing_if = "Option::is_none", default)]
114 pub env: Option<IndexMap<String, String>>,
115 #[serde(skip_serializing_if = "Vec::is_empty", default)]
116 pub watch: Vec<String>,
117 #[serde(default)]
118 pub watch_mode: WatchMode,
119 #[serde(skip_serializing_if = "Option::is_none", default)]
120 pub watch_base_dir: Option<PathBuf>,
121 #[serde(skip_serializing_if = "Option::is_none", default)]
131 pub mise: Option<bool>,
132 #[serde(skip_serializing_if = "Option::is_none", default)]
134 pub user: Option<String>,
135 #[serde(skip_serializing_if = "Option::is_none", default)]
137 pub memory_limit: Option<MemoryLimit>,
138 #[serde(skip_serializing_if = "Option::is_none", default)]
140 pub cpu_limit: Option<CpuLimit>,
141 #[serde(skip_serializing_if = "Option::is_none", default)]
143 pub stop_signal: Option<StopConfig>,
144 #[serde(skip_serializing_if = "Option::is_none", default)]
146 pub archive_hook: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none", default)]
149 pub log_format: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none", default)]
152 pub pty: Option<bool>,
153 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
156 pub config_registered: bool,
157 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
165 pub oneshot: bool,
166 #[serde(default)]
173 pub proxy_idle_timeout_ms: Option<u64>,
174}
175
176#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
177pub struct RunOptions {
178 pub id: DaemonId,
179 pub cmd: Vec<String>,
180 #[serde(skip_serializing_if = "Option::is_none", default)]
183 pub run: Option<String>,
184 pub force: bool,
185 pub shell_pid: Option<u32>,
186 pub dir: Dir,
187 pub autostop: bool,
188 pub cron_schedule: Option<String>,
189 pub cron_retrigger: Option<CronRetrigger>,
190 pub cron_immediate: Option<bool>,
191 pub retry: Retry,
192 pub retry_count: u32,
193 pub ready_delay: Option<u64>,
194 pub ready_output: Option<ReadyOutput>,
195 pub ready_http: Option<ReadyHttp>,
196 pub ready_port: Option<ReadyPort>,
197 pub ready_cmd: Option<ReadyCmd>,
198 pub health_cmd: Option<HealthCmd>,
199 pub health_http: Option<HealthHttp>,
200 pub health_port: Option<HealthPort>,
201 pub port: Option<PortConfig>,
202 pub wait_ready: bool,
203 #[serde(skip_serializing_if = "Vec::is_empty", default)]
204 pub depends: Vec<DaemonId>,
205 #[serde(skip_serializing_if = "Option::is_none", default)]
206 pub env: Option<IndexMap<String, String>>,
207 #[serde(skip_serializing_if = "Vec::is_empty", default)]
208 pub watch: Vec<String>,
209 #[serde(default)]
210 pub watch_mode: WatchMode,
211 #[serde(skip_serializing_if = "Option::is_none", default)]
212 pub watch_base_dir: Option<PathBuf>,
213 #[serde(skip_serializing_if = "Option::is_none", default)]
218 pub mise: Option<bool>,
219 #[serde(skip_serializing_if = "Option::is_none", default)]
221 pub slug: Option<String>,
222 #[serde(skip_serializing_if = "Option::is_none", default)]
224 pub proxy: Option<bool>,
225 #[serde(skip_serializing_if = "Option::is_none", default)]
227 pub user: Option<String>,
228 #[serde(skip_serializing_if = "Option::is_none", default)]
230 pub memory_limit: Option<MemoryLimit>,
231 #[serde(skip_serializing_if = "Option::is_none", default)]
233 pub cpu_limit: Option<CpuLimit>,
234 #[serde(skip_serializing_if = "Option::is_none", default)]
236 pub stop_signal: Option<StopConfig>,
237 #[serde(skip_serializing_if = "Option::is_none", default)]
239 pub archive_hook: Option<String>,
240 #[serde(skip_serializing_if = "Option::is_none", default)]
242 pub log_format: Option<String>,
243 #[serde(skip_serializing_if = "Option::is_none", default)]
245 pub on_output_hook: Option<crate::pitchfork_toml::OnOutputHook>,
246 #[serde(skip_serializing_if = "Option::is_none", default)]
248 pub pty: Option<bool>,
249 #[serde(default)]
256 pub oneshot: bool,
257 #[serde(skip_serializing_if = "Option::is_none", default)]
265 pub oneshot_wait: Option<OneshotWait>,
266 #[serde(default)]
272 pub on_directory_enter: bool,
273 #[serde(default)]
280 pub proxy_idle_timeout_ms: Option<u64>,
281}
282
283impl Daemon {
284 pub fn to_run_options(&self, cmd: Vec<String>) -> RunOptions {
289 let on_output_hook = self
294 .dir
295 .as_deref()
296 .and_then(|dir| crate::pitchfork_toml::PitchforkToml::all_merged_from(dir).ok())
297 .or_else(|| crate::pitchfork_toml::PitchforkToml::all_merged_all_namespaces().ok())
298 .and_then(|pt| {
299 pt.daemons
300 .get(&self.id)
301 .and_then(|d| d.hooks.as_ref())
302 .and_then(|h| h.on_output.clone())
303 });
304
305 RunOptions {
306 id: self.id.clone(),
307 cmd,
308 run: self.run.clone(),
309 force: false,
310 shell_pid: self.shell_pid,
311 dir: Dir(self.dir.clone().unwrap_or_else(|| crate::env::CWD.clone())),
312 autostop: self.autostop,
313 oneshot: self.oneshot,
314 oneshot_wait: None,
318 on_directory_enter: false,
319 proxy_idle_timeout_ms: self.proxy_idle_timeout_ms,
321 cron_schedule: self.cron_schedule.clone(),
322 cron_retrigger: self.cron_retrigger,
323 cron_immediate: self.cron_immediate,
324 retry: self.retry,
325 retry_count: self.retry_count,
326 ready_delay: self.ready_delay,
327 ready_output: self.ready_output.clone(),
328 ready_http: self.ready_http.clone(),
329 ready_port: self.ready_port.clone(),
330 ready_cmd: self.ready_cmd.clone(),
331 health_cmd: self.health_cmd.clone(),
332 health_http: self.health_http.clone(),
333 health_port: self.health_port.clone(),
334 port: self.port.clone(),
335 wait_ready: false,
336 depends: self.depends.clone(),
337 env: self.env.clone(),
338 watch: self.watch.clone(),
339 watch_mode: self.watch_mode,
340 watch_base_dir: self.watch_base_dir.clone(),
341 mise: self.mise,
342 slug: self.slug.clone(),
343 proxy: self.proxy,
344 user: self.user.clone(),
345 memory_limit: self.memory_limit,
346 cpu_limit: self.cpu_limit,
347 stop_signal: self.stop_signal,
348 archive_hook: self.archive_hook.clone(),
349 log_format: self.log_format.clone(),
350 on_output_hook,
351 pty: self.pty,
352 }
353 }
354}
355
356impl Display for Daemon {
357 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358 write!(f, "{}", self.id.qualified())
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_valid_daemon_ids() {
368 assert!(is_valid_daemon_id("myapp"));
370 assert!(is_valid_daemon_id("my-app"));
371 assert!(is_valid_daemon_id("my_app"));
372 assert!(is_valid_daemon_id("my.app"));
373 assert!(is_valid_daemon_id("MyApp123"));
374
375 assert!(is_valid_daemon_id("project/api"));
377 assert!(is_valid_daemon_id("global/web"));
378 assert!(is_valid_daemon_id("my-project/my-app"));
379 }
380
381 #[test]
382 fn test_invalid_daemon_ids() {
383 assert!(!is_valid_daemon_id(""));
385
386 assert!(!is_valid_daemon_id("a/b/c"));
388 assert!(!is_valid_daemon_id("../etc/passwd"));
389
390 assert!(!is_valid_daemon_id("/api"));
392 assert!(!is_valid_daemon_id("project/"));
393
394 assert!(!is_valid_daemon_id("foo\\bar"));
396
397 assert!(!is_valid_daemon_id(".."));
399 assert!(!is_valid_daemon_id("foo..bar"));
400
401 assert!(!is_valid_daemon_id("my--app"));
403 assert!(!is_valid_daemon_id("project--api"));
404 assert!(!is_valid_daemon_id("--app"));
405 assert!(!is_valid_daemon_id("app--"));
406
407 assert!(!is_valid_daemon_id("my app"));
409 assert!(!is_valid_daemon_id(" myapp"));
410 assert!(!is_valid_daemon_id("myapp "));
411
412 assert!(!is_valid_daemon_id("."));
414
415 assert!(!is_valid_daemon_id("my\x00app"));
417 assert!(!is_valid_daemon_id("my\napp"));
418 assert!(!is_valid_daemon_id("my\tapp"));
419
420 assert!(!is_valid_daemon_id("myäpp"));
422 assert!(!is_valid_daemon_id("приложение"));
423
424 assert!(!is_valid_daemon_id("app@host"));
426 assert!(!is_valid_daemon_id("app:8080"));
427 }
428}