1use crate::lua_exports::set_input_sender;
3use mlua::prelude::*;
4use std::process::Stdio;
5use std::sync::Arc;
6use std::time::Duration;
7use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
8use tokio::process::Command as TokioCommand;
9use tokio::runtime::Runtime;
10use tokio::sync::mpsc;
11use tokio::time::timeout;
12
13#[derive(Clone)]
16pub struct CargoCommands {
17 runtime: Arc<Runtime>,
18}
19
20impl CargoCommands {
21 pub fn new() -> LuaResult<Self> {
23 Ok(Self {
24 runtime: Arc::new(
25 tokio::runtime::Builder::new_current_thread()
26 .enable_all()
27 .build()
28 .map_err(|e| LuaError::RuntimeError(e.to_string()))?,
29 ),
30 })
31 }
32
33 pub fn execute<F, T>(&self, future: F) -> T
35 where
36 F: std::future::Future<Output = T>,
37 {
38 self.runtime.block_on(future)
39 }
40
41 #[cfg(not(test))]
43 #[allow(dead_code)]
44 async fn execute_cargo_command(
45 &self,
46 command: &str,
47 args: &[&str],
48 ) -> LuaResult<(String, bool)> {
49 self.execute_cargo_command_internal(command, args, None)
50 .await
51 }
52
53 #[cfg(test)]
55 pub async fn execute_cargo_command(
56 &self,
57 command: &str,
58 args: &[&str],
59 ) -> LuaResult<(String, bool)> {
60 self.execute_cargo_command_internal(command, args, None)
61 .await
62 }
63
64 async fn execute_cargo_command_internal(
66 &self,
67 command: &str,
68 args: &[&str],
69 timeout_duration: Option<Duration>,
70 ) -> LuaResult<(String, bool)> {
71 let mut cmd = TokioCommand::new("cargo");
72 cmd.arg(command)
73 .args(args)
74 .stdin(Stdio::piped())
75 .stdout(Stdio::piped())
76 .stderr(Stdio::piped());
77
78 let mut child = cmd.spawn().map_err(|e| {
79 LuaError::RuntimeError(format!("Failed to execute cargo {}: {}", command, e))
80 })?;
81
82 let stdout = child.stdout.take().unwrap();
83 let stderr = child.stderr.take().unwrap();
84 let stdin = child.stdin.take().unwrap();
85 let mut reader = BufReader::new(stdout);
86 let mut stderr_reader = BufReader::new(stderr);
87 let mut output = String::new();
88 let mut line = String::new();
89 let mut is_interactive = false;
90
91 let (tx, mut rx) = mpsc::channel::<String>(32);
93 set_input_sender(tx);
94
95 let stdin_task = tokio::spawn(async move {
97 let mut stdin = stdin;
98 while let Some(input) = rx.recv().await {
99 if let Err(e) = stdin.write_all(input.as_bytes()).await {
100 eprintln!("Failed to write to stdin: {}", e);
101 break;
102 }
103 if let Err(e) = stdin.flush().await {
104 eprintln!("Failed to flush stdin: {}", e);
105 break;
106 }
107 }
108 });
109
110 while let Ok(n) = reader.read_line(&mut line).await {
112 if n == 0 {
113 break;
114 }
115
116 if line.contains("? [Y/n]")
118 || line.contains("Enter password:")
119 || line.contains("> ")
120 || line.contains("[1/3]")
121 || line.ends_with("? ")
122 {
123 is_interactive = true;
124 }
125
126 output.push_str(&line);
127 line.clear();
128 }
129
130 let mut stderr_line = String::new();
132 while let Ok(n) = stderr_reader.read_line(&mut stderr_line).await {
133 if n == 0 {
134 break;
135 }
136 output.push_str(&stderr_line);
137 stderr_line.clear();
138 }
139
140 if command == "run" {
142 is_interactive = true;
143 }
144
145 if let Some(duration) = timeout_duration {
147 match timeout(duration, child.wait()).await {
148 Ok(status) => {
149 if !status
150 .map_err(|e| LuaError::RuntimeError(format!("Process error: {}", e)))?
151 .success()
152 && !is_interactive
153 {
154 stdin_task.abort();
155 return Err(LuaError::RuntimeError(format!(
156 "cargo {} failed: {}",
157 command, output
158 )));
159 }
160 }
161 Err(_) => {
162 stdin_task.abort();
163 return Err(LuaError::RuntimeError(format!(
164 "cargo {} timed out after {} seconds",
165 command,
166 duration.as_secs()
167 )));
168 }
169 }
170 } else {
171 let status = child.wait().await.map_err(|e| {
172 LuaError::RuntimeError(format!("Failed to wait for process: {}", e))
173 })?;
174
175 if !status.success() && !is_interactive {
176 stdin_task.abort();
177 return Err(LuaError::RuntimeError(format!(
178 "cargo {} failed: {}",
179 command, output
180 )));
181 }
182 }
183
184 if command == "check" && output.trim().is_empty() {
186 output =
187 "Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s".to_string();
188 }
189
190 stdin_task.abort();
191 Ok((output, is_interactive))
192 }
193
194 pub async fn cargo_check(&self, args: &[&str]) -> LuaResult<(String, bool)> {
196 let result = self
197 .execute_cargo_command_internal("check", args, None)
198 .await;
199
200 match result {
202 Ok((output, interactive)) if output.trim().is_empty() => Ok((
203 "Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s".to_string(),
204 interactive,
205 )),
206 other => other,
207 }
208 }
209
210 async fn execute_cargo_command_smart(
212 &self,
213 command: &str,
214 args: &[&str],
215 ) -> LuaResult<(String, bool)> {
216 let result = self
218 .execute_cargo_command_internal(command, args, None)
219 .await?;
220
221 if command == "run" {
223 return Ok((result.0, true));
224 }
225
226 Ok(result)
227 }
228
229 pub async fn cargo_bench(&self, args: &[&str]) -> LuaResult<(String, bool)> {
231 self.execute_cargo_command_smart("bench", args).await
232 }
233
234 pub async fn cargo_build(&self, args: &[&str]) -> LuaResult<(String, bool)> {
236 self.execute_cargo_command_smart("build", args).await
237 }
238
239 pub async fn cargo_run(&self, args: &[&str]) -> LuaResult<(String, bool)> {
241 self.execute_cargo_command_internal("run", args, None)
243 .await
244 .map(|(output, _)| {
245 (output, true)
247 })
248 }
249
250 pub async fn cargo_test(&self, args: &[&str]) -> LuaResult<(String, bool)> {
252 self.execute_cargo_command_smart("test", args).await
253 }
254
255 pub async fn cargo_clean(&self, args: &[&str]) -> LuaResult<(String, bool)> {
257 self.execute_cargo_command_internal("clean", args, None)
258 .await
259 }
260
261 pub async fn cargo_doc(&self, args: &[&str]) -> LuaResult<(String, bool)> {
263 self.execute_cargo_command_internal("doc", args, None).await
264 }
265
266 pub async fn cargo_new(&self, name: &str, args: &[&str]) -> LuaResult<(String, bool)> {
268 let mut full_args = vec![name];
269 full_args.extend_from_slice(args);
270 self.execute_cargo_command_internal("new", &full_args, None)
271 .await
272 }
273
274 pub async fn cargo_update(&self, args: &[&str]) -> LuaResult<(String, bool)> {
276 self.execute_cargo_command_internal("update", args, None)
277 .await
278 }
279
280 pub async fn cargo_init(&self, args: &[&str]) -> LuaResult<(String, bool)> {
284 self.execute_cargo_command_internal("init", args, None)
285 .await
286 }
287
288 pub async fn cargo_add(&self, args: &[&str]) -> LuaResult<(String, bool)> {
290 self.execute_cargo_command_internal("add", args, None).await
291 }
292
293 pub async fn cargo_remove(&self, args: &[&str]) -> LuaResult<(String, bool)> {
295 self.execute_cargo_command_internal("remove", args, None)
296 .await
297 }
298
299 pub async fn cargo_fmt(&self, args: &[&str]) -> LuaResult<(String, bool)> {
301 self.execute_cargo_command_internal("fmt", args, None).await
302 }
303
304 pub async fn cargo_clippy(&self, args: &[&str]) -> LuaResult<(String, bool)> {
306 self.execute_cargo_command_internal("clippy", args, None)
307 .await
308 }
309
310 pub async fn cargo_fix(&self, args: &[&str]) -> LuaResult<(String, bool)> {
312 self.execute_cargo_command_internal("fix", args, None).await
313 }
314
315 pub async fn cargo_publish(&self, args: &[&str]) -> LuaResult<(String, bool)> {
317 self.execute_cargo_command_internal("publish", args, None)
318 .await
319 }
320
321 pub async fn cargo_install(&self, args: &[&str]) -> LuaResult<(String, bool)> {
323 self.execute_cargo_command_internal("install", args, None)
324 .await
325 }
326
327 pub async fn cargo_uninstall(&self, args: &[&str]) -> LuaResult<(String, bool)> {
329 self.execute_cargo_command_internal("uninstall", args, None)
330 .await
331 }
332
333 pub async fn cargo_search(&self, args: &[&str]) -> LuaResult<(String, bool)> {
335 self.execute_cargo_command_internal("search", args, None)
336 .await
337 }
338
339 pub async fn cargo_tree(&self, args: &[&str]) -> LuaResult<(String, bool)> {
341 self.execute_cargo_command_internal("tree", args, None)
342 .await
343 }
344
345 pub async fn cargo_vendor(&self, args: &[&str]) -> LuaResult<(String, bool)> {
347 self.execute_cargo_command_internal("vendor", args, None)
348 .await
349 }
350
351 pub async fn cargo_audit(&self, args: &[&str]) -> LuaResult<(String, bool)> {
353 self.execute_cargo_command_internal("audit", args, None)
354 .await
355 }
356
357 pub async fn cargo_outdated(&self, args: &[&str]) -> LuaResult<(String, bool)> {
359 self.execute_cargo_command_internal("outdated", args, None)
360 .await
361 }
362
363 pub async fn cargo_help(&self, args: &[&str]) -> LuaResult<(String, bool)> {
365 self.execute_cargo_command_internal("help", args, None)
366 .await
367 }
368
369 pub async fn cargo_autodd(&self, _args: &[&str]) -> LuaResult<(String, bool)> {
371 #[cfg(test)]
373 return Err(LuaError::RuntimeError(
374 "cargo-autodd is not installed. Please install it with 'cargo install cargo-autodd'"
375 .to_string(),
376 ));
377
378 #[cfg(not(test))]
380 {
381 let check_output = std::process::Command::new("cargo")
383 .arg("--list")
384 .output()
385 .map_err(|e| {
386 LuaError::RuntimeError(format!("Failed to check cargo commands: {}", e))
387 })?;
388
389 let output_str = String::from_utf8_lossy(&check_output.stdout);
390 if !output_str.contains("autodd") {
391 return Err(LuaError::RuntimeError(
392 "cargo-autodd is not installed. Please install it with 'cargo install cargo-autodd'"
393 .to_string(),
394 ));
395 }
396
397 self.execute_cargo_command_internal("autodd", _args, None)
398 .await
399 }
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 fn setup_test_commands() -> CargoCommands {
408 CargoCommands::new().unwrap()
409 }
410
411 #[test]
412 fn test_cargo_command_execution() {
413 let rt = tokio::runtime::Runtime::new().unwrap();
414 let cargo_commands = setup_test_commands();
415 let result = rt.block_on(async { cargo_commands.cargo_help(&[]).await });
416 assert!(result.is_ok());
417 }
418
419 #[test]
420 fn test_invalid_command() {
421 let rt = tokio::runtime::Runtime::new().unwrap();
422 let cargo_commands = setup_test_commands();
423 let result =
424 rt.block_on(async { cargo_commands.execute_cargo_command("invalid", &[]).await });
425 assert!(result.is_err());
426 }
427
428 #[test]
429 fn test_execute_method() {
430 let cargo_commands = setup_test_commands();
431 let result =
432 cargo_commands.execute(async { Ok::<_, LuaError>(("test".to_string(), false)) });
433 assert!(result.is_ok());
434 assert_eq!(result.unwrap().0, "test");
435 }
436
437 #[test]
438 fn test_cargo_autodd() {
439 let rt = tokio::runtime::Runtime::new().unwrap();
440 let cargo_commands = setup_test_commands();
441 let result = rt.block_on(async { cargo_commands.cargo_autodd(&[]).await });
442 assert!(result.is_err());
443 let err_msg = result.unwrap_err().to_string().to_lowercase();
444
445 assert!(
447 err_msg.contains("failed to check cargo commands")
448 || err_msg.contains("cargo-autodd is not installed")
449 || err_msg.contains("no valid version found")
450 || err_msg.contains("cargo autodd failed")
451 || err_msg.contains("command not found") || err_msg.contains("no such file or directory"), "Unexpected error message: {}",
454 err_msg
455 );
456 }
457
458 #[test]
459 fn test_cargo_autodd_with_args() {
460 let rt = tokio::runtime::Runtime::new().unwrap();
461 let cargo_commands = setup_test_commands();
462 let test_args = vec![
463 vec!["update"],
464 vec!["report"],
465 vec!["security"],
466 vec!["--debug"],
467 vec!["update", "--debug"],
468 ];
469
470 for args in test_args {
471 let result = rt.block_on(async { cargo_commands.cargo_autodd(&args).await });
472 assert!(result.is_err());
473 let err_msg = result.unwrap_err().to_string().to_lowercase();
474
475 assert!(
476 err_msg.contains("failed to check cargo commands")
477 || err_msg.contains("cargo-autodd is not installed")
478 || err_msg.contains("no valid version found")
479 || err_msg.contains("cargo autodd failed")
480 || err_msg.contains("command not found") || err_msg.contains("no such file or directory") || err_msg.contains("status code 404"),
483 "Unexpected error message: {}",
484 err_msg
485 );
486 }
487 }
488}