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 pty: Option<bool>,
126}
127
128#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
129pub struct RunOptions {
130 pub id: DaemonId,
131 pub cmd: Vec<String>,
132 #[serde(skip_serializing_if = "Option::is_none", default)]
135 pub run: Option<String>,
136 pub force: bool,
137 pub shell_pid: Option<u32>,
138 pub dir: Dir,
139 pub autostop: bool,
140 pub cron_schedule: Option<String>,
141 pub cron_retrigger: Option<CronRetrigger>,
142 pub cron_immediate: Option<bool>,
143 pub retry: Retry,
144 pub retry_count: u32,
145 pub ready_delay: Option<u64>,
146 pub ready_output: Option<String>,
147 pub ready_http: Option<ReadyHttp>,
148 pub ready_port: Option<u16>,
149 pub ready_cmd: Option<String>,
150 pub port: Option<PortConfig>,
151 pub wait_ready: bool,
152 #[serde(skip_serializing_if = "Vec::is_empty", default)]
153 pub depends: Vec<DaemonId>,
154 #[serde(skip_serializing_if = "Option::is_none", default)]
155 pub env: Option<IndexMap<String, String>>,
156 #[serde(skip_serializing_if = "Vec::is_empty", default)]
157 pub watch: Vec<String>,
158 #[serde(default)]
159 pub watch_mode: WatchMode,
160 #[serde(skip_serializing_if = "Option::is_none", default)]
161 pub watch_base_dir: Option<PathBuf>,
162 #[serde(skip_serializing_if = "Option::is_none", default)]
167 pub mise: Option<bool>,
168 #[serde(skip_serializing_if = "Option::is_none", default)]
170 pub slug: Option<String>,
171 #[serde(skip_serializing_if = "Option::is_none", default)]
173 pub proxy: Option<bool>,
174 #[serde(skip_serializing_if = "Option::is_none", default)]
176 pub user: Option<String>,
177 #[serde(skip_serializing_if = "Option::is_none", default)]
179 pub memory_limit: Option<MemoryLimit>,
180 #[serde(skip_serializing_if = "Option::is_none", default)]
182 pub cpu_limit: Option<CpuLimit>,
183 #[serde(skip_serializing_if = "Option::is_none", default)]
185 pub stop_signal: Option<StopConfig>,
186 #[serde(skip_serializing_if = "Option::is_none", default)]
188 pub on_output_hook: Option<crate::pitchfork_toml::OnOutputHook>,
189 #[serde(skip_serializing_if = "Option::is_none", default)]
191 pub pty: Option<bool>,
192}
193
194impl Daemon {
195 pub fn to_run_options(&self, cmd: Vec<String>) -> RunOptions {
200 let on_output_hook = self
205 .dir
206 .as_deref()
207 .and_then(|dir| crate::pitchfork_toml::PitchforkToml::all_merged_from(dir).ok())
208 .or_else(|| crate::pitchfork_toml::PitchforkToml::all_merged_all_namespaces().ok())
209 .and_then(|pt| {
210 pt.daemons
211 .get(&self.id)
212 .and_then(|d| d.hooks.as_ref())
213 .and_then(|h| h.on_output.clone())
214 });
215
216 RunOptions {
217 id: self.id.clone(),
218 cmd,
219 run: self.run.clone(),
220 force: false,
221 shell_pid: self.shell_pid,
222 dir: Dir(self.dir.clone().unwrap_or_else(|| crate::env::CWD.clone())),
223 autostop: self.autostop,
224 cron_schedule: self.cron_schedule.clone(),
225 cron_retrigger: self.cron_retrigger,
226 cron_immediate: self.cron_immediate,
227 retry: self.retry,
228 retry_count: self.retry_count,
229 ready_delay: self.ready_delay,
230 ready_output: self.ready_output.clone(),
231 ready_http: self.ready_http.clone(),
232 ready_port: self.ready_port,
233 ready_cmd: self.ready_cmd.clone(),
234 port: self.port.clone(),
235 wait_ready: false,
236 depends: self.depends.clone(),
237 env: self.env.clone(),
238 watch: self.watch.clone(),
239 watch_mode: self.watch_mode,
240 watch_base_dir: self.watch_base_dir.clone(),
241 mise: self.mise,
242 slug: self.slug.clone(),
243 proxy: self.proxy,
244 user: self.user.clone(),
245 memory_limit: self.memory_limit,
246 cpu_limit: self.cpu_limit,
247 stop_signal: self.stop_signal,
248 on_output_hook,
249 pty: self.pty,
250 }
251 }
252}
253
254impl Display for Daemon {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 write!(f, "{}", self.id.qualified())
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn test_valid_daemon_ids() {
266 assert!(is_valid_daemon_id("myapp"));
268 assert!(is_valid_daemon_id("my-app"));
269 assert!(is_valid_daemon_id("my_app"));
270 assert!(is_valid_daemon_id("my.app"));
271 assert!(is_valid_daemon_id("MyApp123"));
272
273 assert!(is_valid_daemon_id("project/api"));
275 assert!(is_valid_daemon_id("global/web"));
276 assert!(is_valid_daemon_id("my-project/my-app"));
277 }
278
279 #[test]
280 fn test_invalid_daemon_ids() {
281 assert!(!is_valid_daemon_id(""));
283
284 assert!(!is_valid_daemon_id("a/b/c"));
286 assert!(!is_valid_daemon_id("../etc/passwd"));
287
288 assert!(!is_valid_daemon_id("/api"));
290 assert!(!is_valid_daemon_id("project/"));
291
292 assert!(!is_valid_daemon_id("foo\\bar"));
294
295 assert!(!is_valid_daemon_id(".."));
297 assert!(!is_valid_daemon_id("foo..bar"));
298
299 assert!(!is_valid_daemon_id("my--app"));
301 assert!(!is_valid_daemon_id("project--api"));
302 assert!(!is_valid_daemon_id("--app"));
303 assert!(!is_valid_daemon_id("app--"));
304
305 assert!(!is_valid_daemon_id("my app"));
307 assert!(!is_valid_daemon_id(" myapp"));
308 assert!(!is_valid_daemon_id("myapp "));
309
310 assert!(!is_valid_daemon_id("."));
312
313 assert!(!is_valid_daemon_id("my\x00app"));
315 assert!(!is_valid_daemon_id("my\napp"));
316 assert!(!is_valid_daemon_id("my\tapp"));
317
318 assert!(!is_valid_daemon_id("myäpp"));
320 assert!(!is_valid_daemon_id("приложение"));
321
322 assert!(!is_valid_daemon_id("app@host"));
324 assert!(!is_valid_daemon_id("app:8080"));
325 }
326}