service_install/install/init/systemd/
disable_existing.rs1use 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}