cog-task 1.2.0

A general-purpose low-latency application to run cognitive tasks
Documentation
pub mod env;
pub mod info;
pub mod page;
pub mod scheduler;
pub mod task;

pub use env::Env;
pub use info::*;
pub use page::*;
pub use scheduler::*;
pub use task::*;

use crate::comm::{QReader, QWriter};
use crate::gui;
use crate::resource::LoggerSignal;
use crate::util::SystemInfo;
use chrono::{DateTime, Local, NaiveDateTime};
use eframe::egui::CentralPanel;
use eframe::glow::HasContext;
use eframe::{egui, App};
use eyre::{Context, Error, Result};
use serde_cbor::Value;
use std::path::PathBuf;
use std::time::Duration;

#[derive(Debug)]
pub enum Progress {
    None,
    Success(DateTime<Local>),
    Interrupt(DateTime<Local>),
    Failure(DateTime<Local>, Error),
    CleanupError(DateTime<Local>, Error),
    LastRun(NaiveDateTime),
}

pub struct Server {
    env: Env,
    task: Task,
    subject: String,
    scale_factor: f32,
    hold_on_rescale: bool,
    scheduler: Option<Scheduler>,
    page: Page,
    blocks: Vec<(String, Progress)>,
    active_block: Option<usize>,
    status: Progress,
    show_magnification: bool,
    bin_hash: String,
    sys_info: SystemInfo,
    sync_reader: QReader<ServerSignal>,
    cleaning_up: u32,
}

impl Server {
    pub fn new(path: PathBuf, bin_hash: String) -> Result<Self> {
        let env = Env::new(path)?;
        let task = Task::new(env.task())
            .wrap_err_with(|| format!("Failed to start task ({:?}).", env.task()))?;
        let blocks = task
            .block_labels()
            .into_iter()
            .map(|label| (label, Progress::None))
            .collect();

        println!("Saving output to: {:?}", env.output());

        Ok(Self {
            env,
            task,
            subject: "".to_owned(),
            scale_factor: 1.0,
            hold_on_rescale: false,
            scheduler: None,
            page: Page::Startup,
            blocks,
            active_block: None,
            status: Progress::None,
            show_magnification: false,
            bin_hash,
            sys_info: SystemInfo::new(),
            sync_reader: QReader::new(),
            cleaning_up: 0,
        })
    }

    pub fn run(mut self) {
        let options = eframe::NativeOptions {
            always_on_top: false,
            maximized: true,
            decorated: true,
            fullscreen: true,
            drag_and_drop_support: false,
            icon_data: None,
            initial_window_pos: None,
            initial_window_size: None,
            min_window_size: None,
            max_window_size: None,
            resizable: false,
            transparent: false,
            vsync: false,
            multisampling: 0,
            depth_buffer: 0,
            stencil_buffer: 0,
            hardware_acceleration: eframe::HardwareAcceleration::Preferred,
            renderer: Default::default(),
            follow_system_theme: false,
            default_theme: eframe::Theme::Light,
            run_and_return: false,
        };

        self.sys_info.renderer = format!("{:#?}", options.renderer);
        self.sys_info.hw_acceleration = format!("{:#?}", options.hardware_acceleration);

        eframe::run_native(
            &self.title(),
            options,
            Box::new(|cc| {
                gui::init(&cc.egui_ctx);
                if let Some(gl) = &cc.gl {
                    self.sys_info
                        .renderer
                        .push_str(&format!(" ({:?})", gl.version()))
                }
                Box::new(self)
            }),
        );
    }

    #[inline(always)]
    fn title(&self) -> String {
        format!("CogTask Server -- {}", self.task.title())
    }

    #[inline(always)]
    pub fn env(&self) -> &Env {
        &self.env
    }

    #[inline(always)]
    pub fn subject(&self) -> &String {
        &self.subject
    }

    #[inline(always)]
    pub fn active_block(&self) -> Option<&Block> {
        self.active_block.map(|i| self.task.block(i))
    }

    #[inline(always)]
    pub fn config(&self) -> &Config {
        self.task.config()
    }

    pub fn style(&self) -> Option<Vec<u8>> {
        let path = self.env.task().join("style.css");
        if path.exists() {
            Some(
                std::fs::read(&path).unwrap_or_else(|_| {
                    panic!("Failed to read custom task styling file: {path:?}")
                }),
            )
        } else {
            None
        }
    }

    #[inline(always)]
    pub fn task(&self) -> &Task {
        &self.task
    }

    fn process(&mut self, _ctx: &egui::Context, signal: ServerSignal) {
        match (self.page, signal) {
            (Page::Loading, ServerSignal::LoadComplete) => {
                if let Some(scheduler) = self.scheduler.as_mut() {
                    self.page = Page::Activity;
                    scheduler.sync_writer().push(SyncSignal::Go);
                } else {
                    self.page = Page::Selection;
                }
            }
            (Page::Loading, ServerSignal::BlockCrashed(e)) => {
                self.status = Progress::Failure(Local::now(), e);
                self.drop_scheduler();
            }
            (Page::Activity, ServerSignal::BlockFinished) => {
                self.status = Progress::Success(Local::now());
                self.drop_scheduler();
            }
            (Page::Activity, ServerSignal::BlockInterrupted) => {
                self.status = Progress::Interrupt(Local::now());
                self.drop_scheduler();
            }
            (Page::Activity, ServerSignal::BlockCrashed(e)) => {
                if let Some(scheduler) = self.scheduler.as_mut() {
                    scheduler.async_writer().push(LoggerSignal::Write(
                        "crash".to_owned(),
                        Value::Text(format!("{e:?}")),
                    ));
                }
                self.status = Progress::Failure(Local::now(), e);
                self.drop_scheduler();
            }
            (Page::CleanUp, ServerSignal::SyncComplete(success))
            | (Page::CleanUp, ServerSignal::AsyncComplete(success)) => {
                self.cleaning_up -= 1;
                if self.cleaning_up == 0 {
                    if let (Progress::Success(_), Err(e)) = (&self.status, success) {
                        self.status = Progress::CleanupError(Local::now(), e);
                    }
                    self.page = Page::Selection;
                }
            }
            _ => {}
        };
    }

    #[inline(always)]
    pub(crate) fn callback_channel(&self) -> QWriter<ServerSignal> {
        self.sync_reader.writer()
    }

    fn valid_subject_id(&self) -> bool {
        !self.subject.is_empty()
            && self
                .subject
                .chars()
                .all(|c| c.is_alphabetic() || c.is_alphanumeric() | "-_".contains(c))
    }

    #[inline(always)]
    pub fn hash(&self) -> String {
        self.bin_hash.clone()
    }

    fn drop_scheduler(&mut self) {
        self.page = Page::CleanUp;
        self.cleaning_up = 2;
        self.scheduler.take();
    }
}

#[derive(Debug)]
pub enum ServerSignal {
    LoadComplete,
    BlockFinished,
    BlockInterrupted,
    BlockCrashed(Error),
    SyncComplete(Result<()>),
    AsyncComplete(Result<()>),
}

impl App for Server {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        while let Some(signal) = self.sync_reader.try_pop() {
            self.process(ctx, signal);
        }

        let frame = egui::Frame::window(&ctx.style())
            .inner_margin(0.0)
            .outer_margin(0.0);

        CentralPanel::default()
            .frame(frame)
            .show(ctx, |ui| match self.page {
                Page::Startup => self.show_startup(ui),
                Page::Selection => self.show_selection(ui),
                Page::Activity => self.show_activity(ui),
                Page::Loading => self.show_loading(ui),
                Page::CleanUp => self.show_cleanup(ui),
            });

        if !self.hold_on_rescale {
            gui::set_fullscreen_scale(ctx, self.scale_factor);
        }
        if !matches!(self.page, Page::Activity) {
            ctx.request_repaint_after(Duration::from_millis(250));
        }
    }
}