plc_control/crates/app_feeder_distributor/src/app.rs

198 lines
6.2 KiB
Rust

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<EventManager>,
pub control_runtime: Arc<control::runtime::ControlRuntimeStore>,
}
impl FromRef<AppState> 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()),
));
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);
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 event_manager = Arc::new(EventManager::new(pool.clone(), Some(ws_manager.clone())));
let platform = PlatformContext::new(pool, connection_manager, ws_manager);
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<dyn Error>> {
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();
}
}
}
}
}