1use crate::{
2 install::{
3 Error, Init, Registration, Runner, Status, System,
4 account::Account,
5 ownership::{LogWriter, hand_over, installer, log_directory},
6 system::text,
7 },
8 platform::{Paths, ensure_defaults},
9};
10use std::{
11 fs::{self, Permissions},
12 io::Write,
13 os::unix::fs::{MetadataExt, PermissionsExt, symlink},
14 path::{Path, PathBuf},
15 thread,
16 time::Duration,
17};
18
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct AdapterFile {
21 pub path: PathBuf,
22 pub content: String,
23 pub mode: u32,
24}
25
26pub struct Adapter;
27
28impl Adapter {
29 pub const BINARY: &'static str = "/usr/bin/urma-node";
30 pub const LOG_DIR: &'static str = "/var/log/urma-node";
31 const UNIT: &'static str = "urma-node.service";
32 const SYSTEMD_UNIT: &'static str = "/etc/systemd/system/urma-node.service";
33 const OPENRC_SCRIPT: &'static str = "/etc/init.d/urma-node";
34 const OPENRC_ENABLED: &'static str = "/etc/runlevels/default/urma-node";
35 const RUNIT_DIR: &'static str = "/etc/sv/urma-node";
36 const RUNIT_RUN: &'static str = "/etc/sv/urma-node/run";
37 const RUNIT_LOG_RUN: &'static str = "/etc/sv/urma-node/log/run";
38 const RUNIT_LOG_SERVICE: &'static str = "/etc/sv/urma-node/log";
39 const RUNIT_SUPERVISED: &'static str = "/etc/sv/urma-node/supervise/ok";
40 const SUPERVISED_STATES: [&'static str; 3] = ["run:", "down:", "finish:"];
41 const RUNSV_POLLS: u32 = 40;
42 const RUNSV_POLL: Duration = Duration::from_millis(250);
43 const RUNIT_SERVICE_DIRS: [&'static str; 3] = [
44 "/var/service",
45 "/etc/runit/runsvdir/default",
46 "/run/runit/service",
47 ];
48 const SYSTEMD: &'static str = include_str!("templates/urma-node.service");
49 const OPENRC: &'static str = include_str!("templates/urma-node.openrc");
50 const RUNIT: &'static str = include_str!("templates/runit-run");
51 const RUNIT_LOG: &'static str = include_str!("templates/runit-log-run");
52 const MARKERS: [(Init, &'static str); 3] = [
53 (Init::Systemd, Self::SYSTEMD_UNIT),
54 (Init::Openrc, Self::OPENRC_SCRIPT),
55 (Init::Runit, Self::RUNIT_RUN),
56 ];
57 const WRITABLE_BY_OTHERS: u32 = 0o022;
58}
59
60fn foreign() -> Error {
61 Error::Failed("the Windows service manager exists only on Windows".into())
62}
63
64fn remove_present(path: &Path) -> Result<(), Error> {
65 if path.is_symlink() || path.exists() {
66 fs::remove_file(path)?;
67 }
68 Ok(())
69}
70
71fn file(path: &str, content: String, mode: u32) -> AdapterFile {
72 AdapterFile {
73 path: PathBuf::from(path),
74 content,
75 mode,
76 }
77}
78
79impl<R: Runner> System<R> {
80 pub fn render(&self, init: Init) -> Result<Vec<AdapterFile>, Error> {
81 let state = Paths::linux_under(Path::new("/")).state_dir;
82 let binary = text(&self.host.binary)?;
83 let state = text(&state)?;
84 let fill = |template: &str| {
85 template
86 .replace("@BIN@", binary)
87 .replace("@STATE@", state)
88 .replace("@LOG@", Adapter::LOG_DIR)
89 };
90 Ok(match init {
91 Init::Systemd => vec![file(Adapter::SYSTEMD_UNIT, fill(Adapter::SYSTEMD), 0o644)],
92 Init::Openrc => vec![file(Adapter::OPENRC_SCRIPT, fill(Adapter::OPENRC), 0o755)],
93 Init::Runit => vec![
94 file(Adapter::RUNIT_RUN, fill(Adapter::RUNIT), 0o755),
95 file(Adapter::RUNIT_LOG_RUN, fill(Adapter::RUNIT_LOG), 0o755),
96 ],
97 Init::Windows => return Err(foreign()),
98 })
99 }
100
101 pub fn trusted_binary(&self) -> Result<(), Error> {
102 let expected = Path::new(Adapter::BINARY);
103 let refuse = || {
104 Error::Failed(format!(
105 "refusing to register {}: the service binary must be {}, a regular file owned by \
106 root and writable by nobody else; install it there first",
107 self.host.binary.display(),
108 Adapter::BINARY
109 ))
110 };
111 if self.host.binary != expected {
112 return Err(refuse());
113 }
114 let meta = fs::symlink_metadata(self.at(expected))?;
115 let sound = meta.is_file()
116 && meta.uid() == installer()?
117 && meta.mode() & Adapter::WRITABLE_BY_OTHERS == 0;
118 match sound {
119 true => Ok(()),
120 false => Err(refuse()),
121 }
122 }
123
124 fn write_adapter(&self, init: Init) -> Result<(), Error> {
125 for adapter in self.render(init)? {
126 let path = self.at(&adapter.path);
127 let Some(parent) = path.parent() else {
128 return Err(Error::Failed(format!("{} has no parent", path.display())));
129 };
130 fs::create_dir_all(parent)?;
131 fs::write(&path, adapter.content)?;
132 fs::set_permissions(&path, Permissions::from_mode(adapter.mode))?;
133 }
134 Ok(())
135 }
136
137 pub fn registration(&self) -> Result<Registration, Error> {
138 for (init, marker) in Adapter::MARKERS {
139 if self.at(Path::new(marker)).is_file() {
140 return Ok(Registration::Registered(init));
141 }
142 }
143 Ok(Registration::Absent)
144 }
145
146 pub fn install(&mut self, init: Init, out: &mut impl Write) -> Result<(), Error> {
147 self.render(init)?;
148 self.trusted_binary()?;
149 let ids = self.ensure_account()?;
150 let paths = self.paths();
151 let created = ensure_defaults(&paths)?;
152 hand_over(&created, &paths, ids)?;
153 let log = self.at(Path::new(Adapter::LOG_DIR));
154 match init {
155 Init::Openrc => log_directory(&log, LogWriter::Root, ids)?,
156 Init::Runit => log_directory(&log, LogWriter::Service, ids)?,
157 Init::Systemd | Init::Windows => {}
158 }
159 let registration = self.registration()?;
160 self.write_adapter(init)?;
161 match registration {
162 Registration::Registered(..) => self.restart(init)?,
163 Registration::Absent => self.start(init)?,
164 }
165 self.join_group(out)
166 }
167
168 pub fn update(&mut self, init: Init) -> Result<(), Error> {
169 self.trusted_binary()?;
170 self.write_adapter(init)?;
171 self.restart(init)
172 }
173
174 pub fn uninstall(&mut self, purge: bool) -> Result<(), Error> {
175 match self.registration()? {
176 Registration::Registered(init) => self.remove(init)?,
177 Registration::Absent => {}
178 }
179 if purge {
180 let paths = self.paths();
181 let log = self.at(Path::new(Adapter::LOG_DIR));
182 for dir in [&paths.settings_dir, &paths.state_dir, &log] {
183 if dir.is_dir() && !dir.is_symlink() {
184 fs::remove_dir_all(dir)?;
185 }
186 }
187 self.remove_account()?;
188 }
189 Ok(())
190 }
191
192 fn start(&mut self, init: Init) -> Result<(), Error> {
193 match init {
194 Init::Systemd => {
195 self.exec("systemctl", &["daemon-reload"])?;
196 self.exec("systemctl", &["enable", "--now", Adapter::UNIT])?;
197 }
198 Init::Openrc => {
199 self.exec("rc-update", &["add", Account::NAME, "default"])?;
200 self.exec("rc-service", &[Account::NAME, "start"])?;
201 }
202 Init::Runit => self.link_runit()?,
203 Init::Windows => return Err(foreign()),
204 }
205 Ok(())
206 }
207
208 fn restart(&mut self, init: Init) -> Result<(), Error> {
209 match init {
210 Init::Systemd => {
211 self.exec("systemctl", &["daemon-reload"])?;
212 self.exec("systemctl", &["restart", Adapter::UNIT])?;
213 }
214 Init::Openrc => {
215 self.exec("rc-service", &[Account::NAME, "restart"])?;
216 }
217 Init::Runit => match self.runit_links()?.is_empty() {
218 true => self.link_runit()?,
219 false => {
220 self.exec("sv", &["restart", Adapter::RUNIT_DIR])?;
221 }
222 },
223 Init::Windows => return Err(foreign()),
224 }
225 Ok(())
226 }
227
228 fn remove(&mut self, init: Init) -> Result<(), Error> {
229 match init {
230 Init::Systemd => {
231 self.exec("systemctl", &["disable", "--now", Adapter::UNIT])?;
232 remove_present(&self.at(Path::new(Adapter::SYSTEMD_UNIT)))?;
233 self.exec("systemctl", &["daemon-reload"])?;
234 }
235 Init::Openrc => {
236 self.query("rc-service", &[Account::NAME, "stop"])?;
237 if self.at(Path::new(Adapter::OPENRC_ENABLED)).is_symlink() {
238 self.exec("rc-update", &["del", Account::NAME, "default"])?;
239 }
240 remove_present(&self.at(Path::new(Adapter::OPENRC_SCRIPT)))?;
241 }
242 Init::Runit => {
243 let supervised = self.at(Path::new(Adapter::RUNIT_SUPERVISED)).exists();
244 if supervised {
245 self.exec("sv", &["-w", "30", "down", Adapter::RUNIT_DIR])?;
246 }
247 for link in self.runit_links()? {
248 remove_present(&link)?;
249 }
250 if supervised {
251 self.query("sv", &["exit", Adapter::RUNIT_LOG_SERVICE])?;
252 self.query("sv", &["exit", Adapter::RUNIT_DIR])?;
253 self.await_runsv_exit()?;
254 }
255 let dir = self.at(Path::new(Adapter::RUNIT_DIR));
256 if dir.is_dir() && !dir.is_symlink() {
257 fs::remove_dir_all(dir)?;
258 }
259 }
260 Init::Windows => return Err(foreign()),
261 }
262 Ok(())
263 }
264
265 fn await_runsv_exit(&mut self) -> Result<(), Error> {
266 for _ in 0..Adapter::RUNSV_POLLS {
267 let status = self.query("sv", &["status", Adapter::RUNIT_DIR])?;
268 let supervised = Adapter::SUPERVISED_STATES
269 .iter()
270 .any(|state| status.stdout.starts_with(state));
271 if !supervised {
272 return Ok(());
273 }
274 thread::sleep(Adapter::RUNSV_POLL);
275 }
276 Err(Error::Failed(format!(
277 "runsv for {} did not exit; remove it once runsvdir has dropped the service",
278 Adapter::RUNIT_DIR
279 )))
280 }
281
282 fn runit_links(&self) -> Result<Vec<PathBuf>, Error> {
283 let mut parents: Vec<PathBuf> = Vec::new();
284 for dir in Adapter::RUNIT_SERVICE_DIRS {
285 let active = self.at(Path::new(dir));
286 if !active.is_dir() {
287 continue;
288 }
289 let parent = fs::canonicalize(&active)?;
290 if !parents.contains(&parent) {
291 parents.push(parent);
292 }
293 }
294 Ok(parents
295 .into_iter()
296 .map(|parent| parent.join(Account::NAME))
297 .filter(|link| link.is_symlink())
298 .collect())
299 }
300
301 fn link_runit(&self) -> Result<(), Error> {
302 for dir in Adapter::RUNIT_SERVICE_DIRS {
303 let active = self.at(Path::new(dir));
304 if active.is_dir() {
305 symlink(Adapter::RUNIT_DIR, active.join(Account::NAME))?;
306 return Ok(());
307 }
308 }
309 Err(Error::Failed(format!(
310 "no active runit service directory ({})",
311 Adapter::RUNIT_SERVICE_DIRS.join(", ")
312 )))
313 }
314
315 pub fn state(&mut self, init: Init) -> Result<Status, Error> {
316 let (enabled, running) = match init {
317 Init::Systemd => {
318 let enabled = self.query("systemctl", &["is-enabled", Adapter::UNIT])?;
319 let active = self.query("systemctl", &["is-active", Adapter::UNIT])?;
320 (
321 enabled.stdout.trim() == "enabled",
322 active.stdout.trim() == "active",
323 )
324 }
325 Init::Openrc => {
326 let enabled = self.at(Path::new(Adapter::OPENRC_ENABLED)).is_symlink();
327 let status = self.query("rc-service", &[Account::NAME, "status"])?;
328 (enabled, status.success)
329 }
330 Init::Runit => {
331 let status = self.query("sv", &["status", Adapter::RUNIT_DIR])?;
332 (
333 !self.runit_links()?.is_empty(),
334 status.stdout.starts_with("run:"),
335 )
336 }
337 Init::Windows => return Err(foreign()),
338 };
339 Ok(Status {
340 init,
341 registered: true,
342 enabled,
343 running,
344 listening: false,
345 })
346 }
347}