example-spa-elm-app/backend/src/main.rs
2025-01-26 16:12:06 -05:00

178 lines
5.4 KiB
Rust

use actix_web::middleware::Compress;
use actix::prelude::*;
use actix_files::Files;
use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer, middleware};
use actix_web_actors::ws;
use chrono::Local;
use log::info;
use serde::{Deserialize, Serialize};
use std::{fs, os::unix::fs::PermissionsExt, time::Duration};
use elm_rs::{Elm, ElmEncode, ElmDecode};
use std::time::Instant;
mod landing;
mod term;
mod terminal_size;
#[derive(Serialize, Debug, Message, Elm, ElmDecode)]
#[rtype(result = "()")]
pub enum DownMsg {
LandingDownMsg(landing::LandingDownMsg),
TermDownMsg(term::TermDownMsg),
}
#[derive(Deserialize, Debug, Elm, ElmEncode)]
enum UpMsg {
LandingUpMsg(landing::LandingUpMsg),
TermUpMsg(term::TermUpMsg),
}
pub struct TryItBackend {
addr: Option<Addr<TryItBackend>>,
graceful_stop: bool
}
fn do_elm_gen() {
let mut encode_decode_top = vec![];
elm_rs::export!("EncodeDecode", &mut encode_decode_top, {
encoders: [UpMsg,
term::TermUpMsg,
landing::LandingUpMsg],
decoders: [DownMsg,
term::TermDownMsg, term::TerminalScreen, term::Cursor, term::Line,
landing::LandingDownMsg]
}).unwrap();
let output = String::from_utf8(encode_decode_top).unwrap();
fs::write("../frontend/src/EncodeDecode.elm", output).unwrap();
}
impl Actor for TryItBackend {
type Context = ws::WebsocketContext<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
info!("WebSocket actor started");
let addr = ctx.address();
self.addr = Some(addr);
ctx.run_interval(Duration::from_secs(1), |_, ctx| {
let current_time = Local::now().format("%H:%M:%S").to_string();
let message = DownMsg::LandingDownMsg(landing::LandingDownMsg::TimeUpdate(current_time));
if let Ok(json_message) = serde_json::to_string(&message) {
ctx.text(json_message);
}
});
}
fn stopped(&mut self, ctx: &mut Self::Context) {
if !self.graceful_stop {
landing::cleanup(self, ctx);
term::cleanup(self, ctx);
}
}
}
impl actix::Handler<DownMsg> for TryItBackend {
type Result = ();
fn handle(&mut self, msg: DownMsg, ctx: &mut ws::WebsocketContext<Self>) {
self.send_down_msg(msg, ctx);
}
}
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for TryItBackend {
fn handle(
&mut self,
msg: Result<ws::Message, ws::ProtocolError>,
ctx: &mut ws::WebsocketContext<Self>,
) {
match msg {
Ok(ws::Message::Text(text)) => {
if let Ok(up_msg) = serde_json::from_str::<UpMsg>(&text) {
match up_msg {
UpMsg::LandingUpMsg(msg) => landing::msg_handler(msg, self, ctx),
UpMsg::TermUpMsg(msg) => term::msg_handler(msg, self, ctx),
}
}
}
Ok(ws::Message::Ping(msg)) => {
ctx.pong(&msg);
}
Ok(ws::Message::Close(reason)) => {
log::info!("WebSocket connection closed: {:?}", reason);
// Call `term::cleanup` to remove the associated terminal instance
term::cleanup(self, ctx);
self.graceful_stop = true;
}
_ => {}
}
}
}
impl TryItBackend {
fn send_down_msg(&self, down_msg: DownMsg, ctx: &mut ws::WebsocketContext<Self>) {
if let Ok(serialized_msg) = serde_json::to_string(&down_msg) {
ctx.text(serialized_msg);
} else {
log::error!("Failed to serialize DownMsg {:?}", down_msg);
}
}
}
async fn websocket_handler(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
let try_it_backend = TryItBackend { addr: None, graceful_stop: false };
ws::start(try_it_backend, &req, stream)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
// Check if an environment variable `GENERATE_ELM` is set
if std::env::var("GENERATE_ELM").is_ok() {
do_elm_gen();
return Ok(());
}
let address = if std::env::var("DEBUG").is_ok() {
"0.0.0.0"
} else {
"127.0.0.1"
};
let port = std::env::var("EXAMPLE_ELM_APP_PORT")
.unwrap_or("8080".to_string())
.parse()
.unwrap();
info!("Starting server at http://{}:{}", address, port);
HttpServer::new(|| {
App::new()
.wrap(Compress::default())
.wrap(middleware::DefaultHeaders::new()
.add(("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"))
.add(("Pragma", "no-cache"))
.add(("Expires", "0"))
)
.route("/ws/", web::get().to(websocket_handler)) // WebSocket endpoint
.service(Files::new("/assets", "./public/assets"))
.service(Files::new("/", "./public").index_file("index.html"))
.default_service(web::route().to(|| async {
let index_html = fs::read_to_string("./public/index.html")
.unwrap_or_else(|_| "404 Not Found".to_string());
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(index_html)
}))
})
.bind((address, port))?
.run()
.await
}