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 pub shell_pid: Option<u32>,
43 pub status: DaemonStatus,
44 pub dir: Option<PathBuf>,
45 #[serde(skip_serializing_if = "Option::is_none", default)]
46 pub cmd: Option<Vec<String>>,
47 #[serde(skip_serializing_if = "Option::is_none", default)]
49 pub run: Option<String>,
50 pub autostop: bool,
51 #[serde(skip_serializing_if = "Option::is_none", default)]
52 pub cron_schedule: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none", default)]
54 pub cron_retrigger: Option<CronRetrigger>,
55 #[serde(skip_serializing_if = "Option::is_none", default)]
56 pub cron_immediate: Option<bool>,
57 #[serde(skip_serializing_if = "Option::is_none", default)]
58 pub last_cron_triggered: Option<chrono::DateTime<chrono::Local>>,
59 #[serde(skip_serializing_if = "Option::is_none", default)]
60 pub last_exit_success: Option<bool>,
61 #[serde(default)]
62 pub retry: Retry,
63 #[serde(default)]
64 pub retry_count: u32,
65 #[serde(skip_serializing_if = "Option::is_none", default)]
66 pub ready_delay: Option<u64>,
67 #[serde(skip_serializing_if = "Option::is_none", default)]
68 pub ready_output: Option<ReadyOutput>,
69 #[serde(skip_serializing_if = "Option::is_none", default)]
70 pub ready_http: Option<ReadyHttp>,
71 #[serde(skip_serializing_if = "Option::is_none", default)]
72 pub ready_port: Option<ReadyPort>,
73 #[serde(skip_serializing_if = "Option::is_none", default)]
74 pub ready_cmd: Option<ReadyCmd>,
75 #[serde(skip_serializing_if = "Option::is_none", default)]
77 pub port: Option<PortConfig>,
78 #[serde(skip_serializing_if = "Vec::is_empty", default)]
80 pub resolved_port: Vec<u16>,
81 #[serde(skip_serializing_if = "Option::is_none", default)]
84 pub active_port: Option<u16>,
85 #[serde(skip_serializing_if = "Option::is_none", default)]
87 pub slug: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none", default)]
90 pub proxy: Option<bool>,
91 #[serde(skip_serializing_if = "Vec::is_empty", default)]
92 pub depends: Vec<DaemonId>,
93 #[serde(skip_serializing_if = "Option::is_none", default)]
94 pub env: Option<IndexMap<String, String>>,
95 #[serde(skip_serializing_if = "Vec::is_empty", default)]
96 pub watch: Vec<String>,
97 #[serde(default)]
98 pub watch_mode: WatchMode,
99 #[serde(skip_serializing_if = "Option::is_none", default)]
100 pub watch_base_dir: Option<PathBuf>,
101 #[serde(skip_serializing_if = "Option::is_none", default)]
111 pub mise: Option<bool>,
112 #[serde(skip_serializing_if = "Option::is_none", default)]
114 pub user: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none", default)]
117 pub memory_limit: Option<MemoryLimit>,
118 #[serde(skip_serializing_if = "Option::is_none", default)]
120 pub cpu_limit: Option<CpuLimit>,
121 #[serde(skip_serializing_if = "Option::is_none", default)]
123 pub stop_signal: Option<StopConfig>,
124 #[serde(skip_serializing_if = "Option::is_none", default)]
126 pub archive_hook: Option<String>,
127 #[serde(skip_serializing_if = "Option::is_none", default)]
129 pub log_format: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none", default)]
132 pub pty: Option<bool>,
133 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
136 pub config_registered: bool,
137}
138
139#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
140pub struct RunOptions {
141 pub id: DaemonId,
142 pub cmd: Vec<String>,
143 #[serde(skip_serializing_if = "Option::is_none", default)]
146 pub run: Option<String>,
147 pub force: bool,
148 pub shell_pid: Option<u32>,
149 pub dir: Dir,
150 pub autostop: bool,
151 pub cron_schedule: Option<String>,
152 pub cron_retrigger: Option<CronRetrigger>,
153 pub cron_immediate: Option<bool>,
154 pub retry: Retry,
155 pub retry_count: u32,
156 pub ready_delay: Option<u64>,
157 pub ready_output: Option<ReadyOutput>,
158 pub ready_http: Option<ReadyHttp>,
159 pub ready_port: Option<ReadyPort>,
160 pub ready_cmd: Option<ReadyCmd>,
161 pub port: Option<PortConfig>,
162 pub wait_ready: bool,
163 #[serde(skip_serializing_if = "Vec::is_empty", default)]
164 pub depends: Vec<DaemonId>,
165 #[serde(skip_serializing_if = "Option::is_none", default)]
166 pub env: Option<IndexMap<String, String>>,
167 #[serde(skip_serializing_if = "Vec::is_empty", default)]
168 pub watch: Vec<String>,
169 #[serde(default)]
170 pub watch_mode: WatchMode,
171 #[serde(skip_serializing_if = "Option::is_none", default)]
172 pub watch_base_dir: Option<PathBuf>,
173 #[serde(skip_serializing_if = "Option::is_none", default)]
178 pub mise: Option<bool>,
179 #[serde(skip_serializing_if = "Option::is_none", default)]
181 pub slug: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none", default)]
184 pub proxy: Option<bool>,
185 #[serde(skip_serializing_if = "Option::is_none", default)]
187 pub user: Option<String>,
188 #[serde(skip_serializing_if = "Option::is_none", default)]
190 pub memory_limit: Option<MemoryLimit>,
191 #[serde(skip_serializing_if = "Option::is_none", default)]
193 pub cpu_limit: Option<CpuLimit>,
194 #[serde(skip_serializing_if = "Option::is_none", default)]
196 pub stop_signal: Option<StopConfig>,
197 #[serde(skip_serializing_if = "Option::is_none", default)]
199 pub archive_hook: Option<String>,
200 #[serde(skip_serializing_if = "Option::is_none", default)]
202 pub log_format: Option<String>,
203 #[serde(skip_serializing_if = "Option::is_none", default)]
205 pub on_output_hook: Option<crate::pitchfork_toml::OnOutputHook>,
206 #[serde(skip_serializing_if = "Option::is_none", default)]
208 pub pty: Option<bool>,
209}
210
211impl Daemon {
212 pub fn to_run_options(&self, cmd: Vec<String>) -> RunOptions {
217 let on_output_hook = self
222 .dir
223 .as_deref()
224 .and_then(|dir| crate::pitchfork_toml::PitchforkToml::all_merged_from(dir).ok())
225 .or_else(|| crate::pitchfork_toml::PitchforkToml::all_merged_all_namespaces().ok())
226 .and_then(|pt| {
227 pt.daemons
228 .get(&self.id)
229 .and_then(|d| d.hooks.as_ref())
230 .and_then(|h| h.on_output.clone())
231 });
232
233 RunOptions {
234 id: self.id.clone(),
235 cmd,
236 run: self.run.clone(),
237 force: false,
238 shell_pid: self.shell_pid,
239 dir: Dir(self.dir.clone().unwrap_or_else(|| crate::env::CWD.clone())),
240 autostop: self.autostop,
241 cron_schedule: self.cron_schedule.clone(),
242 cron_retrigger: self.cron_retrigger,
243 cron_immediate: self.cron_immediate,
244 retry: self.retry,
245 retry_count: self.retry_count,
246 ready_delay: self.ready_delay,
247 ready_output: self.ready_output.clone(),
248 ready_http: self.ready_http.clone(),
249 ready_port: self.ready_port.clone(),
250 ready_cmd: self.ready_cmd.clone(),
251 port: self.port.clone(),
252 wait_ready: false,
253 depends: self.depends.clone(),
254 env: self.env.clone(),
255 watch: self.watch.clone(),
256 watch_mode: self.watch_mode,
257 watch_base_dir: self.watch_base_dir.clone(),
258 mise: self.mise,
259 slug: self.slug.clone(),
260 proxy: self.proxy,
261 user: self.user.clone(),
262 memory_limit: self.memory_limit,
263 cpu_limit: self.cpu_limit,
264 stop_signal: self.stop_signal,
265 archive_hook: self.archive_hook.clone(),
266 log_format: self.log_format.clone(),
267 on_output_hook,
268 pty: self.pty,
269 }
270 }
271}
272
273impl Display for Daemon {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 write!(f, "{}", self.id.qualified())
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn test_valid_daemon_ids() {
285 assert!(is_valid_daemon_id("myapp"));
287 assert!(is_valid_daemon_id("my-app"));
288 assert!(is_valid_daemon_id("my_app"));
289 assert!(is_valid_daemon_id("my.app"));
290 assert!(is_valid_daemon_id("MyApp123"));
291
292 assert!(is_valid_daemon_id("project/api"));
294 assert!(is_valid_daemon_id("global/web"));
295 assert!(is_valid_daemon_id("my-project/my-app"));
296 }
297
298 #[test]
299 fn test_invalid_daemon_ids() {
300 assert!(!is_valid_daemon_id(""));
302
303 assert!(!is_valid_daemon_id("a/b/c"));
305 assert!(!is_valid_daemon_id("../etc/passwd"));
306
307 assert!(!is_valid_daemon_id("/api"));
309 assert!(!is_valid_daemon_id("project/"));
310
311 assert!(!is_valid_daemon_id("foo\\bar"));
313
314 assert!(!is_valid_daemon_id(".."));
316 assert!(!is_valid_daemon_id("foo..bar"));
317
318 assert!(!is_valid_daemon_id("my--app"));
320 assert!(!is_valid_daemon_id("project--api"));
321 assert!(!is_valid_daemon_id("--app"));
322 assert!(!is_valid_daemon_id("app--"));
323
324 assert!(!is_valid_daemon_id("my app"));
326 assert!(!is_valid_daemon_id(" myapp"));
327 assert!(!is_valid_daemon_id("myapp "));
328
329 assert!(!is_valid_daemon_id("."));
331
332 assert!(!is_valid_daemon_id("my\x00app"));
334 assert!(!is_valid_daemon_id("my\napp"));
335 assert!(!is_valid_daemon_id("my\tapp"));
336
337 assert!(!is_valid_daemon_id("myäpp"));
339 assert!(!is_valid_daemon_id("приложение"));
340
341 assert!(!is_valid_daemon_id("app@host"));
343 assert!(!is_valid_daemon_id("app:8080"));
344 }
345}