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>,
|
pub timeout: Option<u32>,
|
||||||
}
|
}
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct VideoProcessRequest {
|
pub struct VideoProcessRequest {
|
||||||
|
|
@ -64,8 +65,10 @@ pub struct AudioTrack {
|
||||||
pub volume: f64,
|
pub volume: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to execute Python commands
|
// Helper function to execute Python commands using Tauri Shell Plugin
|
||||||
async fn execute_python_command(args: &[String]) -> Result<String, String> {
|
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);
|
println!("Executing Python command with args: {:?}", args);
|
||||||
|
|
||||||
// Get the current working directory and move up one level from src-tauri to project root
|
// 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);
|
println!("Python script found: {:?}", script_path);
|
||||||
|
|
||||||
// Try python3 first, then python (for Windows compatibility)
|
// Use Tauri Shell Plugin for better process management
|
||||||
let python_cmd = if cfg!(target_os = "windows") {
|
let python_cmd = if cfg!(target_os = "windows") { "python" } else { "python3" };
|
||||||
"python"
|
|
||||||
|
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 {
|
} else {
|
||||||
"python3"
|
command
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut cmd = Command::new(python_cmd);
|
let (mut rx, _child) = command
|
||||||
cmd.current_dir(&project_root); // Set working directory to project root
|
.spawn()
|
||||||
|
.map_err(|e| format!("Failed to spawn Python process: {}", e))?;
|
||||||
|
|
||||||
for arg in args {
|
let mut stdout_lines = Vec::new();
|
||||||
cmd.arg(arg);
|
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
|
// Check if we have a successful termination event
|
||||||
.output()
|
let mut success = false;
|
||||||
.map_err(|e| {
|
let mut exit_code = None;
|
||||||
let error_msg = format!("Failed to execute Python script: {} (args: {:?}, cwd: {:?})", e, args, project_root);
|
|
||||||
println!("Command execution failed: {}", error_msg);
|
|
||||||
error_msg
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
// Look for termination event in our collected events
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
// If we received a Terminated event, we already have the exit status
|
||||||
|
if let Some(last_event) = rx.try_recv().ok() {
|
||||||
println!("Python script stdout: {}", stdout);
|
if let CommandEvent::Terminated(payload) = last_event {
|
||||||
if !stderr.is_empty() {
|
success = payload.code == Some(0);
|
||||||
println!("Python script stderr: {}", stderr);
|
exit_code = payload.code;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
println!("Python script exit code: {:?}", output.status.code());
|
|
||||||
|
|
||||||
if output.status.success() {
|
// If no termination event was captured, assume success if we have output
|
||||||
Ok(stdout.to_string())
|
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 {
|
} else {
|
||||||
let error_msg = format!(
|
let error_msg = format!(
|
||||||
"Python script failed with exit code {:?}. Stderr: {}. Stdout: {}",
|
"Python script failed with exit code {:?}. Stderr: {}. Stdout: {}",
|
||||||
output.status.code(),
|
exit_code,
|
||||||
stderr,
|
stderr_lines.join("\n"),
|
||||||
stdout
|
stdout_lines.join("\n")
|
||||||
);
|
);
|
||||||
println!("Python script error: {}", error_msg);
|
println!("Python script error: {}", error_msg);
|
||||||
Err(error_msg)
|
Err(error_msg)
|
||||||
|
|
@ -255,7 +305,7 @@ pub async fn load_project(project_path: String) -> Result<ProjectInfo, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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![
|
let mut args = vec![
|
||||||
"python_core/ai_video/video_generator.py".to_string(),
|
"python_core/ai_video/video_generator.py".to_string(),
|
||||||
"--action".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());
|
args.push(timeout.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
execute_python_command(&args).await
|
execute_python_command(app, &args).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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)
|
let prompts_json = serde_json::to_string(&request.prompts)
|
||||||
.map_err(|e| format!("Failed to serialize prompts: {}", e))?;
|
.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());
|
args.push(timeout.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
execute_python_command(&args).await
|
execute_python_command(app, &args).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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...");
|
println!("Testing AI video environment...");
|
||||||
|
|
||||||
// Get project root directory
|
// Get project root directory
|
||||||
|
|
|
||||||
|
|
@ -42,14 +42,6 @@ const AIVideoPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<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">
|
<button className="btn-ghost px-3 py-2">
|
||||||
<Settings size={16} className="mr-2" />
|
<Settings size={16} className="mr-2" />
|
||||||
设置
|
设置
|
||||||
|
|
@ -66,32 +58,6 @@ const AIVideoPage: React.FC = () => {
|
||||||
<div className="h-full flex">
|
<div className="h-full flex">
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1 p-6 overflow-y-auto">
|
<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" />
|
<AIVideoGenerator className="max-w-4xl mx-auto" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue