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>, 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; 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 for TryItBackend { type Result = (); fn handle(&mut self, msg: DownMsg, ctx: &mut ws::WebsocketContext) { self.send_down_msg(msg, ctx); } } impl StreamHandler> for TryItBackend { fn handle( &mut self, msg: Result, ctx: &mut ws::WebsocketContext, ) { match msg { Ok(ws::Message::Text(text)) => { if let Ok(up_msg) = serde_json::from_str::(&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) { 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 { 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 }