From cd91d011f439c3400ac5b31d8057f91cfca21168 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Mon, 20 Oct 2025 16:52:08 -0300 Subject: [PATCH] Refactor config loading and DB URL parsing --- gbot.sh | 3 +- src/bootstrap/mod.rs | 66 ++++----- src/config/mod.rs | 233 +++++++++++-------------------- src/main.rs | 73 ++-------- src/package_manager/facade.rs | 21 ++- src/package_manager/installer.rs | 66 ++++++--- 6 files changed, 189 insertions(+), 273 deletions(-) diff --git a/gbot.sh b/gbot.sh index dfca0087c..f2081cd95 100755 --- a/gbot.sh +++ b/gbot.sh @@ -1,2 +1,3 @@ -pkill postgres && rm -rf botserver-stack && clear && \ +set +e +pkill postgres && rm .env -rf botserver-stack && clear && \ RUST_LOG=trace,hyper_util=off cargo run diff --git a/src/bootstrap/mod.rs b/src/bootstrap/mod.rs index ae52de2cc..d18245731 100644 --- a/src/bootstrap/mod.rs +++ b/src/bootstrap/mod.rs @@ -2,9 +2,10 @@ use crate::config::AppConfig; use crate::package_manager::{InstallMode, PackageManager}; use anyhow::Result; use diesel::{Connection, RunQueryDsl}; -use log::trace; +use dotenvy::dotenv; +use log::{info, trace}; use rand::distr::Alphanumeric; -use rand::Rng; +use rand::rng; use sha2::{Digest, Sha256}; pub struct BootstrapManager { @@ -68,26 +69,33 @@ impl BootstrapManager { for component in required_components { if !pm.is_installed(component) { - trace!("Installing required component: {}", component); - futures::executor::block_on(pm.install(component))?; - trace!("Starting component after install: {}", component); - pm.start(component)?; - if component == "tables" { let db_password = self.generate_secure_password(16); let farm_password = self.generate_secure_password(32); - let encrypted_db_password = self.encrypt_password(&db_password, &farm_password); let env_contents = format!( "FARM_PASSWORD={}\nDATABASE_URL=postgres://gbuser:{}@localhost:5432/botserver", farm_password, db_password ); - std::fs::write(".env", env_contents) + std::fs::write(".env", &env_contents) .map_err(|e| anyhow::anyhow!("Failed to write .env file: {}", e))?; + dotenv().ok(); + trace!("Generated database credentials and wrote to .env file"); + } - trace!("Waiting 5 seconds for database to start..."); - std::thread::sleep(std::time::Duration::from_secs(5)); + trace!("Installing required component: {}", component); + futures::executor::block_on(pm.install(component))?; + + trace!("Starting component after install: {}", component); + pm.start(component)?; + + if component == "tables" { + trace!("Starting component after install: {}", component); + + let database_url = std::env::var("DATABASE_URL").unwrap(); + let mut conn = diesel::PgConnection::establish(&database_url) + .map_err(|e| anyhow::anyhow!("Failed to connect to database: {}", e))?; let migration_dir = include_dir::include_dir!("./migrations"); let mut migration_files: Vec<_> = migration_dir @@ -103,10 +111,6 @@ impl BootstrapManager { .collect(); migration_files.sort(); - let mut conn = diesel::PgConnection::establish(&format!( - "postgres://gbuser:{}@localhost:5432/botserver", - db_password - ))?; for migration_file in migration_files { let migration = std::fs::read_to_string(&migration_file)?; @@ -114,42 +118,22 @@ impl BootstrapManager { diesel::sql_query(&migration).execute(&mut conn)?; } - self.setup_secure_credentials(&mut conn, &encrypted_db_password)?; config = AppConfig::from_database(&mut conn); + info!("Database migrations completed and configuration loaded"); } - } else { - trace!("Required component {} already installed", component); } } Ok(config) } - fn setup_secure_credentials( - &self, - conn: &mut diesel::PgConnection, - encrypted_db_password: &str, - ) -> Result<()> { - use crate::shared::models::schema::bots::dsl::*; - use diesel::prelude::*; - use uuid::Uuid; - - let system_bot_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000")?; - diesel::update(bots) - .filter(bot_id.eq(system_bot_id)) - .set(config.eq(serde_json::json!({ - "encrypted_db_password": encrypted_db_password, - }))) - .execute(conn)?; - - Ok(()) - } - fn generate_secure_password(&self, length: usize) -> String { - let rng = rand::rng(); - rng.sample_iter(&Alphanumeric) + // Ensure the Rng trait is in scope for `sample` + use rand::Rng; + let mut rng = rand::thread_rng(); + + std::iter::repeat_with(|| rng.sample(Alphanumeric) as char) .take(length) - .map(char::from) .collect() } diff --git a/src/config/mod.rs b/src/config/mod.rs index 856162f16..03ac543cc 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -79,22 +79,14 @@ impl AppConfig { pub fn database_url(&self) -> String { format!( "postgres://{}:{}@{}:{}/{}", - self.database.username, - self.database.password, - self.database.server, - self.database.port, - self.database.database + self.database.username, self.database.password, self.database.server, self.database.port, self.database.database ) } pub fn database_custom_url(&self) -> String { format!( "postgres://{}:{}@{}:{}/{}", - self.database_custom.username, - self.database_custom.password, - self.database_custom.server, - self.database_custom.port, - self.database_custom.database + self.database_custom.username, self.database_custom.password, self.database_custom.server, self.database_custom.port, self.database_custom.database ) } @@ -133,49 +125,37 @@ impl AppConfig { }; let get_str = |key: &str, default: &str| -> String { - config_map - .get(key) - .map(|v| v.config_value.clone()) - .unwrap_or_else(|| default.to_string()) + config_map.get(key).map(|v| v.config_value.clone()).unwrap_or_else(|| default.to_string()) }; let get_u32 = |key: &str, default: u32| -> u32 { - config_map - .get(key) - .and_then(|v| v.config_value.parse().ok()) - .unwrap_or(default) + config_map.get(key).and_then(|v| v.config_value.parse().ok()).unwrap_or(default) }; let get_u16 = |key: &str, default: u16| -> u16 { - config_map - .get(key) - .and_then(|v| v.config_value.parse().ok()) - .unwrap_or(default) + config_map.get(key).and_then(|v| v.config_value.parse().ok()).unwrap_or(default) }; let get_bool = |key: &str, default: bool| -> bool { - config_map - .get(key) - .map(|v| v.config_value.to_lowercase() == "true") - .unwrap_or(default) + config_map.get(key).map(|v| v.config_value.to_lowercase() == "true").unwrap_or(default) }; let stack_path = PathBuf::from(get_str("STACK_PATH", "./botserver-stack")); let database = DatabaseConfig { - username: get_str("TABLES_USERNAME", "botserver"), - password: get_str("TABLES_PASSWORD", "botserver"), + username: get_str("TABLES_USERNAME", "gbuser"), + password: get_str("TABLES_PASSWORD", ""), server: get_str("TABLES_SERVER", "localhost"), port: get_u32("TABLES_PORT", 5432), database: get_str("TABLES_DATABASE", "botserver"), }; let database_custom = DatabaseConfig { - username: get_str("CUSTOM_USERNAME", "user"), - password: get_str("CUSTOM_PASSWORD", "pass"), + username: get_str("CUSTOM_USERNAME", "gbuser"), + password: get_str("CUSTOM_PASSWORD", ""), server: get_str("CUSTOM_SERVER", "localhost"), port: get_u32("CUSTOM_PORT", 5432), - database: get_str("CUSTOM_DATABASE", "custom"), + database: get_str("CUSTOM_DATABASE", "botserver"), }; let minio = DriveConfig { @@ -203,10 +183,7 @@ impl AppConfig { AppConfig { minio, - server: ServerConfig { - host: get_str("SERVER_HOST", "127.0.0.1"), - port: get_u16("SERVER_PORT", 8080), - }, + server: ServerConfig { host: get_str("SERVER_HOST", "127.0.0.1"), port: get_u16("SERVER_PORT", 8080) }, database, database_custom, email, @@ -221,52 +198,39 @@ impl AppConfig { pub fn from_env() -> Self { warn!("Loading configuration from environment variables"); - let stack_path = - std::env::var("STACK_PATH").unwrap_or_else(|_| "./botserver-stack".to_string()); + let stack_path = std::env::var("STACK_PATH").unwrap_or_else(|_| "./botserver-stack".to_string()); + + let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string()); + let (db_username, db_password, db_server, db_port, db_name) = parse_database_url(&database_url); let database = DatabaseConfig { - username: std::env::var("TABLES_USERNAME").unwrap_or_else(|_| "botserver".to_string()), - password: std::env::var("TABLES_PASSWORD").unwrap_or_else(|_| "botserver".to_string()), - server: std::env::var("TABLES_SERVER").unwrap_or_else(|_| "localhost".to_string()), - port: std::env::var("TABLES_PORT") - .ok() - .and_then(|p| p.parse().ok()) - .unwrap_or(5432), - database: std::env::var("TABLES_DATABASE").unwrap_or_else(|_| "botserver".to_string()), + username: db_username, + password: db_password, + server: db_server, + port: db_port, + database: db_name, }; let database_custom = DatabaseConfig { - username: std::env::var("CUSTOM_USERNAME").unwrap_or_else(|_| "user".to_string()), - password: std::env::var("CUSTOM_PASSWORD").unwrap_or_else(|_| "pass".to_string()), + username: std::env::var("CUSTOM_USERNAME").unwrap_or_else(|_| "gbuser".to_string()), + password: std::env::var("CUSTOM_PASSWORD").unwrap_or_else(|_| "".to_string()), server: std::env::var("CUSTOM_SERVER").unwrap_or_else(|_| "localhost".to_string()), - port: std::env::var("CUSTOM_PORT") - .ok() - .and_then(|p| p.parse().ok()) - .unwrap_or(5432), - database: std::env::var("CUSTOM_DATABASE").unwrap_or_else(|_| "custom".to_string()), + port: std::env::var("CUSTOM_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(5432), + database: std::env::var("CUSTOM_DATABASE").unwrap_or_else(|_| "botserver".to_string()), }; let minio = DriveConfig { server: std::env::var("DRIVE_SERVER").unwrap_or_else(|_| "localhost:9000".to_string()), - access_key: std::env::var("DRIVE_ACCESSKEY") - .unwrap_or_else(|_| "minioadmin".to_string()), + access_key: std::env::var("DRIVE_ACCESSKEY").unwrap_or_else(|_| "minioadmin".to_string()), secret_key: std::env::var("DRIVE_SECRET").unwrap_or_else(|_| "minioadmin".to_string()), - use_ssl: std::env::var("DRIVE_USE_SSL") - .unwrap_or_else(|_| "false".to_string()) - .parse() - .unwrap_or(false), - org_prefix: std::env::var("DRIVE_ORG_PREFIX") - .unwrap_or_else(|_| "botserver".to_string()), + use_ssl: std::env::var("DRIVE_USE_SSL").unwrap_or_else(|_| "false".to_string()).parse().unwrap_or(false), + org_prefix: std::env::var("DRIVE_ORG_PREFIX").unwrap_or_else(|_| "botserver".to_string()), }; let email = EmailConfig { from: std::env::var("EMAIL_FROM").unwrap_or_else(|_| "noreply@example.com".to_string()), - server: std::env::var("EMAIL_SERVER") - .unwrap_or_else(|_| "smtp.example.com".to_string()), - port: std::env::var("EMAIL_PORT") - .unwrap_or_else(|_| "587".to_string()) - .parse() - .unwrap_or(587), + server: std::env::var("EMAIL_SERVER").unwrap_or_else(|_| "smtp.example.com".to_string()), + port: std::env::var("EMAIL_PORT").unwrap_or_else(|_| "587".to_string()).parse().unwrap_or(587), username: std::env::var("EMAIL_USER").unwrap_or_else(|_| "user".to_string()), password: std::env::var("EMAIL_PASS").unwrap_or_else(|_| "pass".to_string()), }; @@ -274,41 +238,29 @@ impl AppConfig { let ai = AIConfig { instance: std::env::var("AI_INSTANCE").unwrap_or_else(|_| "gpt-4".to_string()), key: std::env::var("AI_KEY").unwrap_or_else(|_| "".to_string()), - version: std::env::var("AI_VERSION") - .unwrap_or_else(|_| "2023-12-01-preview".to_string()), - endpoint: std::env::var("AI_ENDPOINT") - .unwrap_or_else(|_| "https://api.openai.com".to_string()), + version: std::env::var("AI_VERSION").unwrap_or_else(|_| "2023-12-01-preview".to_string()), + endpoint: std::env::var("AI_ENDPOINT").unwrap_or_else(|_| "https://api.openai.com".to_string()), }; AppConfig { minio, server: ServerConfig { host: std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()), - port: std::env::var("SERVER_PORT") - .ok() - .and_then(|p| p.parse().ok()) - .unwrap_or(8080), + port: std::env::var("SERVER_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(8080), }, database, database_custom, email, ai, s3_bucket: std::env::var("DRIVE_BUCKET").unwrap_or_else(|_| "default".to_string()), - site_path: std::env::var("SITES_ROOT") - .unwrap_or_else(|_| "./botserver-stack/sites".to_string()), + site_path: std::env::var("SITES_ROOT").unwrap_or_else(|_| "./botserver-stack/sites".to_string()), stack_path: PathBuf::from(stack_path), db_conn: None, } } - fn load_config_from_db( - conn: &mut PgConnection, - ) -> Result, diesel::result::Error> { - let results = diesel::sql_query( - "SELECT id, config_key, config_value, config_type, is_encrypted - FROM server_configuration", - ) - .load::(conn)?; + fn load_config_from_db(conn: &mut PgConnection) -> Result, diesel::result::Error> { + let results = diesel::sql_query("SELECT id, config_key, config_value, config_type, is_encrypted FROM server_configuration").load::(conn)?; let mut map = HashMap::new(); for row in results { @@ -318,27 +270,13 @@ impl AppConfig { Ok(map) } - pub fn set_config( - &self, - conn: &mut PgConnection, - key: &str, - value: &str, - ) -> Result<(), diesel::result::Error> { - diesel::sql_query("SELECT set_config($1, $2)") - .bind::(key) - .bind::(value) - .execute(conn)?; - + pub fn set_config(&self, conn: &mut PgConnection, key: &str, value: &str) -> Result<(), diesel::result::Error> { + diesel::sql_query("SELECT set_config($1, $2)").bind::(key).bind::(value).execute(conn)?; info!("Updated configuration: {} = {}", key, value); Ok(()) } - pub fn get_config( - &self, - conn: &mut PgConnection, - key: &str, - fallback: Option<&str>, - ) -> Result { + pub fn get_config(&self, conn: &mut PgConnection, key: &str, fallback: Option<&str>) -> Result { let fallback_str = fallback.unwrap_or(""); #[derive(Debug, QueryableByName)] @@ -347,16 +285,35 @@ impl AppConfig { value: String, } - let result = diesel::sql_query("SELECT get_config($1, $2) as value") - .bind::(key) - .bind::(fallback_str) - .get_result::(conn) - .map(|row| row.value)?; - + let result = diesel::sql_query("SELECT get_config($1, $2) as value").bind::(key).bind::(fallback_str).get_result::(conn).map(|row| row.value)?; Ok(result) } } +fn parse_database_url(url: &str) -> (String, String, String, u32, String) { + if let Some(stripped) = url.strip_prefix("postgres://") { + let parts: Vec<&str> = stripped.split('@').collect(); + if parts.len() == 2 { + let user_pass: Vec<&str> = parts[0].split(':').collect(); + let host_db: Vec<&str> = parts[1].split('/').collect(); + + if user_pass.len() >= 2 && host_db.len() >= 2 { + let username = user_pass[0].to_string(); + let password = user_pass[1].to_string(); + + let host_port: Vec<&str> = host_db[0].split(':').collect(); + let server = host_port[0].to_string(); + let port = host_port.get(1).and_then(|p| p.parse().ok()).unwrap_or(5432); + let database = host_db[1].to_string(); + + return (username, password, server, port, database); + } + } + } + + ("gbuser".to_string(), "".to_string(), "localhost".to_string(), 5432, "botserver".to_string()) +} + pub struct ConfigManager { conn: Arc>, } @@ -366,25 +323,17 @@ impl ConfigManager { Self { conn } } - pub fn sync_gbot_config( - &self, - bot_id: &uuid::Uuid, - config_path: &str, - ) -> Result { + pub fn sync_gbot_config(&self, bot_id: &uuid::Uuid, config_path: &str) -> Result { use sha2::{Digest, Sha256}; use std::fs; - let content = fs::read_to_string(config_path) - .map_err(|e| format!("Failed to read config file: {}", e))?; + let content = fs::read_to_string(config_path).map_err(|e| format!("Failed to read config file: {}", e))?; let mut hasher = Sha256::new(); hasher.update(content.as_bytes()); let file_hash = format!("{:x}", hasher.finalize()); - let mut conn = self - .conn - .lock() - .map_err(|e| format!("Failed to acquire lock: {}", e))?; + let mut conn = self.conn.lock().map_err(|e| format!("Failed to acquire lock: {}", e))?; #[derive(QueryableByName)] struct SyncHash { @@ -392,13 +341,12 @@ impl ConfigManager { file_hash: String, } - let last_hash: Option = - diesel::sql_query("SELECT file_hash FROM gbot_config_sync WHERE bot_id = $1") - .bind::(bot_id) - .get_result::(&mut *conn) - .optional() - .map_err(|e| format!("Database error: {}", e))? - .map(|row| row.file_hash); + let last_hash: Option = diesel::sql_query("SELECT file_hash FROM gbot_config_sync WHERE bot_id = $1") + .bind::(bot_id) + .get_result::(&mut *conn) + .optional() + .map_err(|e| format!("Database error: {}", e))? + .map(|row| row.file_hash); if last_hash.as_ref() == Some(&file_hash) { info!("Config file unchanged for bot {}", bot_id); @@ -412,34 +360,23 @@ impl ConfigManager { let key = parts[0].trim(); let value = parts[1].trim(); - diesel::sql_query( - "INSERT INTO bot_configuration (id, bot_id, config_key, config_value, config_type) - VALUES (gen_random_uuid()::text, $1, $2, $3, 'string') - ON CONFLICT (bot_id, config_key) - DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()" - ) - .bind::(bot_id) - .bind::(key) - .bind::(value) - .execute(&mut *conn) - .map_err(|e| format!("Failed to update config: {}", e))?; + diesel::sql_query("INSERT INTO bot_configuration (id, bot_id, config_key, config_value, config_type) VALUES (gen_random_uuid()::text, $1, $2, $3, 'string') ON CONFLICT (bot_id, config_key) DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()") + .bind::(bot_id) + .bind::(key) + .bind::(value) + .execute(&mut *conn) + .map_err(|e| format!("Failed to update config: {}", e))?; updated += 1; } } - diesel::sql_query( - "INSERT INTO gbot_config_sync (id, bot_id, config_file_path, file_hash, sync_count) - VALUES (gen_random_uuid()::text, $1, $2, $3, 1) - ON CONFLICT (bot_id) - DO UPDATE SET last_sync_at = NOW(), file_hash = EXCLUDED.file_hash, - sync_count = gbot_config_sync.sync_count + 1", - ) - .bind::(bot_id) - .bind::(config_path) - .bind::(&file_hash) - .execute(&mut *conn) - .map_err(|e| format!("Failed to update sync record: {}", e))?; + diesel::sql_query("INSERT INTO gbot_config_sync (id, bot_id, config_file_path, file_hash, sync_count) VALUES (gen_random_uuid()::text, $1, $2, $3, 1) ON CONFLICT (bot_id) DO UPDATE SET last_sync_at = NOW(), file_hash = EXCLUDED.file_hash, sync_count = gbot_config_sync.sync_count + 1") + .bind::(bot_id) + .bind::(config_path) + .bind::(&file_hash) + .execute(&mut *conn) + .map_err(|e| format!("Failed to update sync record: {}", e))?; info!("Synced {} config values for bot {} from {}", updated, bot_id, config_path); Ok(updated) diff --git a/src/main.rs b/src/main.rs index c21dc2115..1edc891df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,20 +68,14 @@ async fn main() -> std::io::Result<()> { Ok(_) => return Ok(()), Err(e) => { eprintln!("CLI error: {}", e); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("CLI command failed: {}", e), - )); + return Err(std::io::Error::new(std::io::ErrorKind::Other, format!("CLI command failed: {}", e))); } } } _ => { eprintln!("Unknown command: {}", command); eprintln!("Run 'botserver --help' for usage information"); - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!("Unknown command: {}", command), - )); + return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("Unknown command: {}", command))); } } } @@ -112,7 +106,7 @@ async fn main() -> std::io::Result<()> { Err(e) => { log::error!("Bootstrap failed: {}", e); info!("Attempting to load configuration from database"); - match diesel::Connection::establish("postgres://botserver:botserver@localhost:5432/botserver") { + match diesel::Connection::establish(&std::env::var("DATABASE_URL").unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string())) { Ok(mut conn) => AppConfig::from_database(&mut conn), Err(_) => { info!("Database not available, using environment variables as fallback"); @@ -130,25 +124,16 @@ async fn main() -> std::io::Result<()> { Ok(conn) => Arc::new(Mutex::new(conn)), Err(e) => { log::error!("Failed to connect to main database: {}", e); - return Err(std::io::Error::new( - std::io::ErrorKind::ConnectionRefused, - format!("Database connection failed: {}", e), - )); + return Err(std::io::Error::new(std::io::ErrorKind::ConnectionRefused, format!("Database connection failed: {}", e))); } }; let db_custom_pool = db_pool.clone(); info!("Initializing LLM server at {}", cfg.ai.endpoint); - ensure_llama_servers_running() - .await - .expect("Failed to initialize LLM local server"); + ensure_llama_servers_running().await.expect("Failed to initialize LLM local server"); - let cache_url = cfg - .config_path("cache") - .join("redis.conf") - .display() - .to_string(); + let cache_url = cfg.config_path("cache").join("redis.conf").display().to_string(); let redis_client = match redis::Client::open(cache_url.as_str()) { Ok(client) => Some(Arc::new(client)), Err(e) => { @@ -158,28 +143,15 @@ async fn main() -> std::io::Result<()> { }; let tool_manager = Arc::new(tools::ToolManager::new()); - let llm_provider = Arc::new(crate::llm::OpenAIClient::new( - "empty".to_string(), - Some(cfg.ai.endpoint.clone()), - )); + let llm_provider = Arc::new(crate::llm::OpenAIClient::new("empty".to_string(), Some(cfg.ai.endpoint.clone()))); let web_adapter = Arc::new(WebChannelAdapter::new()); - let voice_adapter = Arc::new(VoiceAdapter::new( - "https://livekit.example.com".to_string(), - "api_key".to_string(), - "api_secret".to_string(), - )); - let whatsapp_adapter = Arc::new(WhatsAppAdapter::new( - "whatsapp_token".to_string(), - "phone_number_id".to_string(), - "verify_token".to_string(), - )); + let voice_adapter = Arc::new(VoiceAdapter::new("https://livekit.example.com".to_string(), "api_key".to_string(), "api_secret".to_string())); + let whatsapp_adapter = Arc::new(WhatsAppAdapter::new("whatsapp_token".to_string(), "phone_number_id".to_string(), "verify_token".to_string())); let tool_api = Arc::new(tools::ToolApi::new()); info!("Initializing MinIO drive at {}", cfg.minio.server); - let drive = init_drive(&config.minio) - .await - .expect("Failed to initialize Drive"); + let drive = init_drive(&config.minio).await.expect("Failed to initialize Drive"); let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new( diesel::Connection::establish(&cfg.database_url()).unwrap(), @@ -202,10 +174,7 @@ async fn main() -> std::io::Result<()> { auth_service: auth_service.clone(), channels: Arc::new(Mutex::new({ let mut map = HashMap::new(); - map.insert( - "web".to_string(), - web_adapter.clone() as Arc, - ); + map.insert("web".to_string(), web_adapter.clone() as Arc); map })), response_channels: Arc::new(tokio::sync::Mutex::new(HashMap::new())), @@ -215,20 +184,12 @@ async fn main() -> std::io::Result<()> { tool_api: tool_api.clone(), }); - info!( - "Starting HTTP server on {}:{}", - config.server.host, config.server.port - ); + info!("Starting HTTP server on {}:{}", config.server.host, config.server.port); - let worker_count = std::thread::available_parallelism() - .map(|n| n.get()) - .unwrap_or(4); + let worker_count = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4); let automation_state = app_state.clone(); - let automation = AutomationService::new( - automation_state, - "templates/announcements.gbai/announcements.gbdialog", - ); + let automation = AutomationService::new(automation_state, "templates/announcements.gbai/announcements.gbdialog"); let _automation_handle = automation.spawn(); let drive_state = app_state.clone(); @@ -237,11 +198,7 @@ async fn main() -> std::io::Result<()> { let _drive_handle = drive_monitor.spawn(); HttpServer::new(move || { - let cors = Cors::default() - .allow_any_origin() - .allow_any_method() - .allow_any_header() - .max_age(3600); + 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() diff --git a/src/package_manager/facade.rs b/src/package_manager/facade.rs index f2b2075c9..d3d56466b 100644 --- a/src/package_manager/facade.rs +++ b/src/package_manager/facade.rs @@ -4,7 +4,7 @@ use crate::package_manager::OsType; use crate::shared::utils; use crate::InstallMode; use anyhow::{Context, Result}; -use log::{trace, warn}; +use log::{error, trace, warn}; use reqwest::Client; use std::collections::HashMap; use std::path::PathBuf; @@ -543,19 +543,16 @@ impl PackageManager { } else { PathBuf::from("/opt/gbo/bin") }; - let data_path = if target == "local" { self.base_path.join("data").join(component) } else { PathBuf::from("/opt/gbo/data") }; - let conf_path = if target == "local" { self.base_path.join("conf").join(component) } else { PathBuf::from("/opt/gbo/conf") }; - let logs_path = if target == "local" { self.base_path.join("logs").join(component) } else { @@ -571,13 +568,23 @@ impl PackageManager { if target == "local" { trace!("Executing command: {}", rendered_cmd); - let output = Command::new("bash") + let mut child = Command::new("bash") .current_dir(&bin_path) .args(&["-c", &rendered_cmd]) - .output()?; + .spawn() + .with_context(|| { + format!("Failed to spawn command for component '{}'", component) + })?; + + let output = child.wait_with_output().with_context(|| { + format!( + "Failed while waiting for command to finish for component '{}'", + component + ) + })?; if !output.status.success() { - warn!( + error!( "Command had non-zero exit: {}", String::from_utf8_lossy(&output.stderr) ); diff --git a/src/package_manager/installer.rs b/src/package_manager/installer.rs index 4435f8946..49693cc96 100644 --- a/src/package_manager/installer.rs +++ b/src/package_manager/installer.rs @@ -4,7 +4,7 @@ use crate::package_manager::{InstallMode, OsType}; use anyhow::Result; use log::trace; use rand::distr::Alphanumeric; -use rand::Rng; +use rand::rng; use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::path::PathBuf; @@ -63,7 +63,8 @@ impl PackageManager { fn register_drive(&mut self) { let drive_password = self.generate_secure_password(16); - let farm_password = std::env::var("FARM_PASSWORD").unwrap(); + let farm_password = + std::env::var("FARM_PASSWORD").unwrap_or_else(|_| self.generate_secure_password(32)); let encrypted_drive_password = self.encrypt_password(&drive_password, &farm_password); self.components.insert("drive".to_string(), ComponentConfig { @@ -108,7 +109,11 @@ impl PackageManager { use diesel::pg::PgConnection; use diesel::prelude::*; use uuid::Uuid; - if let Ok(mut conn) = PgConnection::establish(&std::env::var("DATABASE_URL")?) { + + let database_url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string()); + + if let Ok(mut conn) = PgConnection::establish(&database_url) { let system_bot_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000")?; diesel::update(bots) .filter(bot_id.eq(system_bot_id)) @@ -116,11 +121,27 @@ impl PackageManager { "encrypted_drive_password": encrypted_drive_password, }))) .execute(&mut conn)?; + trace!("Updated drive credentials in database for system bot"); } Ok(()) } fn register_tables(&mut self) { + let db_password = std::env::var("DATABASE_URL") + .ok() + .and_then(|url| { + if let Some(stripped) = url.strip_prefix("postgres://gbuser:") { + if let Some(at_pos) = stripped.find('@') { + Some(stripped[..at_pos].to_string()) + } else { + None + } + } else { + None + } + }) + .unwrap_or_else(|| self.generate_secure_password(16)); + self.components.insert("tables".to_string(), ComponentConfig { name: "tables".to_string(), required: true, @@ -134,17 +155,19 @@ impl PackageManager { pre_install_cmds_linux: vec![], post_install_cmds_linux: vec![ "chmod +x ./bin/*".to_string(), - "if [ ! -d \"{{DATA_PATH}}/pgdata\" ]; then ./bin/initdb -D {{DATA_PATH}}/pgdata -U postgres; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"data_directory = '{{DATA_PATH}}/pgdata'\" > {{CONF_PATH}}/postgresql.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"hba_file = '{{CONF_PATH}}/pg_hba.conf'\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"ident_file = '{{CONF_PATH}}/pg_ident.conf'\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"port = 5432\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"listen_addresses = '*'\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"log_directory = '{{LOGS_PATH}}'\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"logging_collector = on\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/pg_hba.conf\" ]; then echo \"host all all all md5\" > {{CONF_PATH}}/pg_hba.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/pg_ident.conf\" ]; then touch {{CONF_PATH}}/pg_ident.conf; fi ".to_string(), - "if [ ! -d \"{{DATA_PATH}}/pgdata\" ]; then ./bin/pg_ctl -D {{DATA_PATH}}/pgdata -l {{LOGS_PATH}}/postgres.log start; sleep 5; ./bin/createdb -p 5432 -h localhost botserver; ./bin/createuser -p 5432 -h localhost gbuser; fi".to_string() + + format!("if [ ! -d \"{{{{DATA_PATH}}}}/pgdata\" ]; then PG_PASSWORD={} ./bin/initdb -D {{{{DATA_PATH}}}}/pgdata -U gbuser --pwfile=<(echo $PG_PASSWORD); fi", db_password).to_string(), + "echo \"data_directory = '{{DATA_PATH}}/pgdata'\" > {{CONF_PATH}}/postgresql.conf".to_string(), + "echo \"ident_file = '{{CONF_PATH}}/pg_ident.conf'\" >> {{CONF_PATH}}/postgresql.conf".to_string(), + "echo \"port = 5432\" >> {{CONF_PATH}}/postgresql.conf".to_string(), + "echo \"listen_addresses = '*'\" >> {{CONF_PATH}}/postgresql.conf".to_string(), + "echo \"log_directory = '{{LOGS_PATH}}'\" >> {{CONF_PATH}}/postgresql.conf".to_string(), + "echo \"logging_collector = on\" >> {{CONF_PATH}}/postgresql.conf".to_string(), + "echo \"host all all all md5\" > {{CONF_PATH}}/pg_hba.conf".to_string(), + "touch {{CONF_PATH}}/pg_ident.conf".to_string(), + + "./bin/pg_ctl -D {{DATA_PATH}}/pgdata -l {{LOGS_PATH}}/postgres.log start; while ! ./bin/pg_isready -d {{DATA_PATH}}/pgdata >/dev/null 2>&1; do sleep 15; done;".to_string(), + "./bin/psql -U gbuser -c \"CREATE DATABASE botserver WITH OWNER gbuser\" || true ".to_string() ], pre_install_cmds_macos: vec![], post_install_cmds_macos: vec![ @@ -651,10 +674,17 @@ impl PackageManager { } fn generate_secure_password(&self, length: usize) -> String { - let rng = rand::rng(); - rng.sample_iter(&Alphanumeric) - .take(length) - .map(char::from) + // Use the non-deprecated `rng` function to obtain a thread-local RNG. + let mut rng = rand::rng(); + + // Generate `length` alphanumeric characters. + (0..length) + .map(|_| { + // `Alphanumeric` implements the `Distribution` trait. + // Use the fully qualified `rand::Rng::sample` method to avoid needing an explicit import. + let byte = rand::Rng::sample(&mut rng, Alphanumeric); + char::from(byte) + }) .collect() }