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
This commit is contained in:
@@ -1,10 +1,6 @@
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::io::{BufRead, BufReader, Read};
|
use crate::python_executor::execute_python_command;
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use std::thread;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::sync::mpsc;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct AIVideoRequest {
|
pub struct AIVideoRequest {
|
||||||
@@ -26,294 +22,7 @@ pub struct BatchAIVideoRequest {
|
|||||||
pub timeout: Option<u32>,
|
pub timeout: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute_python_command(_app: tauri::AppHandle, args: &[String]) -> Result<String, String> {
|
|
||||||
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::<String>));
|
|
||||||
|
|
||||||
// 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::<serde_json::Value>(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::<serde_json::Value>(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]
|
#[tauri::command]
|
||||||
pub async fn generate_ai_video(app: tauri::AppHandle, request: AIVideoRequest) -> Result<String, String> {
|
pub async fn generate_ai_video(app: tauri::AppHandle, request: AIVideoRequest) -> Result<String, String> {
|
||||||
@@ -342,7 +51,7 @@ pub async fn generate_ai_video(app: tauri::AppHandle, request: AIVideoRequest) -
|
|||||||
args.push(timeout.to_string());
|
args.push(timeout.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
execute_python_command(app, &args).await
|
execute_python_command(app, &args, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -372,7 +81,7 @@ pub async fn batch_generate_ai_videos(app: tauri::AppHandle, request: BatchAIVid
|
|||||||
args.push(timeout.to_string());
|
args.push(timeout.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
execute_python_command(app, &args).await
|
execute_python_command(app, &args, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::process::Command;
|
use crate::python_executor::execute_python_command;
|
||||||
use std::io::{BufRead, BufReader, Read};
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use std::thread;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::sync::mpsc;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct BatchImportRequest {
|
pub struct BatchImportRequest {
|
||||||
@@ -28,271 +23,7 @@ pub struct TemplateInfo {
|
|||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute_python_template_command(_app: tauri::AppHandle, args: &[String]) -> Result<String, String> {
|
|
||||||
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::<String>));
|
|
||||||
|
|
||||||
// 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::<serde_json::Value>(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::<serde_json::Value>(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]
|
#[tauri::command]
|
||||||
pub async fn batch_import_templates(app: tauri::AppHandle, request: BatchImportRequest) -> Result<String, String> {
|
pub async fn batch_import_templates(app: tauri::AppHandle, request: BatchImportRequest) -> Result<String, String> {
|
||||||
@@ -305,7 +36,7 @@ pub async fn batch_import_templates(app: tauri::AppHandle, request: BatchImportR
|
|||||||
request.source_folder,
|
request.source_folder,
|
||||||
];
|
];
|
||||||
|
|
||||||
execute_python_template_command(app, &args).await
|
execute_python_command(app, &args, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -317,7 +48,7 @@ pub async fn get_templates(app: tauri::AppHandle) -> Result<String, String> {
|
|||||||
"get_templates".to_string(),
|
"get_templates".to_string(),
|
||||||
];
|
];
|
||||||
|
|
||||||
execute_python_template_command(app, &args).await
|
execute_python_command(app, &args, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -331,7 +62,7 @@ pub async fn get_template(app: tauri::AppHandle, template_id: String) -> Result<
|
|||||||
template_id,
|
template_id,
|
||||||
];
|
];
|
||||||
|
|
||||||
execute_python_template_command(app, &args).await
|
execute_python_command(app, &args, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -345,5 +76,5 @@ pub async fn delete_template(app: tauri::AppHandle, template_id: String) -> Resu
|
|||||||
template_id,
|
template_id,
|
||||||
];
|
];
|
||||||
|
|
||||||
execute_python_template_command(app, &args).await
|
execute_python_command(app, &args, None).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
mod commands;
|
mod commands;
|
||||||
|
mod python_executor;
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
|
|||||||
306
src-tauri/src/python_executor.rs
Normal file
306
src-tauri/src/python_executor.rs
Normal file
@@ -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<std::path::PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<PythonExecutorConfig>
|
||||||
|
) -> Result<String, String> {
|
||||||
|
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::<String>));
|
||||||
|
|
||||||
|
// 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::<serde_json::Value>(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::<serde_json::Value>(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user