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 serde::{Deserialize, Serialize};
|
||||||
use std::fs::{self, OpenOptions};
|
use std::fs::{self, OpenOptions};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::net::TcpListener;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
|
use std::sync::atomic::{AtomicU16, Ordering};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::time::Duration;
|
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")]
|
#[cfg(target_os = "windows")]
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
@@ -172,12 +201,15 @@ fn spawn_backend(exe_dir: &PathBuf) -> Result<Child, String> {
|
|||||||
cmd.env(format!("DIR_{}_PATH", n), &dir.path);
|
cmd.env(format!("DIR_{}_PATH", n), &dir.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let port = pick_free_port();
|
||||||
|
BACKEND_PORT.store(port, Ordering::Relaxed);
|
||||||
|
|
||||||
let child = cmd
|
let child = cmd
|
||||||
.args([
|
.args([
|
||||||
"-m", "uvicorn",
|
"-m", "uvicorn",
|
||||||
"backend.main:app",
|
"backend.main:app",
|
||||||
"--host", "127.0.0.1",
|
"--host", "127.0.0.1",
|
||||||
"--port", "17890",
|
"--port", &port.to_string(),
|
||||||
"--log-level", "info",
|
"--log-level", "info",
|
||||||
])
|
])
|
||||||
.stdout(Stdio::from(log.try_clone().map_err(|e| format!("{}", e))?))
|
.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> {
|
async fn wait_for_backend() -> Result<(), String> {
|
||||||
let client = reqwest::Client::new();
|
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 {
|
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() => {
|
Ok(resp) if resp.status().is_success() => {
|
||||||
info!("Backend ready (attempt {})", i + 1);
|
info!("Backend ready (attempt {})", i + 1);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -235,7 +267,7 @@ fn kill_backend(child: &mut Child) {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_backend_url() -> String {
|
fn get_backend_url() -> String {
|
||||||
"http://127.0.0.1:17890".to_string()
|
backend_url()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -390,7 +422,7 @@ async fn check_backend_health(state: tauri::State<'_, BackendProcess>) -> Result
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Health check HTTP
|
// 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(resp) if resp.status().is_success() => Ok("healthy".to_string()),
|
||||||
_ => Ok("unhealthy".to_string()),
|
_ => Ok("unhealthy".to_string()),
|
||||||
}
|
}
|
||||||
@@ -570,7 +602,7 @@ fn main() {
|
|||||||
let _ = window.eval(
|
let _ = window.eval(
|
||||||
r#"if (window.__setBootStatus) window.__setBootStatus("Backend prêt — chargement de l'interface…")"#,
|
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"),
|
Ok(_) => info!("Redirect to backend OK"),
|
||||||
Err(e) => error!("Redirect eval failed: {}", e),
|
Err(e) => error!("Redirect eval failed: {}", e),
|
||||||
}
|
}
|
||||||
@@ -580,8 +612,9 @@ fn main() {
|
|||||||
error!("Backend failed: {}", e);
|
error!("Backend failed: {}", e);
|
||||||
if let Some(window) = handle.get_webview_window("main") {
|
if let Some(window) = handle.get_webview_window("main") {
|
||||||
let _ = window.eval(&format!(
|
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('"', "\\\""),
|
e.replace('\'', "\\'").replace('"', "\\\""),
|
||||||
|
backend_url(),
|
||||||
backend_log_path().display().to_string().replace('\\', "\\\\"),
|
backend_log_path().display().to_string().replace('\\', "\\\\"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -934,7 +967,35 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_backend_url() {
|
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]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user