use crate::daemon_id::DaemonId;
use crate::daemon_status::DaemonStatus;
use crate::pitchfork_toml::{
CpuLimit, CronRetrigger, Dir, MemoryLimit, PortConfig, ReadyHttp, Retry, StopConfig, WatchMode,
};
use indexmap::IndexMap;
use std::fmt::Display;
use std::path::PathBuf;
pub fn is_valid_daemon_id(id: &str) -> bool {
if id.contains('/') {
DaemonId::parse(id).is_ok()
} else {
DaemonId::try_new("global", id).is_ok()
}
}
pub fn daemon_id_to_path(id: &str) -> String {
id.replace('/', "--")
}
pub fn daemon_log_path(id: &str) -> std::path::PathBuf {
let safe_id = daemon_id_to_path(id);
crate::env::PITCHFORK_LOGS_DIR
.join(&safe_id)
.join(format!("{safe_id}.log"))
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct Daemon {
pub id: DaemonId,
pub title: Option<String>,
pub pid: Option<u32>,
pub shell_pid: Option<u32>,
pub status: DaemonStatus,
pub dir: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub cmd: Option<Vec<String>>,
pub autostop: bool,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub cron_schedule: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub cron_retrigger: Option<CronRetrigger>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub last_cron_triggered: Option<chrono::DateTime<chrono::Local>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub last_exit_success: Option<bool>,
#[serde(default)]
pub retry: Retry,
#[serde(default)]
pub retry_count: u32,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub ready_delay: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub ready_output: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub ready_http: Option<ReadyHttp>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub ready_port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub ready_cmd: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub port: Option<PortConfig>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub resolved_port: Vec<u16>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub active_port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub slug: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub proxy: Option<bool>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub depends: Vec<DaemonId>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub env: Option<IndexMap<String, String>>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub watch: Vec<String>,
#[serde(default)]
pub watch_mode: WatchMode,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub watch_base_dir: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub mise: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub memory_limit: Option<MemoryLimit>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub cpu_limit: Option<CpuLimit>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub stop_signal: Option<StopConfig>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub pty: Option<bool>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
pub struct RunOptions {
pub id: DaemonId,
pub cmd: Vec<String>,
pub force: bool,
pub shell_pid: Option<u32>,
pub dir: Dir,
pub autostop: bool,
pub cron_schedule: Option<String>,
pub cron_retrigger: Option<CronRetrigger>,
pub retry: Retry,
pub retry_count: u32,
pub ready_delay: Option<u64>,
pub ready_output: Option<String>,
pub ready_http: Option<ReadyHttp>,
pub ready_port: Option<u16>,
pub ready_cmd: Option<String>,
pub port: Option<PortConfig>,
pub wait_ready: bool,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub depends: Vec<DaemonId>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub env: Option<IndexMap<String, String>>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub watch: Vec<String>,
#[serde(default)]
pub watch_mode: WatchMode,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub watch_base_dir: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub mise: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub slug: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub proxy: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub memory_limit: Option<MemoryLimit>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub cpu_limit: Option<CpuLimit>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub stop_signal: Option<StopConfig>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub on_output_hook: Option<crate::pitchfork_toml::OnOutputHook>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub pty: Option<bool>,
}
impl Daemon {
pub fn to_run_options(&self, cmd: Vec<String>) -> RunOptions {
let on_output_hook = crate::pitchfork_toml::PitchforkToml::all_merged()
.ok()
.and_then(|pt| {
pt.daemons
.get(&self.id)
.and_then(|d| d.hooks.as_ref())
.and_then(|h| h.on_output.clone())
});
RunOptions {
id: self.id.clone(),
cmd,
force: false,
shell_pid: self.shell_pid,
dir: Dir(self.dir.clone().unwrap_or_else(|| crate::env::CWD.clone())),
autostop: self.autostop,
cron_schedule: self.cron_schedule.clone(),
cron_retrigger: self.cron_retrigger,
retry: self.retry,
retry_count: self.retry_count,
ready_delay: self.ready_delay,
ready_output: self.ready_output.clone(),
ready_http: self.ready_http.clone(),
ready_port: self.ready_port,
ready_cmd: self.ready_cmd.clone(),
port: self.port.clone(),
wait_ready: false,
depends: self.depends.clone(),
env: self.env.clone(),
watch: self.watch.clone(),
watch_mode: self.watch_mode,
watch_base_dir: self.watch_base_dir.clone(),
mise: self.mise,
slug: self.slug.clone(),
proxy: self.proxy,
user: self.user.clone(),
memory_limit: self.memory_limit,
cpu_limit: self.cpu_limit,
stop_signal: self.stop_signal,
on_output_hook,
pty: self.pty,
}
}
}
impl Display for Daemon {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.id.qualified())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_daemon_ids() {
assert!(is_valid_daemon_id("myapp"));
assert!(is_valid_daemon_id("my-app"));
assert!(is_valid_daemon_id("my_app"));
assert!(is_valid_daemon_id("my.app"));
assert!(is_valid_daemon_id("MyApp123"));
assert!(is_valid_daemon_id("project/api"));
assert!(is_valid_daemon_id("global/web"));
assert!(is_valid_daemon_id("my-project/my-app"));
}
#[test]
fn test_invalid_daemon_ids() {
assert!(!is_valid_daemon_id(""));
assert!(!is_valid_daemon_id("a/b/c"));
assert!(!is_valid_daemon_id("../etc/passwd"));
assert!(!is_valid_daemon_id("/api"));
assert!(!is_valid_daemon_id("project/"));
assert!(!is_valid_daemon_id("foo\\bar"));
assert!(!is_valid_daemon_id(".."));
assert!(!is_valid_daemon_id("foo..bar"));
assert!(!is_valid_daemon_id("my--app"));
assert!(!is_valid_daemon_id("project--api"));
assert!(!is_valid_daemon_id("--app"));
assert!(!is_valid_daemon_id("app--"));
assert!(!is_valid_daemon_id("my app"));
assert!(!is_valid_daemon_id(" myapp"));
assert!(!is_valid_daemon_id("myapp "));
assert!(!is_valid_daemon_id("."));
assert!(!is_valid_daemon_id("my\x00app"));
assert!(!is_valid_daemon_id("my\napp"));
assert!(!is_valid_daemon_id("my\tapp"));
assert!(!is_valid_daemon_id("myäpp"));
assert!(!is_valid_daemon_id("приложение"));
assert!(!is_valid_daemon_id("app@host"));
assert!(!is_valid_daemon_id("app:8080"));
}
}