service_install/install/init/
systemd.rs

1#![allow(clippy::missing_errors_doc)]
2// ^needed as we have a lib and a main, pub crate would
3// only allow access from the lib. However since the lib is not
4// public it makes no sense to document errors.
5
6use std::ffi::OsStr;
7use std::path::{Component, Path, PathBuf};
8use std::time::Duration;
9use std::{fs, io};
10
11use crate::install::builder::Trigger;
12use crate::install::files::NoHomeError;
13
14pub use self::unit::FindExeError;
15use self::unit::Unit;
16
17use super::{ExeLocation, Mode, Params, PathCheckError, RSteps, SetupError, Steps, TearDownError};
18
19mod api;
20mod disable_existing;
21mod setup;
22mod teardown;
23mod unit;
24
25pub(crate) use disable_existing::disable_step;
26pub use disable_existing::DisableError;
27use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, UpdateKind};
28
29#[derive(thiserror::Error, Debug)]
30pub enum SystemCtlError {
31    #[error("Could not run systemctl")]
32    Io(
33        #[from]
34        #[source]
35        std::io::Error,
36    ),
37    #[error("Systemctl failed: {reason}")]
38    Failed { reason: String },
39    #[error("Timed out trying to enable service")]
40    RestartTimeOut,
41    #[error("Timed out trying to disable service")]
42    DisableTimeOut,
43    #[error("Timed out trying to stop service")]
44    StopTimeOut,
45    #[error("Something send a signal to systemctl ending it before it could finish")]
46    Terminated,
47}
48
49#[derive(thiserror::Error, Debug)]
50pub enum Error {
51    #[error("Could not write out unit file to {path}")]
52    Writing {
53        #[source]
54        e: io::Error,
55        path: PathBuf,
56    },
57    #[error("Could not remove the unit files, error: {0}")]
58    Removing(#[source] io::Error),
59    #[error("Could not verify unit files where created by us, could not open them")]
60    Verifying(
61        #[from]
62        #[source]
63        unit::Error,
64    ),
65    #[error("Could not check if this system uses systemd")]
66    CheckingInitSys(
67        #[from]
68        #[source]
69        PathCheckError,
70    ),
71    #[error("Could not check if there is an existing service we will replace")]
72    CheckingRunning(#[source] SystemCtlError),
73    #[error("Could not enable the service")]
74    Enabling(#[source] api::Error),
75    #[error("Could not start the service")]
76    Starting(#[source] api::Error),
77    #[error("Could not restart the service")]
78    Restarting(#[source] api::Error),
79    #[error("Could not disable the service")]
80    Disabling(#[source] api::Error),
81    #[error("Could not stop the service")]
82    Stopping(#[source] api::Error),
83    #[error("Could not check if the service is active or not")]
84    CheckActive(#[source] api::Error),
85    #[error("Error while waiting for service to be started")]
86    WaitingForStart(#[source] api::WaitError),
87    #[error("Error while waiting for service to be stopped")]
88    WaitingForStop(#[source] api::WaitError),
89    #[error("Could not reload services")]
90    Reloading(#[source] api::Error),
91}
92
93pub(crate) fn path_is_systemd(path: &Path) -> Result<bool, PathCheckError> {
94    let path = path.canonicalize().map_err(PathCheckError)?;
95
96    Ok(path
97        .components()
98        .filter_map(|c| match c {
99            Component::Normal(cmp) => Some(cmp),
100            _other => None,
101        })
102        .filter_map(|c| c.to_str())
103        .any(|c| c == "systemd"))
104}
105
106// Check if systemd is the init system (PID 1)
107pub(super) fn not_available() -> Result<bool, SetupError> {
108    use sysinfo::{Pid, System};
109    let mut s = System::new();
110    s.refresh_processes_specifics(
111        ProcessesToUpdate::Some([Pid::from(1)].as_slice()),
112        true,
113        ProcessRefreshKind::nothing().with_cmd(UpdateKind::Always),
114    );
115    let init_sys = &s
116        .process(Pid::from(1))
117        .expect("there should always be an init system")
118        .cmd()
119        .first()
120        .expect("we requested command");
121    Ok(!path_is_systemd(Path::new(init_sys)).map_err(Error::from)?)
122}
123
124pub(super) fn set_up_steps(params: &Params) -> Result<Steps, SetupError> {
125    let path_without_extension = match params.mode {
126        Mode::User => user_path()?,
127        Mode::System => system_path(),
128    }
129    .join(&params.name);
130
131    Ok(match params.trigger {
132        Trigger::OnSchedule(ref schedule) => {
133            setup::with_timer(&path_without_extension, params, schedule)
134        }
135        Trigger::OnBoot => setup::without_timer(&path_without_extension, params)?,
136    })
137}
138
139pub(super) fn tear_down_steps(mode: Mode) -> Result<Option<(RSteps, ExeLocation)>, TearDownError> {
140    let dir = match mode {
141        Mode::User => user_path()?,
142        Mode::System => system_path(),
143    };
144
145    let mut steps = Vec::new();
146    let mut exe_paths = Vec::new();
147
148    for entry in fs::read_dir(dir).unwrap() {
149        let path = entry.unwrap().path();
150        if path.is_dir() {
151            continue;
152        }
153        let Some(extension) = path.extension().and_then(OsStr::to_str) else {
154            continue;
155        };
156        let unit = Unit::from_path(path.clone()).unwrap();
157        if !unit.our_service() {
158            continue;
159        }
160        let Some(service_name) = path.file_stem().and_then(OsStr::to_str) else {
161            continue;
162        };
163
164        match extension {
165            "timer" => {
166                steps.extend(teardown::disable_then_remove_with_timer(
167                    unit.path.clone(),
168                    service_name,
169                    mode,
170                ));
171            }
172            "service" => {
173                steps.extend(teardown::disable_then_remove_service(
174                    unit.path.clone(),
175                    service_name,
176                    mode,
177                ));
178                exe_paths.push(unit.exe_path().map_err(TearDownError::FindingExePath)?);
179            }
180            _ => continue,
181        }
182    }
183
184    exe_paths.dedup();
185    match (steps.len(), exe_paths.as_slice()) {
186        (0, []) => Ok(None),
187        (0, [_, ..]) => unreachable!("if we get an exe path we got one service to remove"),
188        (1.., []) => Err(TearDownError::TimerWithoutService),
189        (1.., [exe_path]) => Ok(Some((steps, exe_path.clone()))),
190        (1.., _) => Err(TearDownError::MultipleExePaths(exe_paths)),
191    }
192}
193
194/// There are other paths, but for now we return the most commonly used one
195fn user_path() -> Result<PathBuf, NoHomeError> {
196    Ok(home::home_dir()
197        .ok_or(NoHomeError)?
198        .join(".config/systemd/user/"))
199}
200
201/// There are other paths, but for now we return the most commonly used one
202fn system_path() -> PathBuf {
203    PathBuf::from("/etc/systemd/system")
204}
205
206async fn enable(unit: &str, mode: Mode, and_start: bool) -> Result<(), Error> {
207    api::reload(mode).await.map_err(Error::Reloading)?;
208    api::enable_service(unit, mode)
209        .await
210        .map_err(Error::Enabling)?;
211    if and_start {
212        api::start_service(unit, mode)
213            .await
214            .map_err(Error::Starting)?;
215        tokio::time::sleep(Duration::from_secs(2)).await;
216        api::wait_for_active(unit, mode)
217            .await
218            .map_err(Error::WaitingForStart)?;
219    }
220    Ok(())
221}
222
223async fn restart(unit_file_name: &str, mode: Mode) -> Result<(), Error> {
224    api::restart(unit_file_name, mode)
225        .await
226        .map_err(Error::Restarting)
227}
228
229async fn disable(unit_file_name: &str, mode: Mode, and_stop: bool) -> Result<(), Error> {
230    api::disable_service(unit_file_name, mode)
231        .await
232        .map_err(Error::Disabling)?;
233    if and_stop {
234        stop(unit_file_name, mode).await?;
235        api::wait_for_inactive(unit_file_name, mode)
236            .await
237            .map_err(Error::WaitingForStop)?;
238    }
239    Ok(())
240}
241
242async fn stop(unit_file_name: &str, mode: Mode) -> Result<(), Error> {
243    api::stop_service(unit_file_name, mode)
244        .await
245        .map_err(Error::Stopping)
246}
247
248async fn is_active(unit_file_name: &str, mode: Mode) -> Result<bool, Error> {
249    api::is_active(unit_file_name, mode)
250        .await
251        .map_err(Error::CheckActive)
252}