use std::{
collections::BTreeMap,
ffi::{OsStr, OsString},
path::{Path, PathBuf},
process::Child,
thread,
time::Duration,
};
use crate::locking::Mutex;
use crate::{prelude::Context, DataDeliveryPolicy};
use crate::{Error, Result};
use eframe::EventLoopBuilderHook;
use std::sync::LazyLock;
use tracing::{error, warn};
pub use eframe;
pub use egui;
static SERVER_INSTANCE: LazyLock<Mutex<Option<Child>>> = LazyLock::new(|| Mutex::new(None));
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ServerOptions {
command: OsString,
kill_command: Option<OsString>,
env: BTreeMap<String, String>,
wait_for: Option<OsString>,
kill_delay: Duration,
spawn_delay: Duration,
xdg_runtime_dir: PathBuf,
}
impl ServerOptions {
pub fn new<C: AsRef<OsStr>>(command: C) -> Self {
Self {
command: command.as_ref().to_owned(),
kill_command: None,
env: <_>::default(),
wait_for: None,
kill_delay: Duration::from_secs(5),
spawn_delay: Duration::from_secs(5),
xdg_runtime_dir: Path::new("/run/roboplc").to_owned(),
}
}
pub fn with_terminate_previous_command<C: AsRef<OsStr>>(mut self, kill_command: C) -> Self {
self.kill_command = Some(kill_command.as_ref().to_owned());
self
}
pub fn with_env(mut self, key: &str, value: &str) -> Self {
self.env.insert(key.to_string(), value.to_string());
self
}
pub fn with_wait_for<C: AsRef<OsStr>>(mut self, wait_for: C) -> Self {
self.wait_for = Some(wait_for.as_ref().to_owned());
self
}
pub fn with_spawn_delay(mut self, delay: Duration) -> Self {
self.spawn_delay = delay;
self
}
pub fn with_kill_delay(mut self, delay: Duration) -> Self {
self.kill_delay = delay;
self
}
pub fn with_xdg_runtime_dir<P: AsRef<Path>>(mut self, path: P) -> Self {
path.as_ref().clone_into(&mut self.xdg_runtime_dir);
self
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ServerKind {
Weston,
WestonLegacy,
Xorg,
}
impl ServerKind {
pub fn options(self: ServerKind) -> ServerOptions {
match self {
ServerKind::Weston | ServerKind::WestonLegacy => {
let mut opts = if self == ServerKind::Weston {
ServerOptions::new("weston --socket=wayland-1")
} else {
ServerOptions::new("weston --tty=1 --socket=wayland-1")
};
opts = opts
.with_env("WAYLAND_DISPLAY", "wayland-1")
.with_wait_for("wayland-1")
.with_terminate_previous_command("pkill -KILL weston");
opts
}
ServerKind::Xorg => {
let mut opts = ServerOptions::new("Xorg :0");
opts = opts
.with_env("DISPLAY", ":0")
.with_wait_for("/tmp/.X11-unix/X0")
.with_terminate_previous_command("pkill -KILL Xorg");
opts
}
}
}
}
#[derive(Clone, Debug)]
pub struct AppOptions {
fullscreen: bool,
title: String,
dimensions: Option<(u16, u16)>,
server_options: Option<ServerOptions>,
}
impl Default for AppOptions {
fn default() -> Self {
Self {
fullscreen: true,
title: "HMI".to_string(),
dimensions: None,
server_options: None,
}
}
}
impl AppOptions {
pub fn new() -> Self {
Self::default()
}
pub fn windowed(mut self) -> Self {
self.fullscreen = false;
self
}
pub fn with_dimensions(mut self, width: u16, height: u16) -> Self {
self.dimensions = Some((width, height));
self
}
pub fn with_server_options(mut self, opts: ServerOptions) -> Self {
self.server_options = Some(opts);
self
}
}
pub trait App {
type M: DataDeliveryPolicy + Send + Sync + Clone;
type V: Send;
fn update(
&mut self,
ctx: &egui::Context,
frame: &mut eframe::Frame,
plc_context: &Context<Self::M, Self::V>,
);
}
pub fn stop() {
if let Some(child) = SERVER_INSTANCE.lock().take() {
let pid = child.id();
#[allow(clippy::cast_possible_wrap)]
crate::thread_rt::kill_pstree(pid as i32, true, None);
}
}
pub fn start_server(server_options: ServerOptions) {
if let Some(kill_command) = &server_options.kill_command {
match std::process::Command::new("sh")
.args([OsString::from("-c"), kill_command.to_owned()])
.spawn()
{
Ok(mut child) => {
let _ = child.wait();
thread::sleep(server_options.kill_delay);
}
Err(error) => {
warn!(?error, "Failed to terminate previous server instance");
}
}
}
while !server_options.xdg_runtime_dir.exists() {
thread::sleep(Duration::from_millis(100));
}
std::env::set_var("XDG_RUNTIME_DIR", &server_options.xdg_runtime_dir);
for key in server_options.env.keys() {
std::env::remove_var(key);
}
let child = match std::process::Command::new("sh")
.args([OsString::from("-c"), server_options.command.clone()])
.spawn()
{
Ok(c) => c,
Err(error) => {
error!(?error, "Failed to start graphics server");
loop {
thread::park();
}
}
};
*SERVER_INSTANCE.lock() = Some(child);
for (key, value) in &server_options.env {
std::env::set_var(key, value);
}
if let Some(wait_for) = server_options.wait_for {
let wait_path = {
let p = Path::new(&wait_for);
if p.is_absolute() {
p.to_owned()
} else {
Path::new(&server_options.xdg_runtime_dir)
.join(&wait_for)
.clone()
}
};
while !wait_path.exists() {
thread::sleep(Duration::from_millis(100));
}
}
thread::sleep(server_options.spawn_delay);
}
pub fn run<A, M, V>(app: A, plc_context: &Context<M, V>, options: AppOptions) -> Result<()>
where
A: App<M = M, V = V>,
M: DataDeliveryPolicy + Send + Sync + Clone + 'static,
V: Send,
{
stop();
if let Some(opts) = options.server_options {
start_server(opts);
}
#[cfg(target_os = "linux")]
let event_loop_builder: Option<EventLoopBuilderHook> = Some(Box::new(|event_loop_builder| {
winit::platform::wayland::EventLoopBuilderExtWayland::with_any_thread(
event_loop_builder,
true,
);
}));
#[cfg(not(target_os = "linux"))]
let event_loop_builder: Option<EventLoopBuilderHook> = None;
let mut viewport = egui::ViewportBuilder::default().with_fullscreen(options.fullscreen);
if let Some((width, height)) = options.dimensions {
viewport = viewport.with_inner_size((f32::from(width), f32::from(height)));
}
let e_options = eframe::NativeOptions {
viewport,
event_loop_builder,
..Default::default()
};
let plc_context = plc_context.clone();
eframe::run_native(
&options.title,
e_options,
Box::new(|_cc| Ok(Box::new(Hmi { app, plc_context }))),
)
.map_err(Error::failed)
}
struct Hmi<A, M, V>
where
A: App<M = M, V = V>,
M: DataDeliveryPolicy + Send + Sync + Clone + 'static,
V: Send,
{
app: A,
plc_context: Context<M, V>,
}
impl<A, M, V> eframe::App for Hmi<A, M, V>
where
A: App<M = M, V = V>,
M: DataDeliveryPolicy + Send + Sync + Clone + 'static,
V: Send,
{
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.app.update(ctx, _frame, &self.plc_context);
}
}