1use anyhow::Result;
2use nanoserde::{DeJson, SerJson};
3use std::{
4 net::{TcpListener, TcpStream},
5 str::FromStr,
6};
7use tiny_http::{Header, Response as HttpResponse, Server};
8use tuna::Tuneable;
9use tungstenite::{accept, WebSocket};
10
11use include_dir::{include_dir, Dir};
12
13static PROJECT_DIR: Dir = include_dir!("html");
14
15fn content_type(url: &str) -> Option<Header> {
16 if url.ends_with(".js") {
17 return Header::from_str("Content-Type: application/javascript; charset=UTF=8").ok();
18 }
19
20 if url.ends_with(".css") {
21 return Header::from_str("Content-Type: text/css; charset=UTF=8").ok();
22 }
23
24 if url.ends_with(".html") {
25 return Header::from_str("Content-Type: text/html; charset=UTF=8").ok();
26 }
27
28 None
29}
30
31#[derive(DeJson, SerJson, Debug)]
32enum TunaMessage {
33 ListAll,
34 Tuneables(tuna::TunaState),
35 Delta((String, String, Tuneable)),
36 Ok((String, String)),
37}
38
39struct TunaClient {
40 websocket: WebSocket<TcpStream>,
41}
42
43impl TunaClient {
44 fn new(stream: TcpStream) -> Result<Self> {
45 let websocket = accept(stream)?;
46
47 Ok(Self { websocket })
48 }
49
50 fn poll(&mut self) -> bool {
51 let msg = self.websocket.read_message().unwrap();
52
53 if msg.is_text() {
54 let contents = msg.into_text().unwrap();
55 let message: TunaMessage = match DeJson::deserialize_json(&contents) {
56 Ok(v) => v,
57 Err(e) => {
58 log::error!("failed deserialization: {}, {}", e, contents);
59 return true;
60 }
61 };
62
63 match message {
64 TunaMessage::ListAll => {
65 let state = tuna::TUNA_STATE.read();
66 let res = TunaMessage::Tuneables((*state).clone());
67
68 let response = SerJson::serialize_json(&res);
69 self.websocket
70 .write_message(tungstenite::Message::Text(response))
71 .unwrap();
72 }
73
74 TunaMessage::Delta((category, name, tuneable)) => {
75 tuneable.apply_to(&category, &name);
76
77 let response = SerJson::serialize_json(&TunaMessage::Ok((category, name)));
78 self.websocket
79 .write_message(tungstenite::Message::Text(response))
80 .unwrap();
81 }
82 TunaMessage::Tuneables(_) | TunaMessage::Ok((_, _)) => {
83 panic!("unexpected message kind")
84 }
85 }
86 } else if msg.is_close() {
87 return false;
88 } else {
89 log::error!("received non-string message: {:?}", msg);
90 }
91
92 true
93 }
94}
95
96pub struct TunaServer {
99 server: TcpListener,
100 http_server: Server,
101}
102
103impl TunaServer {
104 pub fn new(port: u16) -> anyhow::Result<Self> {
107 let http_server = Server::http(("0.0.0.0", port))
108 .map_err(|e| anyhow::format_err!("http server error: {}", e))?;
109
110 let server = TcpListener::bind(("127.0.0.1", port + 1))?;
111
112 server.set_nonblocking(true)?;
113
114 Ok(Self {
115 server,
116 http_server,
117 })
118 }
119
120 pub fn work_http(&mut self) {
122 loop {
123 match self.http_server.try_recv() {
124 Ok(Some(req)) => {
125 log::debug!("request: {:#?}", req);
126 let response = match req.url() {
127 "/" => HttpResponse::from_string(
128 PROJECT_DIR
129 .get_file("index.html")
130 .unwrap()
131 .contents_utf8()
132 .unwrap(),
133 )
134 .with_status_code(200)
135 .with_header(content_type(".html").unwrap()),
136 _ => match PROJECT_DIR.get_file(&req.url()[1..]) {
137 Some(contents) => {
138 HttpResponse::from_string(contents.contents_utf8().unwrap())
139 .with_status_code(200)
140 .with_header(content_type(req.url()).unwrap())
141 }
142 _ => HttpResponse::from_string("not found").with_status_code(404),
143 },
144 };
145
146 let _ = req.respond(response);
147 }
148 Ok(None) => {
149 break;
150 }
151 Err(e) => {
152 log::error!("Http Error: {:?}", e);
153 break;
154 }
155 }
156 }
157 }
158
159 pub fn work_websocket(&mut self) {
161 loop {
162 match self.server.accept() {
163 Ok((stream, addr)) => {
164 log::debug!("New Tuna client from: {:?}", addr);
165
166 match TunaClient::new(stream) {
167 Ok(mut client) => {
168 std::thread::spawn(move || loop {
169 if !client.poll() {
170 break;
171 }
172 });
173 }
174 Err(e) => log::error!("failed to accept client: {}", e),
175 }
176 }
177 Err(e) if e.kind() != std::io::ErrorKind::WouldBlock => {
178 log::error!("Error during accept: {:?}", e);
179 break;
180 }
181 _ => {
182 break;
183 }
184 }
185 }
186 }
187 pub fn loop_once(&mut self) {
190 self.work_http();
191 self.work_websocket();
192 }
193}