cargo_nvim/
cargo_commands.rs

1// src/cargo_commands.rs
2use 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/// Structure for handling Cargo commands
13/// Contains a runtime for async operations
14#[derive(Clone)]
15pub struct CargoCommands {
16    runtime: Arc<Runtime>,
17}
18
19impl CargoCommands {
20    /// Create a new CargoCommands instance
21    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    /// Executes a future on the runtime
33    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    /// Execute a Cargo command with the given arguments
41    #[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    /// Execute a Cargo command with the given arguments (public for testing)
53    #[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    /// Execute a Cargo command with timeout and interactive mode support
64    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        // Always set a timeout (with default values)
78        let command_timeout = timeout_duration.unwrap_or_else(|| {
79            match command {
80                "run" => Duration::from_secs(300),   // 5 minutes
81                "test" => Duration::from_secs(300),  // 5 minutes
82                "bench" => Duration::from_secs(600), // 10 minutes
83                _ => Duration::from_secs(120),       // 2 minutes
84            }
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        // Create buffered streams
96        let mut stdout_reader = BufReader::new(stdout).lines();
97        let mut stderr_reader = BufReader::new(stderr).lines();
98
99        // Interactive mode detection flag
100        let mut is_interactive = false;
101
102        // Assume interactive mode based on command name
103        if command == "run" {
104            // Treat run command as interactive by default
105            is_interactive = true;
106        }
107
108        // Output buffer
109        let output = String::new();
110
111        // Channel for standard input
112        let (tx, mut rx) = mpsc::channel::<String>(32);
113        set_input_sender(tx.clone());
114
115        // Task to handle standard input
116        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        // Asynchronous IO processing and timeout control
135        let output_handle = tokio::spawn(async move {
136            let mut combined_output = String::new();
137            let start_time = std::time::Instant::now();
138
139            // Output reading loop
140            loop {
141                let timeout_remaining = command_timeout
142                    .checked_sub(start_time.elapsed())
143                    .unwrap_or_else(|| Duration::from_secs(1));
144
145                // Monitor both stdout and stderr simultaneously
146                tokio::select! {
147                    // Reading standard output
148                    stdout_result = stdout_reader.next_line() => {
149                        match stdout_result {
150                            Ok(Some(line)) => {
151                                // Detect interactive mode based on specific patterns
152                                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() // Empty line may indicate interactive mode
159                                ) {
160                                    is_interactive = true;
161                                }
162
163                                combined_output.push_str(&line);
164                                combined_output.push('\n');
165                            },
166                            Ok(None) => break, // EOF
167                            Err(_) => break,
168                        }
169                    },
170
171                    // Reading standard error
172                    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) => {}, // Stdout might still have data
179                            Err(_) => {},
180                        }
181                    },
182
183                    // Timeout processing (only for non-interactive mode)
184                    _ = tokio::time::sleep(timeout_remaining), if !is_interactive => {
185                        return (combined_output, is_interactive, true); // Timeout
186                    }
187                }
188
189                // Check timeout (even for interactive mode)
190                if start_time.elapsed() >= command_timeout {
191                    return (combined_output, is_interactive, true);
192                }
193
194                // For interactive mode, use an extended timeout (3x normal timeout)
195                // but still terminate after excessive inactivity
196                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        // Wait for process completion
207        let process_status = tokio::select! {
208            status = child.wait() => {
209                match status {
210                    Ok(s) => (s.success(), false), // (succeeded, timed out)
211                    Err(_) => (false, false),
212                }
213            },
214            _ = tokio::time::sleep(command_timeout) => {
215                // Timeout occurred
216                child.kill().await.ok(); // Force terminate the process
217                (false, true)
218            }
219        };
220
221        // Get results from output processing task
222        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        // Resource cleanup
229        stdin_handle.abort();
230        drop(tx);
231        // rx is already moved into the stdin_handle task
232        // and will be dropped when the task is aborted
233
234        // Process the results
235        let (process_success, process_timeout) = process_status;
236        let (final_output, is_interactive_mode) = output_result;
237
238        // Check if process timed out
239        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        // Check if process failed
248        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    /// Check the project for errors
259    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        // If the command executed successfully but the output is empty, provide a default message
265        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    /// Execute a Cargo command with automatic interactive mode detection
275    async fn execute_cargo_command_smart(
276        &self,
277        command: &str,
278        args: &[&str],
279    ) -> LuaResult<(String, bool)> {
280        // 特定のコマンドは常にインタラクティブモードとして扱う
281        let result = self
282            .execute_cargo_command_internal(command, args, None)
283            .await?;
284
285        // run コマンドは常にインタラクティブモードとして扱う
286        if command == "run" {
287            return Ok((result.0, true));
288        }
289
290        Ok(result)
291    }
292
293    /// Run benchmarks
294    pub async fn cargo_bench(&self, args: &[&str]) -> LuaResult<(String, bool)> {
295        self.execute_cargo_command_smart("bench", args).await
296    }
297
298    /// Build the project
299    pub async fn cargo_build(&self, args: &[&str]) -> LuaResult<(String, bool)> {
300        self.execute_cargo_command_smart("build", args).await
301    }
302
303    /// Run the project
304    pub async fn cargo_run(&self, args: &[&str]) -> LuaResult<(String, bool)> {
305        // Designed to support interactive programs
306        let result = self
307            .execute_cargo_command_internal("run", args, None)
308            .await?;
309
310        // Check if proconio is likely being used by examining Cargo.toml
311        // This is important for competitive programming scenarios where proconio::input! is common
312        let has_proconio = std::fs::read_to_string("Cargo.toml")
313            .map(|content| content.contains("proconio"))
314            .unwrap_or(false);
315
316        // If proconio is used, force interactive mode
317        if has_proconio {
318            return Ok((result.0, true));
319        }
320
321        Ok(result)
322    }
323
324    /// Run the tests
325    pub async fn cargo_test(&self, args: &[&str]) -> LuaResult<(String, bool)> {
326        self.execute_cargo_command_smart("test", args).await
327    }
328
329    /// Clean the target directory
330    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    /// Generate documentation
336    pub async fn cargo_doc(&self, args: &[&str]) -> LuaResult<(String, bool)> {
337        self.execute_cargo_command_internal("doc", args, None).await
338    }
339
340    /// Create a new package
341    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    /// Update dependencies
349    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    // Additional Cargo Commands
355
356    /// Initialize a new package in an existing directory
357    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    /// Add dependencies to a manifest file
363    pub async fn cargo_add(&self, args: &[&str]) -> LuaResult<(String, bool)> {
364        self.execute_cargo_command_internal("add", args, None).await
365    }
366
367    /// Remove dependencies from a manifest file
368    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    /// Format Rust code
374    pub async fn cargo_fmt(&self, args: &[&str]) -> LuaResult<(String, bool)> {
375        self.execute_cargo_command_internal("fmt", args, None).await
376    }
377
378    /// Run the Clippy linter
379    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    /// Automatically fix lint warnings
385    pub async fn cargo_fix(&self, args: &[&str]) -> LuaResult<(String, bool)> {
386        self.execute_cargo_command_internal("fix", args, None).await
387    }
388
389    /// Package and upload crate to registry
390    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    /// Install a Rust binary
396    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    /// Uninstall a Rust binary
402    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    /// Search packages in registry
408    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    /// Display dependency tree
414    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    /// Vendor all dependencies locally
420    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    /// Audit dependencies for security vulnerabilities
426    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    /// Show outdated dependencies
432    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    /// Get Cargo help
438    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    /// Run cargo-autodd command
444    pub async fn cargo_autodd(&self, _args: &[&str]) -> LuaResult<(String, bool)> {
445        // テスト環境では常にエラーを返す
446        #[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        // 実環境ではインストール確認を行う
453        #[cfg(not(test))]
454        {
455            // Check if cargo-autodd is installed
456            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        // More flexible error message checking
520        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") // For Docker environment
526                || err_msg.contains("no such file or directory"), // For Docker environment
527            "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") // For Docker environment
555                    || err_msg.contains("no such file or directory") // For Docker environment
556                    || err_msg.contains("status code 404"),
557                "Unexpected error message: {}",
558                err_msg
559            );
560        }
561    }
562}