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#[derive(Debug, Clone)]
15struct ValidationCacheEntry {
16 is_valid: bool,
17 last_checked: SystemTime,
18 file_modified: SystemTime,
19}
20
21#[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 if now
42 .duration_since(entry.last_checked)
43 .unwrap_or(Duration::MAX)
44 < self.ttl
45 {
46 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
79static 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
86pub struct ShimRunner {
88 config: ShimConfig,
89 shim_file_path: Option<PathBuf>,
90}
91
92impl ShimRunner {
93 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 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 pub fn execute(&self, additional_args: &[String]) -> Result<i32> {
115 let start_time = SystemTime::now();
116
117 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 self.ensure_executable_available()?;
126
127 let executable_path = self.config.get_executable_path()?;
128
129 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 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 let mut cmd = Command::new(&executable_path);
154
155 cmd.args(&self.config.shim.args);
157
158 cmd.args(additional_args);
160
161 if let Some(ref cwd) = self.config.shim.cwd {
163 cmd.current_dir(cwd);
164 }
165
166 let builtin_vars = get_builtin_env_vars();
169 for (key, value) in builtin_vars {
170 cmd.env(key, value);
171 }
172
173 for (key, value) in &self.config.env {
175 cmd.env(key, value);
176 }
177
178 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 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 if let Ok(elapsed) = start_time.elapsed() {
203 debug!("Shim execution took: {:?}", elapsed);
204 }
205
206 result
207 }
208
209 fn validate_executable_fast(&self, path: &Path) -> bool {
211 path.exists() && path.is_file()
212 }
213
214 pub fn config(&self) -> &ShimConfig {
216 &self.config
217 }
218
219 pub fn validate(&self) -> Result<()> {
221 let executable_path = self.config.get_executable_path()?;
222
223 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 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 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 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 #[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 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 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 fn ensure_executable_available(&self) -> Result<()> {
317 if let Some(download_url) = self.config.get_download_url() {
319 let executable_path = match self.config.get_executable_path() {
321 Ok(path) => path,
322 Err(_) => {
323 return self.download_executable_from_url(download_url);
325 }
326 };
327
328 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 let executable_path = match self.config.get_executable_path() {
335 Ok(path) => path,
336 Err(_) => {
337 return self.download_executable_from_url(&self.config.shim.path);
339 }
340 };
341
342 if !executable_path.exists() {
344 return self.download_executable_from_url(&self.config.shim.path);
345 }
346 }
347 Ok(())
348 }
349
350 fn download_executable_from_url(&self, url: &str) -> Result<()> {
352 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 let download_dir = if let Some(ref shim_file_path) = self.shim_file_path {
359 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 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 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}