machine_check_gui/
backend.rs1use std::{borrow::Cow, ffi::OsStr, path::Path};
2
3use http::{header::CONTENT_TYPE, Method};
4use include_dir::{include_dir, Dir};
5use log::{debug, error};
6use machine_check_common::{property::Property, ExecError};
7use machine_check_exec::{Framework, Strategy};
8use mck::concr::FullMachine;
9use sync::BackendSync;
10use window::Window;
11use workspace::Workspace;
12use wry::WebViewId;
13
14use crate::shared::{BackendSpaceInfo, Request};
15
16mod sync;
17mod window;
18mod workspace;
19
20const FAVICON_ICO: &[u8] = include_bytes!("../content/favicon.ico");
21
22pub fn run<M: FullMachine>(
24 system: M,
25 property: Option<Property>,
26 strategy: Strategy,
27) -> Result<(), ExecError> {
28 let exec_name = std::env::current_exe()
30 .ok()
31 .as_ref()
32 .map(Path::new)
33 .and_then(Path::file_stem)
34 .and_then(OsStr::to_str)
35 .map(String::from)
36 .unwrap_or(String::from("Unknown executable"));
37
38 let abstract_system = <M::Abstr as mck::abstr::Abstr<M>>::from_concrete(system);
39 let backend = Backend::new(
41 Workspace::<M>::new(Framework::new(abstract_system, strategy), property),
42 exec_name.clone(),
43 );
44 let response_fn = move |_web_view_id: WebViewId, request: http::Request<Vec<u8>>| {
45 backend.get_http_response(request)
46 };
47
48 let gui = match Window::new(response_fn, &exec_name) {
50 Ok(ok) => ok,
51 Err(err) => {
52 error!("Cannot create GUI: {}", err);
53 return Err(ExecError::GuiError(err.to_string()));
54 }
55 };
56 gui.run()
58}
59
60struct Backend {
62 sync: BackendSync,
63}
64
65struct BackendStats {
70 should_cancel: bool,
71 space_info: BackendSpaceInfo,
72}
73
74impl BackendStats {
75 fn new<M: FullMachine>(framework: &mut Framework<M>) -> Self {
76 Self {
77 should_cancel: false,
78 space_info: extract_space_info(framework),
79 }
80 }
81}
82
83fn extract_space_info<M: FullMachine>(framework: &mut Framework<M>) -> BackendSpaceInfo {
84 let num_refinements = framework.info().num_refinements;
85 let num_states = framework.info().num_final_states;
86 let num_transitions = framework.info().num_final_transitions;
87 BackendSpaceInfo {
88 num_refinements,
89 num_states,
90 num_transitions,
91 }
92}
93
94struct BackendSettings {
95 exec_name: String,
96}
97
98const CONTENT_DIR: Dir = include_dir!("content");
99
100impl Backend {
101 pub fn new<M: FullMachine>(mut workspace: Workspace<M>, exec_name: String) -> Self {
103 let stats = BackendStats::new(&mut workspace.framework);
104 let settings = BackendSettings { exec_name };
105 let sync = BackendSync::new(workspace, stats, settings);
106 Self { sync }
107 }
108
109 fn get_http_response(
113 &self,
114 request: http::Request<Vec<u8>>,
115 ) -> http::Response<Cow<'static, [u8]>> {
116 self.get_http_response_or_error(request)
118 .unwrap_or_else(|err| {
119 error!("Cannot produce a response to frontend: {}", err);
120 let response = http::Response::builder()
121 .header(CONTENT_TYPE, "text/plain")
122 .status(500)
123 .body(Cow::Borrowed("Internal Server Error".as_bytes()))
124 .expect("Internal server error response should be constructable");
125 response
126 })
127 }
128
129 fn get_http_response_or_error(
130 &self,
131 request: http::Request<Vec<u8>>,
132 ) -> Result<http::Response<Cow<'static, [u8]>>, Box<dyn std::error::Error>> {
133 let uri_path = request.uri().path();
135 let method = request.method();
136 debug!("Serving: {}", uri_path);
137
138 let path = match uri_path.strip_prefix('/') {
141 Some(path) => path,
142 None => {
143 if uri_path.is_empty() {
144 ""
145 } else {
146 return Err(anyhow::anyhow!(
147 "Path not empty or starting with slash: {}",
148 uri_path
149 )
150 .into());
151 }
152 }
153 };
154
155 let path = if path.is_empty() { "index.html" } else { path };
157
158 if path == "api" {
159 if method != Method::POST {
161 return Err(anyhow::anyhow!("API method must be POST").into());
162 }
163
164 self.get_api_response(request)
165 } else {
166 if method != Method::GET {
168 return Err(anyhow::anyhow!("Expected method GET: {}", path).into());
169 }
170
171 Self::get_content_response(path)
172 }
173 }
174
175 fn get_content_response(
176 path: &str,
177 ) -> Result<http::Response<Cow<'static, [u8]>>, Box<dyn std::error::Error>> {
178 let content = match CONTENT_DIR.get_file(path) {
179 Some(file) => file.contents(),
180 None => return Err(anyhow::anyhow!("Not found: {}", path).into()),
181 };
182
183 let content_type: Cow<str> = Cow::Owned(
184 mime_guess::from_path(path)
185 .first()
186 .expect("Content should have known content type")
187 .to_string(),
188 );
189
190 http::Response::builder()
191 .header(CONTENT_TYPE, content_type.as_ref())
192 .body(Cow::Borrowed(content))
193 .map_err(Into::into)
194 }
195
196 fn get_api_response(
197 &self,
198 request: http::Request<Vec<u8>>,
199 ) -> Result<http::Response<Cow<'static, [u8]>>, Box<dyn std::error::Error>> {
200 let x_body = request
204 .headers()
205 .get("X-Body")
206 .ok_or(anyhow::anyhow!("Request has no X-Body header"))?;
207 let x_body = x_body
208 .to_str()
209 .map_err(|_| anyhow::anyhow!("Request X-Body header is not ASCII"))?;
210 let decoded_body = hex::decode(x_body).map_err(|err| {
211 anyhow::anyhow!("Request X-Body header does not contain hex: {}", err)
212 })?;
213 let request: Request = rmp_serde::from_slice(&decoded_body).map_err(|err| {
214 anyhow::anyhow!(
215 "Request X-Body header does not contain valid MessagePack data: {}",
216 err
217 )
218 })?;
219
220 let response = self.sync.command(request);
222
223 let content_msgpack = rmp_serde::to_vec(&response)?;
225 http::Response::builder()
226 .header(CONTENT_TYPE, "application/vnd.msgpack")
227 .body(Cow::Owned(content_msgpack))
228 .map_err(Into::into)
229 }
230}