1use crate::systemd::JobSet;
2use crate::systemd::UnitManager;
3mod error;
4mod i18n_lib;
5pub mod systemd;
6mod unit_file;
7
8use i18n_lib::UnitAction;
9use i18n_lib::MSG;
10use std::{
11 collections::HashSet,
12 path::{Path, PathBuf},
13 rc::Rc,
14 time::Duration,
15};
16use systemd::UnitStatus;
17use unit_file::UnitFile;
18
19use anyhow::{Context, Result};
20
21fn pretty_unit_names<I>(unit_names: I) -> String
22where
23 I: IntoIterator,
24 I::Item: AsRef<str>,
25{
26 let mut str_vec = unit_names
27 .into_iter()
28 .map(|s| String::from(s.as_ref()))
29 .collect::<Vec<_>>();
30 str_vec.sort();
31 str_vec.join(", ")
32}
33
34fn is_unit_available(unit_path: &Path) -> bool {
35 unit_path.exists()
36 && !unit_path
37 .canonicalize()
38 .map(|p| p == Path::new("/dev/null"))
39 .unwrap_or(true)
40}
41
42fn parameterized_base_name(unit_name: &str) -> Option<String> {
46 let res = unit_name.splitn(2, '@').collect::<Vec<_>>();
47 match res[..] {
48 [base_name, arg_and_suffix] => {
49 let res = arg_and_suffix.rsplitn(2, '.').collect::<Vec<_>>();
50 match res[..] {
51 [suffix, arg] if !arg.is_empty() => Some(format!("{base_name}@.{suffix}")),
52 _ => None,
53 }
54 }
55 _ => None,
56 }
57}
58
59fn find_unit_file_path(unit_directory: &Path, unit_name: &str) -> Option<PathBuf> {
67 Some(unit_directory.join(unit_name))
68 .filter(|e| is_unit_available(e))
69 .or_else(|| {
70 parameterized_base_name(unit_name)
71 .map(|n| unit_directory.join(n))
72 .filter(|e| is_unit_available(e))
73 })
74}
75
76#[derive(Debug)]
78struct SwitchPlan {
79 stop_units: HashSet<Rc<str>>,
80 start_units: HashSet<Rc<str>>,
81 reload_units: HashSet<Rc<str>>,
82 restart_units: HashSet<Rc<str>>,
83 keep_old_units: HashSet<Rc<str>>,
84 unchanged_units: HashSet<Rc<str>>,
85}
86
87struct UnitWithTarget {
88 unit_path: PathBuf,
89 unit_name: Rc<str>,
90 target_name: Rc<str>,
91}
92
93fn build_switch_plan(
94 old_dir: Option<&Path>,
95 new_dir: &Path,
96 service_manager: &impl systemd::ServiceManager,
97) -> Result<SwitchPlan> {
98 let mut stop_units = HashSet::new();
99 let mut start_units = HashSet::new();
100 let mut reload_units = HashSet::new();
101 let mut restart_units = HashSet::new();
102 let mut keep_old_units = HashSet::new();
103 let mut unchanged_units = HashSet::new();
104
105 let mut active_unit_names = HashSet::new();
106
107 let active_units = service_manager
108 .list_units_by_states(&["active", "activating"])
109 .with_context(|| MSG.err_listing_active_units())?;
110
111 for active_unit in active_units {
114 let new_unit_path_opt = find_unit_file_path(new_dir, active_unit.name());
115 let old_unit_path_opt = old_dir
116 .as_ref()
117 .and_then(|d| find_unit_file_path(d, active_unit.name()));
118
119 let active_unit_name: Rc<str> = active_unit.name().into();
120 active_unit_names.insert(active_unit_name.clone());
121
122 if let Some(new_unit_path) = new_unit_path_opt {
123 let new_unit_file = UnitFile::load(&new_unit_path).with_context(|| {
124 format!("Failed load of new unit file {}", new_unit_path.display())
125 })?;
126
127 if let Some(old_unit_path) = old_unit_path_opt {
128 let old_unit_file = UnitFile::load(&old_unit_path).with_context(|| {
129 format!("Failed load of old unit file {}", old_unit_path.display())
130 })?;
131
132 if old_unit_file.restart_eq(&new_unit_file) {
133 unchanged_units.insert(active_unit_name);
134 } else if old_unit_file.reload_eq(&new_unit_file) {
135 reload_units.insert(active_unit_name);
136 } else if new_unit_file.unit_type() == unit_file::UnitType::Target {
137 if new_unit_file.switch_method() == unit_file::UnitSwitchMethod::StopOnly {
138 keep_old_units.insert(active_unit_name);
139 } else {
140 start_units.insert(active_unit_name);
141 }
142 } else {
143 match new_unit_file.switch_method() {
144 unit_file::UnitSwitchMethod::Reload => {
145 reload_units.insert(active_unit_name);
146 }
147 unit_file::UnitSwitchMethod::Restart => {
148 restart_units.insert(active_unit_name);
149 }
150 unit_file::UnitSwitchMethod::StopStart => {
151 if service_manager
152 .unit_manager(&active_unit)?
153 .refuse_manual_stop()?
154 {
155 keep_old_units.insert(active_unit_name);
156 } else {
157 stop_units.insert(active_unit_name.clone());
158 start_units.insert(active_unit_name);
159 }
160 }
161 unit_file::UnitSwitchMethod::StopOnly => {
162 if service_manager
163 .unit_manager(&active_unit)?
164 .refuse_manual_stop()?
165 {
166 keep_old_units.insert(active_unit_name);
167 } else {
168 stop_units.insert(active_unit_name);
169 }
170 }
171 unit_file::UnitSwitchMethod::KeepOld => {
172 keep_old_units.insert(active_unit_name);
173 }
174 }
175 }
176 } else if service_manager
177 .unit_manager(&active_unit)?
178 .refuse_manual_stop()?
179 || new_unit_file.switch_method() == unit_file::UnitSwitchMethod::StopOnly
180 {
181 keep_old_units.insert(active_unit_name);
182 } else {
183 stop_units.insert(active_unit_name.clone());
184 start_units.insert(active_unit_name);
185 }
186 } else if old_unit_path_opt.is_some() {
187 if service_manager
188 .unit_manager(&active_unit)?
189 .refuse_manual_stop()?
190 {
191 keep_old_units.insert(active_unit_name);
192 } else {
193 stop_units.insert(active_unit_name);
194 }
195 }
196 }
197
198 for wanted_unit in find_wanted_units(new_dir)? {
201 if !active_unit_names.contains(&wanted_unit.target_name) {
203 continue;
204 }
205
206 if active_unit_names.contains(&wanted_unit.unit_name) {
208 continue;
209 }
210
211 let new_unit_file = UnitFile::load(&wanted_unit.unit_path).with_context(|| {
212 format!(
213 "Failed load of wanted unit file {}",
214 wanted_unit.unit_path.display()
215 )
216 })?;
217
218 if new_unit_file.switch_method() == unit_file::UnitSwitchMethod::StopOnly {
219 continue;
220 }
221
222 start_units.insert(wanted_unit.unit_name);
223 }
224
225 Ok(SwitchPlan {
226 stop_units,
227 start_units,
228 reload_units,
229 restart_units,
230 keep_old_units,
231 unchanged_units,
232 })
233}
234
235fn find_wanted_units(new_dir: &Path) -> Result<Vec<UnitWithTarget>> {
236 let mut result = Vec::new();
237
238 for dir_entry in std::fs::read_dir(new_dir)? {
239 let dir_entry = dir_entry.with_context(|| MSG.err_read_dir_entry(new_dir))?;
240
241 let entry_file_name = dir_entry
243 .file_name()
244 .into_string()
245 .expect("unit with valid Unicode file name");
246
247 if dir_entry.metadata()?.is_dir() && entry_file_name.ends_with(".target.wants") {
248 let dir_name = entry_file_name;
249 let target_name: Rc<str> = dir_name
250 .strip_suffix(".wants")
251 .expect("directory name should end in .wants")
252 .into();
253 for wants_entry in std::fs::read_dir(dir_entry.path())? {
254 let wants_entry = wants_entry
255 .with_context(|| MSG.err_read_dir_entry(dir_entry.path().as_path()))?;
256
257 let unit_name = wants_entry
258 .file_name()
259 .into_string()
260 .expect("unit with valid Unicode file name")
261 .into();
262 result.push(UnitWithTarget {
263 unit_path: wants_entry.path(),
264 unit_name,
265 target_name: target_name.clone(),
266 });
267 }
268 }
269 }
270
271 Ok(result)
272}
273
274fn exec_pre_reload<F>(
275 plan: &SwitchPlan,
276 service_manager: &impl systemd::ServiceManager,
277 job_handler: F,
278 dry_run: bool,
279 timeout: Duration,
280) -> Result<()>
281where
282 F: Fn(&str, &str) + Send + 'static,
283{
284 if !plan.stop_units.is_empty() {
285 println!(
286 "{}",
287 MSG.stopping_units(&pretty_unit_names(&plan.stop_units))
288 );
289 if !dry_run {
290 let mut job_set = service_manager.new_job_set()?;
291
292 for uf in &plan.stop_units {
293 job_set
294 .stop_unit(uf)
295 .with_context(|| MSG.err_unit_action_failed(uf, UnitAction::Stop))?;
296 }
297
298 job_set.wait_for_all(job_handler, timeout)?;
299 }
300 }
301
302 Ok(())
303}
304
305fn exec_reload(
306 service_manager: &impl systemd::ServiceManager,
307 dry_run: bool,
308 verbose: bool,
309) -> Result<()> {
310 if !dry_run {
311 if verbose {
312 println!("{}", MSG.resetting_failed_units());
313 }
314 service_manager
315 .reset_failed()
316 .with_context(|| MSG.err_resetting_failed_units())?;
317
318 if verbose {
319 println!("{}", MSG.reloading_systemd());
320 }
321 service_manager
322 .daemon_reload()
323 .with_context(|| MSG.err_reloading_systemd())?;
324 }
325
326 Ok(())
327}
328
329fn exec_post_reload<F>(
330 plan: &SwitchPlan,
331 service_manager: &impl systemd::ServiceManager,
332 job_handler: F,
333 dry_run: bool,
334 verbose: bool,
335 timeout: Duration,
336) -> Result<()>
337where
338 F: Fn(&str, &str) + Send + 'static,
339{
340 let mut job_set = service_manager.new_job_set()?;
341
342 if !plan.reload_units.is_empty() {
343 println!(
344 "{}",
345 MSG.reloading_units(&pretty_unit_names(&plan.reload_units))
346 );
347 if !dry_run {
348 for uf in &plan.reload_units {
349 job_set
350 .reload_unit(uf)
351 .with_context(|| MSG.err_unit_action_failed(uf, UnitAction::Reload))?;
352 }
353 }
354 }
355
356 if !plan.restart_units.is_empty() {
357 println!(
358 "{}",
359 MSG.restarting_units(&pretty_unit_names(&plan.restart_units))
360 );
361 if !dry_run {
362 for uf in &plan.restart_units {
363 job_set
364 .restart_unit(uf)
365 .with_context(|| MSG.err_unit_action_failed(uf, UnitAction::Restart))?;
366 }
367 }
368 }
369
370 if !plan.keep_old_units.is_empty() {
371 println!(
372 "{}",
373 MSG.keeping_old_units(&pretty_unit_names(&plan.keep_old_units))
374 );
375 }
376
377 if !plan.unchanged_units.is_empty() && verbose {
378 println!(
379 "{}",
380 MSG.unchanged_units(&pretty_unit_names(&plan.unchanged_units))
381 );
382 }
383
384 if !plan.start_units.is_empty() {
385 println!(
386 "{}",
387 MSG.starting_units(&pretty_unit_names(&plan.start_units))
388 );
389 if !dry_run {
390 for uf in &plan.start_units {
391 job_set
392 .start_unit(uf)
393 .with_context(|| MSG.err_unit_action_failed(uf, UnitAction::Start))?;
394 }
395 }
396 }
397
398 job_set.wait_for_all(job_handler, timeout)?;
399
400 Ok(())
401}
402
403pub fn switch(
405 service_manager: &impl systemd::ServiceManager,
406 old_dir: Option<&Path>,
408 new_dir: &Path,
409 dry_run: bool,
410 verbose: bool,
411 timeout: Duration,
412) -> Result<()> {
413 let system_status = service_manager.system_status()?;
414
415 if matches!(system_status, systemd::SystemStatus::Degraded) {
418 let units_by_states = service_manager.list_units_by_states(&["failed"])?;
419 let failed: Vec<&str> = units_by_states.iter().map(|status| status.name()).collect();
420 let failed = failed.join(", ");
421 eprintln!(
422 "The service manager is degraded.\n\
423 Failed services: {failed}\n\
424 Attempting to continue anyway..."
425 );
426 }
427
428 let do_switch = {
429 use systemd::SystemStatus::{
430 Degraded, Initializing, Maintenance, Running, Starting, Stopping,
431 };
432 match system_status {
433 Initializing | Starting | Running | Degraded => true,
434 Maintenance | Stopping => false,
435 }
436 };
437
438 if !do_switch {
439 if verbose {
440 println!("Skipping switch since systemd has {system_status} status");
441 }
442 return Ok(());
443 }
444
445 let plan = build_switch_plan(old_dir, new_dir, service_manager)
446 .context("Failed to build switch plan")?;
447
448 let job_handler = move |name: &str, state: &str| {
449 if verbose || state != "done" {
450 println!("{name} {state}");
451 }
452 };
453
454 exec_pre_reload(&plan, service_manager, job_handler, dry_run, timeout)
455 .context("Failed to perform pre-reload tasks")?;
456
457 exec_reload(service_manager, dry_run, verbose)?;
458
459 exec_post_reload(
460 &plan,
461 service_manager,
462 job_handler,
463 dry_run,
464 verbose,
465 timeout,
466 )
467 .context("Failed to perform post-reload tasks")?;
468
469 Ok(())
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 #[test]
477 fn can_get_base_name_for_parameterized_unit() {
478 assert_eq!(
479 parameterized_base_name("foo@bar.service"),
480 Some(String::from("foo@.service"))
481 );
482 assert_eq!(
483 parameterized_base_name("foo@bar.baz.service"),
484 Some(String::from("foo@.service"))
485 );
486 }
487
488 #[test]
489 fn no_base_name_for_nonparameterized_units() {
490 assert_eq!(parameterized_base_name("foo@.service"), None);
491 assert_eq!(parameterized_base_name("foo.service"), None);
492 assert_eq!(parameterized_base_name("foo@barservice"), None);
493 }
494}