use std::sync::Arc; use crate::{control, event::EventManager, router::build_router}; use axum::extract::FromRef; use plc_platform_core::{bootstrap, websocket::WebSocketManager}; use plc_platform_core::{ config::ServerConfig, connection::ConnectionManager, platform_context::PlatformContext, }; use tokio::sync::mpsc; #[derive(Clone)] pub struct AppState { pub config: ServerConfig, pub platform: PlatformContext, pub event_manager: Arc, pub control_runtime: Arc, } impl FromRef for PlatformContext { fn from_ref(state: &AppState) -> Self { state.platform.clone() } } pub async fn run() { let Some(_single_instance) = bootstrap::init_process( "PLCControl.FeederDistributor", "Another feeder distributor instance is already running", ) else { return; }; let config = ServerConfig::from_env("HOST", "0.0.0.0", "PORT", 60309) .expect("Failed to load server configuration"); let builder = bootstrap::bootstrap_platform(&config.database_url) .await .expect("Failed to bootstrap platform"); let control_runtime = Arc::new(control::runtime::ControlRuntimeStore::new()); let platform = builder.build(); let event_manager = Arc::new(EventManager::new( platform.pool.clone(), Some(platform.ws_manager.clone()), platform.metadata.clone(), )); bootstrap::connect_all_enabled_sources(&platform) .await .expect("Failed to connect enabled sources"); let state = AppState { config: config.clone(), platform, event_manager, control_runtime: control_runtime.clone(), }; control::engine::start(state.clone(), control_runtime); if control::simulate::enabled() { tracing::info!("SIMULATE_PLC enabled: starting chaos simulation"); control::simulate::start(state.clone()); } let app = build_router(state.clone()); let ui_url = config.local_ui_url(); let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>(1); let rt_handle = tokio::runtime::Handle::current(); init_tray(ui_url, shutdown_tx.clone(), rt_handle); let connection_manager_for_shutdown = state.platform.connection_manager.clone(); bootstrap::install_ctrl_c_shutdown(shutdown_tx); bootstrap::serve_app_with_graceful_shutdown( &config, "feeder distributor", app, bootstrap::disconnect_all_on_shutdown( shutdown_rx, connection_manager_for_shutdown, "feeder", ), ) .await .unwrap(); } pub fn test_state() -> AppState { let database_url = "postgres://plc:plc@localhost/plc_control_test".to_string(); let pool = sqlx::postgres::PgPoolOptions::new() .connect_lazy(&database_url) .expect("lazy pool should build"); let connection_manager = Arc::new(ConnectionManager::new()); let ws_manager = Arc::new(WebSocketManager::new()); let platform = PlatformContext::new(pool.clone(), connection_manager, ws_manager.clone()); let event_manager = Arc::new(EventManager::new( pool, Some(ws_manager), platform.metadata.clone(), )); AppState { config: ServerConfig { database_url, server_host: "127.0.0.1".to_string(), server_port: 0, }, platform, event_manager, control_runtime: Arc::new(control::runtime::ControlRuntimeStore::new()), } } #[cfg(windows)] fn init_tray(ui_url: String, shutdown_tx: mpsc::Sender<()>, rt_handle: tokio::runtime::Handle) { std::thread::spawn(move || { if let Err(err) = tray::run_tray(ui_url, shutdown_tx, rt_handle) { tracing::warn!("Tray init failed: {}", err); } }); } #[cfg(not(windows))] fn init_tray(_ui_url: String, _shutdown_tx: mpsc::Sender<()>, _rt_handle: tokio::runtime::Handle) {} #[cfg(windows)] mod tray { use std::error::Error; use tokio::sync::mpsc; use tray_icon::{ menu::{Menu, MenuEvent, MenuItem}, Icon, TrayIconBuilder, }; use winit::application::ApplicationHandler; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::platform::windows::EventLoopBuilderExtWindows; pub fn run_tray( ui_url: String, shutdown_tx: mpsc::Sender<()>, rt_handle: tokio::runtime::Handle, ) -> Result<(), Box> { let mut builder = EventLoop::builder(); builder.with_any_thread(true); let event_loop = builder.build()?; let menu = Menu::new(); let open_item = MenuItem::new("Open UI", true, None); let exit_item = MenuItem::new("Exit", true, None); menu.append(&open_item)?; menu.append(&exit_item)?; let icon = Icon::from_rgba(vec![0, 120, 212, 255], 1, 1)?; let _tray = TrayIconBuilder::new() .with_tooltip("PLC Feeder Distributor") .with_menu(Box::new(menu)) .with_icon(icon) .build()?; let menu_rx = MenuEvent::receiver(); let mut app = TrayApp { menu_rx, open_id: open_item.id().clone(), exit_id: exit_item.id().clone(), ui_url, shutdown_tx, rt_handle, }; event_loop.run_app(&mut app).map_err(|err| err.into()) } struct TrayApp { menu_rx: &'static tray_icon::menu::MenuEventReceiver, open_id: tray_icon::menu::MenuId, exit_id: tray_icon::menu::MenuId, ui_url: String, shutdown_tx: mpsc::Sender<()>, rt_handle: tokio::runtime::Handle, } impl ApplicationHandler for TrayApp { fn resumed(&mut self, _event_loop: &ActiveEventLoop) {} fn window_event( &mut self, _event_loop: &ActiveEventLoop, _window_id: winit::window::WindowId, _event: winit::event::WindowEvent, ) { } fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { event_loop.set_control_flow(ControlFlow::Wait); while let Ok(menu_event) = self.menu_rx.try_recv() { if menu_event.id == self.open_id { let _ = webbrowser::open(&self.ui_url); } if menu_event.id == self.exit_id { let _ = self.rt_handle.block_on(self.shutdown_tx.send(())); event_loop.exit(); } } } } }