1use crate::daemon_id::DaemonId;
2use crate::daemon_status::DaemonStatus;
3use crate::pitchfork_toml::{
4 CpuLimit, CronRetrigger, Dir, MemoryLimit, PortConfig, ReadyHttp, Retry, StopConfig, WatchMode,
5};
6use indexmap::IndexMap;
7use std::fmt::Display;
8use std::path::PathBuf;
9
10pub fn is_valid_daemon_id(id: &str) -> bool {
29 if id.contains('/') {
30 DaemonId::parse(id).is_ok()
31 } else {
32 DaemonId::try_new("global", id).is_ok()
33 }
34}
35
36#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
37pub struct Daemon {
38 pub id: DaemonId,
39 pub title: Option<String>,
40 pub pid: Option<u32>,
41 pub shell_pid: Option<u32>,
42 pub status: DaemonStatus,
43 pub dir: Option<PathBuf>,
44 #[serde(skip_serializing_if = "Option::is_none", default)]
45 pub cmd: Option<Vec<String>>,
46 #[serde(skip_serializing_if = "Option::is_none", default)]
48 pub run: Option<String>,
49 pub autostop: bool,
50 #[serde(skip_serializing_if = "Option::is_none", default)]
51 pub cron_schedule: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none", default)]
53 pub cron_retrigger: Option<CronRetrigger>,
54 #[serde(skip_serializing_if = "Option::is_none", default)]
55 pub cron_immediate: Option<bool>,
56 #[serde(skip_serializing_if = "Option::is_none", default)]
57 pub last_cron_triggered: Option<chrono::DateTime<chrono::Local>>,
58 #[serde(skip_serializing_if = "Option::is_none", default)]
59 pub last_exit_success: Option<bool>,
60 #[serde(default)]
61 pub retry: Retry,
62 #[serde(default)]
63 pub retry_count: u32,
64 #[serde(skip_serializing_if = "Option::is_none", default)]
65 pub ready_delay: Option<u64>,
66 #[serde(skip_serializing_if = "Option::is_none", default)]
67 pub ready_output: Option<String>,
68 #[serde(skip_serializing_if = "Option::is_none", default)]
69 pub ready_http: Option<ReadyHttp>,
70 #[serde(skip_serializing_if = "Option::is_none", default)]
71 pub ready_port: Option<u16>,
72 #[serde(skip_serializing_if = "Option::is_none", default)]
73 pub ready_cmd: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none", default)]
76 pub port: Option<PortConfig>,
77 #[serde(skip_serializing_if = "Vec::is_empty", default)]
79 pub resolved_port: Vec<u16>,
80 #[serde(skip_serializing_if = "Option::is_none", default)]
83 pub active_port: Option<u16>,
84 #[serde(skip_serializing_if = "Option::is_none", default)]
86 pub slug: Option<String>,
87 #[serde(skip_serializing_if = "Option::is_none", default)]
89 pub proxy: Option<bool>,
90 #[serde(skip_serializing_if = "Vec::is_empty", default)]
91 pub depends: Vec<DaemonId>,
92 #[serde(skip_serializing_if = "Option::is_none", default)]
93 pub env: Option<IndexMap<String, String>>,
94 #[serde(skip_serializing_if = "Vec::is_empty", default)]
95 pub watch: Vec<String>,
96 #[serde(default)]
97 pub watch_mode: WatchMode,
98 #[serde(skip_serializing_if = "Option::is_none", default)]
99 pub watch_base_dir: Option<PathBuf>,
100 #[serde(skip_serializing_if = "Option::is_none", default)]
110 pub mise: Option<bool>,
111 #[serde(skip_serializing_if = "Option::is_none", default)]
113 pub user: Option<String>,
114 #[serde(skip_serializing_if = "Option::is_none", default)]
116 pub memory_limit: Option<MemoryLimit>,
117 #[serde(skip_serializing_if = "Option::is_none", default)]
119 pub cpu_limit: Option<CpuLimit>,
120 #[serde(skip_serializing_if = "Option::is_none", default)]
122 pub stop_signal: Option<StopConfig>,
123 #[serde(skip_serializing_if = "Option::is_none", default)]
125 pub archive_hook: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none", default)]
128 pub pty: Option<bool>,
129 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
132 pub config_registered: bool,
133}
134
135#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
136pub struct RunOptions {
137 pub id: DaemonId,
138 pub cmd: Vec<String>,
139 #[serde(skip_serializing_if = "Option::is_none", default)]
142 pub run: Option<String>,
143 pub force: bool,
144 pub shell_pid: Option<u32>,
145 pub dir: Dir,
146 pub autostop: bool,
147 pub cron_schedule: Option<String>,
148 pub cron_retrigger: Option<CronRetrigger>,
149 pub cron_immediate: Option<bool>,
150 pub retry: Retry,
151 pub retry_count: u32,
152 pub ready_delay: Option<u64>,
153 pub ready_output: Option<String>,
154 pub ready_http: Option<ReadyHttp>,
155 pub ready_port: Option<u16>,
156 pub ready_cmd: Option<String>,
157 pub port: Option<PortConfig>,
158 pub wait_ready: bool,
159 #[serde(skip_serializing_if = "Vec::is_empty", default)]
160 pub depends: Vec<DaemonId>,
161 #[serde(skip_serializing_if = "Option::is_none", default)]
162 pub env: Option<IndexMap<String, String>>,
163 #[serde(skip_serializing_if = "Vec::is_empty", default)]
164 pub watch: Vec<String>,
165 #[serde(default)]
166 pub watch_mode: WatchMode,
167 #[serde(skip_serializing_if = "Option::is_none", default)]
168 pub watch_base_dir: Option<PathBuf>,
169 #[serde(skip_serializing_if = "Option::is_none", default)]
174 pub mise: Option<bool>,
175 #[serde(skip_serializing_if = "Option::is_none", default)]
177 pub slug: Option<String>,
178 #[serde(skip_serializing_if = "Option::is_none", default)]
180 pub proxy: Option<bool>,
181 #[serde(skip_serializing_if = "Option::is_none", default)]
183 pub user: Option<String>,
184 #[serde(skip_serializing_if = "Option::is_none", default)]
186 pub memory_limit: Option<MemoryLimit>,
187 #[serde(skip_serializing_if = "Option::is_none", default)]
189 pub cpu_limit: Option<CpuLimit>,
190 #[serde(skip_serializing_if = "Option::is_none", default)]
192 pub stop_signal: Option<StopConfig>,
193 #[serde(skip_serializing_if = "Option::is_none", default)]
195 pub archive_hook: Option<String>,
196 #[serde(skip_serializing_if = "Option::is_none", default)]
198 pub on_output_hook: Option<crate::pitchfork_toml::OnOutputHook>,
199 #[serde(skip_serializing_if = "Option::is_none", default)]
201 pub pty: Option<bool>,
202}
203
204impl Daemon {
205 pub fn to_run_options(&self, cmd: Vec<String>) -> RunOptions {
210 let on_output_hook = self
215 .dir
216 .as_deref()
217 .and_then(|dir| crate::pitchfork_toml::PitchforkToml::all_merged_from(dir).ok())
218 .or_else(|| crate::pitchfork_toml::PitchforkToml::all_merged_all_namespaces().ok())
219 .and_then(|pt| {
220 pt.daemons
221 .get(&self.id)
222 .and_then(|d| d.hooks.as_ref())
223 .and_then(|h| h.on_output.clone())
224 });
225
226 RunOptions {
227 id: self.id.clone(),
228 cmd,
229 run: self.run.clone(),
230 force: false,
231 shell_pid: self.shell_pid,
232 dir: Dir(self.dir.clone().unwrap_or_else(|| crate::env::CWD.clone())),
233 autostop: self.autostop,
234 cron_schedule: self.cron_schedule.clone(),
235 cron_retrigger: self.cron_retrigger,
236 cron_immediate: self.cron_immediate,
237 retry: self.retry,
238 retry_count: self.retry_count,
239 ready_delay: self.ready_delay,
240 ready_output: self.ready_output.clone(),
241 ready_http: self.ready_http.clone(),
242 ready_port: self.ready_port,
243 ready_cmd: self.ready_cmd.clone(),
244 port: self.port.clone(),
245 wait_ready: false,
246 depends: self.depends.clone(),
247 env: self.env.clone(),
248 watch: self.watch.clone(),
249 watch_mode: self.watch_mode,
250 watch_base_dir: self.watch_base_dir.clone(),
251 mise: self.mise,
252 slug: self.slug.clone(),
253 proxy: self.proxy,
254 user: self.user.clone(),
255 memory_limit: self.memory_limit,
256 cpu_limit: self.cpu_limit,
257 stop_signal: self.stop_signal,
258 archive_hook: self.archive_hook.clone(),
259 on_output_hook,
260 pty: self.pty,
261 }
262 }
263}
264
265impl Display for Daemon {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 write!(f, "{}", self.id.qualified())
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_valid_daemon_ids() {
277 assert!(is_valid_daemon_id("myapp"));
279 assert!(is_valid_daemon_id("my-app"));
280 assert!(is_valid_daemon_id("my_app"));
281 assert!(is_valid_daemon_id("my.app"));
282 assert!(is_valid_daemon_id("MyApp123"));
283
284 assert!(is_valid_daemon_id("project/api"));
286 assert!(is_valid_daemon_id("global/web"));
287 assert!(is_valid_daemon_id("my-project/my-app"));
288 }
289
290 #[test]
291 fn test_invalid_daemon_ids() {
292 assert!(!is_valid_daemon_id(""));
294
295 assert!(!is_valid_daemon_id("a/b/c"));
297 assert!(!is_valid_daemon_id("../etc/passwd"));
298
299 assert!(!is_valid_daemon_id("/api"));
301 assert!(!is_valid_daemon_id("project/"));
302
303 assert!(!is_valid_daemon_id("foo\\bar"));
305
306 assert!(!is_valid_daemon_id(".."));
308 assert!(!is_valid_daemon_id("foo..bar"));
309
310 assert!(!is_valid_daemon_id("my--app"));
312 assert!(!is_valid_daemon_id("project--api"));
313 assert!(!is_valid_daemon_id("--app"));
314 assert!(!is_valid_daemon_id("app--"));
315
316 assert!(!is_valid_daemon_id("my app"));
318 assert!(!is_valid_daemon_id(" myapp"));
319 assert!(!is_valid_daemon_id("myapp "));
320
321 assert!(!is_valid_daemon_id("."));
323
324 assert!(!is_valid_daemon_id("my\x00app"));
326 assert!(!is_valid_daemon_id("my\napp"));
327 assert!(!is_valid_daemon_id("my\tapp"));
328
329 assert!(!is_valid_daemon_id("myäpp"));
331 assert!(!is_valid_daemon_id("приложение"));
332
333 assert!(!is_valid_daemon_id("app@host"));
335 assert!(!is_valid_daemon_id("app:8080"));
336 }
337}