Skip to main content

csh/
process.rs

1//! SSH process management and output handling
2//!
3//! Handles:
4//! - Spawning SSH subprocess with proper I/O configuration
5//! - Reading and buffering SSH output
6//! - Applying syntax highlighting to output chunks
7//! - Managing non-interactive SSH commands
8//!
9//! # Process Flow
10//! 1. Detect if command is interactive or non-interactive
11//! 2. Spawn SSH process with appropriate stdio configuration
12//! 3. For interactive: read output in chunks, apply highlighting, display
13//! 4. For non-interactive: passthrough directly without processing
14
15use crate::{Result, config, highlighter, log_debug, log_error, log_info, log_ssh};
16use std::{
17    io::{self, BufReader, Read, Write},
18    process::{Command, ExitCode, Stdio},
19    sync::mpsc::{self, Receiver, Sender},
20    thread,
21};
22
23/// Main process handler for SSH subprocess
24///
25/// Manages the SSH subprocess lifecycle, including spawning, output processing,
26/// and exit code handling. Automatically detects non-interactive commands and
27/// uses passthrough mode for them.
28///
29/// # Arguments
30/// * `process_args` - Command-line arguments to pass to SSH
31/// * `is_non_interactive` - Whether this is a non-interactive command (-G, -V, etc.)
32///
33/// # Returns
34/// Exit code from the SSH process
35pub fn process_handler(process_args: Vec<String>, is_non_interactive: bool) -> Result<ExitCode> {
36    log_info!("Starting SSH process with args: {:?}", process_args);
37    log_debug!("Non-interactive mode: {}", is_non_interactive);
38
39    // For non-interactive commands, use direct passthrough
40    if is_non_interactive {
41        log_info!("Using passthrough mode for non-interactive command");
42        return spawn_ssh_passthrough(&process_args);
43    }
44
45    // Spawn the SSH process
46    let mut child = spawn_ssh(&process_args).map_err(|err| {
47        log_error!("Failed to spawn SSH process: {}", err);
48        err
49    })?;
50
51    log_debug!("SSH process spawned successfully (PID: {:?})", child.id());
52
53    let stdout = child.stdout.take().ok_or_else(|| {
54        log_error!("Failed to capture stdout from SSH process");
55        io::Error::other("Failed to capture stdout")
56    })?;
57
58    let mut reader = BufReader::new(stdout);
59
60    // Create a channel for sending and receiving output chunks
61    let (tx, rx): (Sender<String>, Receiver<String>) = mpsc::channel();
62
63    let reset_color = "\x1b[0m"; // ANSI reset color sequence
64
65    // Spawn thread for processing and displaying chunks
66    // This thread applies highlighting and outputs to the terminal
67    let processing_thread = thread::Builder::new()
68        .name("output-processor".to_string())
69        .spawn(move || {
70            log_debug!("Output processing thread started");
71            let mut chunk_id = 0;
72            // Cache rules and track config version for hot-reload support
73            let mut cached_rules = config::get_config().read().unwrap().metadata.compiled_rules.clone();
74            let mut cached_version = config::get_config().read().unwrap().metadata.version;
75
76            while let Ok(chunk) = rx.recv() {
77                // Check if config has been reloaded and update rules if needed
78                let current_version = config::get_config().read().unwrap().metadata.version;
79                if current_version != cached_version {
80                    cached_rules = config::get_config().read().unwrap().metadata.compiled_rules.clone();
81                    cached_version = current_version;
82                    log_debug!("Rules updated due to config reload (version {})", cached_version);
83                }
84
85                let processed = highlighter::process_chunk(chunk, chunk_id, &cached_rules, reset_color);
86                chunk_id += 1;
87                print!("{}", processed); // Print the processed chunk
88                if let Err(e) = io::stdout().flush() {
89                    log_error!("Failed to flush stdout: {}", e);
90                }
91            }
92            log_debug!("Output processing thread finished (processed {} chunks)", chunk_id);
93        })
94        .map_err(|err| {
95            log_error!("Failed to spawn output processing thread: {}", err);
96            io::Error::other( "Failed to spawn processing thread")
97        })?;
98
99    // Buffer for reading data from SSH output (4KB chunks)
100    let mut buffer = [0; 4096];
101    let mut total_bytes = 0;
102
103    log_debug!("Starting to read SSH output...");
104
105    loop {
106        match reader.read(&mut buffer) {
107            Ok(0) => {
108                log_debug!("EOF reached (total bytes read: {})", total_bytes);
109                break; // Exit loop when EOF is reached
110            }
111            Ok(n) => {
112                total_bytes += n;
113                // Convert the read data to a String and send it to the processing thread
114                let chunk = String::from_utf8_lossy(&buffer[..n]).to_string();
115                log_ssh!("{}", chunk);
116
117                if let Err(err) = tx.send(chunk) {
118                    log_error!("Failed to send data to processing thread: {}", err);
119                    break;
120                }
121            }
122            Err(err) => {
123                log_error!("Error reading from SSH process: {}", err);
124                return Err(err.into());
125            }
126        }
127    }
128
129    // Drop the sender to signal the processing thread to finish
130    drop(tx);
131
132    // Wait for the processing thread to finish
133    if let Err(err) = processing_thread.join() {
134        log_error!("Processing thread panicked: {:?}", err);
135    }
136
137    // Wait for the SSH process to finish and use its status code
138    let status = child.wait().map_err(|err| {
139        log_error!("Failed to wait for SSH process (PID: {:?}): {}", child.id(), err);
140        err
141    })?;
142
143    let exit_code = status.code().unwrap_or(1);
144    log_info!("SSH process exited with code: {}", exit_code);
145
146    if status.success() {
147        Ok(ExitCode::SUCCESS)
148    } else {
149        // Clamp exit code to valid u8 range (0-255)
150        let clamped_code = u8::try_from(exit_code).unwrap_or(255);
151        Ok(ExitCode::from(clamped_code))
152    }
153}
154
155/// Spawns an SSH process with the provided arguments.
156///
157/// Configures the process with:
158/// - Inherited stdin (for user input)
159/// - Piped stdout (for processing and highlighting)
160/// - Inherited stderr (for error messages)
161///
162/// # Arguments
163/// * `args` - CLI arguments provided by the user
164///
165/// # Returns
166/// The spawned child process or an I/O error
167pub fn spawn_ssh(args: &[String]) -> std::io::Result<std::process::Child> {
168    log_debug!("Spawning SSH with args: {:?}", args);
169
170    let child = Command::new("ssh")
171        .args(args)
172        .stdin(Stdio::inherit()) // Inherit the input from the current terminal
173        .stdout(Stdio::piped()) // Pipe the output for processing
174        .stderr(Stdio::inherit()) // Inherit the error stream from the SSH process
175        .spawn()
176        .map_err(|err| {
177            log_error!("Failed to spawn SSH command: {}", err);
178            err
179        })?;
180
181    log_debug!("SSH process spawned (PID: {:?})", child.id());
182    Ok(child)
183}
184
185/// Spawns SSH for non-interactive commands with direct stdout passthrough.
186///
187/// Used for commands like -G, -V, -O, -Q, -T that don't need highlighting.
188/// All stdio streams are inherited for direct passthrough.
189///
190/// # Arguments
191/// * `args` - CLI arguments provided by the user
192///
193/// # Returns
194/// The exit code from the SSH process
195fn spawn_ssh_passthrough(args: &[String]) -> Result<ExitCode> {
196    log_debug!("Spawning SSH in passthrough mode with args: {:?}", args);
197
198    let status = Command::new("ssh")
199        .args(args)
200        .stdin(Stdio::inherit())
201        .stdout(Stdio::inherit()) // Pass through directly, no buffering
202        .stderr(Stdio::inherit())
203        .status()
204        .map_err(|err| {
205            log_error!("Failed to execute SSH command in passthrough mode: {}", err);
206            err
207        })?;
208
209    let exit_code = status.code().unwrap_or(1);
210    log_info!("SSH passthrough process exited with code: {}", exit_code);
211
212    if status.success() {
213        Ok(ExitCode::SUCCESS)
214    } else {
215        // Clamp exit code to valid u8 range (0-255)
216        let clamped_code = u8::try_from(exit_code).unwrap_or(255);
217        Ok(ExitCode::from(clamped_code))
218    }
219}