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 = self
220 .dir
221 .as_deref()
222 .and_then(|dir| crate::pitchfork_toml::PitchforkToml::all_merged_from(dir).ok())
223 .or_else(|| crate::pitchfork_toml::PitchforkToml::all_merged().ok())
224 .and_then(|pt| {
225 pt.daemons
226 .get(&self.id)
227 .and_then(|d| d.hooks.as_ref())
228 .and_then(|h| h.on_output.clone())
229 });
230
231 RunOptions {
232 id: self.id.clone(),
233 cmd,
234 force: false,
235 shell_pid: self.shell_pid,
236 dir: Dir(self.dir.clone().unwrap_or_else(|| crate::env::CWD.clone())),
237 autostop: self.autostop,
238 cron_schedule: self.cron_schedule.clone(),
239 cron_retrigger: self.cron_retrigger,
240 retry: self.retry,
241 retry_count: self.retry_count,
242 ready_delay: self.ready_delay,
243 ready_output: self.ready_output.clone(),
244 ready_http: self.ready_http.clone(),
245 ready_port: self.ready_port,
246 ready_cmd: self.ready_cmd.clone(),
247 port: self.port.clone(),
248 wait_ready: false,
249 depends: self.depends.clone(),
250 env: self.env.clone(),
251 watch: self.watch.clone(),
252 watch_mode: self.watch_mode,
253 watch_base_dir: self.watch_base_dir.clone(),
254 mise: self.mise,
255 slug: self.slug.clone(),
256 proxy: self.proxy,
257 user: self.user.clone(),
258 memory_limit: self.memory_limit,
259 cpu_limit: self.cpu_limit,
260 stop_signal: self.stop_signal,
261 on_output_hook,
262 pty: self.pty,
263 }
264 }
265}
266
267impl Display for Daemon {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 write!(f, "{}", self.id.qualified())
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn test_valid_daemon_ids() {
279 assert!(is_valid_daemon_id("myapp"));
281 assert!(is_valid_daemon_id("my-app"));
282 assert!(is_valid_daemon_id("my_app"));
283 assert!(is_valid_daemon_id("my.app"));
284 assert!(is_valid_daemon_id("MyApp123"));
285
286 assert!(is_valid_daemon_id("project/api"));
288 assert!(is_valid_daemon_id("global/web"));
289 assert!(is_valid_daemon_id("my-project/my-app"));
290 }
291
292 #[test]
293 fn test_invalid_daemon_ids() {
294 assert!(!is_valid_daemon_id(""));
296
297 assert!(!is_valid_daemon_id("a/b/c"));
299 assert!(!is_valid_daemon_id("../etc/passwd"));
300
301 assert!(!is_valid_daemon_id("/api"));
303 assert!(!is_valid_daemon_id("project/"));
304
305 assert!(!is_valid_daemon_id("foo\\bar"));
307
308 assert!(!is_valid_daemon_id(".."));
310 assert!(!is_valid_daemon_id("foo..bar"));
311
312 assert!(!is_valid_daemon_id("my--app"));
314 assert!(!is_valid_daemon_id("project--api"));
315 assert!(!is_valid_daemon_id("--app"));
316 assert!(!is_valid_daemon_id("app--"));
317
318 assert!(!is_valid_daemon_id("my app"));
320 assert!(!is_valid_daemon_id(" myapp"));
321 assert!(!is_valid_daemon_id("myapp "));
322
323 assert!(!is_valid_daemon_id("."));
325
326 assert!(!is_valid_daemon_id("my\x00app"));
328 assert!(!is_valid_daemon_id("my\napp"));
329 assert!(!is_valid_daemon_id("my\tapp"));
330
331 assert!(!is_valid_daemon_id("myäpp"));
333 assert!(!is_valid_daemon_id("приложение"));
334
335 assert!(!is_valid_daemon_id("app@host"));
337 assert!(!is_valid_daemon_id("app:8080"));
338 }
339}