gled 2.32.0

gled is an application for creating animations and effects on artnet or dmx installations
//! The web server inside the desktop app: browsers and other devices connect
//! to this laptop's engine the same way they connect to a Raspberry Pi. It is
//! switched on and off, and its ports changed, while gled runs.

use std::{
    collections::BTreeMap,
    net::{Ipv4Addr, SocketAddr},
    time::Instant,
};

use gled_core::{
    engine::{ClientId, EngineConnector},
    paths,
    runtime::persistent_state::{PersistentState, RemoteAccessSettings},
};
use gled_server::{HttpMode, SessionEvent, WebServer, WebServerConfig};
use kanal::Receiver;
use tracing::{error, info};

/// The web server and the interfaces connected to it.
#[derive(Debug)]
pub struct RemoteAccess {
    engine: EngineConnector,
    /// Created when the server is first started.
    runtime: Option<tokio::runtime::Runtime>,
    server: Option<RunningServer>,
    /// The settings the server runs with, or failed to start with.
    applied: RemoteAccessSettings,
    error: Option<String>,
}

#[derive(Debug)]
struct RunningServer {
    server: WebServer,
    session_events: Option<Receiver<SessionEvent>>,
    clients: BTreeMap<ClientId, ConnectedClient>,
}

/// An interface connected over the network.
#[derive(Debug, Clone)]
pub struct ConnectedClient {
    pub peer: SocketAddr,
    pub app_version: String,
    pub since: Instant,
}

impl RemoteAccess {
    /// Starts the server right away if this machine's settings enable it.
    pub fn new(engine: EngineConnector) -> Self {
        let settings = PersistentState::default().remote_access().clone();
        let settings = &settings;
        let mut remote_access = Self {
            engine,
            runtime: None,
            server: None,
            applied: RemoteAccessSettings {
                enabled: false,
                ..settings.clone()
            },
            error: None,
        };
        remote_access.apply(settings);
        remote_access
    }

    /// The settings as stored on this machine.
    pub fn settings(&self) -> RemoteAccessSettings {
        PersistentState::default().remote_access().clone()
    }

    /// Stores `settings` and applies them.
    pub fn change(&mut self, settings: RemoteAccessSettings) {
        let mut state = PersistentState::default();
        if *state.remote_access() != settings {
            state.set_remote_access(settings.clone());
            state.save();
        }
        self.apply(&settings);
    }

    /// Starts, stops or restarts the server to match `settings`.
    pub fn apply(&mut self, settings: &RemoteAccessSettings) {
        if *settings == self.applied && (self.server.is_some() || self.error.is_some()) {
            return;
        }
        self.stop();
        self.applied = settings.clone();
        self.error = None;
        if settings.enabled
            && let Err(err) = self.start(settings)
        {
            error!(%err, "could not start remote access");
            self.error = Some(err);
        }
    }

    fn start(&mut self, settings: &RemoteAccessSettings) -> Result<(), String> {
        if self.runtime.is_none() {
            let runtime = tokio::runtime::Builder::new_multi_thread()
                .worker_threads(2)
                .thread_name("gled:remote")
                .enable_all()
                .build()
                .map_err(|err| format!("could not start the network runtime: {err}"))?;
            self.runtime = Some(runtime);
        }
        let Some(runtime) = &self.runtime else {
            return Err("the network runtime is missing".to_owned());
        };
        let config = WebServerConfig {
            https_listen: SocketAddr::from((Ipv4Addr::UNSPECIFIED, settings.https_port)),
            http_listen: settings
                .http_redirect_port
                .map(|port| SocketAddr::from((Ipv4Addr::UNSPECIFIED, port))),
            http_mode: HttpMode::Redirect,
            tls_dir: paths::tls_dir(),
        };
        // Binding is quick; doing it here reports a port in use right away.
        let mut server = runtime
            .block_on(WebServer::start(config, self.engine.clone()))
            .map_err(|err| err.to_string())?;
        info!(https = %server.https_addr(), "remote access on");
        self.server = Some(RunningServer {
            session_events: server.take_session_events(),
            server,
            clients: BTreeMap::new(),
        });
        Ok(())
    }

    fn stop(&mut self) {
        if let (Some(running), Some(runtime)) = (self.server.take(), &self.runtime) {
            // Open connections get a moment to close; the window need not wait.
            runtime.spawn(running.server.stop());
            info!("remote access off");
        }
    }

    /// Takes the news about connected interfaces. Call once per frame.
    pub fn update(&mut self) {
        let Some(running) = &mut self.server else {
            return;
        };
        let Some(events) = &running.session_events else {
            return;
        };
        while let Ok(Some(event)) = events.try_recv() {
            match event {
                SessionEvent::Connected {
                    id,
                    peer,
                    app_version,
                } => {
                    running.clients.insert(
                        id,
                        ConnectedClient {
                            peer,
                            app_version,
                            since: Instant::now(),
                        },
                    );
                }
                SessionEvent::Disconnected { id } => {
                    running.clients.remove(&id);
                }
            }
        }
    }

    /// The HTTPS port browsers connect to while the server runs.
    pub fn https_port(&self) -> Option<u16> {
        self.server
            .as_ref()
            .map(|running| running.server.https_addr().port())
    }

    /// The fingerprint of the certificate browsers are shown.
    pub fn fingerprint(&self) -> Option<&str> {
        self.server
            .as_ref()
            .map(|running| running.server.fingerprint())
    }

    /// Why the server is not running although it should.
    pub fn error(&self) -> Option<&str> {
        self.error.as_deref()
    }

    pub fn clients(&self) -> impl Iterator<Item = &ConnectedClient> {
        self.server
            .iter()
            .flat_map(|running| running.clients.values())
    }
}

impl Drop for RemoteAccess {
    fn drop(&mut self) {
        if let (Some(running), Some(runtime)) = (self.server.take(), &self.runtime) {
            runtime.block_on(running.server.stop());
        }
    }
}