system_updater/
command.rs

1use crate::*;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::io::Write;
5use std::path::PathBuf;
6use std::process::{Command, ExitStatus, Stdio};
7use std::{fmt, fs, io};
8
9#[derive(Debug, Serialize, Deserialize, PartialEq, Copy, Clone)]
10pub enum UpdateSteps {
11    PreInstall,
12    Install,
13    PostInstall,
14}
15
16/// Root of the machine’s dependency graph
17#[derive(Debug, Serialize, Deserialize)]
18pub struct Updater {
19    pub packagers: BTreeMap<String, Packager>,
20}
21
22/// A list of equivalent executors that will update a given component
23///
24/// Example: the `system` one will try to do update the system for as if it is Debian with `apt update`, if it fails it will try for openSUSE with `zypper refresh`, …
25/// The step will be considered a succes if **any** executor succeed and will skip all the other ones.
26#[derive(Debug, Serialize, Deserialize)]
27pub struct Packager {
28    executors: Vec<Executor>,
29    // TODO: => make a system dependend on another? This will allow to give a "Rust" config which update "rustup", and a custom "git helix" could then be executed after (with the updated toolchain, and NOT concurrently)
30}
31
32/// All the infos for an executor to proceed until completion
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Executor {
35    pub name: String,
36    pre_install: Option<Vec<Cmd>>,
37    install: Cmd,
38    post_install: Option<Vec<Cmd>>,
39    binaries: Option<Vec<String>>, // TODO: find a more explicit name for binaries that need to be present to enable the executor
40}
41
42/// A command to execute on the system as part of an executor
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Cmd {
45    exe: String,
46    params: Option<Vec<String>>,
47    current_dir: Option<PathBuf>,
48    env: Option<BTreeMap<String, String>>,
49}
50
51/// The actual (cleaned) command that will be executed on the system as part of an executor
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ActualCmd {
54    exe: String,
55    params: Vec<String>,
56    current_dir: Option<PathBuf>,
57    env: BTreeMap<String, String>,
58}
59
60impl From<String> for UpdateSteps {
61    fn from(value: String) -> Self {
62        match value.to_lowercase().as_str() {
63            "pre_install" => UpdateSteps::PreInstall,
64            "install" => UpdateSteps::Install,
65            "post_install" => UpdateSteps::PostInstall,
66
67            _ => panic!("Step {} not recognized", value),
68        }
69    }
70}
71
72impl Display for UpdateSteps {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match self {
75            UpdateSteps::PreInstall => write!(f, "pre_install"),
76            UpdateSteps::Install => write!(f, "install"),
77            UpdateSteps::PostInstall => write!(f, "post_install"),
78        }
79    }
80}
81
82pub fn get_packages_folder(opt: &Opt) -> io::Result<PathBuf> {
83    if let Some(p) = opt.config_folder.clone() {
84        return Ok(p);
85    }
86
87    let config_folder = directories::ProjectDirs::from("net", "ZykiCorp", "System Updater")
88        .ok_or(io::Error::new(
89            io::ErrorKind::NotFound,
90            "System’s configuration folder: for its standard location see https://docs.rs/directories/latest/directories/struct.ProjectDirs.html#method.config_dir",
91        ))?
92        .config_dir()
93        .join("packagers");
94
95    Ok(config_folder)
96}
97
98impl Updater {
99    fn new() -> Updater {
100        Updater {
101            packagers: BTreeMap::default(),
102        }
103    }
104
105    /// To create a sample config from code
106    #[doc(hidden)]
107    fn write_config(&self, opt: &Opt) {
108        use std::fs::OpenOptions;
109
110        let config_folder = get_packages_folder(opt).unwrap();
111
112        let mut f = OpenOptions::new()
113            .write(true)
114            .create(true)
115            .truncate(true)
116            .open(config_folder.join("default.yaml"))
117            .unwrap();
118
119        fs::create_dir_all(&config_folder).unwrap();
120
121        f.write_all(serde_yaml::to_string(&self).unwrap().as_bytes())
122            .unwrap();
123    }
124
125    // TODO: add option to use &opt.config_file (or folder?) instead
126    pub fn from_config(opt: &Opt) -> io::Result<Updater> {
127        let mut updater = Updater::new();
128
129        // Example to generate a config file
130        if false {
131            updater
132                .packagers
133                .insert("Test".to_owned(), Packager { executors: vec![] });
134            let sys = updater
135                .packagers
136                .get_mut("Test")
137                .expect("We just created the key");
138
139            sys.executors.push(Executor {
140                name: "Rustup".to_owned(),
141                pre_install: None,
142                install: Cmd {
143                    exe: "rustup".to_owned(),
144                    params: Some(vec!["self".to_owned(), "update".to_owned()]),
145                    current_dir: None,
146                    env: None,
147                },
148                post_install: None,
149                binaries: None,
150            });
151
152            sys.executors.push(Executor {
153                name: "Cargo".to_owned(),
154                pre_install: None,
155                install: Cmd {
156                    exe: "cargo".to_owned(),
157                    params: Some(vec!["install-update".to_owned(), "-a".to_owned()]),
158                    current_dir: None,
159                    env: None,
160                },
161                post_install: None,
162                binaries: Some(vec!["rustc".to_owned()]),
163            });
164
165            updater.write_config(opt);
166
167            panic!("Wrote a config sample.");
168        }
169
170        let packages_folder = get_packages_folder(opt)?;
171        // TODO: Useless match? Still a risck for "time-of-check to time-of-use" bug
172        match packages_folder.try_exists() {
173            Ok(true) => {} // Ok: Exist and should be readable
174            Ok(false) => {
175                return Err(io::Error::new(
176                    io::ErrorKind::Other,
177                    format!(
178                        "Configuration folder not accessible at: {}. (broken symlink?)",
179                        packages_folder.display()
180                    ),
181                ))
182            }
183            Err(e) => return Err(e),
184        }
185
186        for file in packages_folder.read_dir()?.filter(|name| match name {
187            Ok(n) => n.file_name().into_string().unwrap().ends_with(".yaml"),
188            Err(..) => false,
189        }) {
190            let file = file?.path();
191            let sys = std::fs::read_to_string(&file).unwrap();
192            let sys = serde_yaml::from_str(&sys).map_err(|err| {
193                io::Error::new(
194                    io::ErrorKind::Other,
195                    format!(
196                        "Encontered an error while parsing config file {}: {}",
197                        file.display(),
198                        err
199                    ),
200                )
201            })?;
202            updater
203                .packagers
204                .insert(file.file_stem().unwrap().to_str().unwrap().to_owned(), sys);
205        }
206
207        // eprintln!("{:#?}", updater);
208
209        Ok(updater)
210    }
211
212    pub fn update_all(&self, opt: &Opt) -> Summary {
213        let mut status: Vec<_> = vec![];
214
215        // XXX: We may parallelise (iter_par from rayon?) this loop. But the UI will be problematic to handle
216        for (packager_name, packager) in &self.packagers {
217            // TODO: default status should be an error, but… in the same time we should not have en empty list
218            let mut record = Record {
219                packager: String::from("No packager"),
220                executor: String::from("No executor"),
221                status: Err(Error::Config {
222                    source: which::Error::CannotFindBinaryPath,
223                    filename: String::new(),
224                })
225                .into(),
226            };
227
228            assert!(!packager.executors.is_empty());
229
230            for executor in &packager.executors {
231                let mut u = executor.is_usable();
232                let mut is_ok = executor.is_usable().is_ok();
233                if is_ok {
234                    u = self.update(executor, opt);
235                    is_ok = u.is_ok();
236                }
237
238                record = Record {
239                    packager: packager_name.to_string(),
240                    executor: executor.name.clone(),
241                    status: u.into(),
242                };
243
244                if is_ok {
245                    break;
246                }
247            }
248
249            status.push(record);
250        }
251
252        Summary { status }
253    }
254
255    fn update(&self, sys: &Executor, opt: &Opt) -> Result<()> {
256        let steps = &opt.steps;
257        assert!(!steps.is_empty());
258
259        if steps.contains(&UpdateSteps::PreInstall) {
260            sys.pre_install(opt)?;
261        }
262        if steps.contains(&UpdateSteps::Install) {
263            sys.install(opt)?;
264        }
265        if steps.contains(&UpdateSteps::PostInstall) {
266            sys.post_install(opt)?;
267        }
268
269        Ok(())
270    }
271}
272
273fn are_all_binaries_found_string(
274    bins: &[String],
275) -> std::result::Result<(), (which::Error, String)> {
276    let mut outer_b = String::new(); // TODO: This outer_b feels ackish
277    bins.iter()
278        .try_for_each(|b| {
279            outer_b = b.clone();
280            which::which(b)?;
281            Ok::<(), which::Error>(())
282        })
283        .map_err(|s| (s, outer_b))?;
284    Ok(())
285}
286
287fn are_all_binaries_found_cmd(bins: &[Cmd]) -> std::result::Result<(), (which::Error, String)> {
288    let mut outer_b = String::new(); // TODO: This outer_b feels ackish
289    bins.iter()
290        .try_for_each(|b| {
291            outer_b = b.exe.clone();
292            which::which(b.exe.clone())?;
293            Ok::<(), which::Error>(())
294        })
295        .map_err(|s| (s, outer_b))?;
296    Ok(())
297}
298
299impl Executor {
300    // TODO: Transform from user’s executable to another one?
301    pub fn is_usable(&self) -> Result<()> {
302        are_all_binaries_found_cmd(&self.pre_install.clone().unwrap_or_default())
303            .map_err(|(source, filename)| Error::Config { source, filename })?;
304        are_all_binaries_found_cmd(&[self.install.clone()])
305            .map_err(|(source, filename)| Error::Config { source, filename })?;
306
307        are_all_binaries_found_cmd(&self.post_install.clone().unwrap_or_default())
308            .map_err(|(source, filename)| Error::Config { source, filename })?;
309
310        are_all_binaries_found_string(&self.binaries.clone().unwrap_or_default())
311            .map_err(|(source, filename)| Error::Config { source, filename })?;
312
313        Ok(())
314    }
315
316    pub fn pre_install(&self, opt: &Opt) -> Result<()> {
317        if let Some(pre_install) = &self.pre_install {
318            for cmd in pre_install {
319                let cmd = cmd.clone().prepare(opt);
320                let exit_status = cmd.execute(opt).map_err(|err| Error::Execution {
321                    source: err,
322                    step: UpdateSteps::PreInstall,
323                    cmd: cmd.clone(),
324                })?;
325
326                if !exit_status.success() {
327                    return Result::Err(Error::Execution {
328                        source: io::Error::new(io::ErrorKind::Other, format!("{}", exit_status)),
329                        step: UpdateSteps::PreInstall,
330                        cmd,
331                    });
332                }
333            }
334        }
335        Ok(())
336    }
337
338    pub fn install(&self, opt: &Opt) -> Result<()> {
339        let cmd = self.install.clone().prepare(opt);
340        let exit_status = cmd.execute(opt).map_err(|err| Error::Execution {
341            source: err,
342            step: UpdateSteps::Install,
343            cmd: cmd.clone(),
344        })?;
345
346        if !exit_status.success() {
347            return Err(Error::Execution {
348                source: io::Error::new(io::ErrorKind::Other, format!("{}", exit_status)),
349                step: UpdateSteps::Install,
350                cmd,
351            });
352        }
353        Ok(())
354    }
355
356    pub fn post_install(&self, opt: &Opt) -> Result<()> {
357        if let Some(post_install) = &self.post_install {
358            for cmd in post_install {
359                let cmd = cmd.clone().prepare(opt);
360                let exit_status = cmd.execute(opt).map_err(|err| Error::Execution {
361                    source: err,
362                    step: UpdateSteps::PostInstall,
363                    cmd: cmd.clone(),
364                })?;
365
366                if !exit_status.success() {
367                    return Err(Error::Execution {
368                        source: io::Error::new(io::ErrorKind::Other, format!("{}", exit_status)),
369                        step: UpdateSteps::PostInstall,
370                        cmd,
371                    });
372                }
373            }
374        }
375        Ok(())
376    }
377}
378
379impl Display for Executor {
380    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381        write!(f, "{}", self.name)
382    }
383}
384
385impl Cmd {
386    #[allow(dead_code)] // "To easily generate from code"
387    fn new() -> Cmd {
388        Cmd {
389            exe: "".into(),
390            params: None,
391            current_dir: None,
392            env: None,
393        }
394    }
395
396    fn prepare(self, _opt: &Opt) -> ActualCmd {
397        // TODO: I’m not convinced by helping the user and only escaping the PATH. Either all or none
398        // This means I need to know how to know which values to pass for (at least) rustup & cargo
399        let env = match self.env {
400            Some(env) => env,
401            None => BTreeMap::default(),
402        };
403
404        ActualCmd {
405            exe: self.exe,
406            params: self.params.unwrap_or_default(),
407            current_dir: self.current_dir,
408            env,
409        }
410    }
411}
412
413impl ActualCmd {
414    fn execute(&self, opt: &Opt) -> io::Result<ExitStatus> {
415        let mut cmd = Command::new(&self.exe);
416
417        cmd.args(&self.params).envs(&self.env);
418
419        if let Some(cdir) = &self.current_dir {
420            cmd.current_dir(std::fs::canonicalize(cdir)?);
421        }
422
423        println!();
424        println!("*** Executing: {} ***", self);
425        // eprintln!("{:?}", self.params);
426
427        if opt.quiet {
428            // FIXME: stdin does not work with sudo?
429            cmd.stdin(Stdio::null())
430                .stdout(Stdio::null())
431                .stderr(Stdio::null());
432        }
433
434        cmd.status()
435    }
436}
437
438impl fmt::Display for ActualCmd {
439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440        let command = if !self.params.is_empty() {
441            format!("{} {}", &self.exe, &self.params.join(" "))
442        } else {
443            self.exe.clone()
444        };
445        write!(f, "{}", command)?;
446
447        if let Some(cdir) = &self.current_dir {
448            write!(f, " in {:?}", cdir)?;
449        }
450
451        Ok(())
452
453        // // TODO: remove me (too verbose)
454        // if !self.env.is_empty() {
455        //     writeln!(f, " with the following environment variable:")?;
456        //     writeln!(f, "{:#?}", self.env)
457        // } else {
458        //     write!(f, " without any environment variable. ")
459        // }
460    }
461}