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::downloader::Downloader;
7use crate::error::{Result, ShimError};
8use crate::updater::ShimUpdater;
9use crate::utils::merge_env_vars;
10
11/// Shim runner that executes the target executable
12pub struct ShimRunner {
13    config: ShimConfig,
14    shim_file_path: Option<PathBuf>,
15}
16
17impl ShimRunner {
18    /// Create a new shim runner from a configuration file
19    pub fn from_file<P: AsRef<Path>>(shim_file: P) -> Result<Self> {
20        let mut config = ShimConfig::from_file(&shim_file)?;
21        config.expand_env_vars()?;
22
23        Ok(Self {
24            config,
25            shim_file_path: Some(shim_file.as_ref().to_path_buf()),
26        })
27    }
28
29    /// Create a new shim runner from a configuration
30    pub fn from_config(mut config: ShimConfig) -> Result<Self> {
31        config.expand_env_vars()?;
32        Ok(Self {
33            config,
34            shim_file_path: None,
35        })
36    }
37
38    /// Execute the shim with additional arguments
39    pub fn execute(&self, additional_args: &[String]) -> Result<i32> {
40        // Check for updates if auto-update is enabled
41        if let Some(ref auto_update) = self.config.auto_update {
42            if let Some(ref shim_file_path) = self.shim_file_path {
43                self.check_and_update(auto_update, shim_file_path)?;
44            }
45        }
46
47        // Check if we need to download the executable
48        self.ensure_executable_available()?;
49
50        let executable_path = self.config.get_executable_path()?;
51
52        debug!("Executing: {:?}", executable_path);
53        debug!("Default args: {:?}", self.config.shim.args);
54        debug!("Additional args: {:?}", additional_args);
55
56        // Prepare command
57        let mut cmd = Command::new(&executable_path);
58
59        // Add default arguments
60        cmd.args(&self.config.shim.args);
61
62        // Add additional arguments
63        cmd.args(additional_args);
64
65        // Set working directory if specified
66        if let Some(ref cwd) = self.config.shim.cwd {
67            cmd.current_dir(cwd);
68        }
69
70        // Set environment variables
71        let env_vars = merge_env_vars(&self.config.env);
72        for (key, value) in env_vars {
73            cmd.env(key, value);
74        }
75
76        // Configure stdio to inherit from parent
77        cmd.stdin(Stdio::inherit())
78            .stdout(Stdio::inherit())
79            .stderr(Stdio::inherit());
80
81        info!(
82            "Executing shim '{}' -> {:?}",
83            self.config.shim.name, executable_path
84        );
85
86        // Execute the command
87        match cmd.status() {
88            Ok(status) => {
89                let exit_code = status.code().unwrap_or(-1);
90                debug!("Process exited with code: {}", exit_code);
91                Ok(exit_code)
92            }
93            Err(e) => {
94                warn!("Failed to execute process: {}", e);
95                Err(ShimError::ProcessExecution(e.to_string()))
96            }
97        }
98    }
99
100    /// Get the shim configuration
101    pub fn config(&self) -> &ShimConfig {
102        &self.config
103    }
104
105    /// Validate that the target executable exists and is executable
106    pub fn validate(&self) -> Result<()> {
107        let executable_path = self.config.get_executable_path()?;
108
109        if !executable_path.exists() {
110            return Err(ShimError::ExecutableNotFound(
111                executable_path.to_string_lossy().to_string(),
112            ));
113        }
114
115        // Check if it's a file (not a directory)
116        if !executable_path.is_file() {
117            return Err(ShimError::Config(format!(
118                "Path is not a file: {}",
119                executable_path.display()
120            )));
121        }
122
123        // On Unix-like systems, check if the file is executable
124        #[cfg(unix)]
125        {
126            use std::os::unix::fs::PermissionsExt;
127            let metadata = executable_path.metadata().map_err(ShimError::Io)?;
128            let permissions = metadata.permissions();
129
130            if permissions.mode() & 0o111 == 0 {
131                return Err(ShimError::PermissionDenied(format!(
132                    "File is not executable: {}",
133                    executable_path.display()
134                )));
135            }
136        }
137
138        Ok(())
139    }
140
141    /// Check for updates and perform update if needed
142    fn check_and_update(
143        &self,
144        auto_update: &crate::config::AutoUpdate,
145        shim_file_path: &Path,
146    ) -> Result<()> {
147        let executable_path = self.config.get_executable_path()?;
148        let updater = ShimUpdater::new(
149            auto_update.clone(),
150            shim_file_path.to_path_buf(),
151            executable_path,
152        );
153
154        // Use a simple blocking approach for now
155        // In a real implementation, you might want to use async/await
156        let rt = tokio::runtime::Runtime::new().map_err(|e| {
157            ShimError::ProcessExecution(format!("Failed to create async runtime: {}", e))
158        })?;
159
160        rt.block_on(async {
161            match updater.check_update_needed().await {
162                Ok(Some(version)) => {
163                    info!("Auto-update available: {}", version);
164                    if let Err(e) = updater.update_to_version(&version).await {
165                        warn!("Auto-update failed: {}", e);
166                    }
167                }
168                Ok(None) => {
169                    debug!("No update needed");
170                }
171                Err(e) => {
172                    warn!("Update check failed: {}", e);
173                }
174            }
175        });
176
177        Ok(())
178    }
179
180    /// Ensure the executable is available, downloading if necessary
181    fn ensure_executable_available(&self) -> Result<()> {
182        // Check if this shim has a download URL (was created from HTTP)
183        if let Some(download_url) = self.config.get_download_url() {
184            // This shim was created from an HTTP URL
185            let executable_path = match self.config.get_executable_path() {
186                Ok(path) => path,
187                Err(_) => {
188                    // If get_executable_path fails, it means we need to download
189                    return self.download_executable_from_url(download_url);
190                }
191            };
192
193            // Check if the file exists
194            if !executable_path.exists() {
195                return self.download_executable_from_url(download_url);
196            }
197        } else if Downloader::is_url(&self.config.shim.path) {
198            // Legacy: path is still a URL (for backward compatibility)
199            let executable_path = match self.config.get_executable_path() {
200                Ok(path) => path,
201                Err(_) => {
202                    // If get_executable_path fails for a URL, it means we need to download
203                    return self.download_executable_from_url(&self.config.shim.path);
204                }
205            };
206
207            // Check if the file exists
208            if !executable_path.exists() {
209                return self.download_executable_from_url(&self.config.shim.path);
210            }
211        }
212        Ok(())
213    }
214
215    /// Download the executable from HTTP URL
216    fn download_executable_from_url(&self, url: &str) -> Result<()> {
217        // Extract filename from URL
218        let filename = Downloader::extract_filename_from_url(url).ok_or_else(|| {
219            ShimError::Config(format!("Could not extract filename from URL: {}", url))
220        })?;
221
222        // Determine download directory
223        let download_dir = if let Some(ref shim_file_path) = self.shim_file_path {
224            // Use the same directory as the shim file
225            shim_file_path
226                .parent()
227                .ok_or_else(|| {
228                    ShimError::Config("Could not determine shim file directory".to_string())
229                })?
230                .join(&self.config.shim.name)
231                .join("bin")
232        } else {
233            // Fallback to home directory
234            dirs::home_dir()
235                .ok_or_else(|| ShimError::Config("Could not determine home directory".to_string()))?
236                .join(".shimexe")
237                .join(&self.config.shim.name)
238                .join("bin")
239        };
240
241        let download_path = download_dir.join(&filename);
242
243        // Create a runtime for async operations
244        let rt = tokio::runtime::Runtime::new().map_err(|e| {
245            ShimError::ProcessExecution(format!("Failed to create async runtime: {}", e))
246        })?;
247
248        rt.block_on(async {
249            let downloader = Downloader::new();
250            downloader
251                .download_if_missing(url, &download_path)
252                .await
253                .map_err(|e| {
254                    ShimError::ProcessExecution(format!("Failed to download executable: {}", e))
255                })
256        })?;
257
258        info!("Downloaded executable to: {}", download_path.display());
259        Ok(())
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use std::io::Write;
267    use tempfile::NamedTempFile;
268
269    #[test]
270    fn test_runner_from_config() {
271        let config = ShimConfig {
272            shim: crate::config::ShimCore {
273                name: "test".to_string(),
274                path: "echo".to_string(),
275                args: vec!["hello".to_string()],
276                cwd: None,
277                download_url: None,
278            },
279            args: Default::default(),
280            env: std::collections::HashMap::new(),
281            metadata: Default::default(),
282            auto_update: None,
283        };
284
285        let runner = ShimRunner::from_config(config).unwrap();
286        assert_eq!(runner.config().shim.name, "test");
287    }
288
289    #[test]
290    fn test_runner_from_file() {
291        let mut temp_file = NamedTempFile::new().unwrap();
292        writeln!(
293            temp_file,
294            r#"
295[shim]
296name = "test"
297path = "echo"
298args = ["hello"]
299
300[env]
301TEST_VAR = "test_value"
302        "#
303        )
304        .unwrap();
305
306        let runner = ShimRunner::from_file(temp_file.path()).unwrap();
307        assert_eq!(runner.config().shim.name, "test");
308        assert_eq!(
309            runner.config().env.get("TEST_VAR"),
310            Some(&"test_value".to_string())
311        );
312    }
313}