1use isoloom_catalogue::{Catalogue, LogStream};
10use isoloom_spec::syntax::{AbsPath, Argv, PackageName, UnitName, UserName};
11
12use super::family::Family;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Daemon {
17 pub unit: UnitName,
19 pub foreground: Argv,
21 pub user: Option<UserName>,
23 pub logs: Vec<LogLink>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct LogLink {
30 pub path: AbsPath,
32 pub stream: Stream,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38#[non_exhaustive]
39pub enum Stream {
40 Stdout,
42 Stderr,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct FamilyDaemon {
49 pub family: Family,
51 pub package: Option<PackageName>,
53 pub daemon: Daemon,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct DaemonEntry {
60 pub key: UnitName,
62 pub forms: Vec<FamilyDaemon>,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Default)]
68pub struct DaemonTable {
69 entries: Vec<DaemonEntry>,
70}
71
72impl DaemonTable {
73 #[must_use]
75 pub fn new(entries: Vec<DaemonEntry>) -> DaemonTable {
76 DaemonTable { entries }
77 }
78
79 #[must_use]
82 pub fn common() -> DaemonTable {
83 Catalogue::embedded().map(DaemonTable::from).unwrap_or_default()
84 }
85
86 #[must_use]
88 pub fn entries(&self) -> &[DaemonEntry] {
89 &self.entries
90 }
91
92 #[must_use]
94 pub fn find(&self, name: &UnitName, family: Family) -> Option<(&DaemonEntry, &FamilyDaemon)> {
95 self.entries.iter().find_map(|e| {
96 let form = e.forms.iter().find(|f| f.family == family)?;
97 (e.key == *name || form.daemon.unit == *name).then_some((e, form))
98 })
99 }
100
101 #[must_use]
103 pub fn shipped_by(&self, package: &PackageName, family: Family) -> Option<(&DaemonEntry, &FamilyDaemon)> {
104 self.entries.iter().find_map(|e| {
105 let form = e.forms.iter().find(|f| f.family == family && f.package.as_ref() == Some(package))?;
106 Some((e, form))
107 })
108 }
109}
110
111impl From<&Catalogue> for DaemonTable {
112 fn from(catalogue: &Catalogue) -> DaemonTable {
114 let entries = catalogue
115 .daemons()
116 .map(|(key, d)| DaemonEntry {
117 key: key.clone(),
118 forms: d
119 .unit
120 .families()
121 .into_iter()
122 .filter_map(|family| {
123 let daemon = Daemon {
124 unit: d.unit.on(family)?.clone(),
125 foreground: d.foreground.on(family)?.clone(),
126 user: d.user.clone(),
127 logs: d
128 .logs
129 .iter()
130 .map(|(path, stream)| LogLink {
131 path: path.clone(),
132 stream: (*stream).into(),
133 })
134 .collect(),
135 };
136 let package = d.package.as_ref().and_then(|p| p.on(family)).cloned();
137 Some(FamilyDaemon { family, package, daemon })
138 })
139 .collect(),
140 })
141 .collect();
142 DaemonTable::new(entries)
143 }
144}
145
146impl From<LogStream> for Stream {
147 fn from(s: LogStream) -> Stream {
148 match s {
149 LogStream::Stderr => Stream::Stderr,
150 _ => Stream::Stdout,
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 fn unit(s: &str) -> UnitName {
160 s.parse().unwrap()
161 }
162
163 #[test]
164 fn the_catalogue_fills_the_table() {
165 let table = DaemonTable::common();
166 let keys: Vec<&str> = table.entries().iter().map(|e| e.key.as_str()).collect();
167 for key in ["nginx", "apache", "ssh", "cron", "rsyslog"] {
168 assert!(keys.contains(&key), "{key}: {keys:?}");
169 }
170 }
171
172 #[test]
173 fn a_step_finds_a_daemon_by_key_or_by_the_unit_its_family_names() {
174 let table = DaemonTable::common();
175 let (_, debian) = table.find(&unit("ssh"), Family::Debian).unwrap();
176 assert_eq!(debian.daemon.unit.as_str(), "ssh");
177 let (entry, rocky) = table.find(&unit("sshd"), Family::RedHat).unwrap();
178 assert_eq!((entry.key.as_str(), rocky.daemon.foreground.program()), ("ssh", "/usr/sbin/sshd"));
179 assert!(table.find(&unit("cron"), Family::Alpine).is_none(), "no cron form on Alpine");
180 assert!(table.find(&unit("myd"), Family::Debian).is_none());
181 }
182
183 #[test]
184 fn a_package_names_the_daemon_it_ships() {
185 let table = DaemonTable::common();
186 let (entry, form) = table.shipped_by(&"httpd".parse().unwrap(), Family::RedHat).unwrap();
187 assert_eq!((entry.key.as_str(), form.daemon.unit.as_str()), ("apache", "httpd"));
188 assert!(table.shipped_by(&"httpd".parse().unwrap(), Family::Debian).is_none());
189 let (_, nginx) = table.shipped_by(&"nginx".parse().unwrap(), Family::Alpine).unwrap();
190 assert_eq!(nginx.daemon.logs.len(), 2);
191 }
192}