service_install/install/
files.rs

1use std::env::current_exe;
2use std::ffi::OsString;
3use std::fmt::Display;
4use std::fs::{self, Permissions};
5use std::io::{ErrorKind, Read, Write};
6use std::os::unix::fs::PermissionsExt;
7use std::path::{Path, PathBuf};
8
9use itertools::Itertools;
10
11use crate::install::files::process_parent::IdRes;
12use crate::install::RemoveStep;
13
14use super::init::PathCheckError;
15use super::{
16    init, BackupError, InstallError, InstallStep, Mode, RemoveError, RollbackError, RollbackStep,
17    Tense,
18};
19
20pub mod process_parent;
21
22#[derive(thiserror::Error, Debug)]
23pub enum MoveError {
24    #[error("could not find current users home dir")]
25    NoHome(
26        #[from]
27        #[source]
28        NoHomeError,
29    ),
30    #[error("none of the usual dirs for user binaries exist")]
31    UserDirNotAvailable,
32    #[error("none of the usual dirs for system binaries exist")]
33    SystemDirNotAvailable,
34    #[error("the path did not point to a binary")]
35    SourceNotFile,
36    #[error("could not move binary to install location")]
37    IO(#[source] std::io::Error),
38    #[error("overwrite is not set and there is already a file named {name} at {}", dir.display())]
39    TargetExists { name: String, dir: PathBuf },
40    #[error("{0}")]
41    TargetInUse(
42        #[from]
43        #[source]
44        TargetInUseError,
45    ),
46    #[error("could not check if already existing file is read only")]
47    CheckExistingFilePermissions(#[source] std::io::Error),
48    #[error("could not check if we are running from the target location")]
49    ResolveCurrentExe(#[source] std::io::Error),
50    #[error(
51        "could not check if already existing file is identical to what we are about to install"
52    )]
53    CompareFiles(#[source] CompareFileError),
54}
55
56fn system_dir() -> Option<PathBuf> {
57    let possible_paths: &[&'static Path] = &["/usr/bin/"].map(Path::new);
58
59    for path in possible_paths {
60        if path.parent().expect("never root").is_dir() {
61            return Some(path.to_path_buf());
62        }
63    }
64    None
65}
66
67#[derive(Debug, thiserror::Error)]
68#[error("Home directory not known")]
69pub struct NoHomeError;
70
71fn user_dir() -> Result<Option<PathBuf>, NoHomeError> {
72    let possible_paths: &[&'static Path] = &[".local/bin"].map(Path::new);
73
74    for relative in possible_paths {
75        let path = home::home_dir().ok_or(NoHomeError)?.join(relative);
76        if path.parent().expect("never root").is_dir() {
77            return Ok(Some(path));
78        }
79    }
80    Ok(None)
81}
82
83pub(crate) struct Move {
84    name: OsString,
85    source: PathBuf,
86    pub target: PathBuf,
87}
88
89impl InstallStep for Move {
90    fn describe_detailed(&self, tense: Tense) -> String {
91        let verb = match tense {
92            Tense::Past => "Copied",
93            Tense::Questioning => "Copy",
94            Tense::Future => "Will copy",
95            Tense::Active => "Copying",
96        };
97        let name = self.name.to_string_lossy();
98        let source = self
99            .source
100            .parent()
101            .expect("path points to file, so has parent")
102            .display();
103        let target = self
104            .target
105            .parent()
106            .expect("path points to file, so has parent")
107            .display();
108        format!(
109            "{verb} executable `{name}`{}\n| from:\n|\t{source}\n| to:\n|\t{target}",
110            tense.punct()
111        )
112    }
113
114    fn describe(&self, tense: Tense) -> String {
115        let verb = match tense {
116            Tense::Past => "Copied",
117            Tense::Questioning => "Copy",
118            Tense::Future => "Will copy",
119            Tense::Active => "Copying",
120        };
121        let name = self.name.to_string_lossy();
122        let target = self
123            .target
124            .parent()
125            .expect("path points to file, so has parent")
126            .display();
127        format!(
128            "{verb} executable `{name}` to:\n|\t{target}{}",
129            tense.punct()
130        )
131    }
132
133    fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
134        let rollback_step = if self.target.is_file() {
135            let target_content = fs::read(&self.target)
136                .map_err(BackupError::Read)
137                .map_err(InstallError::Backup)?;
138
139            let mut backup = tempfile::tempfile()
140                .map_err(BackupError::Create)
141                .map_err(InstallError::Backup)?;
142            backup
143                .write_all(&target_content)
144                .map_err(BackupError::Write)
145                .map_err(InstallError::Backup)?;
146
147            Box::new(MoveBack {
148                backup,
149                target: self.target.clone(),
150            }) as Box<dyn RollbackStep>
151        } else {
152            Box::new(Remove {
153                target: self.target.clone(),
154            }) as Box<dyn RollbackStep>
155        };
156
157        match std::fs::copy(&self.source, &self.target) {
158            Err(e) => Err(InstallError::CopyExeError(e)),
159            Ok(_) => Ok(Some(rollback_step)),
160        }
161    }
162}
163
164#[derive(Debug, thiserror::Error)]
165pub enum MoveBackError {
166    #[error("Could not read backup from file")]
167    ReadingBackup(#[source] std::io::Error),
168    #[error("Could not write to target")]
169    WritingToTarget(#[source] std::io::Error),
170}
171
172struct MoveBack {
173    /// created by tempfile will be auto cleaned by OS when
174    /// this drops
175    backup: std::fs::File,
176    target: PathBuf,
177}
178
179impl RollbackStep for MoveBack {
180    fn perform(&mut self) -> Result<(), RollbackError> {
181        let mut buf = Vec::new();
182        self.backup
183            .read_to_end(&mut buf)
184            .map_err(MoveBackError::ReadingBackup)
185            .map_err(RollbackError::MovingBack)?;
186        fs::write(&self.target, buf)
187            .map_err(MoveBackError::WritingToTarget)
188            .map_err(RollbackError::MovingBack)
189    }
190
191    fn describe(&self, tense: Tense) -> String {
192        let verb = match tense {
193            Tense::Past => "Moved",
194            Tense::Questioning => "Move",
195            Tense::Active => "Moving",
196            Tense::Future => "Will move",
197        };
198        format!(
199            "{verb} back the file that was origonally at the install location{}",
200            tense.punct()
201        )
202    }
203}
204
205struct SetRootOwner {
206    path: PathBuf,
207}
208
209impl InstallStep for SetRootOwner {
210    fn describe(&self, tense: Tense) -> String {
211        let verb = match tense {
212            Tense::Past | Tense::Questioning => "Set",
213            Tense::Active => "Setting",
214            Tense::Future => "Will set",
215        };
216        format!("{verb} executables owner to root{}", tense.punct())
217    }
218
219    fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
220        const ROOT: u32 = 0;
221        std::os::unix::fs::chown(&self.path, Some(ROOT), Some(ROOT))
222            .map_err(InstallError::SetRootOwner)?;
223        Ok(None)
224    }
225}
226
227#[derive(Debug, thiserror::Error)]
228pub enum SetReadOnlyError {
229    #[error("Could not get current permissions for file")]
230    GetPermissions(#[source] std::io::Error),
231    #[error("Could not set permissions for file")]
232    SetPermissions(#[source] std::io::Error),
233}
234
235struct MakeReadExecOnly {
236    path: PathBuf,
237}
238
239impl InstallStep for MakeReadExecOnly {
240    fn describe(&self, tense: Tense) -> String {
241        let verb = match tense {
242            Tense::Past => "Made",
243            Tense::Questioning => "Make",
244            Tense::Future => "Will make",
245            Tense::Active => "Making",
246        };
247        format!(
248            "{verb} the executable read and execute only{}",
249            tense.punct()
250        )
251    }
252
253    fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
254        use std::os::unix::fs::PermissionsExt;
255
256        let org_permissions = fs::metadata(&self.path)
257            .map_err(SetReadOnlyError::GetPermissions)?
258            .permissions();
259        let mut permissions = org_permissions.clone();
260        permissions.set_mode(0o555);
261        fs::set_permissions(&self.path, permissions).map_err(SetReadOnlyError::SetPermissions)?;
262        Ok(Some(Box::new(RestorePermissions {
263            path: self.path.clone(),
264            org_permissions,
265        })))
266    }
267}
268
269struct RestorePermissions {
270    path: PathBuf,
271    org_permissions: Permissions,
272}
273
274impl RollbackStep for RestorePermissions {
275    fn perform(&mut self) -> Result<(), RollbackError> {
276        match fs::set_permissions(&self.path, self.org_permissions.clone()) {
277            Ok(()) => Ok(()),
278            // overwrite may have been set or the file removed by the user
279            // we should no abort the rollback because the file is not there
280            Err(io) if io.kind() == std::io::ErrorKind::NotFound => {
281                tracing::warn!("Could not restore permissions, file is not there");
282                Ok(())
283            }
284            Err(other) => Err(RollbackError::RestoringPermissions(other)),
285        }
286    }
287
288    fn describe(&self, tense: Tense) -> String {
289        let verb = match tense {
290            Tense::Past => "Restored",
291            Tense::Active => "Restoring",
292            Tense::Questioning => "Restore",
293            Tense::Future => "Will Restore",
294        };
295        format!("{verb} executables previous permissions{}", tense.punct())
296    }
297}
298
299struct FilesAlreadyInstalled {
300    target: PathBuf,
301}
302
303impl InstallStep for FilesAlreadyInstalled {
304    fn describe(&self, tense: Tense) -> String {
305        match tense {
306            Tense::Past => "this binary was already installed in the target location",
307            Tense::Questioning | Tense::Future | Tense::Active => {
308                "this binary is already installed in the target location"
309            }
310        }
311        .to_owned()
312    }
313
314    fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
315        Ok(None)
316    }
317
318    fn describe_detailed(&self, tense: Tense) -> String {
319        format!(
320            "{}\n|\ttarget location: {}",
321            self.describe(tense),
322            self.target.display()
323        )
324    }
325
326    fn options(&self) -> Option<super::StepOptions> {
327        None // this is a notification
328    }
329}
330
331type Steps = Vec<Box<dyn InstallStep>>;
332pub(crate) fn move_files(
333    source: PathBuf,
334    mode: Mode,
335    run_as: Option<&str>,
336    overwrite_existing: bool,
337    init_systems: &[init::System],
338) -> Result<(Steps, PathBuf), MoveError> {
339    let dir = match mode {
340        Mode::User => user_dir()?.ok_or(MoveError::UserDirNotAvailable)?,
341        Mode::System => system_dir().ok_or(MoveError::SystemDirNotAvailable)?,
342    };
343
344    let file_name = source
345        .file_name()
346        .ok_or(MoveError::SourceNotFile)?
347        .to_owned();
348    let target = dir.join(&file_name);
349    let current_exe = current_exe().map_err(MoveError::ResolveCurrentExe)?;
350
351    if target.is_file()
352        && content_identical(&target, &current_exe).map_err(MoveError::CompareFiles)?
353    {
354        let step = FilesAlreadyInstalled {
355            target: target.clone(),
356        };
357        return Ok((vec![Box::new(step) as Box<dyn InstallStep>], target));
358    } else if target.is_file() && !overwrite_existing {
359        return Err(MoveError::TargetExists {
360            name: file_name.to_string_lossy().to_string(),
361            dir,
362        });
363    }
364
365    let mut steps = Vec::new();
366    if let Some(make_removable) = make_removable_if_needed(&target)? {
367        steps.push(make_removable);
368    }
369
370    let disable_steps = disable_if_running(&target, init_systems, mode, run_as)?;
371    steps.extend(disable_steps);
372
373    steps.extend([
374        Box::new(Move {
375            name: file_name,
376            source,
377            target: target.clone(),
378        }) as Box<dyn InstallStep>,
379        Box::new(MakeReadExecOnly {
380            path: target.clone(),
381        }),
382    ]);
383    if let Mode::System = mode {
384        steps.push(Box::new(SetRootOwner {
385            path: target.clone(),
386        }));
387    }
388
389    Ok((steps, target))
390}
391
392#[derive(Debug, thiserror::Error)]
393pub enum CompareFileError {
394    #[error("opening the new file")]
395    IoNew(#[source] std::io::Error),
396    #[error("opening existing file")]
397    IoExisting(#[source] std::io::Error),
398
399    #[error("checking length of new file")]
400    LenNew(#[source] std::io::Error),
401    #[error("checking length of existing file")]
402    LenExisting(#[source] std::io::Error),
403
404    #[error("reading the new file")]
405    ReadNew(#[source] std::io::Error),
406    #[error("reading the existing file")]
407    ReadExisting(#[source] std::io::Error),
408}
409
410fn content_identical(existing: &Path, new: &Path) -> Result<bool, CompareFileError> {
411    use CompareFileError as E;
412
413    if existing == new {
414        return Ok(true);
415    }
416
417    let mut existing = fs::File::open(existing).map_err(E::IoExisting)?;
418    let mut new = fs::File::open(new).map_err(E::IoNew)?;
419
420    if existing.metadata().map_err(E::LenExisting)?.len()
421        != new.metadata().map_err(E::LenNew)?.len()
422    {
423        return Ok(false);
424    }
425
426    let mut e_buf = [0; 4096];
427    let mut n_buf = [0; 4096];
428
429    loop {
430        let e_read = read_into_buf(&mut existing, &mut e_buf).map_err(E::ReadExisting)?;
431        let n_read = read_into_buf(&mut new, &mut n_buf).map_err(E::ReadNew)?;
432        if e_read != n_read {
433            return Ok(false);
434        } else if e_read.len() == 4096 {
435            return Ok(true);
436        }
437    }
438}
439
440/// Alternative to read_exact that always returns the read data. If the length
441/// of the input buffer is longer then the result EOF has been read. The last
442/// data is then in the returned slice.
443fn read_into_buf<'a>(
444    file: &mut fs::File,
445    buf: &'a mut [u8],
446) -> Result<&'a mut [u8], std::io::Error> {
447    let mut total_read = 0;
448    let mut free_buf = &mut buf[..];
449    loop {
450        let n = file.read(free_buf)?;
451        total_read += n;
452        if n == 0 || n == free_buf.len() {
453            break;
454        } else {
455            free_buf = &mut free_buf[n..];
456        }
457    }
458
459    Ok(&mut buf[..total_read])
460}
461
462struct MakeRemovable(PathBuf);
463
464fn make_removable_if_needed(target: &Path) -> Result<Option<Box<dyn InstallStep>>, MoveError> {
465    let permissions = match fs::metadata(target) {
466        Ok(meta) => meta,
467        Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None),
468        Err(e) => return Err(MoveError::CheckExistingFilePermissions(e)),
469    }
470    .permissions();
471
472    Ok(if permissions.readonly() {
473        let step = MakeRemovable(target.to_owned());
474        let step = Box::new(step) as Box<dyn InstallStep>;
475        Some(step)
476    } else {
477        None
478    })
479}
480
481impl InstallStep for MakeRemovable {
482    fn describe(&self, tense: Tense) -> String {
483        let verb = match tense {
484            Tense::Past => "Made",
485            Tense::Questioning => "Make",
486            Tense::Future => "Will make",
487            Tense::Active => "Making",
488        };
489        format!(
490            "{verb} the file taking up the install location removable{}",
491            tense.punct()
492        )
493    }
494
495    fn describe_detailed(&self, tense: Tense) -> String {
496        let verb = match tense {
497            Tense::Past => "Made",
498            Tense::Questioning => "Make",
499            Tense::Future => "Will make",
500            Tense::Active => "Making",
501        };
502        format!("A different read only file is taking up the install location. {verb} it removable by making it writable{}\n| file:\n|\t{}", tense.punct(), self.0.display())
503    }
504
505    fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
506        let org_permissions = fs::metadata(&self.0)
507            .map_err(SetReadOnlyError::GetPermissions)?
508            .permissions();
509        let mut permissions = org_permissions.clone();
510        permissions.set_mode(0o600);
511        fs::set_permissions(&self.0, permissions).map_err(SetReadOnlyError::SetPermissions)?;
512        Ok(Some(Box::new(RestorePermissions {
513            path: self.0.clone(),
514            org_permissions,
515        })))
516    }
517}
518
519#[derive(Debug, thiserror::Error)]
520pub enum TargetInUseError {
521    NoParent,
522    ResolvePath(
523        #[from]
524        #[source]
525        PathCheckError,
526    ),
527    Parents(Vec<PathBuf>),
528    CouldNotDisable(
529        #[from]
530        #[source]
531        DisableError,
532    ),
533}
534
535#[derive(Debug, thiserror::Error)]
536pub enum DisableError {
537    #[error(transparent)]
538    SystemD(#[from] init::systemd::DisableError),
539    #[error(transparent)]
540    Cron(#[from] init::cron::disable::Error),
541}
542
543impl Display for TargetInUseError {
544    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
545        match self {
546            TargetInUseError::NoParent => {
547                writeln!(f, "There is already a file at the install location. It can not be replaced as it is running. We have no information on how it was started as it has no parent")
548            }
549            TargetInUseError::ResolvePath(_) => {
550                writeln!(f, "There is already a file at the install location. It can not be replaced as it is running. While it has a parent we failed to get information about it")
551            }
552            TargetInUseError::Parents(tree) => {
553                let tree = tree.iter().map(|p| p.display().to_string());
554                let tree: String = Itertools::intersperse(tree, " -> ".to_string()).collect();
555                writeln!(f, "There is already a file at the install location. It can not be replaced as it is running.\n\tThe process tree that started that:\n\t`{tree}`\nIn this tree the arrow means left started the right process")
556            }
557            TargetInUseError::CouldNotDisable(err) => {
558                writeln!(
559                    f,
560                    "The file we need to replace is in use by a running service however we could not disable that service. {err}"
561                )
562            }
563        }
564    }
565}
566
567fn disable_if_running(
568    target: &Path,
569    init_systems: &[init::System],
570    mode: Mode,
571    run_as: Option<&str>,
572) -> Result<Vec<Box<dyn InstallStep>>, TargetInUseError> {
573    let mut steps = Vec::new();
574
575    for parent_info in process_parent::list(target, init_systems)? {
576        match parent_info {
577            IdRes::ParentIsInit { init, pid } => {
578                steps.append(&mut init.disable_steps(target, pid, mode, run_as)?);
579            }
580            IdRes::NoParent => return Err(TargetInUseError::NoParent)?,
581            IdRes::ParentNotInit { parents, pid } => {
582                steps.push(process_parent::kill_old_steps(pid, parents));
583            }
584        }
585    }
586
587    Ok(steps)
588}
589
590#[derive(thiserror::Error, Debug)]
591pub enum DeleteError {
592    #[error("could not find current users home dir")]
593    NoHome(
594        #[from]
595        #[source]
596        NoHomeError,
597    ),
598    #[error("none of the usual dirs for user binaries exist")]
599    UserDirNotAvailable,
600    #[error("none of the usual dirs for system binaries exist")]
601    SystemDirNotAvailable,
602    #[error("the path did not point to a binary")]
603    SourceNotFile,
604    #[error("could not move binary to install location")]
605    IO(#[source] std::io::Error),
606    #[error("Could not get the current executable's location")]
607    GetExeLocation(#[source] std::io::Error),
608    #[error("May only uninstall the currently running binary, running: {running} installed: {installed}")]
609    ExeNotInstalled {
610        running: PathBuf,
611        installed: PathBuf,
612    },
613}
614
615pub(crate) struct Remove {
616    target: PathBuf,
617}
618
619impl RemoveStep for Remove {
620    fn describe(&self, tense: Tense) -> String {
621        let verb = match tense {
622            Tense::Past => "Removed",
623            Tense::Questioning => "Remove",
624            Tense::Future => "Will remove",
625            Tense::Active => "Removing",
626        };
627        let bin = self
628            .target
629            .file_name()
630            .expect("In fn exe_path we made sure target is a file")
631            .to_string_lossy();
632        format!("{verb} installed executable `{bin}`{}", tense.punct())
633    }
634
635    fn describe_detailed(&self, tense: Tense) -> String {
636        let verb = match tense {
637            Tense::Past => "Removed",
638            Tense::Questioning => "Remove",
639            Tense::Future => "Will remove",
640            Tense::Active => "Removing",
641        };
642        let bin = self
643            .target
644            .file_name()
645            .expect("In fn exe_path we made sure target is a file")
646            .to_string_lossy();
647        let dir = self
648            .target
649            .parent()
650            .expect("There is always a parent on linux")
651            .display();
652        format!(
653            "{verb} installed executable `{bin}`{} Is installed at:\n|\t{dir}",
654            tense.punct()
655        )
656    }
657
658    fn perform(&mut self) -> Result<(), RemoveError> {
659        std::fs::remove_file(&self.target)
660            .map_err(DeleteError::IO)
661            .map_err(Into::into)
662    }
663}
664
665pub(crate) fn remove_files(installed: PathBuf) -> Remove {
666    Remove { target: installed }
667}