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;
11
12#[derive(Clone)]
15pub struct CargoCommands {
16 runtime: Arc<Runtime>,
17}
18
19impl CargoCommands {
20 pub fn new() -> LuaResult<Self> {
22 Ok(Self {
23 runtime: Arc::new(
24 tokio::runtime::Builder::new_current_thread()
25 .enable_all()
26 .build()
27 .map_err(|e| LuaError::RuntimeError(e.to_string()))?,
28 ),
29 })
30 }
31
32 pub fn execute<F, T>(&self, future: F) -> T
34 where
35 F: std::future::Future<Output = T>,
36 {
37 self.runtime.block_on(future)
38 }
39
40 #[cfg(not(test))]
42 #[allow(dead_code)]
43 async fn execute_cargo_command(
44 &self,
45 command: &str,
46 args: &[&str],
47 ) -> LuaResult<(String, bool)> {
48 self.execute_cargo_command_internal(command, args, None)
49 .await
50 }
51
52 #[cfg(test)]
54 pub async fn execute_cargo_command(
55 &self,
56 command: &str,
57 args: &[&str],
58 ) -> LuaResult<(String, bool)> {
59 self.execute_cargo_command_internal(command, args, None)
60 .await
61 }
62
63 async fn execute_cargo_command_internal(
65 &self,
66 command: &str,
67 args: &[&str],
68 timeout_duration: Option<Duration>,
69 ) -> LuaResult<(String, bool)> {
70 let mut cmd = TokioCommand::new("cargo");
71 cmd.arg(command)
72 .args(args)
73 .stdin(Stdio::piped())
74 .stdout(Stdio::piped())
75 .stderr(Stdio::piped());
76
77 let command_timeout = timeout_duration.unwrap_or_else(|| {
79 match command {
80 "run" => Duration::from_secs(300), "test" => Duration::from_secs(300), "bench" => Duration::from_secs(600), _ => Duration::from_secs(120), }
85 });
86
87 let mut child = cmd.spawn().map_err(|e| {
88 LuaError::RuntimeError(format!("Failed to execute cargo {}: {}", command, e))
89 })?;
90
91 let stdout = child.stdout.take().unwrap();
92 let stderr = child.stderr.take().unwrap();
93 let stdin = child.stdin.take().unwrap();
94
95 let mut stdout_reader = BufReader::new(stdout).lines();
97 let mut stderr_reader = BufReader::new(stderr).lines();
98
99 let mut is_interactive = false;
101
102 if command == "run" {
104 is_interactive = true;
106 }
107
108 let output = String::new();
110
111 let (tx, mut rx) = mpsc::channel::<String>(32);
113 set_input_sender(tx.clone());
114
115 let stdin_handle = tokio::spawn(async move {
117 let mut stdin = stdin;
118 while let Some(input) = rx.recv().await {
119 match stdin.write_all(input.as_bytes()).await {
120 Ok(_) => {
121 if let Err(e) = stdin.flush().await {
122 eprintln!("Failed to flush stdin: {}", e);
123 break;
124 }
125 }
126 Err(e) => {
127 eprintln!("Failed to write to stdin: {}", e);
128 break;
129 }
130 }
131 }
132 });
133
134 let output_handle = tokio::spawn(async move {
136 let mut combined_output = String::new();
137 let start_time = std::time::Instant::now();
138
139 loop {
141 let timeout_remaining = command_timeout
142 .checked_sub(start_time.elapsed())
143 .unwrap_or_else(|| Duration::from_secs(1));
144
145 tokio::select! {
147 stdout_result = stdout_reader.next_line() => {
149 match stdout_result {
150 Ok(Some(line)) => {
151 if !is_interactive && (
153 line.contains("? [Y/n]") ||
154 line.contains("Enter password:") ||
155 line.contains("> ") ||
156 line.contains("[1/3]") ||
157 line.ends_with("? ") ||
158 line.trim().is_empty() ) {
160 is_interactive = true;
161 }
162
163 combined_output.push_str(&line);
164 combined_output.push('\n');
165 },
166 Ok(None) => break, Err(_) => break,
168 }
169 },
170
171 stderr_result = stderr_reader.next_line() => {
173 match stderr_result {
174 Ok(Some(line)) => {
175 combined_output.push_str(&line);
176 combined_output.push('\n');
177 },
178 Ok(None) => {}, Err(_) => {},
180 }
181 },
182
183 _ = tokio::time::sleep(timeout_remaining), if !is_interactive => {
185 return (combined_output, is_interactive, true); }
187 }
188
189 if start_time.elapsed() >= command_timeout {
191 return (combined_output, is_interactive, true);
192 }
193
194 if is_interactive
197 && start_time.elapsed() >= Duration::from_secs(command_timeout.as_secs() * 3)
198 {
199 return (combined_output, is_interactive, true);
200 }
201 }
202
203 (combined_output, is_interactive, false)
204 });
205
206 let process_status = tokio::select! {
208 status = child.wait() => {
209 match status {
210 Ok(s) => (s.success(), false), Err(_) => (false, false),
212 }
213 },
214 _ = tokio::time::sleep(command_timeout) => {
215 child.kill().await.ok(); (false, true)
218 }
219 };
220
221 let output_result = match tokio::time::timeout(Duration::from_secs(5), output_handle).await
223 {
224 Ok(Ok((out, interactive, _))) => (out, interactive),
225 _ => (output, is_interactive),
226 };
227
228 stdin_handle.abort();
230 drop(tx);
231 let (process_success, process_timeout) = process_status;
236 let (final_output, is_interactive_mode) = output_result;
237
238 if process_timeout && !is_interactive_mode {
240 return Err(LuaError::RuntimeError(format!(
241 "cargo {} timed out after {} seconds",
242 command,
243 command_timeout.as_secs()
244 )));
245 }
246
247 if !process_success && !is_interactive_mode {
249 return Err(LuaError::RuntimeError(format!(
250 "cargo {} failed: {}",
251 command, final_output
252 )));
253 }
254
255 Ok((final_output, is_interactive_mode))
256 }
257
258 pub async fn cargo_check(&self, args: &[&str]) -> LuaResult<(String, bool)> {
260 let result = self
261 .execute_cargo_command_internal("check", args, None)
262 .await;
263
264 match result {
266 Ok((output, interactive)) if output.trim().is_empty() => Ok((
267 "Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s".to_string(),
268 interactive,
269 )),
270 other => other,
271 }
272 }
273
274 async fn execute_cargo_command_smart(
276 &self,
277 command: &str,
278 args: &[&str],
279 ) -> LuaResult<(String, bool)> {
280 let result = self
282 .execute_cargo_command_internal(command, args, None)
283 .await?;
284
285 if command == "run" {
287 return Ok((result.0, true));
288 }
289
290 Ok(result)
291 }
292
293 pub async fn cargo_bench(&self, args: &[&str]) -> LuaResult<(String, bool)> {
295 self.execute_cargo_command_smart("bench", args).await
296 }
297
298 pub async fn cargo_build(&self, args: &[&str]) -> LuaResult<(String, bool)> {
300 self.execute_cargo_command_smart("build", args).await
301 }
302
303 pub async fn cargo_run(&self, args: &[&str]) -> LuaResult<(String, bool)> {
305 let result = self
307 .execute_cargo_command_internal("run", args, None)
308 .await?;
309
310 let has_proconio = std::fs::read_to_string("Cargo.toml")
313 .map(|content| content.contains("proconio"))
314 .unwrap_or(false);
315
316 if has_proconio {
318 return Ok((result.0, true));
319 }
320
321 Ok(result)
322 }
323
324 pub async fn cargo_test(&self, args: &[&str]) -> LuaResult<(String, bool)> {
326 self.execute_cargo_command_smart("test", args).await
327 }
328
329 pub async fn cargo_clean(&self, args: &[&str]) -> LuaResult<(String, bool)> {
331 self.execute_cargo_command_internal("clean", args, None)
332 .await
333 }
334
335 pub async fn cargo_doc(&self, args: &[&str]) -> LuaResult<(String, bool)> {
337 self.execute_cargo_command_internal("doc", args, None).await
338 }
339
340 pub async fn cargo_new(&self, name: &str, args: &[&str]) -> LuaResult<(String, bool)> {
342 let mut full_args = vec![name];
343 full_args.extend_from_slice(args);
344 self.execute_cargo_command_internal("new", &full_args, None)
345 .await
346 }
347
348 pub async fn cargo_update(&self, args: &[&str]) -> LuaResult<(String, bool)> {
350 self.execute_cargo_command_internal("update", args, None)
351 .await
352 }
353
354 pub async fn cargo_init(&self, args: &[&str]) -> LuaResult<(String, bool)> {
358 self.execute_cargo_command_internal("init", args, None)
359 .await
360 }
361
362 pub async fn cargo_add(&self, args: &[&str]) -> LuaResult<(String, bool)> {
364 self.execute_cargo_command_internal("add", args, None).await
365 }
366
367 pub async fn cargo_remove(&self, args: &[&str]) -> LuaResult<(String, bool)> {
369 self.execute_cargo_command_internal("remove", args, None)
370 .await
371 }
372
373 pub async fn cargo_fmt(&self, args: &[&str]) -> LuaResult<(String, bool)> {
375 self.execute_cargo_command_internal("fmt", args, None).await
376 }
377
378 pub async fn cargo_clippy(&self, args: &[&str]) -> LuaResult<(String, bool)> {
380 self.execute_cargo_command_internal("clippy", args, None)
381 .await
382 }
383
384 pub async fn cargo_fix(&self, args: &[&str]) -> LuaResult<(String, bool)> {
386 self.execute_cargo_command_internal("fix", args, None).await
387 }
388
389 pub async fn cargo_publish(&self, args: &[&str]) -> LuaResult<(String, bool)> {
391 self.execute_cargo_command_internal("publish", args, None)
392 .await
393 }
394
395 pub async fn cargo_install(&self, args: &[&str]) -> LuaResult<(String, bool)> {
397 self.execute_cargo_command_internal("install", args, None)
398 .await
399 }
400
401 pub async fn cargo_uninstall(&self, args: &[&str]) -> LuaResult<(String, bool)> {
403 self.execute_cargo_command_internal("uninstall", args, None)
404 .await
405 }
406
407 pub async fn cargo_search(&self, args: &[&str]) -> LuaResult<(String, bool)> {
409 self.execute_cargo_command_internal("search", args, None)
410 .await
411 }
412
413 pub async fn cargo_tree(&self, args: &[&str]) -> LuaResult<(String, bool)> {
415 self.execute_cargo_command_internal("tree", args, None)
416 .await
417 }
418
419 pub async fn cargo_vendor(&self, args: &[&str]) -> LuaResult<(String, bool)> {
421 self.execute_cargo_command_internal("vendor", args, None)
422 .await
423 }
424
425 pub async fn cargo_audit(&self, args: &[&str]) -> LuaResult<(String, bool)> {
427 self.execute_cargo_command_internal("audit", args, None)
428 .await
429 }
430
431 pub async fn cargo_outdated(&self, args: &[&str]) -> LuaResult<(String, bool)> {
433 self.execute_cargo_command_internal("outdated", args, None)
434 .await
435 }
436
437 pub async fn cargo_help(&self, args: &[&str]) -> LuaResult<(String, bool)> {
439 self.execute_cargo_command_internal("help", args, None)
440 .await
441 }
442
443 pub async fn cargo_autodd(&self, _args: &[&str]) -> LuaResult<(String, bool)> {
445 #[cfg(test)]
447 return Err(LuaError::RuntimeError(
448 "cargo-autodd is not installed. Please install it with 'cargo install cargo-autodd'"
449 .to_string(),
450 ));
451
452 #[cfg(not(test))]
454 {
455 let check_output = std::process::Command::new("cargo")
457 .arg("--list")
458 .output()
459 .map_err(|e| {
460 LuaError::RuntimeError(format!("Failed to check cargo commands: {}", e))
461 })?;
462
463 let output_str = String::from_utf8_lossy(&check_output.stdout);
464 if !output_str.contains("autodd") {
465 return Err(LuaError::RuntimeError(
466 "cargo-autodd is not installed. Please install it with 'cargo install cargo-autodd'"
467 .to_string(),
468 ));
469 }
470
471 self.execute_cargo_command_internal("autodd", _args, None)
472 .await
473 }
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 fn setup_test_commands() -> CargoCommands {
482 CargoCommands::new().unwrap()
483 }
484
485 #[test]
486 fn test_cargo_command_execution() {
487 let rt = tokio::runtime::Runtime::new().unwrap();
488 let cargo_commands = setup_test_commands();
489 let result = rt.block_on(async { cargo_commands.cargo_help(&[]).await });
490 assert!(result.is_ok());
491 }
492
493 #[test]
494 fn test_invalid_command() {
495 let rt = tokio::runtime::Runtime::new().unwrap();
496 let cargo_commands = setup_test_commands();
497 let result =
498 rt.block_on(async { cargo_commands.execute_cargo_command("invalid", &[]).await });
499 assert!(result.is_err());
500 }
501
502 #[test]
503 fn test_execute_method() {
504 let cargo_commands = setup_test_commands();
505 let result =
506 cargo_commands.execute(async { Ok::<_, LuaError>(("test".to_string(), false)) });
507 assert!(result.is_ok());
508 assert_eq!(result.unwrap().0, "test");
509 }
510
511 #[test]
512 fn test_cargo_autodd() {
513 let rt = tokio::runtime::Runtime::new().unwrap();
514 let cargo_commands = setup_test_commands();
515 let result = rt.block_on(async { cargo_commands.cargo_autodd(&[]).await });
516 assert!(result.is_err());
517 let err_msg = result.unwrap_err().to_string().to_lowercase();
518
519 assert!(
521 err_msg.contains("failed to check cargo commands")
522 || err_msg.contains("cargo-autodd is not installed")
523 || err_msg.contains("no valid version found")
524 || err_msg.contains("cargo autodd failed")
525 || err_msg.contains("command not found") || err_msg.contains("no such file or directory"), "Unexpected error message: {}",
528 err_msg
529 );
530 }
531
532 #[test]
533 fn test_cargo_autodd_with_args() {
534 let rt = tokio::runtime::Runtime::new().unwrap();
535 let cargo_commands = setup_test_commands();
536 let test_args = vec![
537 vec!["update"],
538 vec!["report"],
539 vec!["security"],
540 vec!["--debug"],
541 vec!["update", "--debug"],
542 ];
543
544 for args in test_args {
545 let result = rt.block_on(async { cargo_commands.cargo_autodd(&args).await });
546 assert!(result.is_err());
547 let err_msg = result.unwrap_err().to_string().to_lowercase();
548
549 assert!(
550 err_msg.contains("failed to check cargo commands")
551 || err_msg.contains("cargo-autodd is not installed")
552 || err_msg.contains("no valid version found")
553 || err_msg.contains("cargo autodd failed")
554 || err_msg.contains("command not found") || err_msg.contains("no such file or directory") || err_msg.contains("status code 404"),
557 "Unexpected error message: {}",
558 err_msg
559 );
560 }
561 }
562}