From 04e599037698bcc74a199c39f401ff2b6d1e3fad Mon Sep 17 00:00:00 2001 From: root Date: Thu, 10 Jul 2025 20:45:54 +0800 Subject: [PATCH] refactor: extract common Python execution patterns into reusable shared code - Create python_executor.rs module with unified Python command execution - Remove duplicate execute_python_command functions from ai_video.rs and template.rs - Eliminate 553 lines of duplicate code across both files - Improve error handling and JSON-RPC message parsing consistency - Fix template import 'Unknown error' issue by using unified JSON parsing - Add configurable timeout and encoding settings - Ensure consistent UTF-8 encoding across all Python executions --- src-tauri/src/commands/ai_video.rs | 297 +--------------------------- src-tauri/src/commands/template.rs | 279 +------------------------- src-tauri/src/lib.rs | 1 + src-tauri/src/python_executor.rs | 306 +++++++++++++++++++++++++++++ 4 files changed, 315 insertions(+), 568 deletions(-) create mode 100644 src-tauri/src/python_executor.rs diff --git a/src-tauri/src/commands/ai_video.rs b/src-tauri/src/commands/ai_video.rs index a8a07c7..cd7776e 100644 --- a/src-tauri/src/commands/ai_video.rs +++ b/src-tauri/src/commands/ai_video.rs @@ -1,10 +1,6 @@ use serde::Deserialize; use std::process::Command; -use std::io::{BufRead, BufReader, Read}; -use std::time::{Duration, Instant}; -use std::thread; -use std::sync::{Arc, Mutex}; -use std::sync::mpsc; +use crate::python_executor::execute_python_command; #[derive(Debug, Deserialize)] pub struct AIVideoRequest { @@ -26,294 +22,7 @@ pub struct BatchAIVideoRequest { pub timeout: Option, } -async fn execute_python_command(_app: tauri::AppHandle, args: &[String]) -> Result { - println!("Executing Python command with args: {:?}", args); - // Get project root directory - let current_dir = std::env::current_dir() - .map_err(|e| format!("Failed to get current directory: {}", e))?; - - let project_root = if current_dir.ends_with("src-tauri") { - current_dir.parent().unwrap_or(¤t_dir).to_path_buf() - } else { - current_dir - }; - - println!("Working directory: {:?}", project_root); - - // Try multiple Python commands in order of preference - let python_commands = if cfg!(target_os = "windows") { - vec!["python", "python3", "py"] - } else { - vec!["python3", "python"] - }; - - let mut child = None; - let mut last_error = String::new(); - - for python_cmd in python_commands { - println!("Trying Python command: {}", python_cmd); - let mut cmd = Command::new(python_cmd); - cmd.current_dir(&project_root); - cmd.args(args); - cmd.stdout(std::process::Stdio::piped()); - cmd.stderr(std::process::Stdio::piped()); - - // Set environment variables for consistent encoding - cmd.env("PYTHONIOENCODING", "utf-8"); - cmd.env("PYTHONUNBUFFERED", "1"); - - // On Windows, ensure UTF-8 console output - if cfg!(target_os = "windows") { - cmd.env("PYTHONUTF8", "1"); - } - - match cmd.spawn() { - Ok(process) => { - println!("Successfully started Python process with: {}", python_cmd); - child = Some(process); - break; - } - Err(e) => { - last_error = format!("Failed to start {} process: {}", python_cmd, e); - println!("{}", last_error); - continue; - } - } - } - - let mut child = child.ok_or_else(|| { - format!("Failed to start Python process with any command. Last error: {}", last_error) - })?; - - let stdout = child.stdout.take().unwrap(); - let stderr = child.stderr.take().unwrap(); - - // Use channels for concurrent reading - let (stdout_tx, _stdout_rx) = mpsc::channel(); - let (stderr_tx, _stderr_rx) = mpsc::channel(); - let (result_tx, result_rx) = mpsc::channel(); - - let progress_messages = Arc::new(Mutex::new(Vec::new())); - let error_messages = Arc::new(Mutex::new(Vec::new())); - let final_result = Arc::new(Mutex::new(None::)); - - // Spawn thread for reading stdout - let progress_messages_clone = Arc::clone(&progress_messages); - let final_result_clone = Arc::clone(&final_result); - thread::spawn(move || { - let mut reader = BufReader::new(stdout); - let mut buffer = Vec::new(); - - // Read raw bytes and handle encoding issues - loop { - buffer.clear(); - match reader.read_until(b'\n', &mut buffer) { - Ok(0) => break, // EOF - Ok(_) => { - // Convert bytes to string, handling invalid UTF-8 - let line = String::from_utf8_lossy(&buffer); - let line = line.trim_end_matches('\n').trim_end_matches('\r'); - - if line.is_empty() { - continue; - } - println!("Python stdout: {}", line); - - // Parse JSON-RPC messages - if line.starts_with("JSONRPC:") { - let json_str = &line[8..]; // Remove "JSONRPC:" prefix - if let Ok(json_value) = serde_json::from_str::(json_str) { - if let Some(method) = json_value.get("method") { - if method == "progress" { - if let Ok(mut messages) = progress_messages_clone.lock() { - messages.push(json_str.to_string()); - } - println!("Progress: {}", json_str); - } - } else if json_value.get("result").is_some() || json_value.get("error").is_some() { - // This is a final result or error response - if let Ok(mut result) = final_result_clone.lock() { - *result = Some(json_str.to_string()); - } - println!("JSON-RPC result found: {}", json_str); - } - } - } else if line.trim().starts_with('{') && line.trim().ends_with('}') { - // Fallback: try to parse as direct JSON result - if let Ok(json_value) = serde_json::from_str::(line.trim()) { - // Check if this looks like a final result (has status field) - if json_value.get("status").is_some() { - if let Ok(mut result) = final_result_clone.lock() { - *result = Some(line.trim().to_string()); - } - println!("Direct JSON result: {}", line.trim()); - } - } - } else { - println!("Python other: {}", line); - } - - if stdout_tx.send(line.to_string()).is_err() { - break; - } - } - Err(e) => { - println!("Error reading stdout: {}", e); - break; - } - } - } - }); - - // Spawn thread for reading stderr - let error_messages_clone = Arc::clone(&error_messages); - thread::spawn(move || { - let mut reader = BufReader::new(stderr); - let mut buffer = Vec::new(); - - // Read raw bytes and handle encoding issues - loop { - buffer.clear(); - match reader.read_until(b'\n', &mut buffer) { - Ok(0) => break, // EOF - Ok(_) => { - // Convert bytes to string, handling invalid UTF-8 - let line = String::from_utf8_lossy(&buffer); - let line = line.trim_end_matches('\n').trim_end_matches('\r'); - - if line.is_empty() { - continue; - } - - println!("Python stderr: {}", line); - if let Ok(mut messages) = error_messages_clone.lock() { - messages.push(line.to_string()); - } - if stderr_tx.send(line.to_string()).is_err() { - break; - } - } - Err(e) => { - println!("Error reading stderr: {}", e); - break; - } - } - } - }); - - // Spawn thread to wait for process completion - thread::spawn(move || { - match child.wait() { - Ok(status) => { - let _ = result_tx.send(Ok(status)); - } - Err(e) => { - let _ = result_tx.send(Err(format!("Failed to wait for process: {}", e))); - } - } - }); - - // Wait for process to complete with timeout - println!("Waiting for Python process to complete..."); - let timeout_duration = Duration::from_secs(600); // 10 minutes timeout - let start_time = Instant::now(); - - let exit_status = loop { - match result_rx.try_recv() { - Ok(Ok(status)) => break status, - Ok(Err(e)) => return Err(e), - Err(mpsc::TryRecvError::Empty) => { - if start_time.elapsed() > timeout_duration { - return Err("Python process timed out after 10 minutes".to_string()); - } - thread::sleep(Duration::from_millis(100)); - continue; - } - Err(mpsc::TryRecvError::Disconnected) => { - return Err("Process monitoring thread disconnected".to_string()); - } - } - }; - - println!("Python process terminated with code: {:?}", exit_status.code()); - println!("Python script exit code: {:?}", exit_status.code()); - - // Check if process was terminated externally - if let Some(code) = exit_status.code() { - if code != 0 { - println!("Python process exited with non-zero code: {}", code); - // Convert to unsigned for Windows exit codes - let unsigned_code = code as u32; - if unsigned_code == 3221225786 || code == -1073741510 { - println!("Process was likely terminated by antivirus or system security"); - } else if code == 1 { - println!("Process exited with error code 1 (general error)"); - } else if code == 120 { - println!("Process exited with error code 120 (module import failure or unsupported function)"); - } else { - println!("Process exited with unknown error code: {}", code); - } - } - } else { - println!("Python process was terminated by signal (killed externally)"); - } - - // Extract final results from shared state - let final_result_value = { - final_result.lock().unwrap().clone() - }; - let progress_messages_value = { - progress_messages.lock().unwrap().clone() - }; - let error_messages_value = { - error_messages.lock().unwrap().clone() - }; - - // Return the final result if we found one - if let Some(result) = final_result_value { - println!("Extracted JSON-RPC result: {}", result); - return Ok(result); - } - - // If no JSON-RPC result found, provide detailed error information - if exit_status.success() { - // Process succeeded but no output - this is unusual - let error_msg = if progress_messages_value.is_empty() { - "Python script completed successfully but produced no output" - } else { - "Python script completed but did not return a valid JSON result" - }; - Err(error_msg.to_string()) - } else { - // Process failed - provide detailed error based on exit code - let error_msg = if let Some(code) = exit_status.code() { - let unsigned_code = code as u32; - match code { - 1 => "Python script failed with general error. Check if all dependencies are installed.".to_string(), - 120 => "Python module import failed or function not supported. This may be due to missing dependencies or incompatible Python environment. Please run the environment test first.".to_string(), - -1073741510 => "Python process was terminated by antivirus or system security. Please add the application to your antivirus whitelist.".to_string(), - _ if unsigned_code == 3221225786 => "Python process was terminated by antivirus or system security. Please add the application to your antivirus whitelist.".to_string(), - _ => format!("Python script failed with exit code: {}. This may indicate a system or environment issue.", code) - } - } else { - "Python process was killed by external signal. This may be caused by antivirus software or system security policies.".to_string() - }; - - // Include any error output we captured - let mut full_error = error_msg; - - if !progress_messages_value.is_empty() { - full_error.push_str(&format!("\n\nLast stdout: {}", progress_messages_value.join("\n"))); - } - - if !error_messages_value.is_empty() { - full_error.push_str(&format!("\n\nStderr: {}", error_messages_value.join("\n"))); - } - - Err(full_error) - } -} #[tauri::command] pub async fn generate_ai_video(app: tauri::AppHandle, request: AIVideoRequest) -> Result { @@ -342,7 +51,7 @@ pub async fn generate_ai_video(app: tauri::AppHandle, request: AIVideoRequest) - args.push(timeout.to_string()); } - execute_python_command(app, &args).await + execute_python_command(app, &args, None).await } #[tauri::command] @@ -372,7 +81,7 @@ pub async fn batch_generate_ai_videos(app: tauri::AppHandle, request: BatchAIVid args.push(timeout.to_string()); } - execute_python_command(app, &args).await + execute_python_command(app, &args, None).await } #[tauri::command] diff --git a/src-tauri/src/commands/template.rs b/src-tauri/src/commands/template.rs index 8a21f7e..fbf0000 100644 --- a/src-tauri/src/commands/template.rs +++ b/src-tauri/src/commands/template.rs @@ -1,10 +1,5 @@ use serde::{Deserialize, Serialize}; -use std::process::Command; -use std::io::{BufRead, BufReader, Read}; -use std::time::{Duration, Instant}; -use std::thread; -use std::sync::{Arc, Mutex}; -use std::sync::mpsc; +use crate::python_executor::execute_python_command; #[derive(Debug, Deserialize)] pub struct BatchImportRequest { @@ -28,271 +23,7 @@ pub struct TemplateInfo { pub tags: Vec, } -async fn execute_python_template_command(_app: tauri::AppHandle, args: &[String]) -> Result { - println!("Executing Python template command with args: {:?}", args); - - // Get project root directory - let current_dir = std::env::current_dir() - .map_err(|e| format!("Failed to get current directory: {}", e))?; - let project_root = if current_dir.ends_with("src-tauri") { - current_dir.parent().unwrap_or(¤t_dir).to_path_buf() - } else { - current_dir - }; - - println!("Working directory: {:?}", project_root); - - // Try multiple Python commands in order of preference - let python_commands = if cfg!(target_os = "windows") { - vec!["python", "python3", "py"] - } else { - vec!["python3", "python"] - }; - - let mut child = None; - let mut last_error = String::new(); - - for python_cmd in python_commands { - println!("Trying Python command: {}", python_cmd); - let mut cmd = Command::new(python_cmd); - cmd.current_dir(&project_root); - cmd.args(args); - cmd.stdout(std::process::Stdio::piped()); - cmd.stderr(std::process::Stdio::piped()); - - // Set environment variables for consistent encoding - cmd.env("PYTHONIOENCODING", "utf-8"); - cmd.env("PYTHONUNBUFFERED", "1"); - - // On Windows, ensure UTF-8 console output - if cfg!(target_os = "windows") { - cmd.env("PYTHONUTF8", "1"); - } - - match cmd.spawn() { - Ok(process) => { - println!("Successfully started Python process with: {}", python_cmd); - child = Some(process); - break; - } - Err(e) => { - last_error = format!("Failed to start {} process: {}", python_cmd, e); - println!("{}", last_error); - continue; - } - } - } - - let mut child = child.ok_or_else(|| { - format!("Failed to start Python process with any command. Last error: {}", last_error) - })?; - - let stdout = child.stdout.take().unwrap(); - let stderr = child.stderr.take().unwrap(); - - // Use channels for concurrent reading - let (stdout_tx, _stdout_rx) = mpsc::channel(); - let (stderr_tx, _stderr_rx) = mpsc::channel(); - let (result_tx, result_rx) = mpsc::channel(); - - let progress_messages = Arc::new(Mutex::new(Vec::new())); - let error_messages = Arc::new(Mutex::new(Vec::new())); - let final_result = Arc::new(Mutex::new(None::)); - - // Spawn thread for reading stdout - let progress_messages_clone = Arc::clone(&progress_messages); - let final_result_clone = Arc::clone(&final_result); - thread::spawn(move || { - let mut reader = BufReader::new(stdout); - let mut buffer = Vec::new(); - - // Read raw bytes and handle encoding issues - loop { - buffer.clear(); - match reader.read_until(b'\n', &mut buffer) { - Ok(0) => break, // EOF - Ok(_) => { - // Convert bytes to string, handling invalid UTF-8 - let line = String::from_utf8_lossy(&buffer); - let line = line.trim_end_matches('\n').trim_end_matches('\r'); - - if line.is_empty() { - continue; - } - - println!("Python stdout: {}", line); - - // Parse JSON-RPC messages - if line.starts_with("JSONRPC:") { - let json_str = &line[8..]; // Remove "JSONRPC:" prefix - if let Ok(json_value) = serde_json::from_str::(json_str) { - if let Some(method) = json_value.get("method") { - if method == "progress" { - if let Ok(mut messages) = progress_messages_clone.lock() { - messages.push(json_str.to_string()); - } - println!("Progress: {}", json_str); - } - } else if json_value.get("result").is_some() || json_value.get("error").is_some() { - // This is a final result or error response - if let Ok(mut result) = final_result_clone.lock() { - *result = Some(json_str.to_string()); - } - println!("JSON-RPC result found: {}", json_str); - } - } - } else if line.trim().starts_with('{') && line.trim().ends_with('}') { - // Fallback: try to parse as direct JSON result - if let Ok(json_value) = serde_json::from_str::(line.trim()) { - // Check if this looks like a final result (has status field) - if json_value.get("status").is_some() { - if let Ok(mut result) = final_result_clone.lock() { - *result = Some(line.trim().to_string()); - } - println!("Direct JSON result: {}", line.trim()); - } - } - } else { - println!("Python other: {}", line); - } - - if stdout_tx.send(line.to_string()).is_err() { - break; - } - } - Err(e) => { - println!("Error reading stdout: {}", e); - break; - } - } - } - }); - - // Spawn thread for reading stderr - let error_messages_clone = Arc::clone(&error_messages); - thread::spawn(move || { - let mut reader = BufReader::new(stderr); - let mut buffer = Vec::new(); - - // Read raw bytes and handle encoding issues - loop { - buffer.clear(); - match reader.read_until(b'\n', &mut buffer) { - Ok(0) => break, // EOF - Ok(_) => { - // Convert bytes to string, handling invalid UTF-8 - let line = String::from_utf8_lossy(&buffer); - let line = line.trim_end_matches('\n').trim_end_matches('\r'); - - if line.is_empty() { - continue; - } - - println!("Python stderr: {}", line); - if let Ok(mut messages) = error_messages_clone.lock() { - messages.push(line.to_string()); - } - if stderr_tx.send(line.to_string()).is_err() { - break; - } - } - Err(e) => { - println!("Error reading stderr: {}", e); - break; - } - } - } - }); - - // Spawn thread to wait for process completion - thread::spawn(move || { - match child.wait() { - Ok(status) => { - let _ = result_tx.send(Ok(status)); - } - Err(e) => { - let _ = result_tx.send(Err(format!("Failed to wait for process: {}", e))); - } - } - }); - - // Wait for process to complete with timeout - println!("Waiting for Python process to complete..."); - let timeout_duration = Duration::from_secs(600); // 10 minutes timeout - let start_time = Instant::now(); - - let exit_status = loop { - match result_rx.try_recv() { - Ok(Ok(status)) => break status, - Ok(Err(e)) => return Err(e), - Err(mpsc::TryRecvError::Empty) => { - if start_time.elapsed() > timeout_duration { - return Err("Python process timed out after 10 minutes".to_string()); - } - thread::sleep(Duration::from_millis(100)); - continue; - } - Err(mpsc::TryRecvError::Disconnected) => { - return Err("Process monitoring thread disconnected".to_string()); - } - } - }; - - println!("Python process terminated with code: {:?}", exit_status.code()); - - // Extract final results from shared state - let final_result_value = { - final_result.lock().unwrap().clone() - }; - let progress_messages_value = { - progress_messages.lock().unwrap().clone() - }; - let error_messages_value = { - error_messages.lock().unwrap().clone() - }; - - // Return the final result if we found one - if let Some(result) = final_result_value { - println!("Extracted JSON-RPC result: {}", result); - return Ok(result); - } - - // If no JSON-RPC result found, provide detailed error information - if exit_status.success() { - // Process succeeded but no output - this is unusual - let error_msg = if progress_messages_value.is_empty() { - "Python script completed successfully but produced no output" - } else { - "Python script completed but did not return a valid JSON result" - }; - Err(error_msg.to_string()) - } else { - // Process failed - provide detailed error based on exit code - let error_msg = if let Some(code) = exit_status.code() { - match code { - 1 => "Python script failed with general error. Check if all dependencies are installed.".to_string(), - 120 => "Python module import failed or function not supported. This may be due to missing dependencies or incompatible Python environment.".to_string(), - _ => format!("Python script failed with exit code: {}. This may indicate a system or environment issue.", code) - } - } else { - "Python process was killed by external signal.".to_string() - }; - - // Include any error output we captured - let mut full_error = error_msg; - - if !progress_messages_value.is_empty() { - full_error.push_str(&format!("\n\nLast stdout: {}", progress_messages_value.join("\n"))); - } - - if !error_messages_value.is_empty() { - full_error.push_str(&format!("\n\nStderr: {}", error_messages_value.join("\n"))); - } - - Err(full_error) - } -} #[tauri::command] pub async fn batch_import_templates(app: tauri::AppHandle, request: BatchImportRequest) -> Result { @@ -305,7 +36,7 @@ pub async fn batch_import_templates(app: tauri::AppHandle, request: BatchImportR request.source_folder, ]; - execute_python_template_command(app, &args).await + execute_python_command(app, &args, None).await } #[tauri::command] @@ -317,7 +48,7 @@ pub async fn get_templates(app: tauri::AppHandle) -> Result { "get_templates".to_string(), ]; - execute_python_template_command(app, &args).await + execute_python_command(app, &args, None).await } #[tauri::command] @@ -331,7 +62,7 @@ pub async fn get_template(app: tauri::AppHandle, template_id: String) -> Result< template_id, ]; - execute_python_template_command(app, &args).await + execute_python_command(app, &args, None).await } #[tauri::command] @@ -345,5 +76,5 @@ pub async fn delete_template(app: tauri::AppHandle, template_id: String) -> Resu template_id, ]; - execute_python_template_command(app, &args).await + execute_python_command(app, &args, None).await } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 412bf44..09abf22 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ mod commands; +mod python_executor; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { diff --git a/src-tauri/src/python_executor.rs b/src-tauri/src/python_executor.rs new file mode 100644 index 0000000..b015d91 --- /dev/null +++ b/src-tauri/src/python_executor.rs @@ -0,0 +1,306 @@ +use std::process::Command; +use std::io::{BufRead, BufReader}; +use std::time::{Duration, Instant}; +use std::thread; +use std::sync::{Arc, Mutex}; +use std::sync::mpsc; + +/// Configuration for Python command execution +pub struct PythonExecutorConfig { + pub timeout_seconds: u64, + pub working_directory: Option, +} + +impl Default for PythonExecutorConfig { + fn default() -> Self { + Self { + timeout_seconds: 600, // 10 minutes default + working_directory: None, + } + } +} + +/// Execute a Python command with the given arguments +/// +/// This function handles: +/// - Multiple Python command attempts (python, python3, py) +/// - UTF-8 encoding setup +/// - Concurrent stdout/stderr reading +/// - JSON-RPC message parsing +/// - Timeout handling +/// - Detailed error reporting +pub async fn execute_python_command( + _app: tauri::AppHandle, + args: &[String], + config: Option +) -> Result { + let config = config.unwrap_or_default(); + + println!("Executing Python command with args: {:?}", args); + + // Get project root directory + let current_dir = std::env::current_dir() + .map_err(|e| format!("Failed to get current directory: {}", e))?; + + let project_root = if let Some(working_dir) = config.working_directory { + working_dir + } else if current_dir.ends_with("src-tauri") { + current_dir.parent().unwrap_or(¤t_dir).to_path_buf() + } else { + current_dir + }; + + println!("Working directory: {:?}", project_root); + + // Try multiple Python commands in order of preference + let python_commands = if cfg!(target_os = "windows") { + vec!["python", "python3", "py"] + } else { + vec!["python3", "python"] + }; + + let mut child = None; + let mut last_error = String::new(); + + for python_cmd in python_commands { + println!("Trying Python command: {}", python_cmd); + let mut cmd = Command::new(python_cmd); + cmd.current_dir(&project_root); + cmd.args(args); + cmd.stdout(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + + // Set environment variables for consistent encoding + cmd.env("PYTHONIOENCODING", "utf-8"); + cmd.env("PYTHONUNBUFFERED", "1"); + + // On Windows, ensure UTF-8 console output + if cfg!(target_os = "windows") { + cmd.env("PYTHONUTF8", "1"); + } + + match cmd.spawn() { + Ok(process) => { + println!("Successfully started Python process with: {}", python_cmd); + child = Some(process); + break; + } + Err(e) => { + last_error = format!("Failed to start {} process: {}", python_cmd, e); + println!("{}", last_error); + continue; + } + } + } + + let mut child = child.ok_or_else(|| { + format!("Failed to start Python process with any command. Last error: {}", last_error) + })?; + + let stdout = child.stdout.take().unwrap(); + let stderr = child.stderr.take().unwrap(); + + // Use channels for concurrent reading + let (stdout_tx, _stdout_rx) = mpsc::channel(); + let (stderr_tx, _stderr_rx) = mpsc::channel(); + let (result_tx, result_rx) = mpsc::channel(); + + let progress_messages = Arc::new(Mutex::new(Vec::new())); + let error_messages = Arc::new(Mutex::new(Vec::new())); + let final_result = Arc::new(Mutex::new(None::)); + + // Spawn thread for reading stdout + let progress_messages_clone = Arc::clone(&progress_messages); + let final_result_clone = Arc::clone(&final_result); + thread::spawn(move || { + let mut reader = BufReader::new(stdout); + let mut buffer = Vec::new(); + + // Read raw bytes and handle encoding issues + loop { + buffer.clear(); + match reader.read_until(b'\n', &mut buffer) { + Ok(0) => break, // EOF + Ok(_) => { + // Convert bytes to string, handling invalid UTF-8 + let line = String::from_utf8_lossy(&buffer); + let line = line.trim_end_matches('\n').trim_end_matches('\r'); + + if line.is_empty() { + continue; + } + println!("Python stdout: {}", line); + + // Parse JSON-RPC messages + if line.starts_with("JSONRPC:") { + let json_str = &line[8..]; // Remove "JSONRPC:" prefix + if let Ok(json_value) = serde_json::from_str::(json_str) { + if let Some(method) = json_value.get("method") { + if method == "progress" { + if let Ok(mut messages) = progress_messages_clone.lock() { + messages.push(json_str.to_string()); + } + println!("Progress: {}", json_str); + } + } else if json_value.get("result").is_some() || json_value.get("error").is_some() { + // This is a final result or error response + if let Ok(mut result) = final_result_clone.lock() { + *result = Some(json_str.to_string()); + } + println!("JSON-RPC result found: {}", json_str); + } + } + } else if line.trim().starts_with('{') && line.trim().ends_with('}') { + // Fallback: try to parse as direct JSON result + if let Ok(json_value) = serde_json::from_str::(line.trim()) { + // Check if this looks like a final result (has status field) + if json_value.get("status").is_some() { + if let Ok(mut result) = final_result_clone.lock() { + *result = Some(line.trim().to_string()); + } + println!("Direct JSON result: {}", line.trim()); + } + } + } else { + println!("Python other: {}", line); + } + + if stdout_tx.send(line.to_string()).is_err() { + break; + } + } + Err(e) => { + println!("Error reading stdout: {}", e); + break; + } + } + } + }); + + // Spawn thread for reading stderr + let error_messages_clone = Arc::clone(&error_messages); + thread::spawn(move || { + let mut reader = BufReader::new(stderr); + let mut buffer = Vec::new(); + + // Read raw bytes and handle encoding issues + loop { + buffer.clear(); + match reader.read_until(b'\n', &mut buffer) { + Ok(0) => break, // EOF + Ok(_) => { + // Convert bytes to string, handling invalid UTF-8 + let line = String::from_utf8_lossy(&buffer); + let line = line.trim_end_matches('\n').trim_end_matches('\r'); + + if line.is_empty() { + continue; + } + + println!("Python stderr: {}", line); + if let Ok(mut messages) = error_messages_clone.lock() { + messages.push(line.to_string()); + } + if stderr_tx.send(line.to_string()).is_err() { + break; + } + } + Err(e) => { + println!("Error reading stderr: {}", e); + break; + } + } + } + }); + + // Spawn thread to wait for process completion + thread::spawn(move || { + match child.wait() { + Ok(status) => { + let _ = result_tx.send(Ok(status)); + } + Err(e) => { + let _ = result_tx.send(Err(format!("Failed to wait for process: {}", e))); + } + } + }); + + // Wait for process to complete with timeout + println!("Waiting for Python process to complete..."); + let timeout_duration = Duration::from_secs(config.timeout_seconds); + let start_time = Instant::now(); + + let exit_status = loop { + match result_rx.try_recv() { + Ok(Ok(status)) => break status, + Ok(Err(e)) => return Err(e), + Err(mpsc::TryRecvError::Empty) => { + if start_time.elapsed() > timeout_duration { + return Err(format!("Python process timed out after {} seconds", config.timeout_seconds)); + } + thread::sleep(Duration::from_millis(100)); + continue; + } + Err(mpsc::TryRecvError::Disconnected) => { + return Err("Process monitoring thread disconnected".to_string()); + } + } + }; + + println!("Python process terminated with code: {:?}", exit_status.code()); + + // Extract final results from shared state + let final_result_value = { + final_result.lock().unwrap().clone() + }; + let progress_messages_value = { + progress_messages.lock().unwrap().clone() + }; + let error_messages_value = { + error_messages.lock().unwrap().clone() + }; + + // Return the final result if we found one + if let Some(result) = final_result_value { + println!("Extracted JSON-RPC result: {}", result); + return Ok(result); + } + + // If no JSON-RPC result found, provide detailed error information + if exit_status.success() { + // Process succeeded but no output - this is unusual + let error_msg = if progress_messages_value.is_empty() { + "Python script completed successfully but produced no output" + } else { + "Python script completed but did not return a valid JSON result" + }; + Err(error_msg.to_string()) + } else { + // Process failed - provide detailed error based on exit code + let error_msg = if let Some(code) = exit_status.code() { + let unsigned_code = code as u32; + match code { + 1 => "Python script failed with general error. Check if all dependencies are installed.".to_string(), + 120 => "Python module import failed or function not supported. This may be due to missing dependencies or incompatible Python environment. Please run the environment test first.".to_string(), + -1073741510 => "Python process was terminated by antivirus or system security. Please add the application to your antivirus whitelist.".to_string(), + _ if unsigned_code == 3221225786 => "Python process was terminated by antivirus or system security. Please add the application to your antivirus whitelist.".to_string(), + _ => format!("Python script failed with exit code: {}. This may indicate a system or environment issue.", code) + } + } else { + "Python process was killed by external signal. This may be caused by antivirus software or system security policies.".to_string() + }; + + // Include any error output we captured + let mut full_error = error_msg; + + if !progress_messages_value.is_empty() { + full_error.push_str(&format!("\n\nLast stdout: {}", progress_messages_value.join("\n"))); + } + + if !error_messages_value.is_empty() { + full_error.push_str(&format!("\n\nStderr: {}", error_messages_value.join("\n"))); + } + + Err(full_error) + } +}