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}