1use crate::systemd::JobSet;
2use crate::systemd::UnitManager;
3mod error;
4mod i18n_lib;
5pub mod systemd;
6mod unit_file;
7
8use i18n_lib::MSG;
9use std::collections::BTreeMap;
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
76struct SwitchPlan {
78 unit_plan: BTreeMap<Rc<str>, UnitPlan>,
79}
80
81impl SwitchPlan {
82 fn build_unit_plan(
83 &'_ mut self,
84 name: Rc<str>,
85 populate_decisions: bool,
86 ) -> UnitPlanBuilder<'_> {
87 UnitPlanBuilder {
88 switch_plan: self,
89 populate_decisions,
90 name,
91 decisions: Vec::new(),
92 }
93 }
94
95 fn stop_units(&self) -> BTreeMap<&str, &UnitPlan> {
96 self.units_with_action(|a| *a == UnitAction::Stop || *a == UnitAction::StopStart)
97 }
98
99 fn start_units(&self) -> BTreeMap<&str, &UnitPlan> {
100 self.units_with_action(|a| *a == UnitAction::Start || *a == UnitAction::StopStart)
101 }
102
103 fn reload_units(&self) -> BTreeMap<&str, &UnitPlan> {
104 self.units_with_action(|a| *a == UnitAction::Reload)
105 }
106
107 fn restart_units(&self) -> BTreeMap<&str, &UnitPlan> {
108 self.units_with_action(|a| *a == UnitAction::Restart)
109 }
110
111 fn keep_old_units(&self) -> BTreeMap<&str, &UnitPlan> {
112 self.units_with_action(|a| *a == UnitAction::KeepOld)
113 }
114
115 fn unchanged_units(&self) -> BTreeMap<&str, &UnitPlan> {
116 self.units_with_action(|a| *a == UnitAction::NoAction)
117 }
118
119 fn units_with_action(
120 &self,
121 predicate: impl Fn(&UnitAction) -> bool,
122 ) -> BTreeMap<&str, &UnitPlan> {
123 self.unit_plan
124 .iter()
125 .filter(|(_, v)| predicate(&v.action))
126 .map(|(k, v)| (k.as_ref(), v))
127 .collect()
128 }
129}
130
131struct UnitPlan {
136 action: UnitAction,
137 decisions: Vec<UnitDecision>,
140}
141
142struct UnitPlanBuilder<'a> {
143 switch_plan: &'a mut SwitchPlan,
144 populate_decisions: bool,
145 name: Rc<str>,
146 decisions: Vec<UnitDecision>,
147}
148
149impl<'a> UnitPlanBuilder<'a> {
150 fn push_decision(mut self, decision: UnitDecision) -> Self {
151 if self.populate_decisions {
152 self.decisions.push(decision);
153 }
154 self
155 }
156
157 fn action(self, action: UnitAction) {
158 self.switch_plan.unit_plan.insert(
159 self.name,
160 UnitPlan {
161 action,
162 decisions: self.decisions,
163 },
164 );
165 }
166}
167
168#[derive(Debug, PartialEq, Eq)]
170enum UnitAction {
171 NoAction,
172 StopStart,
173 Start,
174 Stop,
175 Restart,
176 Reload,
177 KeepOld,
178}
179
180#[derive(Debug, Copy, Clone, PartialEq, Eq)]
182enum UnitDecision {
183 NewExists,
185 OldExists,
187 OldNotExists,
189 RestartEq,
191 ReloadEq,
193 UnitType(unit_file::UnitType),
195 SwitchMethod(unit_file::UnitSwitchMethod),
197 ActiveRefuseManualStop,
199 WantedByActiveTarget,
201}
202
203struct UnitWithTarget {
204 unit_path: PathBuf,
205 unit_name: Rc<str>,
206 target_name: Rc<str>,
207}
208
209fn build_switch_plan(
210 old_dir: Option<&Path>,
211 new_dir: &Path,
212 populate_decisions: bool,
213 service_manager: &impl systemd::ServiceManager,
214) -> Result<SwitchPlan> {
215 let mut switch_plan = SwitchPlan {
216 unit_plan: BTreeMap::new(),
217 };
218
219 let mut active_unit_names = HashSet::new();
220
221 let active_units = service_manager
222 .list_units_by_states(&["active", "activating"])
223 .with_context(|| MSG.err_listing_active_units())?;
224
225 for active_unit in active_units {
228 let new_unit_path_opt = find_unit_file_path(new_dir, active_unit.name());
229 let old_unit_path_opt = old_dir
230 .as_ref()
231 .and_then(|d| find_unit_file_path(d, active_unit.name()));
232
233 let active_unit_name: Rc<str> = active_unit.name().into();
234 active_unit_names.insert(active_unit_name.clone());
235
236 let mut upb = switch_plan.build_unit_plan(active_unit_name, populate_decisions);
237
238 if let Some(new_unit_path) = new_unit_path_opt {
239 let new_unit_file = UnitFile::load(&new_unit_path)
240 .with_context(|| MSG.err_read_unit_file(&new_unit_path))?;
241
242 upb = upb.push_decision(UnitDecision::NewExists);
243
244 if let Some(old_unit_path) = old_unit_path_opt {
245 let old_unit_file = UnitFile::load(&old_unit_path)
246 .with_context(|| MSG.err_read_unit_file(&old_unit_path))?;
247
248 upb = upb.push_decision(UnitDecision::OldExists);
249
250 if old_unit_file.restart_eq(&new_unit_file) {
251 upb.push_decision(UnitDecision::RestartEq)
252 .action(UnitAction::NoAction);
253 } else if old_unit_file.reload_eq(&new_unit_file) {
254 upb.push_decision(UnitDecision::ReloadEq)
255 .action(UnitAction::Reload);
256 } else if new_unit_file.unit_type() == unit_file::UnitType::Target {
257 upb = upb.push_decision(UnitDecision::UnitType(new_unit_file.unit_type()));
258
259 if new_unit_file.switch_method() == unit_file::UnitSwitchMethod::StopOnly {
260 upb.push_decision(UnitDecision::SwitchMethod(
261 new_unit_file.switch_method(),
262 ))
263 .action(UnitAction::KeepOld);
264 } else {
265 upb.action(UnitAction::Start);
266 }
267 } else {
268 upb = upb
269 .push_decision(UnitDecision::SwitchMethod(new_unit_file.switch_method()));
270
271 match new_unit_file.switch_method() {
272 unit_file::UnitSwitchMethod::Reload => {
273 upb.action(UnitAction::Reload);
274 }
275 unit_file::UnitSwitchMethod::Restart => {
276 upb.action(UnitAction::Restart);
277 }
278 unit_file::UnitSwitchMethod::StopStart => {
279 if service_manager
280 .unit_manager(&active_unit)?
281 .refuse_manual_stop()?
282 {
283 upb.push_decision(UnitDecision::ActiveRefuseManualStop)
284 .action(UnitAction::NoAction);
285 } else {
286 upb.action(UnitAction::StopStart);
287 }
288 }
289 unit_file::UnitSwitchMethod::StopOnly => {
290 if service_manager
291 .unit_manager(&active_unit)?
292 .refuse_manual_stop()?
293 {
294 upb.push_decision(UnitDecision::ActiveRefuseManualStop)
295 .action(UnitAction::NoAction);
296 } else {
297 upb.action(UnitAction::Stop);
298 }
299 }
300 unit_file::UnitSwitchMethod::KeepOld => {
301 upb.action(UnitAction::KeepOld);
302 }
303 }
304 }
305 } else {
306 upb = upb.push_decision(UnitDecision::OldNotExists);
307
308 if service_manager
309 .unit_manager(&active_unit)?
310 .refuse_manual_stop()?
311 {
312 upb.push_decision(UnitDecision::ActiveRefuseManualStop)
313 .action(UnitAction::KeepOld);
314 } else if new_unit_file.switch_method() == unit_file::UnitSwitchMethod::StopOnly {
315 upb.push_decision(UnitDecision::SwitchMethod(new_unit_file.switch_method()))
316 .action(UnitAction::KeepOld);
317 } else {
318 upb.action(UnitAction::StopStart);
319 }
320 }
321 } else if old_unit_path_opt.is_some() {
322 upb = upb.push_decision(UnitDecision::OldExists);
323
324 if service_manager
325 .unit_manager(&active_unit)?
326 .refuse_manual_stop()?
327 {
328 upb.push_decision(UnitDecision::ActiveRefuseManualStop)
329 .action(UnitAction::KeepOld);
330 } else {
331 upb.action(UnitAction::Stop);
332 }
333 }
334 }
335
336 for wanted_unit in find_wanted_units(new_dir)? {
339 if !active_unit_names.contains(&wanted_unit.target_name) {
341 continue;
342 }
343
344 if active_unit_names.contains(&wanted_unit.unit_name) {
346 continue;
347 }
348
349 let new_unit_file = UnitFile::load(&wanted_unit.unit_path)
350 .with_context(|| MSG.err_read_unit_file(&wanted_unit.unit_path))?;
351
352 let mut upb = switch_plan.build_unit_plan(wanted_unit.unit_name, populate_decisions);
353
354 upb = upb
355 .push_decision(UnitDecision::NewExists)
356 .push_decision(UnitDecision::WantedByActiveTarget);
357
358 if new_unit_file.switch_method() == unit_file::UnitSwitchMethod::StopOnly {
359 upb.push_decision(UnitDecision::SwitchMethod(new_unit_file.switch_method()))
360 .action(UnitAction::NoAction);
361 } else {
362 upb.action(UnitAction::Start);
363 }
364 }
365
366 Ok(switch_plan)
367}
368
369fn find_wanted_units(new_dir: &Path) -> Result<Vec<UnitWithTarget>> {
370 let mut result = Vec::new();
371
372 for dir_entry in std::fs::read_dir(new_dir)? {
373 let dir_entry = dir_entry.with_context(|| MSG.err_read_dir_entry(new_dir))?;
374
375 let entry_file_name = dir_entry
377 .file_name()
378 .into_string()
379 .expect("unit with valid Unicode file name");
380
381 if dir_entry.metadata()?.is_dir() && entry_file_name.ends_with(".target.wants") {
382 let dir_name = entry_file_name;
383 let target_name: Rc<str> = dir_name
384 .strip_suffix(".wants")
385 .expect("directory name should end in .wants")
386 .into();
387 for wants_entry in std::fs::read_dir(dir_entry.path())? {
388 let wants_entry = wants_entry
389 .with_context(|| MSG.err_read_dir_entry(dir_entry.path().as_path()))?;
390
391 let unit_name = wants_entry
392 .file_name()
393 .into_string()
394 .expect("unit with valid Unicode file name")
395 .into();
396 result.push(UnitWithTarget {
397 unit_path: wants_entry.path(),
398 unit_name,
399 target_name: target_name.clone(),
400 });
401 }
402 }
403 }
404
405 Ok(result)
406}
407
408fn exec_pre_reload<F>(
409 plan: &SwitchPlan,
410 service_manager: &impl systemd::ServiceManager,
411 job_handler: F,
412 dry_run: bool,
413 timeout: Duration,
414) -> Result<()>
415where
416 F: Fn(&str, &str) + Send + 'static,
417{
418 let stop_units = plan.stop_units();
419
420 if stop_units.is_empty() {
421 return Ok(());
422 }
423
424 println!(
425 "{}",
426 MSG.stopping_units(&pretty_unit_names(stop_units.keys()))
427 );
428
429 if !dry_run {
430 let mut job_set = service_manager.new_job_set()?;
431
432 for uf in stop_units.keys() {
433 job_set
434 .stop_unit(uf)
435 .with_context(|| MSG.err_unit_action_failed(uf, UnitAction::Stop))?;
436 }
437
438 job_set.wait_for_all(job_handler, timeout)?;
439 }
440
441 Ok(())
442}
443
444fn exec_reload(
445 service_manager: &impl systemd::ServiceManager,
446 dry_run: bool,
447 verbose: bool,
448) -> Result<()> {
449 if !dry_run {
450 if verbose {
451 println!("{}", MSG.resetting_failed_units());
452 }
453 service_manager
454 .reset_failed()
455 .with_context(|| MSG.err_resetting_failed_units())?;
456
457 if verbose {
458 println!("{}", MSG.reloading_systemd());
459 }
460 service_manager
461 .daemon_reload()
462 .with_context(|| MSG.err_reloading_systemd())?;
463 }
464
465 Ok(())
466}
467
468fn exec_post_reload<F>(
469 plan: &SwitchPlan,
470 service_manager: &impl systemd::ServiceManager,
471 job_handler: F,
472 dry_run: bool,
473 verbose: bool,
474 timeout: Duration,
475) -> Result<()>
476where
477 F: Fn(&str, &str) + Send + 'static,
478{
479 let mut job_set = service_manager.new_job_set()?;
480
481 {
482 let units = plan.reload_units();
483 if !units.is_empty() {
484 println!("{}", MSG.reloading_units(&pretty_unit_names(units.keys())));
485
486 if !dry_run {
487 for uf in units.keys() {
488 job_set
489 .reload_unit(uf)
490 .with_context(|| MSG.err_unit_action_failed(uf, UnitAction::Reload))?;
491 }
492 }
493 }
494 }
495
496 {
497 let units = plan.restart_units();
498 if !units.is_empty() {
499 println!("{}", MSG.restarting_units(&pretty_unit_names(units.keys())));
500
501 if !dry_run {
502 for uf in units.keys() {
503 job_set
504 .restart_unit(uf)
505 .with_context(|| MSG.err_unit_action_failed(uf, UnitAction::Restart))?;
506 }
507 }
508 }
509 }
510
511 {
512 let units = plan.keep_old_units();
513 if !units.is_empty() {
514 println!(
515 "{}",
516 MSG.keeping_old_units(&pretty_unit_names(units.keys()))
517 );
518 }
519 }
520
521 if verbose {
522 let units = plan.unchanged_units();
523 if !units.is_empty() {
524 println!("{}", MSG.unchanged_units(&pretty_unit_names(units.keys())));
525 }
526 }
527
528 {
529 let units = plan.start_units();
530 if !units.is_empty() {
531 println!("{}", MSG.starting_units(&pretty_unit_names(units.keys())));
532
533 if !dry_run {
534 for uf in units.keys() {
535 job_set
536 .start_unit(uf)
537 .with_context(|| MSG.err_unit_action_failed(uf, UnitAction::Start))?;
538 }
539 }
540 }
541 }
542
543 job_set.wait_for_all(job_handler, timeout)?;
544
545 Ok(())
546}
547
548fn print_plan(plan: &SwitchPlan) {
549 if plan.unit_plan.is_empty() {
550 println!("The calculated switch plan is empty.");
551 return;
552 }
553
554 println!("Unit switch plan:");
555
556 for (unit_file, unit_plan) in &plan.unit_plan {
557 let action = match unit_plan.action {
558 UnitAction::NoAction => "No action",
559 UnitAction::StopStart => "Stop/Start",
560 UnitAction::Start => "Start",
561 UnitAction::Stop => "Stop",
562 UnitAction::Restart => "Restart",
563 UnitAction::Reload => "Reload",
564 UnitAction::KeepOld => "Keep active",
565 };
566 let decisions = pretty_unit_names(unit_plan.decisions.iter().map(|v| format!("{:?}", v)));
567 println!(" {action} {unit_file} ({decisions})");
568 }
569}
570
571pub fn switch(
573 service_manager: &impl systemd::ServiceManager,
574 old_dir: Option<&Path>,
575 new_dir: &Path,
576 dry_run: bool,
577 verbose: bool,
578 timeout: Duration,
579) -> Result<()> {
580 let system_status = service_manager.system_status()?;
581
582 if matches!(system_status, systemd::SystemStatus::Degraded) {
585 let units_by_states = service_manager.list_units_by_states(&["failed"])?;
586 let failed: Vec<&str> = units_by_states.iter().map(|status| status.name()).collect();
587 let failed = failed.join(", ");
588 eprintln!(
589 "The service manager is degraded.\n\
590 Failed services: {failed}\n\
591 Attempting to continue anyway..."
592 );
593 }
594
595 let do_switch = {
596 use systemd::SystemStatus::{
597 Degraded, Initializing, Maintenance, Running, Starting, Stopping,
598 };
599 match system_status {
600 Initializing | Starting | Running | Degraded => true,
601 Maintenance | Stopping => false,
602 }
603 };
604
605 if !do_switch {
606 if verbose {
607 println!("Skipping switch since systemd has {system_status} status");
608 }
609 return Ok(());
610 }
611
612 let plan = build_switch_plan(old_dir, new_dir, verbose, service_manager)
613 .context("Failed to build switch plan")?;
614
615 if verbose {
616 print_plan(&plan);
617 }
618
619 let job_handler = move |name: &str, state: &str| {
620 if verbose || state != "done" {
621 println!("{name} {state}");
622 }
623 };
624
625 exec_pre_reload(&plan, service_manager, job_handler, dry_run, timeout)
626 .context("Failed to perform pre-reload tasks")?;
627
628 exec_reload(service_manager, dry_run, verbose)?;
629
630 exec_post_reload(
631 &plan,
632 service_manager,
633 job_handler,
634 dry_run,
635 verbose,
636 timeout,
637 )
638 .context("Failed to perform post-reload tasks")?;
639
640 Ok(())
641}
642
643#[cfg(test)]
644mod tests {
645 use super::*;
646
647 #[test]
648 fn can_get_base_name_for_parameterized_unit() {
649 assert_eq!(
650 parameterized_base_name("foo@bar.service"),
651 Some(String::from("foo@.service"))
652 );
653 assert_eq!(
654 parameterized_base_name("foo@bar.baz.service"),
655 Some(String::from("foo@.service"))
656 );
657 }
658
659 #[test]
660 fn no_base_name_for_nonparameterized_units() {
661 assert_eq!(parameterized_base_name("foo@.service"), None);
662 assert_eq!(parameterized_base_name("foo.service"), None);
663 assert_eq!(parameterized_base_name("foo@barservice"), None);
664 }
665}