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::get_builtin_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 (inherit current env and add custom ones)
167        // First, set built-in environment variables
168        let builtin_vars = get_builtin_env_vars();
169        for (key, value) in builtin_vars {
170            cmd.env(key, value);
171        }
172
173        // Then, set custom environment variables (these will override built-ins if same key)
174        for (key, value) in &self.config.env {
175            cmd.env(key, value);
176        }
177
178        // Configure stdio to inherit from parent
179        cmd.stdin(Stdio::inherit())
180            .stdout(Stdio::inherit())
181            .stderr(Stdio::inherit());
182
183        info!(
184            "Executing shim '{}' -> {:?}",
185            self.config.shim.name, executable_path
186        );
187
188        // Execute the command
189        let result = match cmd.status() {
190            Ok(status) => {
191                let exit_code = status.code().unwrap_or(-1);
192                debug!("Process exited with code: {}", exit_code);
193                Ok(exit_code)
194            }
195            Err(e) => {
196                warn!("Failed to execute process: {}", e);
197                Err(ShimError::ProcessExecution(e.to_string()))
198            }
199        };
200
201        // Log execution time for performance monitoring
202        if let Ok(elapsed) = start_time.elapsed() {
203            debug!("Shim execution took: {:?}", elapsed);
204        }
205
206        result
207    }
208
209    /// Fast executable validation without full metadata checks
210    fn validate_executable_fast(&self, path: &Path) -> bool {
211        path.exists() && path.is_file()
212    }
213
214    /// Get the shim configuration
215    pub fn config(&self) -> &ShimConfig {
216        &self.config
217    }
218
219    /// Validate that the target executable exists and is executable
220    pub fn validate(&self) -> Result<()> {
221        let executable_path = self.config.get_executable_path()?;
222
223        // Use cached validation if available
224        let cache = get_executable_cache();
225        if let Some(is_valid) = cache.is_valid(&executable_path) {
226            if is_valid {
227                return Ok(());
228            } else {
229                return Err(ShimError::ExecutableNotFound(
230                    executable_path.to_string_lossy().to_string(),
231                ));
232            }
233        }
234
235        // Perform full validation and cache the result
236        let validation_result = self.validate_executable_full(&executable_path);
237        let is_valid = validation_result.is_ok();
238        cache.set_valid(&executable_path, is_valid);
239        validation_result
240    }
241
242    /// Perform full executable validation with all checks
243    fn validate_executable_full(&self, executable_path: &Path) -> Result<()> {
244        if !executable_path.exists() {
245            return Err(ShimError::ExecutableNotFound(
246                executable_path.to_string_lossy().to_string(),
247            ));
248        }
249
250        // Check if it's a file (not a directory)
251        if !executable_path.is_file() {
252            return Err(ShimError::Config(format!(
253                "Path is not a file: {}",
254                executable_path.display()
255            )));
256        }
257
258        // On Unix-like systems, check if the file is executable
259        #[cfg(unix)]
260        {
261            use std::os::unix::fs::PermissionsExt;
262            let metadata = executable_path.metadata().map_err(ShimError::Io)?;
263            let permissions = metadata.permissions();
264
265            if permissions.mode() & 0o111 == 0 {
266                return Err(ShimError::PermissionDenied(format!(
267                    "File is not executable: {}",
268                    executable_path.display()
269                )));
270            }
271        }
272
273        Ok(())
274    }
275
276    /// Check for updates and perform update if needed
277    fn check_and_update(
278        &self,
279        auto_update: &crate::config::AutoUpdate,
280        shim_file_path: &Path,
281    ) -> Result<()> {
282        let executable_path = self.config.get_executable_path()?;
283        let updater = ShimUpdater::new(
284            auto_update.clone(),
285            shim_file_path.to_path_buf(),
286            executable_path,
287        );
288
289        // Use a simple blocking approach for now
290        // In a real implementation, you might want to use async/await
291        let rt = tokio::runtime::Runtime::new().map_err(|e| {
292            ShimError::ProcessExecution(format!("Failed to create async runtime: {}", e))
293        })?;
294
295        rt.block_on(async {
296            match updater.check_update_needed().await {
297                Ok(Some(version)) => {
298                    info!("Auto-update available: {}", version);
299                    if let Err(e) = updater.update_to_version(&version).await {
300                        warn!("Auto-update failed: {}", e);
301                    }
302                }
303                Ok(None) => {
304                    debug!("No update needed");
305                }
306                Err(e) => {
307                    warn!("Update check failed: {}", e);
308                }
309            }
310        });
311
312        Ok(())
313    }
314
315    /// Ensure the executable is available, downloading if necessary
316    fn ensure_executable_available(&self) -> Result<()> {
317        // Check if this shim has a download URL (was created from HTTP)
318        if let Some(download_url) = self.config.get_download_url() {
319            // This shim was created from an HTTP URL
320            let executable_path = match self.config.get_executable_path() {
321                Ok(path) => path,
322                Err(_) => {
323                    // If get_executable_path fails, it means we need to download
324                    return self.download_executable_from_url(download_url);
325                }
326            };
327
328            // Check if the file exists
329            if !executable_path.exists() {
330                return self.download_executable_from_url(download_url);
331            }
332        } else if Downloader::is_url(&self.config.shim.path) {
333            // Legacy: path is still a URL (for backward compatibility)
334            let executable_path = match self.config.get_executable_path() {
335                Ok(path) => path,
336                Err(_) => {
337                    // If get_executable_path fails for a URL, it means we need to download
338                    return self.download_executable_from_url(&self.config.shim.path);
339                }
340            };
341
342            // Check if the file exists
343            if !executable_path.exists() {
344                return self.download_executable_from_url(&self.config.shim.path);
345            }
346        }
347        Ok(())
348    }
349
350    /// Download the executable from HTTP URL
351    fn download_executable_from_url(&self, url: &str) -> Result<()> {
352        // Extract filename from URL
353        let filename = Downloader::extract_filename_from_url(url).ok_or_else(|| {
354            ShimError::Config(format!("Could not extract filename from URL: {}", url))
355        })?;
356
357        // Determine download directory
358        let download_dir = if let Some(ref shim_file_path) = self.shim_file_path {
359            // Use the same directory as the shim file
360            shim_file_path
361                .parent()
362                .ok_or_else(|| {
363                    ShimError::Config("Could not determine shim file directory".to_string())
364                })?
365                .join(&self.config.shim.name)
366                .join("bin")
367        } else {
368            // Fallback to home directory
369            dirs::home_dir()
370                .ok_or_else(|| ShimError::Config("Could not determine home directory".to_string()))?
371                .join(".shimexe")
372                .join(&self.config.shim.name)
373                .join("bin")
374        };
375
376        let download_path = download_dir.join(&filename);
377
378        // Create a runtime for async operations
379        let rt = tokio::runtime::Runtime::new().map_err(|e| {
380            ShimError::ProcessExecution(format!("Failed to create async runtime: {}", e))
381        })?;
382
383        rt.block_on(async {
384            let mut downloader = Downloader::new().await.map_err(|e| {
385                ShimError::ProcessExecution(format!("Failed to create downloader: {}", e))
386            })?;
387            downloader
388                .download_if_missing(url, &download_path)
389                .await
390                .map_err(|e| {
391                    ShimError::ProcessExecution(format!("Failed to download executable: {}", e))
392                })
393        })?;
394
395        info!("Downloaded executable to: {}", download_path.display());
396        Ok(())
397    }
398}