File size: 7,192 Bytes
fc69e8e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | //! Python Sidecar Commands
use tauri::{AppHandle, Manager};
use tauri_plugin_shell::process::{CommandEvent, CommandChild};
use tauri_plugin_shell::ShellExt;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tauri::Emitter;
use tokio::sync::Mutex as TokioMutex;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PythonBackendConfig {
pub host: String,
pub port: u16,
pub python_path: Option<String>,
pub script_path: String,
pub args: Vec<String>,
pub env: HashMap<String, String>,
}
impl Default for PythonBackendConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 8080,
python_path: None,
script_path: "backend/app/main.py".to_string(),
args: vec![],
env: HashMap::new(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BackendStatus {
pub running: bool,
pub pid: Option<u32>,
pub url: Option<String>,
pub error: Option<String>,
}
type BackendState = Arc<TokioMutex<Option<CommandChild>>>;
type StatusState = Arc<TokioMutex<BackendStatus>>;
static BACKEND_STATE: std::sync::OnceLock<BackendState> = std::sync::OnceLock::new();
static STATUS_STATE: std::sync::OnceLock<StatusState> = std::sync::OnceLock::new();
fn get_backend_state() -> &'static BackendState {
BACKEND_STATE.get_or_init(|| Arc::new(TokioMutex::new(None)))
}
fn get_status_state() -> &'static StatusState {
STATUS_STATE.get_or_init(|| Arc::new(TokioMutex::new(BackendStatus::default())))
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
struct BackendStatus {
running: bool,
pid: Option<u32>,
url: Option<String>,
error: Option<String>,
}
#[tauri::command]
pub async fn start_python_backend(app: AppHandle, config: PythonBackendConfig) -> Result<BackendStatus, String> {
let state = get_backend_state();
let status = get_status_state();
// Check if already running
{
let mut status_guard = status.lock().await;
if status_guard.running {
return Ok(status_guard.clone());
}
}
// Determine python path
let python_cmd = config.python_path.unwrap_or_else(|| {
if cfg!(target_os = "windows") {
"python.exe".to_string()
} else {
"python3".to_string()
}
});
// Build command
let mut cmd = tauri_plugin_shell::Command::new(python_cmd);
cmd.arg(&config.script_path);
for arg in &config.args {
cmd.arg(arg);
}
// Set environment variables
for (key, value) in &config.env {
cmd.env(key, value);
}
// Add PYTHONPATH for relative imports
let backend_dir = std::path::Path::new(&config.script_path).parent().unwrap_or(std::path::Path::new("."));
cmd.env("PYTHONPATH", backend_dir);
// Spawn the process
let (mut rx, child) = cmd.spawn().map_err(|e| format!("Failed to spawn python backend: {}", e))?;
let pid = child.pid();
// Update state
{
let mut backend_guard = state.lock().await;
*backend_guard = Some(child);
}
// Update status
let backend_url = format!("http://{}:{}", config.host, config.port);
{
let mut status_guard = status.lock().await;
status_guard.running = true;
status_guard.pid = Some(pid);
status_guard.url = Some(backend_url.clone());
status_guard.error = None;
}
// Listen for process output
let app_handle = app.clone();
tauri::async_runtime::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line) => {
let output = String::from_utf8_lossy(&line);
tracing::info!("[Python Backend] {}", output.trim());
// Check if server started
if output.contains("Application startup complete") ||
output.contains("Uvicorn running on") ||
output.contains("Running on") {
let mut status_guard = status.lock().await;
status_guard.running = true;
drop(status_guard);
// Emit event to frontend
let _ = app_handle.emit("backend:ready", BackendStatus {
running: true,
pid: Some(pid),
url: Some(backend_url.clone()),
error: None,
});
}
}
CommandEvent::Stderr(line) => {
let output = String::from_utf8_lossy(&line);
tracing::error!("[Python Backend Error] {}", output.trim());
}
CommandEvent::Error(err) => {
tracing::error!("[Python Backend Process Error] {}", err);
let mut status_guard = status.lock().await;
status_guard.running = false;
status_guard.error = Some(err.to_string());
}
CommandEvent::Exit(code) => {
tracing::info!("[Python Backend] Exited with code: {:?}", code);
let mut status_guard = status.lock().await;
status_guard.running = false;
status_guard.pid = None;
status_guard.url = None;
let _ = app_handle.emit("backend:stopped", BackendStatus {
running: false,
pid: None,
url: None,
error: Some("Process exited".to_string()),
});
}
_ => {}
}
}
});
// Return current status
let status_guard = status.lock().await;
Ok(status_guard.clone())
}
#[tauri::command]
pub async fn stop_python_backend(app: AppHandle) -> Result<BackendStatus, String> {
let state = get_backend_state();
let status = get_status_state();
let mut backend_guard = state.lock().await;
if let Some(mut child) = backend_guard.take() {
child.kill().map_err(|e| format!("Failed to kill python backend: {}", e))?;
}
let mut status_guard = status.lock().await;
status_guard.running = false;
status_guard.pid = None;
status_guard.url = None;
let _ = app.emit("backend:stopped", status_guard.clone());
Ok(status_guard.clone())
}
#[tauri::command]
pub async fn get_backend_status() -> Result<BackendStatus, String> {
let status = get_status_state();
let status_guard = status.lock().await;
Ok(status_guard.clone())
}
#[tauri::command]
pub async fn restart_python_backend(app: AppHandle, config: PythonBackendConfig) -> Result<BackendStatus, String> {
let _ = stop_python_backend(app.clone()).await;
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
start_python_backend(app, config).await
} |