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}