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
36pub fn daemon_id_to_path(id: &str) -> String {
45 id.replace('/', "--")
46}
47
48pub fn daemon_log_path(id: &str) -> std::path::PathBuf {
55 let safe_id = daemon_id_to_path(id);
56 crate::env::PITCHFORK_LOGS_DIR
57 .join(&safe_id)
58 .join(format!("{safe_id}.log"))
59}
60
61#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
62pub struct Daemon {
63 pub id: DaemonId,
64 pub title: Option<String>,
65 pub pid: Option<u32>,
66 pub shell_pid: Option<u32>,
67 pub status: DaemonStatus,
68 pub dir: Option<PathBuf>,
69 #[serde(skip_serializing_if = "Option::is_none", default)]
70 pub cmd: Option<Vec<String>>,
71 pub autostop: bool,
72 #[serde(skip_serializing_if = "Option::is_none", default)]
73 pub cron_schedule: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none", default)]
75 pub cron_retrigger: Option<CronRetrigger>,
76 #[serde(skip_serializing_if = "Option::is_none", default)]
77 pub last_cron_triggered: Option<chrono::DateTime<chrono::Local>>,
78 #[serde(skip_serializing_if = "Option::is_none", default)]
79 pub last_exit_success: Option<bool>,
80 #[serde(default)]
81 pub retry: Retry,
82 #[serde(default)]
83 pub retry_count: u32,
84 #[serde(skip_serializing_if = "Option::is_none", default)]
85 pub ready_delay: Option<u64>,
86 #[serde(skip_serializing_if = "Option::is_none", default)]
87 pub ready_output: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none", default)]
89 pub ready_http: Option<ReadyHttp>,
90 #[serde(skip_serializing_if = "Option::is_none", default)]
91 pub ready_port: Option<u16>,
92 #[serde(skip_serializing_if = "Option::is_none", default)]
93 pub ready_cmd: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none", default)]
96 pub port: Option<PortConfig>,
97 #[serde(skip_serializing_if = "Vec::is_empty", default)]
99 pub resolved_port: Vec<u16>,
100 #[serde(skip_serializing_if = "Option::is_none", default)]
103 pub active_port: Option<u16>,
104 #[serde(skip_serializing_if = "Option::is_none", default)]
106 pub slug: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none", default)]
109 pub proxy: Option<bool>,
110 #[serde(skip_serializing_if = "Vec::is_empty", default)]
111 pub depends: Vec<DaemonId>,
112 #[serde(skip_serializing_if = "Option::is_none", default)]
113 pub env: Option<IndexMap<String, String>>,
114 #[serde(skip_serializing_if = "Vec::is_empty", default)]
115 pub watch: Vec<String>,
116 #[serde(default)]
117 pub watch_mode: WatchMode,
118 #[serde(skip_serializing_if = "Option::is_none", default)]
119 pub watch_base_dir: Option<PathBuf>,
120 #[serde(skip_serializing_if = "Option::is_none", default)]
130 pub mise: Option<bool>,
131 #[serde(skip_serializing_if = "Option::is_none", default)]
133 pub user: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none", default)]
136 pub memory_limit: Option<MemoryLimit>,
137 #[serde(skip_serializing_if = "Option::is_none", default)]
139 pub cpu_limit: Option<CpuLimit>,
140 #[serde(skip_serializing_if = "Option::is_none", default)]
142 pub stop_signal: Option<StopConfig>,
143 #[serde(skip_serializing_if = "Option::is_none", default)]
145 pub pty: Option<bool>,
146}
147
148#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
149pub struct RunOptions {
150 pub id: DaemonId,
151 pub cmd: Vec<String>,
152 pub force: bool,
153 pub shell_pid: Option<u32>,
154 pub dir: Dir,
155 pub autostop: bool,
156 pub cron_schedule: Option<String>,
157 pub cron_retrigger: Option<CronRetrigger>,
158 pub retry: Retry,
159 pub retry_count: u32,
160 pub ready_delay: Option<u64>,
161 pub ready_output: Option<String>,
162 pub ready_http: Option<ReadyHttp>,
163 pub ready_port: Option<u16>,
164 pub ready_cmd: Option<String>,
165 pub port: Option<PortConfig>,
166 pub wait_ready: bool,
167 #[serde(skip_serializing_if = "Vec::is_empty", default)]
168 pub depends: Vec<DaemonId>,
169 #[serde(skip_serializing_if = "Option::is_none", default)]
170 pub env: Option<IndexMap<String, String>>,
171 #[serde(skip_serializing_if = "Vec::is_empty", default)]
172 pub watch: Vec<String>,
173 #[serde(default)]
174 pub watch_mode: WatchMode,
175 #[serde(skip_serializing_if = "Option::is_none", default)]
176 pub watch_base_dir: Option<PathBuf>,
177 #[serde(skip_serializing_if = "Option::is_none", default)]
182 pub mise: Option<bool>,
183 #[serde(skip_serializing_if = "Option::is_none", default)]
185 pub slug: Option<String>,
186 #[serde(skip_serializing_if = "Option::is_none", default)]
188 pub proxy: Option<bool>,
189 #[serde(skip_serializing_if = "Option::is_none", default)]
191 pub user: Option<String>,
192 #[serde(skip_serializing_if = "Option::is_none", default)]
194 pub memory_limit: Option<MemoryLimit>,
195 #[serde(skip_serializing_if = "Option::is_none", default)]
197 pub cpu_limit: Option<CpuLimit>,
198 #[serde(skip_serializing_if = "Option::is_none", default)]
200 pub stop_signal: Option<StopConfig>,
201 #[serde(skip_serializing_if = "Option::is_none", default)]
203 pub on_output_hook: Option<crate::pitchfork_toml::OnOutputHook>,
204 #[serde(skip_serializing_if = "Option::is_none", default)]
206 pub pty: Option<bool>,
207}
208
209impl Daemon {
210 pub fn to_run_options(&self, cmd: Vec<String>) -> RunOptions {
215 let on_output_hook = crate::pitchfork_toml::PitchforkToml::all_merged()
218 .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 force: false,
230 shell_pid: self.shell_pid,
231 dir: Dir(self.dir.clone().unwrap_or_else(|| crate::env::CWD.clone())),
232 autostop: self.autostop,
233 cron_schedule: self.cron_schedule.clone(),
234 cron_retrigger: self.cron_retrigger,
235 retry: self.retry,
236 retry_count: self.retry_count,
237 ready_delay: self.ready_delay,
238 ready_output: self.ready_output.clone(),
239 ready_http: self.ready_http.clone(),
240 ready_port: self.ready_port,
241 ready_cmd: self.ready_cmd.clone(),
242 port: self.port.clone(),
243 wait_ready: false,
244 depends: self.depends.clone(),
245 env: self.env.clone(),
246 watch: self.watch.clone(),
247 watch_mode: self.watch_mode,
248 watch_base_dir: self.watch_base_dir.clone(),
249 mise: self.mise,
250 slug: self.slug.clone(),
251 proxy: self.proxy,
252 user: self.user.clone(),
253 memory_limit: self.memory_limit,
254 cpu_limit: self.cpu_limit,
255 stop_signal: self.stop_signal,
256 on_output_hook,
257 pty: self.pty,
258 }
259 }
260}
261
262impl Display for Daemon {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 write!(f, "{}", self.id.qualified())
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn test_valid_daemon_ids() {
274 assert!(is_valid_daemon_id("myapp"));
276 assert!(is_valid_daemon_id("my-app"));
277 assert!(is_valid_daemon_id("my_app"));
278 assert!(is_valid_daemon_id("my.app"));
279 assert!(is_valid_daemon_id("MyApp123"));
280
281 assert!(is_valid_daemon_id("project/api"));
283 assert!(is_valid_daemon_id("global/web"));
284 assert!(is_valid_daemon_id("my-project/my-app"));
285 }
286
287 #[test]
288 fn test_invalid_daemon_ids() {
289 assert!(!is_valid_daemon_id(""));
291
292 assert!(!is_valid_daemon_id("a/b/c"));
294 assert!(!is_valid_daemon_id("../etc/passwd"));
295
296 assert!(!is_valid_daemon_id("/api"));
298 assert!(!is_valid_daemon_id("project/"));
299
300 assert!(!is_valid_daemon_id("foo\\bar"));
302
303 assert!(!is_valid_daemon_id(".."));
305 assert!(!is_valid_daemon_id("foo..bar"));
306
307 assert!(!is_valid_daemon_id("my--app"));
309 assert!(!is_valid_daemon_id("project--api"));
310 assert!(!is_valid_daemon_id("--app"));
311 assert!(!is_valid_daemon_id("app--"));
312
313 assert!(!is_valid_daemon_id("my app"));
315 assert!(!is_valid_daemon_id(" myapp"));
316 assert!(!is_valid_daemon_id("myapp "));
317
318 assert!(!is_valid_daemon_id("."));
320
321 assert!(!is_valid_daemon_id("my\x00app"));
323 assert!(!is_valid_daemon_id("my\napp"));
324 assert!(!is_valid_daemon_id("my\tapp"));
325
326 assert!(!is_valid_daemon_id("myäpp"));
328 assert!(!is_valid_daemon_id("приложение"));
329
330 assert!(!is_valid_daemon_id("app@host"));
332 assert!(!is_valid_daemon_id("app:8080"));
333 }
334}