leftwm_core/utils/
child_process.rs

1//! Starts programs in autostart, runs global 'up' script, and boots theme. Provides function to
2//! boot other desktop files also.
3use std::cmp::Reverse;
4use std::collections::BinaryHeap;
5use std::collections::HashMap;
6use std::ffi::OsStr;
7use std::fs;
8use std::iter::{Extend, FromIterator};
9use std::path::{Path, PathBuf};
10use std::process::{Child, Command, Stdio};
11use std::sync::{atomic::AtomicBool, Arc};
12
13use xdg::BaseDirectories;
14
15use crate::errors::Result;
16
17pub type ChildID = u32;
18
19#[derive(Default)]
20pub struct Nanny {}
21
22impl Nanny {
23    /// Retrieve the path to the config directory. Tries to create it if it does not exist.
24    ///
25    /// # Errors
26    ///
27    /// Will error if unable to open or create the config directory.
28    /// Could be caused by inadequate permissions.
29    fn get_config_dir() -> Result<PathBuf> {
30        BaseDirectories::with_prefix("leftwm")?
31            .create_config_directory("")
32            .map_err(Into::into)
33    }
34
35    /// Runs a script if it exits
36    fn run_script(path: &Path) -> Result<Child> {
37        Command::new(path)
38            .stdin(Stdio::null())
39            .stdout(Stdio::null())
40            .stderr(Stdio::null())
41            .spawn()
42            .map_err(Into::into)
43    }
44
45    /// Runs the 'up' script in the config directory, if there is one.
46    ///
47    /// # Errors
48    ///
49    /// Will error if unable to open current config directory.
50    /// Could be caused by inadequate permissions.
51    pub fn run_global_up_script() -> Result<Child> {
52        let mut path = Self::get_config_dir()?;
53        let mut scripts = Self::get_files_in_path_with_ext(&path, "up")?;
54
55        while let Some(Reverse(script)) = scripts.pop() {
56            if let Err(e) = Self::run_script(&script) {
57                tracing::error!("Unable to run script {script:?}, error: {e}");
58            }
59        }
60
61        path.push("up");
62        Self::run_script(&path)
63    }
64
65    /// Returns a min-heap of files with the specified extension.
66    ///
67    /// # Errors
68    ///
69    /// Comes from `std::fs::read_dir()`.
70    fn get_files_in_path_with_ext(
71        path: impl AsRef<Path>,
72        ext: impl AsRef<OsStr>,
73    ) -> Result<BinaryHeap<Reverse<PathBuf>>> {
74        let dir = fs::read_dir(&path)?;
75
76        let mut files = BinaryHeap::new();
77
78        for entry in dir.flatten() {
79            let file = entry.path();
80
81            if let Some(extension) = file.extension() {
82                if extension == ext.as_ref() {
83                    files.push(Reverse(file));
84                }
85            }
86        }
87
88        Ok(files)
89    }
90
91    /// Runs the 'up' script of the current theme, if there is one.
92    ///
93    /// # Errors
94    ///
95    /// Will error if unable to open current theme directory.
96    /// Could be caused by inadequate permissions.
97    pub fn boot_current_theme() -> Result<Child> {
98        let mut path = Self::get_config_dir()?;
99        path.push("themes");
100        path.push("current");
101        path.push("up");
102        Self::run_script(&path)
103    }
104}
105
106/// A struct managing children processes.
107#[derive(Debug, Default)]
108pub struct Children {
109    inner: HashMap<ChildID, Child>,
110}
111
112impl Children {
113    #[must_use]
114    pub fn new() -> Self {
115        Self::default()
116    }
117    #[must_use]
118    pub fn len(&self) -> usize {
119        self.inner.len()
120    }
121    #[must_use]
122    pub fn is_empty(&self) -> bool {
123        self.inner.is_empty()
124    }
125    /// Insert a `Child` in the `Children`.
126    ///
127    /// # Returns
128    /// - `true` if `child` is a new child-process
129    /// - `false` if `child` is already known
130    pub fn insert(&mut self, child: Child) -> bool {
131        self.inner.insert(child.id(), child).is_none()
132    }
133
134    /// Merge another `Children` into this `Children`.
135    pub fn merge(&mut self, reaper: Self) {
136        self.inner.extend(reaper.inner);
137    }
138
139    /// Remove all children precosses which finished
140    pub fn remove_finished_children(&mut self) {
141        self.inner
142            .retain(|_, child| child.try_wait().map_or(true, |ret| ret.is_none()));
143    }
144}
145
146impl FromIterator<Child> for Children {
147    fn from_iter<T: IntoIterator<Item = Child>>(iter: T) -> Self {
148        Self {
149            inner: iter.into_iter().map(|child| (child.id(), child)).collect(),
150        }
151    }
152}
153
154impl Extend<Child> for Children {
155    fn extend<T: IntoIterator<Item = Child>>(&mut self, iter: T) {
156        self.inner
157            .extend(iter.into_iter().map(|child| (child.id(), child)));
158    }
159}
160
161/// Register the `SIGCHLD` signal handler. Once the signal is received,
162/// the flag will be set true. User needs to manually clear the flag.
163pub fn register_child_hook(flag: Arc<AtomicBool>) {
164    _ = signal_hook::flag::register(signal_hook::consts::signal::SIGCHLD, flag)
165        .map_err(|err| tracing::error!("Cannot register SIGCHLD signal handler: {:?}", err));
166}
167
168/// Sends command to shell for execution
169/// Assumes STDIN/STDERR/STDOUT unwanted.
170pub fn exec_shell(command: &str, children: &mut Children) -> Option<ChildID> {
171    exec_shell_with_args(command, Vec::new(), children)
172}
173
174/// Sends command to shell for execution including arguments.
175/// Assumes STDIN/STDERR/STDOUT unwanted.
176pub fn exec_shell_with_args(
177    command: &str,
178    args: Vec<String>,
179    children: &mut Children,
180) -> Option<ChildID> {
181    let child = Command::new(command)
182        .args(args)
183        .stdin(Stdio::null())
184        .stdout(Stdio::null())
185        .stderr(Stdio::null())
186        .spawn()
187        .ok()?;
188    let pid = child.id();
189    children.insert(child);
190    Some(pid)
191}