Skip to main content

urma_node/install/
system.rs

1#[cfg(unix)]
2use crate::install::Outcome;
3use crate::{
4    config::SERVICE_LISTEN,
5    install::{Action, Commands, Error, Host, InitChoice, Runner},
6    platform::{Paths, ServiceSettings},
7};
8use serde::Serialize;
9use std::{
10    io::{ErrorKind, Write},
11    net::{SocketAddr, TcpStream},
12    path::{Component, Path, PathBuf},
13    time::Duration,
14};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
17#[serde(rename_all = "lowercase")]
18pub enum Init {
19    Systemd,
20    Openrc,
21    Runit,
22    Windows,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum Registration {
27    Registered(Init),
28    Absent,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
32pub struct Status {
33    pub init: Init,
34    pub registered: bool,
35    pub enabled: bool,
36    pub running: bool,
37    pub listening: bool,
38}
39
40pub struct Search;
41
42impl Search {
43    pub const DIRS: [&'static str; 4] = ["/usr/sbin", "/usr/bin", "/sbin", "/bin"];
44}
45
46pub struct System<R: Runner> {
47    pub root: PathBuf,
48    pub host: Host,
49    pub runner: R,
50}
51
52pub fn run(action: Action, host: &Host, out: &mut impl Write) -> Result<(), Error> {
53    let mut system = System {
54        root: PathBuf::from("/"),
55        host: host.clone(),
56        runner: Commands,
57    };
58    system.execute(action, out)
59}
60
61#[cfg(unix)]
62pub(crate) fn text(path: &Path) -> Result<&str, Error> {
63    match path.to_str() {
64        Some(text) => Ok(text),
65        None => Err(Error::Failed(format!(
66            "{} is not valid UTF-8",
67            path.display()
68        ))),
69    }
70}
71
72impl<R: Runner> System<R> {
73    pub fn execute(&mut self, action: Action, out: &mut impl Write) -> Result<(), Error> {
74        match action {
75            Action::Install { init } => {
76                let init = self.choose(init)?;
77                self.install(init, out)
78            }
79            Action::Update => {
80                let Registration::Registered(init) = self.registration()? else {
81                    return Err(Error::NotInstalled);
82                };
83                self.update(init)
84            }
85            Action::Uninstall { purge } => self.uninstall(purge),
86            Action::Status => {
87                let status = self.status()?;
88                serde_json::to_writer(&mut *out, &status)?;
89                writeln!(out)?;
90                Ok(())
91            }
92        }
93    }
94
95    pub fn paths(&self) -> Paths {
96        #[cfg(windows)]
97        {
98            Paths::system()
99        }
100        #[cfg(not(windows))]
101        {
102            Paths::linux_under(&self.root)
103        }
104    }
105
106    pub fn at(&self, logical: &Path) -> PathBuf {
107        let relative: PathBuf = logical
108            .components()
109            .filter(|part| !matches!(part, Component::RootDir | Component::Prefix(..)))
110            .collect();
111        self.root.join(relative)
112    }
113
114    pub fn has_program(&self, name: &str) -> bool {
115        Search::DIRS
116            .iter()
117            .any(|dir| self.at(&Path::new(dir).join(name)).is_file())
118    }
119
120    #[cfg(unix)]
121    pub fn program(&self, name: &str) -> Result<String, Error> {
122        for dir in Search::DIRS {
123            let program = Path::new(dir).join(name);
124            if self.at(&program).is_file() {
125                return Ok(format!("{dir}/{name}"));
126            }
127        }
128        Err(Error::Failed(format!(
129            "{name} is not in {}",
130            Search::DIRS.join(":")
131        )))
132    }
133
134    #[cfg(unix)]
135    pub fn exec(&mut self, name: &str, args: &[&str]) -> Result<Outcome, Error> {
136        let program = self.program(name)?;
137        self.runner.require(&program, args)
138    }
139
140    #[cfg(unix)]
141    pub fn query(&mut self, name: &str, args: &[&str]) -> Result<Outcome, Error> {
142        let program = self.program(name)?;
143        self.runner.run(&program, args)
144    }
145
146    pub fn detect(&self) -> Result<Init, Error> {
147        if cfg!(windows) {
148            return Ok(Init::Windows);
149        }
150        if self.at(Path::new("/run/systemd/system")).is_dir() {
151            return Ok(Init::Systemd);
152        }
153        if self.at(Path::new("/run/openrc")).is_dir() || self.has_program("openrc-run") {
154            return Ok(Init::Openrc);
155        }
156        let runit = self.at(Path::new("/run/runit")).is_dir()
157            || self.at(Path::new("/etc/runit/runsvdir")).is_dir();
158        if runit && self.has_program("sv") {
159            return Ok(Init::Runit);
160        }
161        Err(Error::NoInit)
162    }
163
164    pub fn choose(&self, choice: InitChoice) -> Result<Init, Error> {
165        match choice {
166            InitChoice::Auto => self.detect(),
167            InitChoice::Systemd => Ok(Init::Systemd),
168            InitChoice::Openrc => Ok(Init::Openrc),
169            InitChoice::Runit => Ok(Init::Runit),
170            InitChoice::Windows => Ok(Init::Windows),
171        }
172    }
173
174    fn status(&mut self) -> Result<Status, Error> {
175        let mut status = match self.registration()? {
176            Registration::Registered(init) => self.state(init)?,
177            Registration::Absent => Status {
178                init: self.detect()?,
179                registered: false,
180                enabled: false,
181                running: false,
182                listening: false,
183            },
184        };
185        status.listening = listening(self.control_address()?)?;
186        Ok(status)
187    }
188
189    pub fn control_address(&self) -> Result<SocketAddr, Error> {
190        let settings = self.paths().settings;
191        match settings.try_exists()? {
192            true => Ok(ServiceSettings::read(&settings)?.listen),
193            false => Ok(SERVICE_LISTEN),
194        }
195    }
196}
197
198struct Probe;
199
200impl Probe {
201    const TIMEOUT: Duration = Duration::from_secs(1);
202}
203
204fn listening(address: SocketAddr) -> Result<bool, Error> {
205    match TcpStream::connect_timeout(&address, Probe::TIMEOUT) {
206        Ok(stream) => {
207            drop(stream);
208            Ok(true)
209        }
210        Err(error) => match error.kind() {
211            ErrorKind::ConnectionRefused | ErrorKind::TimedOut => Ok(false),
212            _ => Err(error.into()),
213        },
214    }
215}