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}
167
168#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
169pub struct RunOptions {
170 pub id: DaemonId,
171 pub cmd: Vec<String>,
172 #[serde(skip_serializing_if = "Option::is_none", default)]
175 pub run: Option<String>,
176 pub force: bool,
177 pub shell_pid: Option<u32>,
178 pub dir: Dir,
179 pub autostop: bool,
180 pub cron_schedule: Option<String>,
181 pub cron_retrigger: Option<CronRetrigger>,
182 pub cron_immediate: Option<bool>,
183 pub retry: Retry,
184 pub retry_count: u32,
185 pub ready_delay: Option<u64>,
186 pub ready_output: Option<ReadyOutput>,
187 pub ready_http: Option<ReadyHttp>,
188 pub ready_port: Option<ReadyPort>,
189 pub ready_cmd: Option<ReadyCmd>,
190 pub health_cmd: Option<HealthCmd>,
191 pub health_http: Option<HealthHttp>,
192 pub health_port: Option<HealthPort>,
193 pub port: Option<PortConfig>,
194 pub wait_ready: bool,
195 #[serde(skip_serializing_if = "Vec::is_empty", default)]
196 pub depends: Vec<DaemonId>,
197 #[serde(skip_serializing_if = "Option::is_none", default)]
198 pub env: Option<IndexMap<String, String>>,
199 #[serde(skip_serializing_if = "Vec::is_empty", default)]
200 pub watch: Vec<String>,
201 #[serde(default)]
202 pub watch_mode: WatchMode,
203 #[serde(skip_serializing_if = "Option::is_none", default)]
204 pub watch_base_dir: Option<PathBuf>,
205 #[serde(skip_serializing_if = "Option::is_none", default)]
210 pub mise: Option<bool>,
211 #[serde(skip_serializing_if = "Option::is_none", default)]
213 pub slug: Option<String>,
214 #[serde(skip_serializing_if = "Option::is_none", default)]
216 pub proxy: Option<bool>,
217 #[serde(skip_serializing_if = "Option::is_none", default)]
219 pub user: Option<String>,
220 #[serde(skip_serializing_if = "Option::is_none", default)]
222 pub memory_limit: Option<MemoryLimit>,
223 #[serde(skip_serializing_if = "Option::is_none", default)]
225 pub cpu_limit: Option<CpuLimit>,
226 #[serde(skip_serializing_if = "Option::is_none", default)]
228 pub stop_signal: Option<StopConfig>,
229 #[serde(skip_serializing_if = "Option::is_none", default)]
231 pub archive_hook: Option<String>,
232 #[serde(skip_serializing_if = "Option::is_none", default)]
234 pub log_format: Option<String>,
235 #[serde(skip_serializing_if = "Option::is_none", default)]
237 pub on_output_hook: Option<crate::pitchfork_toml::OnOutputHook>,
238 #[serde(skip_serializing_if = "Option::is_none", default)]
240 pub pty: Option<bool>,
241 #[serde(default)]
248 pub oneshot: bool,
249 #[serde(skip_serializing_if = "Option::is_none", default)]
257 pub oneshot_wait: Option<OneshotWait>,
258 #[serde(default)]
264 pub on_directory_enter: bool,
265}
266
267impl Daemon {
268 pub fn to_run_options(&self, cmd: Vec<String>) -> RunOptions {
273 let on_output_hook = self
278 .dir
279 .as_deref()
280 .and_then(|dir| crate::pitchfork_toml::PitchforkToml::all_merged_from(dir).ok())
281 .or_else(|| crate::pitchfork_toml::PitchforkToml::all_merged_all_namespaces().ok())
282 .and_then(|pt| {
283 pt.daemons
284 .get(&self.id)
285 .and_then(|d| d.hooks.as_ref())
286 .and_then(|h| h.on_output.clone())
287 });
288
289 RunOptions {
290 id: self.id.clone(),
291 cmd,
292 run: self.run.clone(),
293 force: false,
294 shell_pid: self.shell_pid,
295 dir: Dir(self.dir.clone().unwrap_or_else(|| crate::env::CWD.clone())),
296 autostop: self.autostop,
297 oneshot: self.oneshot,
298 oneshot_wait: None,
302 on_directory_enter: false,
303 cron_schedule: self.cron_schedule.clone(),
304 cron_retrigger: self.cron_retrigger,
305 cron_immediate: self.cron_immediate,
306 retry: self.retry,
307 retry_count: self.retry_count,
308 ready_delay: self.ready_delay,
309 ready_output: self.ready_output.clone(),
310 ready_http: self.ready_http.clone(),
311 ready_port: self.ready_port.clone(),
312 ready_cmd: self.ready_cmd.clone(),
313 health_cmd: self.health_cmd.clone(),
314 health_http: self.health_http.clone(),
315 health_port: self.health_port.clone(),
316 port: self.port.clone(),
317 wait_ready: false,
318 depends: self.depends.clone(),
319 env: self.env.clone(),
320 watch: self.watch.clone(),
321 watch_mode: self.watch_mode,
322 watch_base_dir: self.watch_base_dir.clone(),
323 mise: self.mise,
324 slug: self.slug.clone(),
325 proxy: self.proxy,
326 user: self.user.clone(),
327 memory_limit: self.memory_limit,
328 cpu_limit: self.cpu_limit,
329 stop_signal: self.stop_signal,
330 archive_hook: self.archive_hook.clone(),
331 log_format: self.log_format.clone(),
332 on_output_hook,
333 pty: self.pty,
334 }
335 }
336}
337
338impl Display for Daemon {
339 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340 write!(f, "{}", self.id.qualified())
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_valid_daemon_ids() {
350 assert!(is_valid_daemon_id("myapp"));
352 assert!(is_valid_daemon_id("my-app"));
353 assert!(is_valid_daemon_id("my_app"));
354 assert!(is_valid_daemon_id("my.app"));
355 assert!(is_valid_daemon_id("MyApp123"));
356
357 assert!(is_valid_daemon_id("project/api"));
359 assert!(is_valid_daemon_id("global/web"));
360 assert!(is_valid_daemon_id("my-project/my-app"));
361 }
362
363 #[test]
364 fn test_invalid_daemon_ids() {
365 assert!(!is_valid_daemon_id(""));
367
368 assert!(!is_valid_daemon_id("a/b/c"));
370 assert!(!is_valid_daemon_id("../etc/passwd"));
371
372 assert!(!is_valid_daemon_id("/api"));
374 assert!(!is_valid_daemon_id("project/"));
375
376 assert!(!is_valid_daemon_id("foo\\bar"));
378
379 assert!(!is_valid_daemon_id(".."));
381 assert!(!is_valid_daemon_id("foo..bar"));
382
383 assert!(!is_valid_daemon_id("my--app"));
385 assert!(!is_valid_daemon_id("project--api"));
386 assert!(!is_valid_daemon_id("--app"));
387 assert!(!is_valid_daemon_id("app--"));
388
389 assert!(!is_valid_daemon_id("my app"));
391 assert!(!is_valid_daemon_id(" myapp"));
392 assert!(!is_valid_daemon_id("myapp "));
393
394 assert!(!is_valid_daemon_id("."));
396
397 assert!(!is_valid_daemon_id("my\x00app"));
399 assert!(!is_valid_daemon_id("my\napp"));
400 assert!(!is_valid_daemon_id("my\tapp"));
401
402 assert!(!is_valid_daemon_id("myäpp"));
404 assert!(!is_valid_daemon_id("приложение"));
405
406 assert!(!is_valid_daemon_id("app@host"));
408 assert!(!is_valid_daemon_id("app:8080"));
409 }
410}