shimexe_core/
runner.rs

1use std::path::{Path, PathBuf};
2use std::process::{Command, Stdio};
3use std::sync::{Arc, Mutex, OnceLock};
4use std::time::{Duration, SystemTime};
5use tracing::{debug, info, warn};
6
7use crate::config::ShimConfig;
8use crate::downloader::Downloader;
9use crate::error::{Result, ShimError};
10use crate::updater::ShimUpdater;
11use crate::utils::merge_env_vars;
12
13/// Cache entry for executable validation results
14#[derive(Debug, Clone)]
15struct ValidationCacheEntry {
16    is_valid: bool,
17    last_checked: SystemTime,
18    file_modified: SystemTime,
19}
20
21/// Performance cache for executable validation
22#[derive(Debug, Clone)]
23struct ExecutableCache {
24    cache: Arc<Mutex<std::collections::HashMap<PathBuf, ValidationCacheEntry>>>,
25    ttl: Duration,
26}
27
28impl ExecutableCache {
29    fn new(ttl: Duration) -> Self {
30        Self {
31            cache: Arc::new(Mutex::new(std::collections::HashMap::new())),
32            ttl,
33        }
34    }
35
36    fn is_valid(&self, path: &Path) -> Option<bool> {
37        let now = SystemTime::now();
38        if let Ok(cache) = self.cache.lock() {
39            if let Some(entry) = cache.get(path) {
40                // Check if cache entry is still valid
41                if now
42                    .duration_since(entry.last_checked)
43                    .unwrap_or(Duration::MAX)
44                    < self.ttl
45                {
46                    // Check if file hasn't been modified
47                    if let Ok(metadata) = std::fs::metadata(path) {
48                        if let Ok(modified) = metadata.modified() {
49                            if modified <= entry.file_modified {
50                                return Some(entry.is_valid);
51                            }
52                        }
53                    }
54                }
55            }
56        }
57        None
58    }
59
60    fn set_valid(&self, path: &Path, is_valid: bool) {
61        let now = SystemTime::now();
62        let file_modified = std::fs::metadata(path)
63            .and_then(|m| m.modified())
64            .unwrap_or(now);
65
66        if let Ok(mut cache) = self.cache.lock() {
67            cache.insert(
68                path.to_path_buf(),
69                ValidationCacheEntry {
70                    is_valid,
71                    last_checked: now,
72                    file_modified,
73                },
74            );
75        }
76    }
77}
78
79// Global cache instance with 30-second TTL
80static EXECUTABLE_CACHE: OnceLock<ExecutableCache> = OnceLock::new();
81
82fn get_executable_cache() -> &'static ExecutableCache {
83    EXECUTABLE_CACHE.get_or_init(|| ExecutableCache::new(Duration::from_secs(30)))
84}
85
86/// Shim runner that executes the target executable
87pub struct ShimRunner {
88    config: ShimConfig,
89    shim_file_path: Option<PathBuf>,
90}
91
92impl ShimRunner {
93    /// Create a new shim runner from a configuration file
94    pub fn from_file<P: AsRef<Path>>(shim_file: P) -> Result<Self> {
95        let mut config = ShimConfig::from_file(&shim_file)?;
96        config.expand_env_vars()?;
97
98        Ok(Self {
99            config,
100            shim_file_path: Some(shim_file.as_ref().to_path_buf()),
101        })
102    }
103
104    /// Create a new shim runner from a configuration
105    pub fn from_config(mut config: ShimConfig) -> Result<Self> {
106        config.expand_env_vars()?;
107        Ok(Self {
108            config,
109            shim_file_path: None,
110        })
111    }
112
113    /// Execute the shim with additional arguments
114    pub fn execute(&self, additional_args: &[String]) -> Result<i32> {
115        let start_time = SystemTime::now();
116
117        // Check for updates if auto-update is enabled
118        if let Some(ref auto_update) = self.config.auto_update {
119            if let Some(ref shim_file_path) = self.shim_file_path {
120                self.check_and_update(auto_update, shim_file_path)?;
121            }
122        }
123
124        // Check if we need to download the executable
125        self.ensure_executable_available()?;
126
127        let executable_path = self.config.get_executable_path()?;
128
129        // Use cached validation if available
130        let cache = get_executable_cache();
131        if let Some(is_valid) = cache.is_valid(&executable_path) {
132            if !is_valid {
133                return Err(ShimError::ExecutableNotFound(
134                    executable_path.to_string_lossy().to_string(),
135                ));
136            }
137        } else {
138            // Validate and cache the result
139            let is_valid = self.validate_executable_fast(&executable_path);
140            cache.set_valid(&executable_path, is_valid);
141            if !is_valid {
142                return Err(ShimError::ExecutableNotFound(
143                    executable_path.to_string_lossy().to_string(),
144                ));
145            }
146        }
147
148        debug!("Executing: {:?}", executable_path);
149        debug!("Default args: {:?}", self.config.shim.args);
150        debug!("Additional args: {:?}", additional_args);
151
152        // Prepare command with optimized environment variable handling
153        let mut cmd = Command::new(&executable_path);
154
155        // Add default arguments
156        cmd.args(&self.config.shim.args);
157
158        // Add additional arguments
159        cmd.args(additional_args);
160
161        // Set working directory if specified
162        if let Some(ref cwd) = self.config.shim.cwd {
163            cmd.current_dir(cwd);
164        }
165
166        // Set environment variables (optimized to avoid unnecessary allocations)
167        if !self.config.env.is_empty() {
168            let env_vars = merge_env_vars(&self.config.env);
169            for (key, value) in env_vars {
170                cmd.env(key, value);
171            }
172        }
173
174        // Configure stdio to inherit from parent
175        cmd.stdin(Stdio::inherit())
176            .stdout(Stdio::inherit())
177            .stderr(Stdio::inherit());
178
179        info!(
180            "Executing shim '{}' -> {:?}",
181            self.config.shim.name, executable_path
182        );
183
184        // Execute the command
185        let result = match cmd.status() {
186            Ok(status) => {
187                let exit_code = status.code().unwrap_or(-1);
188                debug!("Process exited with code: {}", exit_code);
189                Ok(exit_code)
190            }
191            Err(e) => {
192                warn!("Failed to execute process: {}", e);
193                Err(ShimError::ProcessExecution(e.to_string()))
194            }
195        };
196
197        // Log execution time for performance monitoring
198        if let Ok(elapsed) = start_time.elapsed() {
199            debug!("Shim execution took: {:?}", elapsed);
200        }
201
202        result
203    }
204
205    /// Fast executable validation without full metadata checks
206    fn validate_executable_fast(&self, path: &Path) -> bool {
207        path.exists() && path.is_file()
208    }
209
210    /// Get the shim configuration
211    pub fn config(&self) -> &ShimConfig {
212        &self.config
213    }
214
215    /// Validate that the target executable exists and is executable
216    pub fn validate(&self) -> Result<()> {
217        let executable_path = self.config.get_executable_path()?;
218
219        // Use cached validation if available
220        let cache = get_executable_cache();
221        if let Some(is_valid) = cache.is_valid(&executable_path) {
222            if is_valid {
223                return Ok(());
224            } else {
225                return Err(ShimError::ExecutableNotFound(
226                    executable_path.to_string_lossy().to_string(),
227                ));
228            }
229        }
230
231        // Perform full validation and cache the result
232        let validation_result = self.validate_executable_full(&executable_path);
233        let is_valid = validation_result.is_ok();
234        cache.set_valid(&executable_path, is_valid);
235        validation_result
236    }
237
238    /// Perform full executable validation with all checks
239    fn validate_executable_full(&self, executable_path: &Path) -> Result<()> {
240        if !executable_path.exists() {
241            return Err(ShimError::ExecutableNotFound(
242                executable_path.to_string_lossy().to_string(),
243            ));
244        }
245
246        // Check if it's a file (not a directory)
247        if !executable_path.is_file() {
248            return Err(ShimError::Config(format!(
249                "Path is not a file: {}",
250                executable_path.display()
251            )));
252        }
253
254        // On Unix-like systems, check if the file is executable
255        #[cfg(unix)]
256        {
257            use std::os::unix::fs::PermissionsExt;
258            let metadata = executable_path.metadata().map_err(ShimError::Io)?;
259            let permissions = metadata.permissions();
260
261            if permissions.mode() & 0o111 == 0 {
262                return Err(ShimError::PermissionDenied(format!(
263                    "File is not executable: {}",
264                    executable_path.display()
265                )));
266            }
267        }
268
269        Ok(())
270    }
271
272    /// Check for updates and perform update if needed
273    fn check_and_update(
274        &self,
275        auto_update: &crate::config::AutoUpdate,
276        shim_file_path: &Path,
277    ) -> Result<()> {
278        let executable_path = self.config.get_executable_path()?;
279        let updater = ShimUpdater::new(
280            auto_update.clone(),
281            shim_file_path.to_path_buf(),
282            executable_path,
283        );
284
285        // Use a simple blocking approach for now
286        // In a real implementation, you might want to use async/await
287        let rt = tokio::runtime::Runtime::new().map_err(|e| {
288            ShimError::ProcessExecution(format!("Failed to create async runtime: {}", e))
289        })?;
290
291        rt.block_on(async {
292            match updater.check_update_needed().await {
293                Ok(Some(version)) => {
294                    info!("Auto-update available: {}", version);
295                    if let Err(e) = updater.update_to_version(&version).await {
296                        warn!("Auto-update failed: {}", e);
297                    }
298                }
299                Ok(None) => {
300                    debug!("No update needed");
301                }
302                Err(e) => {
303                    warn!("Update check failed: {}", e);
304                }
305            }
306        });
307
308        Ok(())
309    }
310
311    /// Ensure the executable is available, downloading if necessary
312    fn ensure_executable_available(&self) -> Result<()> {
313        // Check if this shim has a download URL (was created from HTTP)
314        if let Some(download_url) = self.config.get_download_url() {
315            // This shim was created from an HTTP URL
316            let executable_path = match self.config.get_executable_path() {
317                Ok(path) => path,
318                Err(_) => {
319                    // If get_executable_path fails, it means we need to download
320                    return self.download_executable_from_url(download_url);
321                }
322            };
323
324            // Check if the file exists
325            if !executable_path.exists() {
326                return self.download_executable_from_url(download_url);
327            }
328        } else if Downloader::is_url(&self.config.shim.path) {
329            // Legacy: path is still a URL (for backward compatibility)
330            let executable_path = match self.config.get_executable_path() {
331                Ok(path) => path,
332                Err(_) => {
333                    // If get_executable_path fails for a URL, it means we need to download
334                    return self.download_executable_from_url(&self.config.shim.path);
335                }
336            };
337
338            // Check if the file exists
339            if !executable_path.exists() {
340                return self.download_executable_from_url(&self.config.shim.path);
341            }
342        }
343        Ok(())
344    }
345
346    /// Download the executable from HTTP URL
347    fn download_executable_from_url(&self, url: &str) -> Result<()> {
348        // Extract filename from URL
349        let filename = Downloader::extract_filename_from_url(url).ok_or_else(|| {
350            ShimError::Config(format!("Could not extract filename from URL: {}", url))
351        })?;
352
353        // Determine download directory
354        let download_dir = if let Some(ref shim_file_path) = self.shim_file_path {
355            // Use the same directory as the shim file
356            shim_file_path
357                .parent()
358                .ok_or_else(|| {
359                    ShimError::Config("Could not determine shim file directory".to_string())
360                })?
361                .join(&self.config.shim.name)
362                .join("bin")
363        } else {
364            // Fallback to home directory
365            dirs::home_dir()
366                .ok_or_else(|| ShimError::Config("Could not determine home directory".to_string()))?
367                .join(".shimexe")
368                .join(&self.config.shim.name)
369                .join("bin")
370        };
371
372        let download_path = download_dir.join(&filename);
373
374        // Create a runtime for async operations
375        let rt = tokio::runtime::Runtime::new().map_err(|e| {
376            ShimError::ProcessExecution(format!("Failed to create async runtime: {}", e))
377        })?;
378
379        rt.block_on(async {
380            let downloader = Downloader::new();
381            downloader
382                .download_if_missing(url, &download_path)
383                .await
384                .map_err(|e| {
385                    ShimError::ProcessExecution(format!("Failed to download executable: {}", e))
386                })
387        })?;
388
389        info!("Downloaded executable to: {}", download_path.display());
390        Ok(())
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use std::io::Write;
398    use tempfile::NamedTempFile;
399
400    #[test]
401    fn test_runner_from_config() {
402        let config = ShimConfig {
403            shim: crate::config::ShimCore {
404                name: "test".to_string(),
405                path: "echo".to_string(),
406                args: vec!["hello".to_string()],
407                cwd: None,
408                download_url: None,
409            },
410            args: Default::default(),
411            env: std::collections::HashMap::new(),
412            metadata: Default::default(),
413            auto_update: None,
414        };
415
416        let runner = ShimRunner::from_config(config).unwrap();
417        assert_eq!(runner.config().shim.name, "test");
418    }
419
420    #[test]
421    fn test_runner_from_file() {
422        let mut temp_file = NamedTempFile::new().unwrap();
423        writeln!(
424            temp_file,
425            r#"
426[shim]
427name = "test"
428path = "echo"
429args = ["hello"]
430
431[env]
432TEST_VAR = "test_value"
433        "#
434        )
435        .unwrap();
436
437        let runner = ShimRunner::from_file(temp_file.path()).unwrap();
438        assert_eq!(runner.config().shim.name, "test");
439        assert_eq!(
440            runner.config().env.get("TEST_VAR"),
441            Some(&"test_value".to_string())
442        );
443    }
444}