pispas_elevator/
cli.rs

1use std::fmt::Display;
2use std::path::PathBuf;
3#[cfg(not(target_os = "macos"))]
4use std::thread::sleep;
5use clap::{Parser};
6use lazy_static::lazy_static;
7use easy_trace::instruments::tracing;
8
9#[derive(Parser, Debug)]
10#[command(author, version, about)]
11pub struct PkgCli {
12    /// Install Service
13    #[clap(long, short = 'i')]
14    pub install_service: bool,
15    /// uninstall Service
16    #[clap(long, short = 'r')]
17    pub uninstall_service: bool,
18    /// Link installed files
19    #[arg(long, short = 'l')]
20    pub link_files: bool,
21    /// Remove installed files
22    #[arg(long, short = 'd')]
23    pub remove_files: bool,
24    /// Stop Services
25    #[arg(long, short = 's')]
26    pub stop_services: bool,
27    /// Launch Tray Icon
28    #[arg(long, short = 't')]
29    pub launch_tray: bool,
30    /// Copy files to install dir
31    #[arg(long, short = 'p')]
32    pub update_files: bool,
33    /// Check and update files
34    /// will update files and restart services
35    #[arg(long, short = 'u')]
36    pub check_and_update: bool,
37
38}
39
40
41lazy_static! {
42    pub static ref PKG_CLI: PkgCli = {
43        let cli = PkgCli::parse();
44        cli
45    };
46}
47
48pub fn parse_args() -> &'static PKG_CLI {
49    &PKG_CLI
50}
51
52impl PkgCli {
53    pub fn is_uninstall(&self) -> bool {
54        self.stop_services && self.remove_files
55    }
56
57    pub fn uninstall_args() -> &'static str {
58        "-sdr"
59    }
60
61    pub fn install_args() -> &'static str {
62        "-ltpi"
63    }
64
65    pub fn update_args() -> &'static str {
66        "-sultpi"
67    }
68}
69
70#[derive(PartialEq)]
71pub enum ElevatorMode {
72    Install,
73    Uninstall,
74    Update,
75}
76
77impl Display for ElevatorMode {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            ElevatorMode::Install => write!(f, "{}", PkgCli::install_args()),
81            ElevatorMode::Uninstall => write!(f, "{}", PkgCli::uninstall_args()),
82            ElevatorMode::Update => write!(f, "{}", PkgCli::update_args()),
83        }
84    }
85}
86
87
88
89pub fn launch_elevator(mode: ElevatorMode, working_dir: &PathBuf) -> sharing::PisPasResult<()> {
90    tracing::info!("Elevator in {} with args => {}", working_dir.display(), mode);
91    let elevator = sharing::paths::get_elevator_path(working_dir);
92
93    // En macOS el elevator necesita privilegios de administrador: instala un
94    // LaunchDaemon en /Library/LaunchDaemons y escribe en /var/log/pispas.
95    // Lo lanzamos con el diálogo nativo de administrador (osascript), pero
96    // conservando el HOME del usuario real para que `dirs::home_dir()` siga
97    // resolviendo ~/.config/pispas (bajo root apuntaría a /var/root y se
98    // instalaría todo en el sitio equivocado). Es síncrono a propósito: así
99    // el directorio temporal con los binarios sigue vivo mientras copia.
100    #[cfg(target_os = "macos")]
101    {
102        // Escapa un valor para insertarlo entre comillas simples en la shell
103        // (por si HOME o la ruta del elevator contienen un apóstrofo).
104        fn sh_quote(s: &str) -> String {
105            format!("'{}'", s.replace('\'', "'\\''"))
106        }
107
108        // Si ya somos root (el elevator se re-lanza a sí mismo desde un temporal
109        // durante la desinstalación, o venimos del postinstall del .pkg), se
110        // ejecuta SÍNCRONO: si fuera detached, PackageKit mataría el proceso al
111        // cerrar el sandbox del postinstall y no llegaría a instalar nada. El
112        // tray se lanza aparte vía `launchctl asuser`, así que sobrevive.
113        if unsafe { libc::geteuid() } == 0 {
114            let status = std::process::Command::new(&elevator)
115                .arg(mode.to_string())
116                .status()?;
117            if !status.success() {
118                return Err(anyhow::anyhow!("elevator (root) falló con estado {}", status));
119            }
120            tracing::info!("Elevator (root) terminó correctamente");
121            return Ok(());
122        }
123
124        // No somos root: diálogo nativo de administrador, conservando el HOME del
125        // usuario real para que dirs::home_dir() resuelva ~/.config/pispas.
126        let home = std::env::var("HOME").unwrap_or_default();
127        let shell_cmd = format!(
128            "HOME={} {} {}",
129            sh_quote(&home),
130            sh_quote(&elevator.to_string_lossy()),
131            mode
132        );
133        let script = format!(
134            "do shell script \"{}\" with administrator privileges",
135            shell_cmd.replace('\\', "\\\\").replace('"', "\\\"")
136        );
137        let status = std::process::Command::new("osascript")
138            .arg("-e")
139            .arg(&script)
140            .status()?;
141        if !status.success() {
142            return Err(anyhow::anyhow!("osascript (admin) falló con estado {}", status));
143        }
144        tracing::info!("Elevación (osascript admin) completada");
145        return Ok(());
146    }
147
148    #[cfg(not(target_os = "macos"))]
149    {
150        let child = std::process::Command::new(elevator)
151            .arg(mode.to_string())
152            .spawn()?;
153        tracing::info!("Elevated process launched detached with pid => {:?}", child.id());
154        //it's necessary why elevator need obtain information from parent process
155        if mode != ElevatorMode::Update {
156            sleep(std::time::Duration::from_secs(3));
157        }
158        Ok(())
159    }
160}
161