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
11pub struct ShimRunner {
13 config: ShimConfig,
14 shim_file_path: Option<PathBuf>,
15}
16
17impl ShimRunner {
18 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 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 pub fn execute(&self, additional_args: &[String]) -> Result<i32> {
40 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 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 let mut cmd = Command::new(&executable_path);
58
59 cmd.args(&self.config.shim.args);
61
62 cmd.args(additional_args);
64
65 if let Some(ref cwd) = self.config.shim.cwd {
67 cmd.current_dir(cwd);
68 }
69
70 let env_vars = merge_env_vars(&self.config.env);
72 for (key, value) in env_vars {
73 cmd.env(key, value);
74 }
75
76 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 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 pub fn config(&self) -> &ShimConfig {
102 &self.config
103 }
104
105 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 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 #[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 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 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 fn ensure_executable_available(&self) -> Result<()> {
182 if let Some(download_url) = self.config.get_download_url() {
184 let executable_path = match self.config.get_executable_path() {
186 Ok(path) => path,
187 Err(_) => {
188 return self.download_executable_from_url(download_url);
190 }
191 };
192
193 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 let executable_path = match self.config.get_executable_path() {
200 Ok(path) => path,
201 Err(_) => {
202 return self.download_executable_from_url(&self.config.shim.path);
204 }
205 };
206
207 if !executable_path.exists() {
209 return self.download_executable_from_url(&self.config.shim.path);
210 }
211 }
212 Ok(())
213 }
214
215 fn download_executable_from_url(&self, url: &str) -> Result<()> {
217 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 let download_dir = if let Some(ref shim_file_path) = self.shim_file_path {
224 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 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 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}