gled 2.31.1

gled is an application for creating animations and effects on artnet or dmx installations
//! The desktop app as an interface of an engine on another machine
//! (`gled --connect`): the same messages as in one process, over a
//! WebSocket.

use std::{collections::BTreeMap, fs, sync::Arc};

use gled_core::paths::known_servers_file;
use gled_protocol::{
    message::{ClientMessage, ServerMessage},
    state::EngineEvent,
};
use gled_server::client::{ClientEvent, ConnectError, RemoteClient, RemoteTarget, server_key};
use gled_ui::link::{BackgroundSender, EngineLink};

#[derive(Debug)]
pub struct RemoteLink {
    client: RemoteClient,
    url: String,
    open: bool,
    /// Whether a connection existed; from then on the interface shows
    /// interruptions itself.
    was_open: bool,
}

impl RemoteLink {
    /// Connects to `url`, accepting the certificate with `fingerprint` or,
    /// without one, the fingerprint accepted for this server before.
    pub fn new(
        url: String,
        fingerprint: Option<String>,
        ctx: egui::Context,
    ) -> std::io::Result<Self> {
        let fingerprint = fingerprint.or_else(|| known_servers().remove(&server_key(&url)));
        let client = RemoteClient::start(
            RemoteTarget {
                url: url.clone(),
                fingerprint,
            },
            format!("{} desktop", env!("CARGO_PKG_VERSION")),
            move || ctx.request_repaint(),
        )?;
        Ok(Self {
            client,
            url,
            open: false,
            was_open: false,
        })
    }
}

impl EngineLink for RemoteLink {
    fn send(&mut self, message: ClientMessage) {
        self.client.send(message);
    }

    fn background_sender(&self) -> Option<BackgroundSender> {
        Some(Arc::new(self.client.sender()))
    }

    fn receive(&mut self) -> Vec<ServerMessage> {
        let mut messages = Vec::new();
        for event in self.client.events() {
            match event {
                ClientEvent::Message(message) => messages.push(message),
                ClientEvent::Connected { fingerprint } => {
                    self.open = true;
                    self.was_open = true;
                    if let Some(fingerprint) = fingerprint {
                        remember(&server_key(&self.url), fingerprint);
                    }
                }
                ClientEvent::Failed(error) => {
                    self.open = false;
                    let interruption = self.was_open
                        && matches!(error, ConnectError::Lost(_) | ConnectError::Connect { .. });
                    if !interruption {
                        messages.push(ServerMessage::Event(EngineEvent::Error(error.to_string())));
                    }
                }
            }
        }
        messages
    }

    fn is_remote(&self) -> bool {
        true
    }

    fn is_open(&self) -> bool {
        self.open
    }

    fn target(&self) -> Option<String> {
        Some(server_key(&self.url))
    }
}

/// Accepted certificate fingerprints by `host:port`.
fn known_servers() -> BTreeMap<String, String> {
    known_servers_file()
        .and_then(|path| fs::read(path).ok())
        .and_then(|bytes| serde_json::from_slice(&bytes).ok())
        .unwrap_or_default()
}

fn remember(server: &str, fingerprint: String) {
    let mut known = known_servers();
    if known.get(server) == Some(&fingerprint) {
        return;
    }
    known.insert(server.to_owned(), fingerprint);
    let Some(path) = known_servers_file() else {
        return;
    };
    let written = path
        .parent()
        .map_or(Ok(()), fs::create_dir_all)
        .and_then(|()| {
            serde_json::to_vec_pretty(&known)
                .map_err(std::io::Error::other)
                .and_then(|json| fs::write(&path, json))
        });
    if let Err(err) = written {
        tracing::warn!(
            "Could not remember the server's certificate in {}: {err}",
            path.display()
        );
    }
}