diff --git a/src/lib.rs b/src/lib.rs index 5075c7f..a6d0c62 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,36 +1,188 @@ -use bottles_core::proto::{self, bottles_server::Bottles, wine_bridge_client::WineBridgeClient}; +use bottles_core::proto::bottles::{self, management_server::{self, Management}}; +use bottles_core::bottle::{Bottle, BottleType}; +use crate::state::SharedState; +use tonic::{Request, Response, Status}; -#[derive(Debug, Default)] -pub struct BottlesService; +pub mod state; +pub mod orchestrator; + +pub struct BottlesService { + state: SharedState, +} + +impl BottlesService { + pub fn new(state: SharedState) -> Self { + Self { state } + } +} #[tonic::async_trait] -impl Bottles for BottlesService { - async fn health( +impl Management for BottlesService { + async fn create_bottle( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + tracing::info!("Received CreateBottle request for: {}", req.name); + let name = req.name; + + let mut state = self.state.write().map_err(|_| Status::internal("Lock error"))?; + + // Validation: Check if exists + if state.bottles.iter().any(|b| b.name == name) { + tracing::warn!("Bottle {} already exists", name); + return Err(Status::already_exists("Bottle already exists")); + } + + // Logic (stub path for now, usually should be derived from config) + let path = std::path::PathBuf::from(format!("/home/mirko/.local/share/bottles/bottles/{}", name)); // TODO: use proper config + let kind = match req.r#type.as_str() { + "Gaming" => BottleType::Gaming, + "Software" => BottleType::Software, + _ => BottleType::Custom, + }; + + let bottle = Bottle::new(name.clone(), path, kind); + + // Save + state.bottles.push(bottle.clone()); + state.save(); + tracing::info!("Bottle {} created successfully", name); + + // Map to Proto + Ok(Response::new(bottles::Bottle { + name: bottle.name, + path: bottle.path.to_string_lossy().to_string(), + r#type: req.r#type, // Simplified mapping + active: false, + config: None, // Default config + })) + } + + async fn delete_bottle( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { - let request = request.get_ref(); - tracing::info!("Received request: {:?}", request); - Ok(tonic::Response::new(proto::HealthResponse { ok: true })) + request: Request, + ) -> Result, Status> { + let name = request.into_inner().name; + tracing::info!("Received DeleteBottle request for: {}", name); + let mut state = self.state.write().map_err(|_| Status::internal("Lock error"))?; + + if let Some(pos) = state.bottles.iter().position(|b| b.name == name) { + state.bottles.remove(pos); + state.save(); + tracing::info!("Bottle {} deleted successfully", name); + Ok(Response::new(bottles::ResultResponse { + success: true, + error_message: String::new(), + })) + } else { + tracing::warn!("Bottle {} not found for deletion", name); + Err(Status::not_found("Bottle not found")) + } } - async fn notify( + async fn list_bottles( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { - let request = request.get_ref(); - tracing::info!("Received request: {:?}", request); - let mut client = WineBridgeClient::connect("http://[::1]:50051") - .await - .map_err(|e| tonic::Status::from_error(Box::new(e)))?; - - let request = proto::MessageRequest { - message: request.message.clone(), - }; - let response = client.message(request).await?; - let response = response.get_ref(); - Ok(tonic::Response::new(proto::NotifyResponse { - success: response.success, + _request: Request, + ) -> Result, Status> { + tracing::info!("Received ListBottles request"); + let state = self.state.read().map_err(|_| Status::internal("Lock error"))?; + + let bottles: Vec = state.bottles.iter().map(|b| { + let active = state.orchestrator.is_running(&b.name); + bottles::Bottle { + name: b.name.clone(), + path: b.path.to_string_lossy().to_string(), + r#type: format!("{:?}", b.kind), + active, + config: None, + } + }).collect(); + + tracing::info!("Returning {} bottles", bottles.len()); + + Ok(Response::new(bottles::ListBottlesResponse { bottles })) + } + + async fn get_bottle( + &self, + request: Request, + ) -> Result, Status> { + let name = request.into_inner().name; + let state = self.state.read().map_err(|_| Status::internal("Lock error"))?; + + let bottle = state.bottles.iter().find(|b| b.name == name) + .ok_or_else(|| Status::not_found("Bottle not found"))?; + + let active = state.orchestrator.is_running(&name); + + Ok(Response::new(bottles::Bottle { + name: bottle.name.clone(), + path: bottle.path.to_string_lossy().to_string(), + r#type: format!("{:?}", bottle.kind), + active, + config: None, + })) + } + + async fn start_bottle( + &self, + request: Request, + ) -> Result, Status> { + let name = request.into_inner().name; + tracing::info!("Starting bottle: {}", name); + + // TODO: In the future, this method will interact with the Orchestrator to: + // 1. Resolve the correct Runner via Component Manager. + // 2. Launch the Agent process. + // 3. Establish gRPC connection with the Agent. + // + // Currently, it updates the internal state to simulate a running bottle + // for protocol verification. + + let state = self.state.read().map_err(|_| Status::internal("Lock error"))?; + // Check existence + if !state.bottles.iter().any(|b| b.name == name) { + return Err(Status::not_found("Bottle not found")); + } + + state.orchestrator.start_bottle(name.clone()) + .map_err(|e| Status::internal(e))?; + + tracing::info!("Bottle {} started (simulated)", name); + + Ok(Response::new(bottles::ResultResponse { + success: true, + error_message: String::new(), })) } + + async fn stop_bottle( + &self, + request: Request, + ) -> Result, Status> { + let name = request.into_inner().name; + tracing::info!("Stopping bottle: {}", name); + + let state = self.state.read().map_err(|_| Status::internal("Lock error"))?; + state.orchestrator.stop_bottle(&name) + .map_err(|e| Status::internal(e))?; + + Ok(Response::new(bottles::ResultResponse { + success: true, + error_message: String::new(), + })) + } + + async fn restart_bottle( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let stop_req = Request::new(bottles::BottleRequest { name: req.name.clone() }); + let _ = self.stop_bottle(stop_req).await; + + let start_req = Request::new(bottles::BottleRequest { name: req.name }); + self.start_bottle(start_req).await + } } diff --git a/src/main.rs b/src/main.rs index 537eace..4d36944 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,31 @@ -use bottles_core::proto::bottles_server::BottlesServer; -use bottles_server::BottlesService; +use bottles_core::proto::bottles::management_server::ManagementServer; +use bottles_server::{BottlesService, state::AppState}; +use std::sync::{Arc, RwLock}; use tracing_subscriber::EnvFilter; #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) + .with_env_filter( + EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info")) + ) .init(); + // Initialize State + let data_path = std::path::PathBuf::from("/home/mirko/.local/share/bottles/next"); + let state = Arc::new(RwLock::new(AppState::new(data_path))); + let addr = "[::1]:50052".parse().unwrap(); - let service = BottlesService::default(); - tracing::info!("Listening on {}", addr); + let service = BottlesService::new(state); + + tracing::info!("Bottles Next Server listening on {}", addr); + tonic::transport::Server::builder() - .add_service(BottlesServer::new(service)) + .add_service(ManagementServer::new(service)) + // .add_service(ConfigurationServer::new(...)) // To be implemented .serve(addr) .await?; + Ok(()) } diff --git a/src/orchestrator.rs b/src/orchestrator.rs new file mode 100644 index 0000000..8685626 --- /dev/null +++ b/src/orchestrator.rs @@ -0,0 +1,56 @@ +use std::collections::HashSet; +use std::sync::{Arc, Mutex}; + +/// The Orchestrator manages the lifecycle of bottles. +/// +/// TODO: This implementation currently serves as a functional mock to validate the gRPC +/// protocol and orchestration flow. It does not yet integrate with the actual Component Manager +/// or spawn real processes (Agent:WineBridge). +/// +/// Future implementation requirements: +/// - Integration with Component Manager for runner/dependency resolution. +/// - Spawning of actual Agent processes. +#[derive(Clone)] +pub struct Orchestrator { + // In a real implementation, this would hold handles to Child processes or gRPC clients. + running_bottles: Arc>>, +} + +impl Orchestrator { + pub fn new() -> Self { + Self { + running_bottles: Arc::new(Mutex::new(HashSet::new())), + } + } + + pub fn start_bottle(&self, name: String) -> Result<(), String> { + let mut bottles = self.running_bottles.lock().map_err(|_| "Lock poisoning")?; + + if bottles.contains(&name) { + return Err("Bottle is already running".to_string()); + } + + // TODO: Real process spawning logic goes here. + // - Resolve Runner via Component Manager. + // - Launch Agent process. + // - Wait for Agent readiness. + + bottles.insert(name); + Ok(()) + } + + pub fn stop_bottle(&self, name: &str) -> Result<(), String> { + let mut bottles = self.running_bottles.lock().map_err(|_| "Lock poisoning")?; + + if !bottles.remove(name) { + return Err("Bottle is not running".to_string()); + } + + Ok(()) + } + + pub fn is_running(&self, name: &str) -> bool { + let bottles = self.running_bottles.lock().unwrap(); + bottles.contains(name) + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..0dc3d8d --- /dev/null +++ b/src/state.rs @@ -0,0 +1,36 @@ +use bottles_core::bottle::Bottle; +use bottles_core::persistence::Persistence; +use std::sync::{Arc, RwLock}; +use std::path::PathBuf; +use crate::orchestrator::Orchestrator; + +pub struct AppState { + pub bottles: Vec, + pub persistence: Persistence, + pub orchestrator: Orchestrator, +} + +impl AppState { + pub fn new(data_path: PathBuf) -> Self { + let persistence = Persistence::new(data_path); + let bottles = persistence.load_bottles().unwrap_or_else(|e| { + tracing::error!("Failed to load bottles: {}", e); + Vec::new() + }); + let orchestrator = Orchestrator::new(); + + Self { + bottles, + persistence, + orchestrator, + } + } + + pub fn save(&self) { + if let Err(e) = self.persistence.save_bottles(&self.bottles) { + tracing::error!("Failed to save bottles: {}", e); + } + } +} + +pub type SharedState = Arc>;