sd_switch/
lib.rs

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
42/// Given an active unit name this returns the actual unit file name. In the
43/// case of a parameterized unit, e.g., `foo@bar.service` this function returns
44/// `foo@.service`. For nonparameterized unit names it returns none.
45fn 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
59/// Returns the file path of the given unit name within the given directory.
60///
61/// If no matching file is found then `None` is returned.
62///
63/// If the given unit name is a parameterized named then an exactly matching
64/// file is returned, if it exists, otherwise the template file path is
65/// returned.
66fn 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/// A plan of unit actions needed to accomplish the switch.
77#[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    // Handle units that are currently active, typically this implies restarting
112    // the unit in some way.
113    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    // Handle units that are not currently active but are wanted by an active
199    // target. Typically this is simply a matter of starting the new unit.
200    for wanted_unit in find_wanted_units(new_dir)? {
201        // Skip if the unit is not wanted by an active target.
202        if !active_unit_names.contains(&wanted_unit.target_name) {
203            continue;
204        }
205
206        // Skip if the unit is actually active.
207        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        // Get the file name as a string. Ignore the string if we cannot parse it.
242        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
403/// Performs a systemd unit "switch".
404pub fn switch(
405    service_manager: &impl systemd::ServiceManager,
406    //connection: &zbus::blocking::Connection,
407    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 the systemd manager status is degraded then inform the user about the
416    // failed units. We will still attempt to perform the switch, though.
417    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}