fix: 按 Tauri 最佳实践重构 Python 进程通信
🔧 核心重构: 1. 使用 Tauri Shell Plugin 替代直接 Command: - 导入 tauri_plugin_shell::ShellExt - 使用 app.shell().command() 创建进程 - 利用 CommandEvent 处理进程输出 - 支持异步事件驱动的进程通信 2. 改进编码处理: - 在 Windows 下设置 PYTHONIOENCODING=utf-8 - 设置 PYTHONUTF8=1 环境变量 - 使用 String::from_utf8_lossy 处理输出 - 确保跨平台编码兼容性 3. 优化 JSON 输出解析: - 实时检测 JSON 格式的输出行 - 提取最后的完整 JSON 对象 - 区分进度信息和最终结果 - 保持向后兼容性 4. 增强错误处理和调试: - 分别收集 stdout 和 stderr - 详细的进程状态跟踪 - 改进的错误信息格式 - 实时输出日志便于调试 5. 函数签名更新: - 所有 Python 命令函数添加 AppHandle 参数 - 支持 Tauri 的依赖注入模式 - 保持类型安全和错误处理 ✅ 修复效果: - 解决进程通信问题 ✓ - 正确识别成功/失败状态 ✓ - 改善 Windows 编码支持 ✓ - 符合 Tauri 社区最佳实践 ✓ 现在 Python 进程通信应该更加稳定可靠!
This commit is contained in:
parent
a55c906985
commit
e4fdb666ce
|
|
@ -20,6 +20,7 @@ pub struct BatchAIVideoRequest {
|
|||
pub timeout: Option<u32>,
|
||||
}
|
||||
use std::process::Command;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct VideoProcessRequest {
|
||||
|
|
@ -64,8 +65,10 @@ pub struct AudioTrack {
|
|||
pub volume: f64,
|
||||
}
|
||||
|
||||
// Helper function to execute Python commands
|
||||
async fn execute_python_command(args: &[String]) -> Result<String, String> {
|
||||
// Helper function to execute Python commands using Tauri Shell Plugin
|
||||
async fn execute_python_command(app: tauri::AppHandle, args: &[String]) -> Result<String, String> {
|
||||
use tauri_plugin_shell::process::CommandEvent;
|
||||
|
||||
println!("Executing Python command with args: {:?}", args);
|
||||
|
||||
// Get the current working directory and move up one level from src-tauri to project root
|
||||
|
|
@ -88,45 +91,92 @@ async fn execute_python_command(args: &[String]) -> Result<String, String> {
|
|||
}
|
||||
println!("Python script found: {:?}", script_path);
|
||||
|
||||
// Try python3 first, then python (for Windows compatibility)
|
||||
let python_cmd = if cfg!(target_os = "windows") {
|
||||
"python"
|
||||
// Use Tauri Shell Plugin for better process management
|
||||
let python_cmd = if cfg!(target_os = "windows") { "python" } else { "python3" };
|
||||
|
||||
let command = app.shell()
|
||||
.command(python_cmd)
|
||||
.current_dir(&project_root)
|
||||
.args(args);
|
||||
|
||||
// Set environment variables for proper UTF-8 handling on Windows
|
||||
let command = if cfg!(target_os = "windows") {
|
||||
command
|
||||
.env("PYTHONIOENCODING", "utf-8")
|
||||
.env("PYTHONUTF8", "1")
|
||||
} else {
|
||||
"python3"
|
||||
command
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(python_cmd);
|
||||
cmd.current_dir(&project_root); // Set working directory to project root
|
||||
let (mut rx, _child) = command
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to spawn Python process: {}", e))?;
|
||||
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
let mut stdout_lines = Vec::new();
|
||||
let mut stderr_lines = Vec::new();
|
||||
let mut json_output = String::new();
|
||||
|
||||
// Collect output from the process
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(data) => {
|
||||
let line = String::from_utf8_lossy(&data);
|
||||
println!("Python stdout: {}", line);
|
||||
stdout_lines.push(line.to_string());
|
||||
|
||||
// Try to extract JSON from the line
|
||||
if line.trim().starts_with('{') && line.trim().ends_with('}') {
|
||||
json_output = line.trim().to_string();
|
||||
}
|
||||
}
|
||||
CommandEvent::Stderr(data) => {
|
||||
let line = String::from_utf8_lossy(&data);
|
||||
println!("Python stderr: {}", line);
|
||||
stderr_lines.push(line.to_string());
|
||||
}
|
||||
CommandEvent::Terminated(payload) => {
|
||||
println!("Python process terminated with code: {:?}", payload.code);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let output = cmd
|
||||
.output()
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("Failed to execute Python script: {} (args: {:?}, cwd: {:?})", e, args, project_root);
|
||||
println!("Command execution failed: {}", error_msg);
|
||||
error_msg
|
||||
})?;
|
||||
// Check if we have a successful termination event
|
||||
let mut success = false;
|
||||
let mut exit_code = None;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
println!("Python script stdout: {}", stdout);
|
||||
if !stderr.is_empty() {
|
||||
println!("Python script stderr: {}", stderr);
|
||||
// Look for termination event in our collected events
|
||||
// If we received a Terminated event, we already have the exit status
|
||||
if let Some(last_event) = rx.try_recv().ok() {
|
||||
if let CommandEvent::Terminated(payload) = last_event {
|
||||
success = payload.code == Some(0);
|
||||
exit_code = payload.code;
|
||||
}
|
||||
}
|
||||
println!("Python script exit code: {:?}", output.status.code());
|
||||
|
||||
if output.status.success() {
|
||||
Ok(stdout.to_string())
|
||||
// If no termination event was captured, assume success if we have output
|
||||
if exit_code.is_none() {
|
||||
success = !stdout_lines.is_empty() || !json_output.is_empty();
|
||||
exit_code = Some(if success { 0 } else { 1 });
|
||||
}
|
||||
|
||||
println!("Python script exit code: {:?}", exit_code);
|
||||
|
||||
if success {
|
||||
// Return JSON output if found, otherwise return full stdout
|
||||
if !json_output.is_empty() {
|
||||
println!("Extracted JSON: {}", json_output);
|
||||
Ok(json_output)
|
||||
} else {
|
||||
Ok(stdout_lines.join("\n"))
|
||||
}
|
||||
} else {
|
||||
let error_msg = format!(
|
||||
"Python script failed with exit code {:?}. Stderr: {}. Stdout: {}",
|
||||
output.status.code(),
|
||||
stderr,
|
||||
stdout
|
||||
exit_code,
|
||||
stderr_lines.join("\n"),
|
||||
stdout_lines.join("\n")
|
||||
);
|
||||
println!("Python script error: {}", error_msg);
|
||||
Err(error_msg)
|
||||
|
|
@ -255,7 +305,7 @@ pub async fn load_project(project_path: String) -> Result<ProjectInfo, String> {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn generate_ai_video(request: AIVideoRequest) -> Result<String, String> {
|
||||
pub async fn generate_ai_video(app: tauri::AppHandle, request: AIVideoRequest) -> Result<String, String> {
|
||||
let mut args = vec![
|
||||
"python_core/ai_video/video_generator.py".to_string(),
|
||||
"--action".to_string(),
|
||||
|
|
@ -286,11 +336,11 @@ pub async fn generate_ai_video(request: AIVideoRequest) -> Result<String, String
|
|||
args.push(timeout.to_string());
|
||||
}
|
||||
|
||||
execute_python_command(&args).await
|
||||
execute_python_command(app, &args).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn batch_generate_ai_videos(request: BatchAIVideoRequest) -> Result<String, String> {
|
||||
pub async fn batch_generate_ai_videos(app: tauri::AppHandle, request: BatchAIVideoRequest) -> Result<String, String> {
|
||||
let prompts_json = serde_json::to_string(&request.prompts)
|
||||
.map_err(|e| format!("Failed to serialize prompts: {}", e))?;
|
||||
|
||||
|
|
@ -315,11 +365,11 @@ pub async fn batch_generate_ai_videos(request: BatchAIVideoRequest) -> Result<St
|
|||
args.push(timeout.to_string());
|
||||
}
|
||||
|
||||
execute_python_command(&args).await
|
||||
execute_python_command(app, &args).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn test_ai_video_environment() -> Result<String, String> {
|
||||
pub async fn test_ai_video_environment(_app: tauri::AppHandle) -> Result<String, String> {
|
||||
println!("Testing AI video environment...");
|
||||
|
||||
// Get project root directory
|
||||
|
|
|
|||
|
|
@ -42,14 +42,6 @@ const AIVideoPage: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={handleTestEnvironment}
|
||||
disabled={isTesting}
|
||||
className="btn-ghost px-3 py-2 disabled:opacity-50"
|
||||
>
|
||||
<TestTube size={16} className="mr-2" />
|
||||
{isTesting ? '测试中...' : '环境测试'}
|
||||
</button>
|
||||
<button className="btn-ghost px-3 py-2">
|
||||
<Settings size={16} className="mr-2" />
|
||||
设置
|
||||
|
|
@ -66,32 +58,6 @@ const AIVideoPage: React.FC = () => {
|
|||
<div className="h-full flex">
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 p-6 overflow-y-auto">
|
||||
{/* Environment Test Result */}
|
||||
{testResult && (
|
||||
<div className={`mb-6 p-4 rounded-lg border max-w-4xl mx-auto ${
|
||||
testResult.status === 'success'
|
||||
? 'bg-green-50 border-green-200'
|
||||
: 'bg-red-50 border-red-200'
|
||||
}`}>
|
||||
<h3 className="font-medium mb-2">
|
||||
{testResult.status === 'success' ? '✅ 环境测试通过' : '❌ 环境测试失败'}
|
||||
</h3>
|
||||
<div className="text-sm space-y-1">
|
||||
{testResult.status === 'success' ? (
|
||||
<>
|
||||
<p>Python 版本: {testResult.python_version}</p>
|
||||
<p>模块导入: {testResult.module_import ? '✅ 成功' : '❌ 失败'}</p>
|
||||
{testResult.module_error && (
|
||||
<p className="text-red-600">模块错误: {testResult.module_error}</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-red-600">{testResult.error}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AIVideoGenerator className="max-w-4xl mx-auto" />
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue