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 pub autostop: bool,
47 #[serde(skip_serializing_if = "Option::is_none", default)]
48 pub cron_schedule: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none", default)]
50 pub cron_retrigger: Option<CronRetrigger>,
51 #[serde(skip_serializing_if = "Option::is_none", default)]
52 pub cron_immediate: Option<bool>,
53 #[serde(skip_serializing_if = "Option::is_none", default)]
54 pub last_cron_triggered: Option<chrono::DateTime<chrono::Local>>,
55 #[serde(skip_serializing_if = "Option::is_none", default)]
56 pub last_exit_success: Option<bool>,
57 #[serde(default)]
58 pub retry: Retry,
59 #[serde(default)]
60 pub retry_count: u32,
61 #[serde(skip_serializing_if = "Option::is_none", default)]
62 pub ready_delay: Option<u64>,
63 #[serde(skip_serializing_if = "Option::is_none", default)]
64 pub ready_output: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none", default)]
66 pub ready_http: Option<ReadyHttp>,
67 #[serde(skip_serializing_if = "Option::is_none", default)]
68 pub ready_port: Option<u16>,
69 #[serde(skip_serializing_if = "Option::is_none", default)]
70 pub ready_cmd: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none", default)]
73 pub port: Option<PortConfig>,
74 #[serde(skip_serializing_if = "Vec::is_empty", default)]
76 pub resolved_port: Vec<u16>,
77 #[serde(skip_serializing_if = "Option::is_none", default)]
80 pub active_port: Option<u16>,
81 #[serde(skip_serializing_if = "Option::is_none", default)]
83 pub slug: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none", default)]
86 pub proxy: Option<bool>,
87 #[serde(skip_serializing_if = "Vec::is_empty", default)]
88 pub depends: Vec<DaemonId>,
89 #[serde(skip_serializing_if = "Option::is_none", default)]
90 pub env: Option<IndexMap<String, String>>,
91 #[serde(skip_serializing_if = "Vec::is_empty", default)]
92 pub watch: Vec<String>,
93 #[serde(default)]
94 pub watch_mode: WatchMode,
95 #[serde(skip_serializing_if = "Option::is_none", default)]
96 pub watch_base_dir: Option<PathBuf>,
97 #[serde(skip_serializing_if = "Option::is_none", default)]
107 pub mise: Option<bool>,
108 #[serde(skip_serializing_if = "Option::is_none", default)]
110 pub user: Option<String>,
111 #[serde(skip_serializing_if = "Option::is_none", default)]
113 pub memory_limit: Option<MemoryLimit>,
114 #[serde(skip_serializing_if = "Option::is_none", default)]
116 pub cpu_limit: Option<CpuLimit>,
117 #[serde(skip_serializing_if = "Option::is_none", default)]
119 pub stop_signal: Option<StopConfig>,
120 #[serde(skip_serializing_if = "Option::is_none", default)]
122 pub pty: Option<bool>,
123}
124
125#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
126pub struct RunOptions {
127 pub id: DaemonId,
128 pub cmd: Vec<String>,
129 pub force: bool,
130 pub shell_pid: Option<u32>,
131 pub dir: Dir,
132 pub autostop: bool,
133 pub cron_schedule: Option<String>,
134 pub cron_retrigger: Option<CronRetrigger>,
135 pub cron_immediate: Option<bool>,
136 pub retry: Retry,
137 pub retry_count: u32,
138 pub ready_delay: Option<u64>,
139 pub ready_output: Option<String>,
140 pub ready_http: Option<ReadyHttp>,
141 pub ready_port: Option<u16>,
142 pub ready_cmd: Option<String>,
143 pub port: Option<PortConfig>,
144 pub wait_ready: bool,
145 #[serde(skip_serializing_if = "Vec::is_empty", default)]
146 pub depends: Vec<DaemonId>,
147 #[serde(skip_serializing_if = "Option::is_none", default)]
148 pub env: Option<IndexMap<String, String>>,
149 #[serde(skip_serializing_if = "Vec::is_empty", default)]
150 pub watch: Vec<String>,
151 #[serde(default)]
152 pub watch_mode: WatchMode,
153 #[serde(skip_serializing_if = "Option::is_none", default)]
154 pub watch_base_dir: Option<PathBuf>,
155 #[serde(skip_serializing_if = "Option::is_none", default)]
160 pub mise: Option<bool>,
161 #[serde(skip_serializing_if = "Option::is_none", default)]
163 pub slug: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none", default)]
166 pub proxy: Option<bool>,
167 #[serde(skip_serializing_if = "Option::is_none", default)]
169 pub user: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none", default)]
172 pub memory_limit: Option<MemoryLimit>,
173 #[serde(skip_serializing_if = "Option::is_none", default)]
175 pub cpu_limit: Option<CpuLimit>,
176 #[serde(skip_serializing_if = "Option::is_none", default)]
178 pub stop_signal: Option<StopConfig>,
179 #[serde(skip_serializing_if = "Option::is_none", default)]
181 pub on_output_hook: Option<crate::pitchfork_toml::OnOutputHook>,
182 #[serde(skip_serializing_if = "Option::is_none", default)]
184 pub pty: Option<bool>,
185}
186
187impl Daemon {
188 pub fn to_run_options(&self, cmd: Vec<String>) -> RunOptions {
193 let on_output_hook = self
198 .dir
199 .as_deref()
200 .and_then(|dir| crate::pitchfork_toml::PitchforkToml::all_merged_from(dir).ok())
201 .or_else(|| crate::pitchfork_toml::PitchforkToml::all_merged_all_namespaces().ok())
202 .and_then(|pt| {
203 pt.daemons
204 .get(&self.id)
205 .and_then(|d| d.hooks.as_ref())
206 .and_then(|h| h.on_output.clone())
207 });
208
209 RunOptions {
210 id: self.id.clone(),
211 cmd,
212 force: false,
213 shell_pid: self.shell_pid,
214 dir: Dir(self.dir.clone().unwrap_or_else(|| crate::env::CWD.clone())),
215 autostop: self.autostop,
216 cron_schedule: self.cron_schedule.clone(),
217 cron_retrigger: self.cron_retrigger,
218 cron_immediate: self.cron_immediate,
219 retry: self.retry,
220 retry_count: self.retry_count,
221 ready_delay: self.ready_delay,
222 ready_output: self.ready_output.clone(),
223 ready_http: self.ready_http.clone(),
224 ready_port: self.ready_port,
225 ready_cmd: self.ready_cmd.clone(),
226 port: self.port.clone(),
227 wait_ready: false,
228 depends: self.depends.clone(),
229 env: self.env.clone(),
230 watch: self.watch.clone(),
231 watch_mode: self.watch_mode,
232 watch_base_dir: self.watch_base_dir.clone(),
233 mise: self.mise,
234 slug: self.slug.clone(),
235 proxy: self.proxy,
236 user: self.user.clone(),
237 memory_limit: self.memory_limit,
238 cpu_limit: self.cpu_limit,
239 stop_signal: self.stop_signal,
240 on_output_hook,
241 pty: self.pty,
242 }
243 }
244}
245
246impl Display for Daemon {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 write!(f, "{}", self.id.qualified())
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_valid_daemon_ids() {
258 assert!(is_valid_daemon_id("myapp"));
260 assert!(is_valid_daemon_id("my-app"));
261 assert!(is_valid_daemon_id("my_app"));
262 assert!(is_valid_daemon_id("my.app"));
263 assert!(is_valid_daemon_id("MyApp123"));
264
265 assert!(is_valid_daemon_id("project/api"));
267 assert!(is_valid_daemon_id("global/web"));
268 assert!(is_valid_daemon_id("my-project/my-app"));
269 }
270
271 #[test]
272 fn test_invalid_daemon_ids() {
273 assert!(!is_valid_daemon_id(""));
275
276 assert!(!is_valid_daemon_id("a/b/c"));
278 assert!(!is_valid_daemon_id("../etc/passwd"));
279
280 assert!(!is_valid_daemon_id("/api"));
282 assert!(!is_valid_daemon_id("project/"));
283
284 assert!(!is_valid_daemon_id("foo\\bar"));
286
287 assert!(!is_valid_daemon_id(".."));
289 assert!(!is_valid_daemon_id("foo..bar"));
290
291 assert!(!is_valid_daemon_id("my--app"));
293 assert!(!is_valid_daemon_id("project--api"));
294 assert!(!is_valid_daemon_id("--app"));
295 assert!(!is_valid_daemon_id("app--"));
296
297 assert!(!is_valid_daemon_id("my app"));
299 assert!(!is_valid_daemon_id(" myapp"));
300 assert!(!is_valid_daemon_id("myapp "));
301
302 assert!(!is_valid_daemon_id("."));
304
305 assert!(!is_valid_daemon_id("my\x00app"));
307 assert!(!is_valid_daemon_id("my\napp"));
308 assert!(!is_valid_daemon_id("my\tapp"));
309
310 assert!(!is_valid_daemon_id("myäpp"));
312 assert!(!is_valid_daemon_id("приложение"));
313
314 assert!(!is_valid_daemon_id("app@host"));
316 assert!(!is_valid_daemon_id("app:8080"));
317 }
318}