1use crate::daemon_id::DaemonId;
2use crate::daemon_status::DaemonStatus;
3use crate::pitchfork_toml::{
4 CpuLimit, CronRetrigger, Dir, MemoryLimit, PortConfig, ReadyCmd, ReadyHttp, ReadyOutput,
5 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)]
90 pub port: Option<PortConfig>,
91 #[serde(skip_serializing_if = "Vec::is_empty", default)]
93 pub resolved_port: Vec<u16>,
94 #[serde(skip_serializing_if = "Option::is_none", default)]
97 pub active_port: Option<u16>,
98 #[serde(skip_serializing_if = "Option::is_none", default)]
100 pub slug: Option<String>,
101 #[serde(skip_serializing_if = "Option::is_none", default)]
103 pub proxy: Option<bool>,
104 #[serde(skip_serializing_if = "Vec::is_empty", default)]
105 pub depends: Vec<DaemonId>,
106 #[serde(skip_serializing_if = "Option::is_none", default)]
107 pub env: Option<IndexMap<String, String>>,
108 #[serde(skip_serializing_if = "Vec::is_empty", default)]
109 pub watch: Vec<String>,
110 #[serde(default)]
111 pub watch_mode: WatchMode,
112 #[serde(skip_serializing_if = "Option::is_none", default)]
113 pub watch_base_dir: Option<PathBuf>,
114 #[serde(skip_serializing_if = "Option::is_none", default)]
124 pub mise: Option<bool>,
125 #[serde(skip_serializing_if = "Option::is_none", default)]
127 pub user: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none", default)]
130 pub memory_limit: Option<MemoryLimit>,
131 #[serde(skip_serializing_if = "Option::is_none", default)]
133 pub cpu_limit: Option<CpuLimit>,
134 #[serde(skip_serializing_if = "Option::is_none", default)]
136 pub stop_signal: Option<StopConfig>,
137 #[serde(skip_serializing_if = "Option::is_none", default)]
139 pub archive_hook: Option<String>,
140 #[serde(skip_serializing_if = "Option::is_none", default)]
142 pub log_format: Option<String>,
143 #[serde(skip_serializing_if = "Option::is_none", default)]
145 pub pty: Option<bool>,
146 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
149 pub config_registered: bool,
150}
151
152#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
153pub struct RunOptions {
154 pub id: DaemonId,
155 pub cmd: Vec<String>,
156 #[serde(skip_serializing_if = "Option::is_none", default)]
159 pub run: Option<String>,
160 pub force: bool,
161 pub shell_pid: Option<u32>,
162 pub dir: Dir,
163 pub autostop: bool,
164 pub cron_schedule: Option<String>,
165 pub cron_retrigger: Option<CronRetrigger>,
166 pub cron_immediate: Option<bool>,
167 pub retry: Retry,
168 pub retry_count: u32,
169 pub ready_delay: Option<u64>,
170 pub ready_output: Option<ReadyOutput>,
171 pub ready_http: Option<ReadyHttp>,
172 pub ready_port: Option<ReadyPort>,
173 pub ready_cmd: Option<ReadyCmd>,
174 pub port: Option<PortConfig>,
175 pub wait_ready: bool,
176 #[serde(skip_serializing_if = "Vec::is_empty", default)]
177 pub depends: Vec<DaemonId>,
178 #[serde(skip_serializing_if = "Option::is_none", default)]
179 pub env: Option<IndexMap<String, String>>,
180 #[serde(skip_serializing_if = "Vec::is_empty", default)]
181 pub watch: Vec<String>,
182 #[serde(default)]
183 pub watch_mode: WatchMode,
184 #[serde(skip_serializing_if = "Option::is_none", default)]
185 pub watch_base_dir: Option<PathBuf>,
186 #[serde(skip_serializing_if = "Option::is_none", default)]
191 pub mise: Option<bool>,
192 #[serde(skip_serializing_if = "Option::is_none", default)]
194 pub slug: Option<String>,
195 #[serde(skip_serializing_if = "Option::is_none", default)]
197 pub proxy: Option<bool>,
198 #[serde(skip_serializing_if = "Option::is_none", default)]
200 pub user: Option<String>,
201 #[serde(skip_serializing_if = "Option::is_none", default)]
203 pub memory_limit: Option<MemoryLimit>,
204 #[serde(skip_serializing_if = "Option::is_none", default)]
206 pub cpu_limit: Option<CpuLimit>,
207 #[serde(skip_serializing_if = "Option::is_none", default)]
209 pub stop_signal: Option<StopConfig>,
210 #[serde(skip_serializing_if = "Option::is_none", default)]
212 pub archive_hook: Option<String>,
213 #[serde(skip_serializing_if = "Option::is_none", default)]
215 pub log_format: Option<String>,
216 #[serde(skip_serializing_if = "Option::is_none", default)]
218 pub on_output_hook: Option<crate::pitchfork_toml::OnOutputHook>,
219 #[serde(skip_serializing_if = "Option::is_none", default)]
221 pub pty: Option<bool>,
222}
223
224impl Daemon {
225 pub fn to_run_options(&self, cmd: Vec<String>) -> RunOptions {
230 let on_output_hook = self
235 .dir
236 .as_deref()
237 .and_then(|dir| crate::pitchfork_toml::PitchforkToml::all_merged_from(dir).ok())
238 .or_else(|| crate::pitchfork_toml::PitchforkToml::all_merged_all_namespaces().ok())
239 .and_then(|pt| {
240 pt.daemons
241 .get(&self.id)
242 .and_then(|d| d.hooks.as_ref())
243 .and_then(|h| h.on_output.clone())
244 });
245
246 RunOptions {
247 id: self.id.clone(),
248 cmd,
249 run: self.run.clone(),
250 force: false,
251 shell_pid: self.shell_pid,
252 dir: Dir(self.dir.clone().unwrap_or_else(|| crate::env::CWD.clone())),
253 autostop: self.autostop,
254 cron_schedule: self.cron_schedule.clone(),
255 cron_retrigger: self.cron_retrigger,
256 cron_immediate: self.cron_immediate,
257 retry: self.retry,
258 retry_count: self.retry_count,
259 ready_delay: self.ready_delay,
260 ready_output: self.ready_output.clone(),
261 ready_http: self.ready_http.clone(),
262 ready_port: self.ready_port.clone(),
263 ready_cmd: self.ready_cmd.clone(),
264 port: self.port.clone(),
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 stop_signal: self.stop_signal,
278 archive_hook: self.archive_hook.clone(),
279 log_format: self.log_format.clone(),
280 on_output_hook,
281 pty: self.pty,
282 }
283 }
284}
285
286impl Display for Daemon {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 write!(f, "{}", self.id.qualified())
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_valid_daemon_ids() {
298 assert!(is_valid_daemon_id("myapp"));
300 assert!(is_valid_daemon_id("my-app"));
301 assert!(is_valid_daemon_id("my_app"));
302 assert!(is_valid_daemon_id("my.app"));
303 assert!(is_valid_daemon_id("MyApp123"));
304
305 assert!(is_valid_daemon_id("project/api"));
307 assert!(is_valid_daemon_id("global/web"));
308 assert!(is_valid_daemon_id("my-project/my-app"));
309 }
310
311 #[test]
312 fn test_invalid_daemon_ids() {
313 assert!(!is_valid_daemon_id(""));
315
316 assert!(!is_valid_daemon_id("a/b/c"));
318 assert!(!is_valid_daemon_id("../etc/passwd"));
319
320 assert!(!is_valid_daemon_id("/api"));
322 assert!(!is_valid_daemon_id("project/"));
323
324 assert!(!is_valid_daemon_id("foo\\bar"));
326
327 assert!(!is_valid_daemon_id(".."));
329 assert!(!is_valid_daemon_id("foo..bar"));
330
331 assert!(!is_valid_daemon_id("my--app"));
333 assert!(!is_valid_daemon_id("project--api"));
334 assert!(!is_valid_daemon_id("--app"));
335 assert!(!is_valid_daemon_id("app--"));
336
337 assert!(!is_valid_daemon_id("my app"));
339 assert!(!is_valid_daemon_id(" myapp"));
340 assert!(!is_valid_daemon_id("myapp "));
341
342 assert!(!is_valid_daemon_id("."));
344
345 assert!(!is_valid_daemon_id("my\x00app"));
347 assert!(!is_valid_daemon_id("my\napp"));
348 assert!(!is_valid_daemon_id("my\tapp"));
349
350 assert!(!is_valid_daemon_id("myäpp"));
352 assert!(!is_valid_daemon_id("приложение"));
353
354 assert!(!is_valid_daemon_id("app@host"));
356 assert!(!is_valid_daemon_id("app:8080"));
357 }
358}