chore: bump several dependencies and refactor CLI/UI startup flow

- Updated Cargo.lock to use newer versions:
  - syn 2.0.110 (from 2.0.108)
  - actix-web 4.12.0 (from 4.11.0)
  - socket2 0.6.1 (from 0.5.10)
  - aho-corasick 1.1.4 (from 1.1.3)
  - anstyle-query 1.1.5 (from 1.1.4)
  - anstyle-wincon 3.0.11 (from 3.0.10)
  - windows-sys 0.61.2 (from 0.60.2)
- Added a comment clarifying CLI command handling.
- Simplified the default match arm for unknown CLI arguments.
- Added explanatory comment for UI thread initialization.
- Modified UI startup logic to conditionally spawn the UI thread only when not in `no_ui` and not in desktop mode, returning an `Option` handle.
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-19 14:00:57 -03:00
parent df36448f14
commit e0293f9f94
3 changed files with 442 additions and 398 deletions

552
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,7 @@ use dotenvy::dotenv;
use log::{error, info}; use log::{error, info};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
mod auth; mod auth;
mod automation; mod automation;
mod basic; mod basic;
@ -28,6 +29,7 @@ mod shared;
pub mod tests; pub mod tests;
mod ui_tree; mod ui_tree;
mod web_server; mod web_server;
use crate::auth::auth_handler; use crate::auth::auth_handler;
use crate::automation::AutomationService; use crate::automation::AutomationService;
use crate::bootstrap::BootstrapManager; use crate::bootstrap::BootstrapManager;
@ -46,6 +48,7 @@ use crate::session::{create_session, get_session_history, get_sessions, start_se
use crate::shared::state::AppState; use crate::shared::state::AppState;
use crate::shared::utils::create_conn; use crate::shared::utils::create_conn;
use crate::shared::utils::create_s3_operator; use crate::shared::utils::create_s3_operator;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum BootstrapProgress { pub enum BootstrapProgress {
StartingBootstrap, StartingBootstrap,
@ -57,6 +60,59 @@ pub enum BootstrapProgress {
BootstrapComplete, BootstrapComplete,
BootstrapError(String), BootstrapError(String),
} }
async fn run_http_server(
app_state: Arc<AppState>,
port: u16,
worker_count: usize,
) -> std::io::Result<()> {
HttpServer::new(move || {
let cors = Cors::default()
.allow_any_origin()
.allow_any_method()
.allow_any_header()
.max_age(3600);
let mut app = App::new()
.wrap(cors)
.wrap(Logger::new("HTTP REQUEST: %a %{User-Agent}i"))
.app_data(web::Data::from(app_state.clone()))
.service(auth_handler)
.service(create_session)
.service(get_session_history)
.service(get_sessions)
.service(start_session)
.service(upload_file)
.service(voice_start)
.service(voice_stop)
.service(websocket_handler)
.service(crate::bot::create_bot_handler)
.service(crate::bot::mount_bot_handler)
.service(crate::bot::handle_user_input_handler)
.service(crate::bot::get_user_sessions_handler)
.service(crate::bot::get_conversation_history_handler)
.service(crate::bot::send_warning_handler);
#[cfg(feature = "email")]
{
app = app
.service(get_latest_email_from)
.service(get_emails)
.service(list_emails)
.service(send_email)
.service(save_draft)
.service(save_click);
}
app.configure(web_server::configure_app)
})
.workers(worker_count)
.bind(("0.0.0.0", port))?
.run()
.await
}
#[tokio::main] #[tokio::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
dotenv().ok(); dotenv().ok();
@ -68,21 +124,22 @@ async fn main() -> std::io::Result<()> {
use crate::llm::local::ensure_llama_servers_running; use crate::llm::local::ensure_llama_servers_running;
use botserver::config::ConfigManager; use botserver::config::ConfigManager;
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
let no_ui = args.contains(&"--noui".to_string()); let no_ui = args.contains(&"--noui".to_string());
let desktop_mode = args.contains(&"--desktop".to_string()); let desktop_mode = args.contains(&"--desktop".to_string());
dotenv().ok(); dotenv().ok();
let (progress_tx, progress_rx) = tokio::sync::mpsc::unbounded_channel::<BootstrapProgress>(); let (progress_tx, progress_rx) = tokio::sync::mpsc::unbounded_channel::<BootstrapProgress>();
let (state_tx, state_rx) = tokio::sync::mpsc::channel::<Arc<AppState>>(1); let (state_tx, state_rx) = tokio::sync::mpsc::channel::<Arc<AppState>>(1);
let (http_tx, http_rx) = tokio::sync::oneshot::channel();
// Handle CLI commands
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 { if args.len() > 1 {
let command = &args[1]; let command = &args[1];
match command.as_str() { match command.as_str() {
"install" | "remove" | "list" | "status" | "start" | "stop" | "restart" | "--help" "install" | "remove" | "list" | "status" | "start" | "stop" | "restart"
| "-h" => match package_manager::cli::run().await { | "--help" | "-h" => match package_manager::cli::run().await {
Ok(_) => return Ok(()), Ok(_) => return Ok(()),
Err(e) => { Err(e) => {
eprintln!("CLI error: {}", e); eprintln!("CLI error: {}", e);
@ -92,24 +149,27 @@ async fn main() -> std::io::Result<()> {
)); ));
} }
}, },
_ => { _ => {}
}
} }
} }
// Start UI thread if not in no-ui mode and not in desktop mode
if !no_ui { let ui_handle = if !no_ui && !desktop_mode {
let progress_rx = Arc::new(tokio::sync::Mutex::new(progress_rx)); let progress_rx = Arc::new(tokio::sync::Mutex::new(progress_rx));
let state_rx = Arc::new(tokio::sync::Mutex::new(state_rx)); let state_rx = Arc::new(tokio::sync::Mutex::new(state_rx));
let handle = std::thread::Builder::new()
Some(
std::thread::Builder::new()
.name("ui-thread".to_string()) .name("ui-thread".to_string())
.spawn(move || { .spawn(move || {
let mut ui = crate::ui_tree::XtreeUI::new(); let mut ui = crate::ui_tree::XtreeUI::new();
ui.set_progress_channel(progress_rx.clone()); ui.set_progress_channel(progress_rx.clone());
let rt = tokio::runtime::Builder::new_current_thread() let rt = tokio::runtime::Builder::new_current_thread()
.enable_all() .enable_all()
.build() .build()
.expect("Failed to create UI runtime"); .expect("Failed to create UI runtime");
rt.block_on(async { rt.block_on(async {
tokio::select! { tokio::select! {
result = async { result = async {
@ -120,41 +180,47 @@ async fn main() -> std::io::Result<()> {
ui.set_app_state(app_state); ui.set_app_state(app_state);
} }
} }
_ = http_rx => {}
_ = tokio::time::sleep(tokio::time::Duration::from_secs(300)) => { _ = tokio::time::sleep(tokio::time::Duration::from_secs(300)) => {
eprintln!("UI initialization timeout"); eprintln!("UI initialization timeout");
} }
} }
}); });
if let Err(e) = ui.start_ui() { if let Err(e) = ui.start_ui() {
eprintln!("UI error: {}", e); eprintln!("UI error: {}", e);
} }
}) })
.expect("Failed to spawn UI thread"); .expect("Failed to spawn UI thread"),
Some(handle) )
} else { } else {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
.write_style(env_logger::WriteStyle::Always) .write_style(env_logger::WriteStyle::Always)
.init(); .init();
None None
}; };
let install_mode = if args.contains(&"--container".to_string()) { let install_mode = if args.contains(&"--container".to_string()) {
InstallMode::Container InstallMode::Container
} else { } else {
InstallMode::Local InstallMode::Local
}; };
let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") {
args.get(idx + 1).cloned() args.get(idx + 1).cloned()
} else { } else {
None None
}; };
// Bootstrap
let progress_tx_clone = progress_tx.clone(); let progress_tx_clone = progress_tx.clone();
let cfg = { let cfg = {
progress_tx_clone progress_tx_clone
.send(BootstrapProgress::StartingBootstrap) .send(BootstrapProgress::StartingBootstrap)
.ok(); .ok();
let mut bootstrap = BootstrapManager::new(install_mode.clone(), tenant.clone()).await; let mut bootstrap = BootstrapManager::new(install_mode.clone(), tenant.clone()).await;
let env_path = std::env::current_dir().unwrap().join(".env"); let env_path = std::env::current_dir().unwrap().join(".env");
let cfg = if env_path.exists() { let cfg = if env_path.exists() {
progress_tx_clone progress_tx_clone
.send(BootstrapProgress::StartingComponent( .send(BootstrapProgress::StartingComponent(
@ -168,6 +234,7 @@ async fn main() -> std::io::Result<()> {
progress_tx_clone progress_tx_clone
.send(BootstrapProgress::ConnectingDatabase) .send(BootstrapProgress::ConnectingDatabase)
.ok(); .ok();
match create_conn() { match create_conn() {
Ok(pool) => AppConfig::from_database(&pool) Ok(pool) => AppConfig::from_database(&pool)
.unwrap_or_else(|_| AppConfig::from_env().expect("Failed to load config")), .unwrap_or_else(|_| AppConfig::from_env().expect("Failed to load config")),
@ -175,7 +242,6 @@ async fn main() -> std::io::Result<()> {
} }
} else { } else {
_ = bootstrap.bootstrap().await; _ = bootstrap.bootstrap().await;
progress_tx_clone progress_tx_clone
.send(BootstrapProgress::StartingComponent( .send(BootstrapProgress::StartingComponent(
"all services".to_string(), "all services".to_string(),
@ -195,6 +261,7 @@ async fn main() -> std::io::Result<()> {
progress_tx_clone progress_tx_clone
.send(BootstrapProgress::UploadingTemplates) .send(BootstrapProgress::UploadingTemplates)
.ok(); .ok();
if let Err(e) = bootstrap.upload_templates_to_drive(&cfg).await { if let Err(e) = bootstrap.upload_templates_to_drive(&cfg).await {
progress_tx_clone progress_tx_clone
.send(BootstrapProgress::BootstrapError(format!( .send(BootstrapProgress::BootstrapError(format!(
@ -203,13 +270,18 @@ async fn main() -> std::io::Result<()> {
))) )))
.ok(); .ok();
} }
Ok::<AppConfig, std::io::Error>(cfg) Ok::<AppConfig, std::io::Error>(cfg)
}; };
let cfg = cfg?; let cfg = cfg?;
dotenv().ok(); dotenv().ok();
let refreshed_cfg = AppConfig::from_env().expect("Failed to load config from env"); let refreshed_cfg = AppConfig::from_env().expect("Failed to load config from env");
let config = std::sync::Arc::new(refreshed_cfg.clone()); let config = std::sync::Arc::new(refreshed_cfg.clone());
progress_tx.send(BootstrapProgress::ConnectingDatabase).ok(); progress_tx.send(BootstrapProgress::ConnectingDatabase).ok();
let pool = match create_conn() { let pool = match create_conn() {
Ok(pool) => pool, Ok(pool) => pool,
Err(e) => { Err(e) => {
@ -226,8 +298,9 @@ async fn main() -> std::io::Result<()> {
)); ));
} }
}; };
let cache_url =
std::env::var("CACHE_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string()); let cache_url = std::env::var("CACHE_URL")
.unwrap_or_else(|_| "redis://localhost:6379".to_string());
let redis_client = match redis::Client::open(cache_url.as_str()) { let redis_client = match redis::Client::open(cache_url.as_str()) {
Ok(client) => Some(Arc::new(client)), Ok(client) => Some(Arc::new(client)),
Err(e) => { Err(e) => {
@ -235,26 +308,34 @@ async fn main() -> std::io::Result<()> {
None None
} }
}; };
let web_adapter = Arc::new(WebChannelAdapter::new()); let web_adapter = Arc::new(WebChannelAdapter::new());
let voice_adapter = Arc::new(VoiceAdapter::new()); let voice_adapter = Arc::new(VoiceAdapter::new());
let drive = create_s3_operator(&config.drive) let drive = create_s3_operator(&config.drive)
.await .await
.expect("Failed to initialize Drive"); .expect("Failed to initialize Drive");
let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new( let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new(
pool.get().unwrap(), pool.get().unwrap(),
redis_client.clone(), redis_client.clone(),
))); )));
let auth_service = Arc::new(tokio::sync::Mutex::new(auth::AuthService::new())); let auth_service = Arc::new(tokio::sync::Mutex::new(auth::AuthService::new()));
let config_manager = ConfigManager::new(pool.clone()); let config_manager = ConfigManager::new(pool.clone());
let mut bot_conn = pool.get().expect("Failed to get database connection"); let mut bot_conn = pool.get().expect("Failed to get database connection");
let (default_bot_id, _default_bot_name) = crate::bot::get_default_bot(&mut bot_conn); let (default_bot_id, _default_bot_name) = crate::bot::get_default_bot(&mut bot_conn);
let llm_url = config_manager let llm_url = config_manager
.get_config(&default_bot_id, "llm-url", Some("http://localhost:8081")) .get_config(&default_bot_id, "llm-url", Some("http://localhost:8081"))
.unwrap_or_else(|_| "http://localhost:8081".to_string()); .unwrap_or_else(|_| "http://localhost:8081".to_string());
let llm_provider = Arc::new(crate::llm::OpenAIClient::new( let llm_provider = Arc::new(crate::llm::OpenAIClient::new(
"empty".to_string(), "empty".to_string(),
Some(llm_url.clone()), Some(llm_url.clone()),
)); ));
let app_state = Arc::new(AppState { let app_state = Arc::new(AppState {
drive: Some(drive), drive: Some(drive),
config: Some(cfg.clone()), config: Some(cfg.clone()),
@ -276,116 +357,75 @@ async fn main() -> std::io::Result<()> {
web_adapter: web_adapter.clone(), web_adapter: web_adapter.clone(),
voice_adapter: voice_adapter.clone(), voice_adapter: voice_adapter.clone(),
}); });
state_tx.send(app_state.clone()).await.ok(); state_tx.send(app_state.clone()).await.ok();
progress_tx.send(BootstrapProgress::BootstrapComplete).ok(); progress_tx.send(BootstrapProgress::BootstrapComplete).ok();
info!( info!(
"Starting HTTP server on {}:{}", "Starting HTTP server on {}:{}",
config.server.host, config.server.port config.server.host, config.server.port
); );
let worker_count = std::thread::available_parallelism() let worker_count = std::thread::available_parallelism()
.map(|n| n.get()) .map(|n| n.get())
.unwrap_or(4); .unwrap_or(4);
let http_handle = { // Mount bots
let app_state = app_state.clone();
let config = config.clone();
let worker_count = worker_count;
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create HTTP runtime");
rt.block_on(async {
let server = HttpServer::new(move || {
let cors = Cors::default()
.allow_any_origin()
.allow_any_method()
.allow_any_header()
.max_age(3600);
let app_state_clone = app_state.clone();
let mut app = App::new()
.wrap(cors)
.wrap(Logger::default())
.wrap(Logger::new("HTTP REQUEST: %a %{User-Agent}i"))
.app_data(web::Data::from(app_state_clone))
.service(auth_handler)
.service(create_session)
.service(get_session_history)
.service(get_sessions)
.service(start_session)
.service(upload_file)
.service(voice_start)
.service(voice_stop)
.service(websocket_handler)
.service(crate::bot::create_bot_handler)
.service(crate::bot::mount_bot_handler)
.service(crate::bot::handle_user_input_handler)
.service(crate::bot::get_user_sessions_handler)
.service(crate::bot::get_conversation_history_handler)
.service(crate::bot::send_warning_handler);
#[cfg(feature = "email")]
{
app = app
.service(get_latest_email_from)
.service(get_emails)
.service(list_emails)
.service(send_email)
.service(save_draft)
.service(save_click);
}
app = app.configure(web_server::configure_app);
app
})
.workers(worker_count)
.bind((config.server.host.clone(), config.server.port))?
.run();
let _ = http_tx.send(());
server.await
})
})
};
let bot_orchestrator = BotOrchestrator::new(app_state.clone()); let bot_orchestrator = BotOrchestrator::new(app_state.clone());
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = bot_orchestrator.mount_all_bots().await { if let Err(e) = bot_orchestrator.mount_all_bots().await {
error!("Failed to mount bots: {}", e); error!("Failed to mount bots: {}", e);
} }
}); });
// Start automation service
let automation_state = app_state.clone(); let automation_state = app_state.clone();
std::thread::spawn(move || { tokio::spawn(async move {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create runtime for automation");
let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move {
let automation = AutomationService::new(automation_state); let automation = AutomationService::new(automation_state);
automation.spawn().await.ok(); automation.spawn().await.ok();
}); });
});
// Start LLM servers
let app_state_for_llm = app_state.clone(); let app_state_for_llm = app_state.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = ensure_llama_servers_running(app_state_for_llm).await { if let Err(e) = ensure_llama_servers_running(app_state_for_llm).await {
error!("Failed to start LLM servers: {}", e); error!("Failed to start LLM servers: {}", e);
} }
}); });
// Handle desktop mode vs server mode
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
if desktop_mode { if desktop_mode {
// Tauri desktop mode // For desktop mode: Run HTTP server in a separate thread with its own runtime
let app_state_for_server = app_state.clone();
let port = config.server.port;
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create HTTP runtime");
rt.block_on(async move {
if let Err(e) = run_http_server(app_state_for_server, port, worker_count).await {
error!("HTTP server error: {}", e);
}
});
});
// Run Tauri on main thread (GUI requires main thread)
let tauri_app = tauri::Builder::default() let tauri_app = tauri::Builder::default()
.setup(|app| { .setup(|app| {
use tauri::WebviewWindowBuilder; use tauri::WebviewWindowBuilder;
match WebviewWindowBuilder::new( match WebviewWindowBuilder::new(
app, app,
"main", "main",
tauri::WebviewUrl::App("index.html".into()), tauri::WebviewUrl::App("index.html".into()),
) )
.build() { .build()
{
Ok(_window) => Ok(()), Ok(_window) => Ok(()),
Err(e) if e.to_string().contains("WebviewLabelAlreadyExists") => { Err(e) if e.to_string().contains("WebviewLabelAlreadyExists") => {
log::warn!("Main window already exists, reusing existing window"); log::warn!("Main window already exists, reusing existing window");
Ok(()) Ok(())
} }
Err(e) => Err(e.into()) Err(e) => Err(e.into()),
} }
}) })
.build(tauri::generate_context!()) .build(tauri::generate_context!())
@ -397,9 +437,17 @@ async fn main() -> std::io::Result<()> {
} }
_ => {} _ => {}
}); });
return Ok(()); return Ok(());
} }
http_handle.join().ok(); // Non-desktop mode: Run HTTP server directly
run_http_server(app_state, config.server.port, worker_count).await?;
// Wait for UI thread to finish if it was started
if let Some(handle) = ui_handle {
handle.join().ok();
}
Ok(()) Ok(())
} }

View file

@ -1,6 +1,6 @@
use actix_files::Files; use actix_files::Files;
use actix_web::{HttpRequest, HttpResponse, Result}; use actix_web::{HttpResponse, Result};
use log::{debug, error}; use log::error;
use std::{fs, path::Path}; use std::{fs, path::Path};
#[actix_web::get("/")] #[actix_web::get("/")]