Skip to main content

urma_node/install/
linux.rs

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}