service_install/install/init/systemd/
disable_existing.rs

1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3use std::{fs, io};
4
5use itertools::Itertools;
6use tracing::debug;
7
8use crate::install::{InstallError, InstallStep, RollbackError, RollbackStep};
9use crate::Tense;
10
11use super::api::on_seperate_tokio_thread;
12use super::unit::{self, Unit};
13use super::{system_path, user_path, FindExeError, Mode};
14
15struct ReEnable {
16    units: Vec<Unit>,
17    mode: Mode,
18}
19
20impl RollbackStep for ReEnable {
21    fn perform(&mut self) -> Result<(), RollbackError> {
22        for unit in &self.units {
23            on_seperate_tokio_thread! {{
24                super::enable(&unit.file_name, self.mode, true).await.map_err(RollbackError::ReEnabling)
25            }}?;
26        }
27        Ok(())
28    }
29
30    fn describe(&self, tense: Tense) -> String {
31        let verb = match tense {
32            Tense::Past => "Re-enabled",
33            Tense::Active => "Re-enabling",
34            Tense::Questioning => "Re-enable",
35            Tense::Future => "Will re-enable",
36        };
37        format!(
38            "{verb} the {} services that spawned the original file{}",
39            self.mode,
40            tense.punct()
41        )
42    }
43}
44
45struct Disable {
46    services: Vec<Unit>,
47    timers: Vec<Unit>,
48    mode: Mode,
49}
50
51impl InstallStep for Disable {
52    fn describe(&self, tense: Tense) -> String {
53        let verb = match tense {
54            Tense::Past => "Disabled",
55            Tense::Active => "Disabling",
56            Tense::Questioning => "Disable",
57            Tense::Future => "Will disable",
58        };
59        format!(
60            "{verb} the {} services and/or timers running the file at the install location{}",
61            self.mode, tense.punct()
62        )
63    }
64
65    fn describe_detailed(&self, tense: Tense) -> String {
66        let verb = match tense {
67            Tense::Past => "Disabled",
68            Tense::Active => "Disabling",
69            Tense::Questioning => "Disable",
70            Tense::Future => "Will disable",
71        };
72        #[allow(clippy::format_collect)]
73        let services: String = self
74            .services
75            .iter()
76            .map(|unit| unit.file_name.to_string())
77            .map(|unit| format!("\n|\t- {unit}"))
78            .collect();
79        #[allow(clippy::format_collect)]
80        let timers: String = self
81            .timers
82            .iter()
83            .map(|unit| unit.file_name.to_string())
84            .map(|unit| format!("\n|\t- {unit}"))
85            .collect();
86
87        match (services.is_empty(), timers.is_empty()) {
88            (false, false) => 
89        format!(
90            "{verb} the {} services and/or timers running the file at the install location{}\n| services:{services}\n| timers:{timers}",
91            self.mode, tense.punct()
92        ) ,
93            (false, true) => 
94        format!(
95            "{verb} the {} services running the file at the install location{}\n| services:{services}", self.mode, tense.punct()),
96            (true, false) => 
97        format!(
98            "{verb} the {} timers running the file at the install location{}\n| timers:{timers}",
99            self.mode, tense.punct()
100        ),
101            (true, true) => unreachable!("Would have triggered error while constructing the disable installstep.")
102        }
103    }
104
105    fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
106        let mut rollback = Box::new(ReEnable {
107            mode: self.mode,
108            units: Vec::new(),
109        });
110        on_seperate_tokio_thread!{{
111            for unit in &self.services {
112                super::disable(&unit.file_name, self.mode, true).await?;
113                rollback.units.push(unit.clone());
114            }
115            for unit in &self.timers {
116                super::disable(&unit.file_name, self.mode, true).await?;
117                super::stop(&unit.file_name, self.mode).await?;
118                rollback.units.push(unit.clone());
119            }
120            Ok(())
121        }}.map_err(InstallError::Systemd)?;
122        let rollback = rollback as Box<dyn RollbackStep>;
123        Ok(Some(rollback))
124    }
125}
126
127#[derive(Debug, thiserror::Error)]
128pub enum DisableError {
129    #[error("Could not find the service")]
130    CouldNotFindIt(#[from] #[source] FindError),
131    #[error("Could not open systemd unit")]
132    CouldNotReadUnit(#[from] #[source] unit::Error),
133    #[error("Could not find the service or (timer) that keeps the file in use")]
134    NoServiceOrTimerFound,
135}
136
137pub(crate) fn disable_step(
138    target: &Path,
139    mode: Mode,
140) -> Result<Vec<Box<dyn InstallStep>>, DisableError> {
141    let path = match mode {
142        Mode::User => user_path().unwrap(),
143        Mode::System => system_path(),
144    };
145    let services: Vec<_> = collect_services(&path)
146        .map_err(FindError::CouldNotReadDir)?
147        .into_iter()
148        .map(Unit::from_path)
149        .collect::<Result<_, _>>()
150        .map_err(DisableError::CouldNotReadUnit)?;
151    let timers: Vec<_> = collect_timers(&path)
152        .map_err(FindError::CouldNotReadDir)?
153        .into_iter()
154        .map(Unit::from_path)
155        .collect::<Result<_, _>>()
156        .map_err(DisableError::CouldNotReadUnit)?;
157
158    let services = find_services_with_target_exe(services, target);
159    let names: HashSet<_> = services.iter().map(Unit::name).collect();
160    let mut timers: Vec<_> = timers
161        .into_iter()
162        .filter(|timer| names.contains(&timer.name()))
163        .collect();
164    timers.dedup_by_key(|u| u.name());
165    timers.sort_by_key(Unit::name);
166
167    let mut services: Vec<_> = services.into_iter().filter(Unit::has_install).collect();
168    services.dedup_by_key(|u| u.name());
169    services.sort_by_key(Unit::name);
170
171    if services.is_empty() && timers.is_empty() {
172        return Err(DisableError::NoServiceOrTimerFound);
173    }
174    let disable = Box::new(Disable {
175        services,
176        timers,
177        mode,
178    });
179    let disable = disable as Box<dyn InstallStep>;
180    Ok(vec![disable])
181}
182
183fn find_services_with_target_exe(units: Vec<Unit>, target: &Path) -> Vec<Unit> {
184    let (units, errs): (Vec<_>, Vec<_>) = units
185        .into_iter()
186        .map(|unit| unit.exe_path().map(|exe| (exe, unit)))
187        .filter_ok(|(exe, _)| exe == target)
188        .map_ok(|(_, unit)| unit)
189        .partition_result();
190
191    if !errs.is_empty() {
192        debug!("Some service files failed to parse: {errs:#?}");
193    }
194
195    units
196}
197
198#[derive(Debug, thiserror::Error)]
199pub enum FindError {
200    #[error(
201        "No service spawning the target file found, could not parse some services however: {0:#?}"
202    )]
203    NotFoundWithErrors(Vec<FindExeError>),
204    #[error("Could not read directory")]
205    CouldNotReadDir(#[from] #[source] std::io::Error),
206}
207
208fn walk_dir(dir: &Path, process_file: &mut impl FnMut(&Path)) -> io::Result<()> {
209    if dir.is_dir() {
210        for entry in fs::read_dir(dir)? {
211            let entry = entry?;
212            let path = entry.path();
213            if path.is_dir() {
214                walk_dir(&path, process_file)?;
215            } else if path.is_file() {
216                (process_file)(&path);
217            }
218        }
219    }
220    Ok(())
221}
222fn collect_services(dir: &Path) -> io::Result<Vec<PathBuf>> {
223    let mut units = Vec::new();
224    walk_dir(dir, &mut |path| {
225        if path.extension().is_some_and(|e| e == "service") {
226            units.push(path.to_owned());
227        }
228    })?;
229    Ok(units)
230}
231
232fn collect_timers(dir: &Path) -> io::Result<Vec<PathBuf>> {
233    let mut units = Vec::new();
234    walk_dir(dir, &mut |path| {
235        if path.extension().is_some_and(|e| e == "timer") {
236            units.push(path.to_owned());
237        }
238    })?;
239    Ok(units)
240}