1use crate::daemon_id::DaemonId;
2use crate::daemon_status::DaemonStatus;
3use crate::pitchfork_toml::{
4 CpuLimit, CronRetrigger, Dir, HealthCmd, HealthHttp, HealthPort, MemoryLimit, PortConfig,
5 ReadyCmd, ReadyHttp, ReadyOutput, ReadyPort, Retry, StopConfig, WatchMode,
6};
7use indexmap::IndexMap;
8use std::fmt::Display;
9use std::path::PathBuf;
10
11pub fn is_valid_daemon_id(id: &str) -> bool {
30 if id.contains('/') {
31 DaemonId::parse(id).is_ok()
32 } else {
33 DaemonId::try_new("global", id).is_ok()
34 }
35}
36
37#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
38pub struct Daemon {
39 pub id: DaemonId,
40 pub title: Option<String>,
41 pub pid: Option<u32>,
42 #[serde(skip_serializing_if = "Option::is_none", default)]
47 pub start_time: Option<u64>,
48 #[serde(skip_serializing_if = "Option::is_none", default)]
54 pub boot_time: Option<u64>,
55 pub shell_pid: Option<u32>,
56 pub status: DaemonStatus,
57 pub dir: Option<PathBuf>,
58 #[serde(skip_serializing_if = "Option::is_none", default)]
59 pub cmd: Option<Vec<String>>,
60 #[serde(skip_serializing_if = "Option::is_none", default)]
62 pub run: Option<String>,
63 pub autostop: bool,
64 #[serde(skip_serializing_if = "Option::is_none", default)]
65 pub cron_schedule: Option<String>,
66 #[serde(skip_serializing_if = "Option::is_none", default)]
67 pub cron_retrigger: Option<CronRetrigger>,
68 #[serde(skip_serializing_if = "Option::is_none", default)]
69 pub cron_immediate: Option<bool>,
70 #[serde(skip_serializing_if = "Option::is_none", default)]
71 pub last_cron_triggered: Option<chrono::DateTime<chrono::Local>>,
72 #[serde(skip_serializing_if = "Option::is_none", default)]
73 pub last_exit_success: Option<bool>,
74 #[serde(default)]
75 pub retry: Retry,
76 #[serde(default)]
77 pub retry_count: u32,
78 #[serde(skip_serializing_if = "Option::is_none", default)]
79 pub ready_delay: Option<u64>,
80 #[serde(skip_serializing_if = "Option::is_none", default)]
81 pub ready_output: Option<ReadyOutput>,
82 #[serde(skip_serializing_if = "Option::is_none", default)]
83 pub ready_http: Option<ReadyHttp>,
84 #[serde(skip_serializing_if = "Option::is_none", default)]
85 pub ready_port: Option<ReadyPort>,
86 #[serde(skip_serializing_if = "Option::is_none", default)]
87 pub ready_cmd: Option<ReadyCmd>,
88 #[serde(skip_serializing_if = "Option::is_none", default)]
89 pub health_cmd: Option<HealthCmd>,
90 #[serde(skip_serializing_if = "Option::is_none", default)]
91 pub health_http: Option<HealthHttp>,
92 #[serde(skip_serializing_if = "Option::is_none", default)]
93 pub health_port: Option<HealthPort>,
94 #[serde(skip_serializing_if = "Option::is_none", default)]
96 pub port: Option<PortConfig>,
97 #[serde(skip_serializing_if = "Vec::is_empty", default)]
99 pub resolved_port: Vec<u16>,
100 #[serde(skip_serializing_if = "Option::is_none", default)]
103 pub active_port: Option<u16>,
104 #[serde(skip_serializing_if = "Option::is_none", default)]
106 pub slug: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none", default)]
109 pub proxy: Option<bool>,
110 #[serde(skip_serializing_if = "Vec::is_empty", default)]
111 pub depends: Vec<DaemonId>,
112 #[serde(skip_serializing_if = "Option::is_none", default)]
113 pub env: Option<IndexMap<String, String>>,
114 #[serde(skip_serializing_if = "Vec::is_empty", default)]
115 pub watch: Vec<String>,
116 #[serde(default)]
117 pub watch_mode: WatchMode,
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 user: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none", default)]
136 pub memory_limit: Option<MemoryLimit>,
137 #[serde(skip_serializing_if = "Option::is_none", default)]
139 pub cpu_limit: Option<CpuLimit>,
140 #[serde(skip_serializing_if = "Option::is_none", default)]
142 pub stop_signal: Option<StopConfig>,
143 #[serde(skip_serializing_if = "Option::is_none", default)]
145 pub archive_hook: Option<String>,
146 #[serde(skip_serializing_if = "Option::is_none", default)]
148 pub log_format: Option<String>,
149 #[serde(skip_serializing_if = "Option::is_none", default)]
151 pub pty: Option<bool>,
152 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
155 pub config_registered: bool,
156}
157
158#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
159pub struct RunOptions {
160 pub id: DaemonId,
161 pub cmd: Vec<String>,
162 #[serde(skip_serializing_if = "Option::is_none", default)]
165 pub run: Option<String>,
166 pub force: bool,
167 pub shell_pid: Option<u32>,
168 pub dir: Dir,
169 pub autostop: bool,
170 pub cron_schedule: Option<String>,
171 pub cron_retrigger: Option<CronRetrigger>,
172 pub cron_immediate: Option<bool>,
173 pub retry: Retry,
174 pub retry_count: u32,
175 pub ready_delay: Option<u64>,
176 pub ready_output: Option<ReadyOutput>,
177 pub ready_http: Option<ReadyHttp>,
178 pub ready_port: Option<ReadyPort>,
179 pub ready_cmd: Option<ReadyCmd>,
180 pub health_cmd: Option<HealthCmd>,
181 pub health_http: Option<HealthHttp>,
182 pub health_port: Option<HealthPort>,
183 pub port: Option<PortConfig>,
184 pub wait_ready: bool,
185 #[serde(skip_serializing_if = "Vec::is_empty", default)]
186 pub depends: Vec<DaemonId>,
187 #[serde(skip_serializing_if = "Option::is_none", default)]
188 pub env: Option<IndexMap<String, String>>,
189 #[serde(skip_serializing_if = "Vec::is_empty", default)]
190 pub watch: Vec<String>,
191 #[serde(default)]
192 pub watch_mode: WatchMode,
193 #[serde(skip_serializing_if = "Option::is_none", default)]
194 pub watch_base_dir: Option<PathBuf>,
195 #[serde(skip_serializing_if = "Option::is_none", default)]
200 pub mise: Option<bool>,
201 #[serde(skip_serializing_if = "Option::is_none", default)]
203 pub slug: Option<String>,
204 #[serde(skip_serializing_if = "Option::is_none", default)]
206 pub proxy: Option<bool>,
207 #[serde(skip_serializing_if = "Option::is_none", default)]
209 pub user: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none", default)]
212 pub memory_limit: Option<MemoryLimit>,
213 #[serde(skip_serializing_if = "Option::is_none", default)]
215 pub cpu_limit: Option<CpuLimit>,
216 #[serde(skip_serializing_if = "Option::is_none", default)]
218 pub stop_signal: Option<StopConfig>,
219 #[serde(skip_serializing_if = "Option::is_none", default)]
221 pub archive_hook: Option<String>,
222 #[serde(skip_serializing_if = "Option::is_none", default)]
224 pub log_format: Option<String>,
225 #[serde(skip_serializing_if = "Option::is_none", default)]
227 pub on_output_hook: Option<crate::pitchfork_toml::OnOutputHook>,
228 #[serde(skip_serializing_if = "Option::is_none", default)]
230 pub pty: Option<bool>,
231}
232
233impl Daemon {
234 pub fn to_run_options(&self, cmd: Vec<String>) -> RunOptions {
239 let on_output_hook = self
244 .dir
245 .as_deref()
246 .and_then(|dir| crate::pitchfork_toml::PitchforkToml::all_merged_from(dir).ok())
247 .or_else(|| crate::pitchfork_toml::PitchforkToml::all_merged_all_namespaces().ok())
248 .and_then(|pt| {
249 pt.daemons
250 .get(&self.id)
251 .and_then(|d| d.hooks.as_ref())
252 .and_then(|h| h.on_output.clone())
253 });
254
255 RunOptions {
256 id: self.id.clone(),
257 cmd,
258 run: self.run.clone(),
259 force: false,
260 shell_pid: self.shell_pid,
261 dir: Dir(self.dir.clone().unwrap_or_else(|| crate::env::CWD.clone())),
262 autostop: self.autostop,
263 cron_schedule: self.cron_schedule.clone(),
264 cron_retrigger: self.cron_retrigger,
265 cron_immediate: self.cron_immediate,
266 retry: self.retry,
267 retry_count: self.retry_count,
268 ready_delay: self.ready_delay,
269 ready_output: self.ready_output.clone(),
270 ready_http: self.ready_http.clone(),
271 ready_port: self.ready_port.clone(),
272 ready_cmd: self.ready_cmd.clone(),
273 health_cmd: self.health_cmd.clone(),
274 health_http: self.health_http.clone(),
275 health_port: self.health_port.clone(),
276 port: self.port.clone(),
277 wait_ready: false,
278 depends: self.depends.clone(),
279 env: self.env.clone(),
280 watch: self.watch.clone(),
281 watch_mode: self.watch_mode,
282 watch_base_dir: self.watch_base_dir.clone(),
283 mise: self.mise,
284 slug: self.slug.clone(),
285 proxy: self.proxy,
286 user: self.user.clone(),
287 memory_limit: self.memory_limit,
288 cpu_limit: self.cpu_limit,
289 stop_signal: self.stop_signal,
290 archive_hook: self.archive_hook.clone(),
291 log_format: self.log_format.clone(),
292 on_output_hook,
293 pty: self.pty,
294 }
295 }
296}
297
298impl Display for Daemon {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 write!(f, "{}", self.id.qualified())
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn test_valid_daemon_ids() {
310 assert!(is_valid_daemon_id("myapp"));
312 assert!(is_valid_daemon_id("my-app"));
313 assert!(is_valid_daemon_id("my_app"));
314 assert!(is_valid_daemon_id("my.app"));
315 assert!(is_valid_daemon_id("MyApp123"));
316
317 assert!(is_valid_daemon_id("project/api"));
319 assert!(is_valid_daemon_id("global/web"));
320 assert!(is_valid_daemon_id("my-project/my-app"));
321 }
322
323 #[test]
324 fn test_invalid_daemon_ids() {
325 assert!(!is_valid_daemon_id(""));
327
328 assert!(!is_valid_daemon_id("a/b/c"));
330 assert!(!is_valid_daemon_id("../etc/passwd"));
331
332 assert!(!is_valid_daemon_id("/api"));
334 assert!(!is_valid_daemon_id("project/"));
335
336 assert!(!is_valid_daemon_id("foo\\bar"));
338
339 assert!(!is_valid_daemon_id(".."));
341 assert!(!is_valid_daemon_id("foo..bar"));
342
343 assert!(!is_valid_daemon_id("my--app"));
345 assert!(!is_valid_daemon_id("project--api"));
346 assert!(!is_valid_daemon_id("--app"));
347 assert!(!is_valid_daemon_id("app--"));
348
349 assert!(!is_valid_daemon_id("my app"));
351 assert!(!is_valid_daemon_id(" myapp"));
352 assert!(!is_valid_daemon_id("myapp "));
353
354 assert!(!is_valid_daemon_id("."));
356
357 assert!(!is_valid_daemon_id("my\x00app"));
359 assert!(!is_valid_daemon_id("my\napp"));
360 assert!(!is_valid_daemon_id("my\tapp"));
361
362 assert!(!is_valid_daemon_id("myäpp"));
364 assert!(!is_valid_daemon_id("приложение"));
365
366 assert!(!is_valid_daemon_id("app@host"));
368 assert!(!is_valid_daemon_id("app:8080"));
369 }
370}