1use crate::daemon_id::DaemonId;
2use crate::daemon_status::DaemonStatus;
3use crate::pitchfork_toml::CronRetrigger;
4use indexmap::IndexMap;
5use std::fmt::Display;
6use std::path::PathBuf;
7
8pub fn is_valid_daemon_id(id: &str) -> bool {
27 if id.contains('/') {
28 DaemonId::parse(id).is_ok()
29 } else {
30 DaemonId::try_new("global", id).is_ok()
31 }
32}
33
34pub fn daemon_id_to_path(id: &str) -> String {
43 id.replace('/', "--")
44}
45
46pub fn daemon_log_path(id: &str) -> std::path::PathBuf {
53 let safe_id = daemon_id_to_path(id);
54 crate::env::PITCHFORK_LOGS_DIR
55 .join(&safe_id)
56 .join(format!("{safe_id}.log"))
57}
58
59#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
60pub struct Daemon {
61 pub id: DaemonId,
62 pub title: Option<String>,
63 pub pid: Option<u32>,
64 pub shell_pid: Option<u32>,
65 pub status: DaemonStatus,
66 pub dir: Option<PathBuf>,
67 #[serde(skip_serializing_if = "Option::is_none", default)]
68 pub cmd: Option<Vec<String>>,
69 pub autostop: bool,
70 #[serde(skip_serializing_if = "Option::is_none", default)]
71 pub cron_schedule: Option<String>,
72 #[serde(skip_serializing_if = "Option::is_none", default)]
73 pub cron_retrigger: Option<CronRetrigger>,
74 #[serde(skip_serializing_if = "Option::is_none", default)]
75 pub last_cron_triggered: Option<chrono::DateTime<chrono::Local>>,
76 #[serde(skip_serializing_if = "Option::is_none", default)]
77 pub last_exit_success: Option<bool>,
78 #[serde(default)]
79 pub retry: u32,
80 #[serde(default)]
81 pub retry_count: u32,
82 #[serde(skip_serializing_if = "Option::is_none", default)]
83 pub ready_delay: Option<u64>,
84 #[serde(skip_serializing_if = "Option::is_none", default)]
85 pub ready_output: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none", default)]
87 pub ready_http: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none", default)]
89 pub ready_port: Option<u16>,
90 #[serde(skip_serializing_if = "Option::is_none", default)]
91 pub ready_cmd: Option<String>,
92 #[serde(skip_serializing_if = "Vec::is_empty", default)]
94 pub expected_port: Vec<u16>,
95 #[serde(skip_serializing_if = "Vec::is_empty", default)]
97 pub resolved_port: Vec<u16>,
98 #[serde(default)]
99 pub auto_bump_port: bool,
100 #[serde(default)]
101 pub port_bump_attempts: u32,
102 #[serde(skip_serializing_if = "Vec::is_empty", default)]
103 pub depends: Vec<DaemonId>,
104 #[serde(skip_serializing_if = "Option::is_none", default)]
105 pub env: Option<IndexMap<String, String>>,
106 #[serde(skip_serializing_if = "Vec::is_empty", default)]
107 pub watch: Vec<String>,
108 #[serde(skip_serializing_if = "Option::is_none", default)]
109 pub watch_base_dir: Option<PathBuf>,
110 #[serde(default)]
111 pub mise: bool,
112}
113
114#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
115pub struct RunOptions {
116 pub id: DaemonId,
117 pub cmd: Vec<String>,
118 pub force: bool,
119 pub shell_pid: Option<u32>,
120 pub dir: PathBuf,
121 pub autostop: bool,
122 pub cron_schedule: Option<String>,
123 pub cron_retrigger: Option<CronRetrigger>,
124 pub retry: u32,
125 pub retry_count: u32,
126 pub ready_delay: Option<u64>,
127 pub ready_output: Option<String>,
128 pub ready_http: Option<String>,
129 pub ready_port: Option<u16>,
130 pub ready_cmd: Option<String>,
131 pub expected_port: Vec<u16>,
132 pub auto_bump_port: bool,
133 pub port_bump_attempts: u32,
134 pub wait_ready: bool,
135 #[serde(skip_serializing_if = "Vec::is_empty", default)]
136 pub depends: Vec<DaemonId>,
137 #[serde(skip_serializing_if = "Option::is_none", default)]
138 pub env: Option<IndexMap<String, String>>,
139 #[serde(skip_serializing_if = "Vec::is_empty", default)]
140 pub watch: Vec<String>,
141 #[serde(skip_serializing_if = "Option::is_none", default)]
142 pub watch_base_dir: Option<PathBuf>,
143 #[serde(default)]
144 pub mise: bool,
145}
146
147impl Display for Daemon {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 write!(f, "{}", self.id.qualified())
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn test_valid_daemon_ids() {
159 assert!(is_valid_daemon_id("myapp"));
161 assert!(is_valid_daemon_id("my-app"));
162 assert!(is_valid_daemon_id("my_app"));
163 assert!(is_valid_daemon_id("my.app"));
164 assert!(is_valid_daemon_id("MyApp123"));
165
166 assert!(is_valid_daemon_id("project/api"));
168 assert!(is_valid_daemon_id("global/web"));
169 assert!(is_valid_daemon_id("my-project/my-app"));
170 }
171
172 #[test]
173 fn test_invalid_daemon_ids() {
174 assert!(!is_valid_daemon_id(""));
176
177 assert!(!is_valid_daemon_id("a/b/c"));
179 assert!(!is_valid_daemon_id("../etc/passwd"));
180
181 assert!(!is_valid_daemon_id("/api"));
183 assert!(!is_valid_daemon_id("project/"));
184
185 assert!(!is_valid_daemon_id("foo\\bar"));
187
188 assert!(!is_valid_daemon_id(".."));
190 assert!(!is_valid_daemon_id("foo..bar"));
191
192 assert!(!is_valid_daemon_id("my--app"));
194 assert!(!is_valid_daemon_id("project--api"));
195 assert!(!is_valid_daemon_id("--app"));
196 assert!(!is_valid_daemon_id("app--"));
197
198 assert!(!is_valid_daemon_id("my app"));
200 assert!(!is_valid_daemon_id(" myapp"));
201 assert!(!is_valid_daemon_id("myapp "));
202
203 assert!(!is_valid_daemon_id("."));
205
206 assert!(!is_valid_daemon_id("my\x00app"));
208 assert!(!is_valid_daemon_id("my\napp"));
209 assert!(!is_valid_daemon_id("my\tapp"));
210
211 assert!(!is_valid_daemon_id("myäpp"));
213 assert!(!is_valid_daemon_id("приложение"));
214
215 assert!(!is_valid_daemon_id("app@host"));
217 assert!(!is_valid_daemon_id("app:8080"));
218 }
219}