shimexe_core/
runner.rs

1use std::path::{Path, PathBuf};
2use std::process::{Command, Stdio};
3use tracing::{debug, info, warn};
4
5use crate::config::ShimConfig;
6use crate::error::{Result, ShimError};
7use crate::updater::ShimUpdater;
8use crate::utils::merge_env_vars;
9
10/// Shim runner that executes the target executable
11pub struct ShimRunner {
12    config: ShimConfig,
13    shim_file_path: Option<PathBuf>,
14}
15
16impl ShimRunner {
17    /// Create a new shim runner from a configuration file
18    pub fn from_file<P: AsRef<Path>>(shim_file: P) -> Result<Self> {
19        let mut config = ShimConfig::from_file(&shim_file)?;
20        config.expand_env_vars()?;
21
22        Ok(Self {
23            config,
24            shim_file_path: Some(shim_file.as_ref().to_path_buf()),
25        })
26    }
27
28    /// Create a new shim runner from a configuration
29    pub fn from_config(mut config: ShimConfig) -> Result<Self> {
30        config.expand_env_vars()?;
31        Ok(Self {
32            config,
33            shim_file_path: None,
34        })
35    }
36
37    /// Execute the shim with additional arguments
38    pub fn execute(&self, additional_args: &[String]) -> Result<i32> {
39        // Check for updates if auto-update is enabled
40        if let Some(ref auto_update) = self.config.auto_update {
41            if let Some(ref shim_file_path) = self.shim_file_path {
42                self.check_and_update(auto_update, shim_file_path)?;
43            }
44        }
45
46        let executable_path = self.config.get_executable_path()?;
47
48        debug!("Executing: {:?}", executable_path);
49        debug!("Default args: {:?}", self.config.shim.args);
50        debug!("Additional args: {:?}", additional_args);
51
52        // Prepare command
53        let mut cmd = Command::new(&executable_path);
54
55        // Add default arguments
56        cmd.args(&self.config.shim.args);
57
58        // Add additional arguments
59        cmd.args(additional_args);
60
61        // Set working directory if specified
62        if let Some(ref cwd) = self.config.shim.cwd {
63            cmd.current_dir(cwd);
64        }
65
66        // Set environment variables
67        let env_vars = merge_env_vars(&self.config.env);
68        for (key, value) in env_vars {
69            cmd.env(key, value);
70        }
71
72        // Configure stdio to inherit from parent
73        cmd.stdin(Stdio::inherit())
74            .stdout(Stdio::inherit())
75            .stderr(Stdio::inherit());
76
77        info!(
78            "Executing shim '{}' -> {:?}",
79            self.config.shim.name, executable_path
80        );
81
82        // Execute the command
83        match cmd.status() {
84            Ok(status) => {
85                let exit_code = status.code().unwrap_or(-1);
86                debug!("Process exited with code: {}", exit_code);
87                Ok(exit_code)
88            }
89            Err(e) => {
90                warn!("Failed to execute process: {}", e);
91                Err(ShimError::ProcessExecution(e.to_string()))
92            }
93        }
94    }
95
96    /// Get the shim configuration
97    pub fn config(&self) -> &ShimConfig {
98        &self.config
99    }
100
101    /// Validate that the target executable exists and is executable
102    pub fn validate(&self) -> Result<()> {
103        let executable_path = self.config.get_executable_path()?;
104
105        if !executable_path.exists() {
106            return Err(ShimError::ExecutableNotFound(
107                executable_path.to_string_lossy().to_string(),
108            ));
109        }
110
111        // Check if it's a file (not a directory)
112        if !executable_path.is_file() {
113            return Err(ShimError::Config(format!(
114                "Path is not a file: {}",
115                executable_path.display()
116            )));
117        }
118
119        // On Unix-like systems, check if the file is executable
120        #[cfg(unix)]
121        {
122            use std::os::unix::fs::PermissionsExt;
123            let metadata = executable_path.metadata().map_err(ShimError::Io)?;
124            let permissions = metadata.permissions();
125
126            if permissions.mode() & 0o111 == 0 {
127                return Err(ShimError::PermissionDenied(format!(
128                    "File is not executable: {}",
129                    executable_path.display()
130                )));
131            }
132        }
133
134        Ok(())
135    }
136
137    /// Check for updates and perform update if needed
138    fn check_and_update(
139        &self,
140        auto_update: &crate::config::AutoUpdate,
141        shim_file_path: &Path,
142    ) -> Result<()> {
143        let executable_path = self.config.get_executable_path()?;
144        let updater = ShimUpdater::new(
145            auto_update.clone(),
146            shim_file_path.to_path_buf(),
147            executable_path,
148        );
149
150        // Use a simple blocking approach for now
151        // In a real implementation, you might want to use async/await
152        let rt = tokio::runtime::Runtime::new().map_err(|e| {
153            ShimError::ProcessExecution(format!("Failed to create async runtime: {}", e))
154        })?;
155
156        rt.block_on(async {
157            match updater.check_update_needed().await {
158                Ok(Some(version)) => {
159                    info!("Auto-update available: {}", version);
160                    if let Err(e) = updater.update_to_version(&version).await {
161                        warn!("Auto-update failed: {}", e);
162                    }
163                }
164                Ok(None) => {
165                    debug!("No update needed");
166                }
167                Err(e) => {
168                    warn!("Update check failed: {}", e);
169                }
170            }
171        });
172
173        Ok(())
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use std::io::Write;
181    use tempfile::NamedTempFile;
182
183    #[test]
184    fn test_runner_from_config() {
185        let config = ShimConfig {
186            shim: crate::config::ShimCore {
187                name: "test".to_string(),
188                path: "echo".to_string(),
189                args: vec!["hello".to_string()],
190                cwd: None,
191            },
192            args: Default::default(),
193            env: std::collections::HashMap::new(),
194            metadata: Default::default(),
195            auto_update: None,
196        };
197
198        let runner = ShimRunner::from_config(config).unwrap();
199        assert_eq!(runner.config().shim.name, "test");
200    }
201
202    #[test]
203    fn test_runner_from_file() {
204        let mut temp_file = NamedTempFile::new().unwrap();
205        writeln!(
206            temp_file,
207            r#"
208[shim]
209name = "test"
210path = "echo"
211args = ["hello"]
212
213[env]
214TEST_VAR = "test_value"
215        "#
216        )
217        .unwrap();
218
219        let runner = ShimRunner::from_file(temp_file.path()).unwrap();
220        assert_eq!(runner.config().shim.name, "test");
221        assert_eq!(
222            runner.config().env.get("TEST_VAR"),
223            Some(&"test_value".to_string())
224        );
225    }
226}