1use std::path::{Path, PathBuf};
2use std::process::{Command, Stdio};
3use tracing::{debug, info, warn};
4
5use crate::config::ShimConfig;
6use crate::error::{Result, ShimError};
7use crate::updater::ShimUpdater;
8use crate::utils::merge_env_vars;
9
10pub struct ShimRunner {
12 config: ShimConfig,
13 shim_file_path: Option<PathBuf>,
14}
15
16impl ShimRunner {
17 pub fn from_file<P: AsRef<Path>>(shim_file: P) -> Result<Self> {
19 let mut config = ShimConfig::from_file(&shim_file)?;
20 config.expand_env_vars()?;
21
22 Ok(Self {
23 config,
24 shim_file_path: Some(shim_file.as_ref().to_path_buf()),
25 })
26 }
27
28 pub fn from_config(mut config: ShimConfig) -> Result<Self> {
30 config.expand_env_vars()?;
31 Ok(Self {
32 config,
33 shim_file_path: None,
34 })
35 }
36
37 pub fn execute(&self, additional_args: &[String]) -> Result<i32> {
39 if let Some(ref auto_update) = self.config.auto_update {
41 if let Some(ref shim_file_path) = self.shim_file_path {
42 self.check_and_update(auto_update, shim_file_path)?;
43 }
44 }
45
46 let executable_path = self.config.get_executable_path()?;
47
48 debug!("Executing: {:?}", executable_path);
49 debug!("Default args: {:?}", self.config.shim.args);
50 debug!("Additional args: {:?}", additional_args);
51
52 let mut cmd = Command::new(&executable_path);
54
55 cmd.args(&self.config.shim.args);
57
58 cmd.args(additional_args);
60
61 if let Some(ref cwd) = self.config.shim.cwd {
63 cmd.current_dir(cwd);
64 }
65
66 let env_vars = merge_env_vars(&self.config.env);
68 for (key, value) in env_vars {
69 cmd.env(key, value);
70 }
71
72 cmd.stdin(Stdio::inherit())
74 .stdout(Stdio::inherit())
75 .stderr(Stdio::inherit());
76
77 info!(
78 "Executing shim '{}' -> {:?}",
79 self.config.shim.name, executable_path
80 );
81
82 match cmd.status() {
84 Ok(status) => {
85 let exit_code = status.code().unwrap_or(-1);
86 debug!("Process exited with code: {}", exit_code);
87 Ok(exit_code)
88 }
89 Err(e) => {
90 warn!("Failed to execute process: {}", e);
91 Err(ShimError::ProcessExecution(e.to_string()))
92 }
93 }
94 }
95
96 pub fn config(&self) -> &ShimConfig {
98 &self.config
99 }
100
101 pub fn validate(&self) -> Result<()> {
103 let executable_path = self.config.get_executable_path()?;
104
105 if !executable_path.exists() {
106 return Err(ShimError::ExecutableNotFound(
107 executable_path.to_string_lossy().to_string(),
108 ));
109 }
110
111 if !executable_path.is_file() {
113 return Err(ShimError::Config(format!(
114 "Path is not a file: {}",
115 executable_path.display()
116 )));
117 }
118
119 #[cfg(unix)]
121 {
122 use std::os::unix::fs::PermissionsExt;
123 let metadata = executable_path.metadata().map_err(ShimError::Io)?;
124 let permissions = metadata.permissions();
125
126 if permissions.mode() & 0o111 == 0 {
127 return Err(ShimError::PermissionDenied(format!(
128 "File is not executable: {}",
129 executable_path.display()
130 )));
131 }
132 }
133
134 Ok(())
135 }
136
137 fn check_and_update(
139 &self,
140 auto_update: &crate::config::AutoUpdate,
141 shim_file_path: &Path,
142 ) -> Result<()> {
143 let executable_path = self.config.get_executable_path()?;
144 let updater = ShimUpdater::new(
145 auto_update.clone(),
146 shim_file_path.to_path_buf(),
147 executable_path,
148 );
149
150 let rt = tokio::runtime::Runtime::new().map_err(|e| {
153 ShimError::ProcessExecution(format!("Failed to create async runtime: {}", e))
154 })?;
155
156 rt.block_on(async {
157 match updater.check_update_needed().await {
158 Ok(Some(version)) => {
159 info!("Auto-update available: {}", version);
160 if let Err(e) = updater.update_to_version(&version).await {
161 warn!("Auto-update failed: {}", e);
162 }
163 }
164 Ok(None) => {
165 debug!("No update needed");
166 }
167 Err(e) => {
168 warn!("Update check failed: {}", e);
169 }
170 }
171 });
172
173 Ok(())
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use std::io::Write;
181 use tempfile::NamedTempFile;
182
183 #[test]
184 fn test_runner_from_config() {
185 let config = ShimConfig {
186 shim: crate::config::ShimCore {
187 name: "test".to_string(),
188 path: "echo".to_string(),
189 args: vec!["hello".to_string()],
190 cwd: None,
191 },
192 args: Default::default(),
193 env: std::collections::HashMap::new(),
194 metadata: Default::default(),
195 auto_update: None,
196 };
197
198 let runner = ShimRunner::from_config(config).unwrap();
199 assert_eq!(runner.config().shim.name, "test");
200 }
201
202 #[test]
203 fn test_runner_from_file() {
204 let mut temp_file = NamedTempFile::new().unwrap();
205 writeln!(
206 temp_file,
207 r#"
208[shim]
209name = "test"
210path = "echo"
211args = ["hello"]
212
213[env]
214TEST_VAR = "test_value"
215 "#
216 )
217 .unwrap();
218
219 let runner = ShimRunner::from_file(temp_file.path()).unwrap();
220 assert_eq!(runner.config().shim.name, "test");
221 assert_eq!(
222 runner.config().env.get("TEST_VAR"),
223 Some(&"test_value".to_string())
224 );
225 }
226}