wterm 0.3.4

Serial port to WebSocket bridge with embedded web terminal
#[cfg(debug_assertions)]
use actix_files::NamedFile;
use actix_web::{web, App, HttpRequest, HttpServer, Responder};
use bridge::Bridge;
use futures_util::stream::StreamExt;
#[cfg(debug_assertions)]
use std::path::PathBuf;
use std::{io, net::SocketAddr};
use structopt::StructOpt;

mod bridge;

#[derive(Debug, StructOpt)]
struct Cli {
    #[structopt(
        short = "a",
        long = "addr",
        default_value = "127.0.0.1:4242",
        help = "Webserver address."
    )]
    addr: SocketAddr,
}

#[cfg(debug_assertions)]
async fn index(req: HttpRequest) -> NamedFile {
    let filename: String = req
        .match_info()
        .query("filename")
        .parse()
        .expect("Failed to parse filename");
    let mut path: PathBuf = PathBuf::from("./static");
    if filename.is_empty() {
        path.push("index.html");
    } else {
        path.push(filename);
    }
    NamedFile::open(path).unwrap()
}

#[cfg(not(debug_assertions))]
async fn index(req: HttpRequest) -> actix_web::HttpResponse {
    static ICON: &[u8] = include_bytes!("./../static/favicon.png");
    static HOME: &str = include_str!("./../static/index.html");
    static JS: &str = include_str!("./../static/wterm.js");

    let filename = req
        .match_info()
        .query("filename")
        .parse::<String>()
        .expect("Failed to parse filename");

    match filename.as_ref() {
        "" | "index.html" => actix_web::HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(HOME),
        "wterm.js" => actix_web::HttpResponse::Ok()
            .content_type("text/javascript; charset=utf-8")
            .body(JS),
        "favicon.png" => actix_web::HttpResponse::Ok()
            .content_type("image/png")
            .body(ICON),
        _ => actix_web::HttpResponse::NotFound().finish(),
    }
}

async fn ws(
    req: HttpRequest,
    body: web::Payload,
    bridge: web::Data<Bridge>,
) -> actix_web::Result<impl Responder> {
    let (response, session, mut stream) = actix_ws::handle(&req, body)?;
    let serial_bridge = bridge.clone();
    let mut poll_session = session.clone();

    actix_web::rt::spawn(async move {
        serial_bridge.poll_serial(&mut poll_session).await
    });

    let ws_bridge = bridge.clone();
    let mut ws_session = session.clone();
    actix_web::rt::spawn(async move {
        while let Some(Ok(msg)) = stream.next().await {
            if ws_bridge.handle(msg, &mut ws_session).await.is_err() {
                break;
            }
        }
        let _ = ws_session.close(None).await;
    });

    Ok(response)
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> io::Result<()> {
    let cli = Cli::from_args();
    println!("http://{}", cli.addr);
    let session = Bridge::new();
    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(session.clone()))
            .service(web::resource("/ws").to(ws))
            .route("/{filename:.*}", web::get().to(index))
    })
    .bind(cli.addr)
    .expect("Failed to bind to network interface")
    .run()
    .await?;

    Ok(())
}