feat(desktop): #77 B5 port auto-increment si 17890 occupe (pick_free_port) + 3 tests Rust
This commit is contained in:
+69
-8
@@ -9,11 +9,40 @@ use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::net::TcpListener;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicU16, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
// ── Backend port (ROADMAP #77 B: auto-increment if 17890 is busy) ────
|
||||
|
||||
const DEFAULT_BACKEND_PORT: u16 = 17890;
|
||||
const PORT_SCAN_ATTEMPTS: u16 = 10;
|
||||
|
||||
static BACKEND_PORT: AtomicU16 = AtomicU16::new(DEFAULT_BACKEND_PORT);
|
||||
|
||||
fn backend_url() -> String {
|
||||
format!("http://127.0.0.1:{}", BACKEND_PORT.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// First free TCP port starting at DEFAULT_BACKEND_PORT.
|
||||
fn pick_free_port() -> u16 {
|
||||
for offset in 0..PORT_SCAN_ATTEMPTS {
|
||||
let port = DEFAULT_BACKEND_PORT + offset;
|
||||
if TcpListener::bind(("127.0.0.1", port)).is_ok() {
|
||||
if offset > 0 {
|
||||
info!("Port {} busy — using {} instead", DEFAULT_BACKEND_PORT, port);
|
||||
}
|
||||
return port;
|
||||
}
|
||||
}
|
||||
warn!("No free port in {}..{}, falling back to default",
|
||||
DEFAULT_BACKEND_PORT, DEFAULT_BACKEND_PORT + PORT_SCAN_ATTEMPTS - 1);
|
||||
DEFAULT_BACKEND_PORT
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
use tauri::Manager;
|
||||
@@ -172,12 +201,15 @@ fn spawn_backend(exe_dir: &PathBuf) -> Result<Child, String> {
|
||||
cmd.env(format!("DIR_{}_PATH", n), &dir.path);
|
||||
}
|
||||
|
||||
let port = pick_free_port();
|
||||
BACKEND_PORT.store(port, Ordering::Relaxed);
|
||||
|
||||
let child = cmd
|
||||
.args([
|
||||
"-m", "uvicorn",
|
||||
"backend.main:app",
|
||||
"--host", "127.0.0.1",
|
||||
"--port", "17890",
|
||||
"--port", &port.to_string(),
|
||||
"--log-level", "info",
|
||||
])
|
||||
.stdout(Stdio::from(log.try_clone().map_err(|e| format!("{}", e))?))
|
||||
@@ -191,10 +223,10 @@ fn spawn_backend(exe_dir: &PathBuf) -> Result<Child, String> {
|
||||
|
||||
async fn wait_for_backend() -> Result<(), String> {
|
||||
let client = reqwest::Client::new();
|
||||
let url = "http://127.0.0.1:17890/api/health";
|
||||
let url = format!("{}/api/health", backend_url());
|
||||
|
||||
for i in 0..30 {
|
||||
match client.get(url).timeout(Duration::from_secs(2)).send().await {
|
||||
match client.get(&url).timeout(Duration::from_secs(2)).send().await {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
info!("Backend ready (attempt {})", i + 1);
|
||||
return Ok(());
|
||||
@@ -235,7 +267,7 @@ fn kill_backend(child: &mut Child) {
|
||||
|
||||
#[tauri::command]
|
||||
fn get_backend_url() -> String {
|
||||
"http://127.0.0.1:17890".to_string()
|
||||
backend_url()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -390,7 +422,7 @@ async fn check_backend_health(state: tauri::State<'_, BackendProcess>) -> Result
|
||||
}
|
||||
|
||||
// Health check HTTP
|
||||
match reqwest::get("http://127.0.0.1:17890/api/health").await {
|
||||
match reqwest::get(format!("{}/api/health", backend_url())).await {
|
||||
Ok(resp) if resp.status().is_success() => Ok("healthy".to_string()),
|
||||
_ => Ok("unhealthy".to_string()),
|
||||
}
|
||||
@@ -570,7 +602,7 @@ fn main() {
|
||||
let _ = window.eval(
|
||||
r#"if (window.__setBootStatus) window.__setBootStatus("Backend prêt — chargement de l'interface…")"#,
|
||||
);
|
||||
match window.eval("window.location.href = 'http://127.0.0.1:17890'") {
|
||||
match window.eval(&format!("window.location.href = '{}'", backend_url())) {
|
||||
Ok(_) => info!("Redirect to backend OK"),
|
||||
Err(e) => error!("Redirect eval failed: {}", e),
|
||||
}
|
||||
@@ -580,8 +612,9 @@ fn main() {
|
||||
error!("Backend failed: {}", e);
|
||||
if let Some(window) = handle.get_webview_window("main") {
|
||||
let _ = window.eval(&format!(
|
||||
r#"document.body.innerHTML = '<div style="padding:40px;text-align:center;font-family:sans-serif"><h2 style="color:#c0392b">ObsiGate n\'a pas pu démarrer</h2><p style="color:#666">{}</p><p><button onclick="location.reload()" style="padding:10px 20px;font-size:16px;cursor:pointer;background:#7C3AED;color:white;border:none;border-radius:6px">Réessayer</button></p><p style="margin-top:20px;font-size:12px;color:#999">Backend: http://127.0.0.1:17890 | Logs: {}</p></div>'"#,
|
||||
r#"document.body.innerHTML = '<div style="padding:40px;text-align:center;font-family:sans-serif"><h2 style="color:#c0392b">ObsiGate n\'a pas pu démarrer</h2><p style="color:#666">{}</p><p><button onclick="location.reload()" style="padding:10px 20px;font-size:16px;cursor:pointer;background:#7C3AED;color:white;border:none;border-radius:6px">Réessayer</button></p><p style="margin-top:20px;font-size:12px;color:#999">Backend: {} | Logs: {}</p></div>'"#,
|
||||
e.replace('\'', "\\'").replace('"', "\\\""),
|
||||
backend_url(),
|
||||
backend_log_path().display().to_string().replace('\\', "\\\\"),
|
||||
));
|
||||
}
|
||||
@@ -934,7 +967,35 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_backend_url() {
|
||||
assert_eq!(get_backend_url(), "http://127.0.0.1:17890");
|
||||
let port = BACKEND_PORT.load(Ordering::Relaxed);
|
||||
assert_eq!(get_backend_url(), format!("http://127.0.0.1:{}", port));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pick_free_port_is_free() {
|
||||
let port = pick_free_port();
|
||||
assert!((DEFAULT_BACKEND_PORT..DEFAULT_BACKEND_PORT + PORT_SCAN_ATTEMPTS).contains(&port));
|
||||
assert!(TcpListener::bind(("127.0.0.1", port)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pick_free_port_skips_busy_port() {
|
||||
let blocker = match TcpListener::bind(("127.0.0.1", DEFAULT_BACKEND_PORT)) {
|
||||
Ok(l) => l,
|
||||
Err(_) => return, // default port already busy on this machine — skip
|
||||
};
|
||||
let port = pick_free_port();
|
||||
assert_ne!(port, DEFAULT_BACKEND_PORT);
|
||||
assert!(port < DEFAULT_BACKEND_PORT + PORT_SCAN_ATTEMPTS);
|
||||
drop(blocker);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backend_url_reflects_port() {
|
||||
let saved = BACKEND_PORT.load(Ordering::Relaxed);
|
||||
BACKEND_PORT.store(17893, Ordering::Relaxed);
|
||||
assert_eq!(backend_url(), "http://127.0.0.1:17893");
|
||||
BACKEND_PORT.store(saved, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user