cog_task/server/
mod.rs

1pub mod env;
2pub mod info;
3pub mod page;
4pub mod scheduler;
5pub mod task;
6
7pub use env::Env;
8pub use info::*;
9pub use page::*;
10pub use scheduler::*;
11pub use task::*;
12
13use crate::comm::{QReader, QWriter};
14use crate::gui;
15use crate::resource::LoggerSignal;
16use crate::util::SystemInfo;
17use chrono::{DateTime, Local, NaiveDateTime};
18use eframe::egui::CentralPanel;
19use eframe::glow::HasContext;
20use eframe::{egui, App};
21use eyre::{Context, Error, Result};
22use serde_cbor::Value;
23use std::path::PathBuf;
24use std::time::Duration;
25
26#[derive(Debug)]
27pub enum Progress {
28    None,
29    Success(DateTime<Local>),
30    Interrupt(DateTime<Local>),
31    Failure(DateTime<Local>, Error),
32    CleanupError(DateTime<Local>, Error),
33    LastRun(NaiveDateTime),
34}
35
36pub struct Server {
37    env: Env,
38    task: Task,
39    subject: String,
40    scale_factor: f32,
41    hold_on_rescale: bool,
42    scheduler: Option<Scheduler>,
43    page: Page,
44    blocks: Vec<(String, Progress)>,
45    active_block: Option<usize>,
46    status: Progress,
47    show_magnification: bool,
48    bin_hash: String,
49    sys_info: SystemInfo,
50    sync_reader: QReader<ServerSignal>,
51    cleaning_up: u32,
52}
53
54impl Server {
55    pub fn new(path: PathBuf, bin_hash: String) -> Result<Self> {
56        let env = Env::new(path)?;
57        let task = Task::new(env.task())
58            .wrap_err_with(|| format!("Failed to start task ({:?}).", env.task()))?;
59        let blocks = task
60            .block_labels()
61            .into_iter()
62            .map(|label| (label, Progress::None))
63            .collect();
64
65        println!("Saving output to: {:?}", env.output());
66
67        Ok(Self {
68            env,
69            task,
70            subject: "".to_owned(),
71            scale_factor: 1.0,
72            hold_on_rescale: false,
73            scheduler: None,
74            page: Page::Startup,
75            blocks,
76            active_block: None,
77            status: Progress::None,
78            show_magnification: false,
79            bin_hash,
80            sys_info: SystemInfo::new(),
81            sync_reader: QReader::new(),
82            cleaning_up: 0,
83        })
84    }
85
86    pub fn run(mut self) {
87        let options = eframe::NativeOptions {
88            always_on_top: false,
89            maximized: true,
90            decorated: true,
91            fullscreen: true,
92            drag_and_drop_support: false,
93            icon_data: None,
94            initial_window_pos: None,
95            initial_window_size: None,
96            min_window_size: None,
97            max_window_size: None,
98            resizable: false,
99            transparent: false,
100            vsync: false,
101            multisampling: 0,
102            depth_buffer: 0,
103            stencil_buffer: 0,
104            hardware_acceleration: eframe::HardwareAcceleration::Preferred,
105            renderer: Default::default(),
106            follow_system_theme: false,
107            default_theme: eframe::Theme::Light,
108            run_and_return: false,
109        };
110
111        self.sys_info.renderer = format!("{:#?}", options.renderer);
112        self.sys_info.hw_acceleration = format!("{:#?}", options.hardware_acceleration);
113
114        eframe::run_native(
115            &self.title(),
116            options,
117            Box::new(|cc| {
118                gui::init(&cc.egui_ctx);
119                if let Some(gl) = &cc.gl {
120                    self.sys_info
121                        .renderer
122                        .push_str(&format!(" ({:?})", gl.version()))
123                }
124                Box::new(self)
125            }),
126        );
127    }
128
129    #[inline(always)]
130    fn title(&self) -> String {
131        format!("CogTask Server -- {}", self.task.title())
132    }
133
134    #[inline(always)]
135    pub fn env(&self) -> &Env {
136        &self.env
137    }
138
139    #[inline(always)]
140    pub fn subject(&self) -> &String {
141        &self.subject
142    }
143
144    #[inline(always)]
145    pub fn active_block(&self) -> Option<&Block> {
146        self.active_block.map(|i| self.task.block(i))
147    }
148
149    #[inline(always)]
150    pub fn config(&self) -> &Config {
151        self.task.config()
152    }
153
154    pub fn style(&self) -> Option<Vec<u8>> {
155        let path = self.env.task().join("style.css");
156        if path.exists() {
157            Some(
158                std::fs::read(&path).unwrap_or_else(|_| {
159                    panic!("Failed to read custom task styling file: {path:?}")
160                }),
161            )
162        } else {
163            None
164        }
165    }
166
167    #[inline(always)]
168    pub fn task(&self) -> &Task {
169        &self.task
170    }
171
172    fn process(&mut self, _ctx: &egui::Context, signal: ServerSignal) {
173        match (self.page, signal) {
174            (Page::Loading, ServerSignal::LoadComplete) => {
175                if let Some(scheduler) = self.scheduler.as_mut() {
176                    self.page = Page::Activity;
177                    scheduler.sync_writer().push(SyncSignal::Go);
178                } else {
179                    self.page = Page::Selection;
180                }
181            }
182            (Page::Loading, ServerSignal::BlockCrashed(e)) => {
183                self.status = Progress::Failure(Local::now(), e);
184                self.drop_scheduler();
185            }
186            (Page::Activity, ServerSignal::BlockFinished) => {
187                self.status = Progress::Success(Local::now());
188                self.drop_scheduler();
189            }
190            (Page::Activity, ServerSignal::BlockInterrupted) => {
191                self.status = Progress::Interrupt(Local::now());
192                self.drop_scheduler();
193            }
194            (Page::Activity, ServerSignal::BlockCrashed(e)) => {
195                if let Some(scheduler) = self.scheduler.as_mut() {
196                    scheduler.async_writer().push(LoggerSignal::Write(
197                        "crash".to_owned(),
198                        Value::Text(format!("{e:?}")),
199                    ));
200                }
201                self.status = Progress::Failure(Local::now(), e);
202                self.drop_scheduler();
203            }
204            (Page::CleanUp, ServerSignal::SyncComplete(success))
205            | (Page::CleanUp, ServerSignal::AsyncComplete(success)) => {
206                self.cleaning_up -= 1;
207                if self.cleaning_up == 0 {
208                    if let (Progress::Success(_), Err(e)) = (&self.status, success) {
209                        self.status = Progress::CleanupError(Local::now(), e);
210                    }
211                    self.page = Page::Selection;
212                }
213            }
214            _ => {}
215        };
216    }
217
218    #[inline(always)]
219    pub(crate) fn callback_channel(&self) -> QWriter<ServerSignal> {
220        self.sync_reader.writer()
221    }
222
223    fn valid_subject_id(&self) -> bool {
224        !self.subject.is_empty()
225            && self
226                .subject
227                .chars()
228                .all(|c| c.is_alphabetic() || c.is_alphanumeric() | "-_".contains(c))
229    }
230
231    #[inline(always)]
232    pub fn hash(&self) -> String {
233        self.bin_hash.clone()
234    }
235
236    fn drop_scheduler(&mut self) {
237        self.page = Page::CleanUp;
238        self.cleaning_up = 2;
239        self.scheduler.take();
240    }
241}
242
243#[derive(Debug)]
244pub enum ServerSignal {
245    LoadComplete,
246    BlockFinished,
247    BlockInterrupted,
248    BlockCrashed(Error),
249    SyncComplete(Result<()>),
250    AsyncComplete(Result<()>),
251}
252
253impl App for Server {
254    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
255        while let Some(signal) = self.sync_reader.try_pop() {
256            self.process(ctx, signal);
257        }
258
259        let frame = egui::Frame::window(&ctx.style())
260            .inner_margin(0.0)
261            .outer_margin(0.0);
262
263        CentralPanel::default()
264            .frame(frame)
265            .show(ctx, |ui| match self.page {
266                Page::Startup => self.show_startup(ui),
267                Page::Selection => self.show_selection(ui),
268                Page::Activity => self.show_activity(ui),
269                Page::Loading => self.show_loading(ui),
270                Page::CleanUp => self.show_cleanup(ui),
271            });
272
273        if !self.hold_on_rescale {
274            gui::set_fullscreen_scale(ctx, self.scale_factor);
275        }
276        if !matches!(self.page, Page::Activity) {
277            ctx.request_repaint_after(Duration::from_millis(250));
278        }
279    }
280}