rlink/runtime/worker/
web_server.rs

1use std::convert::Infallible;
2use std::net::SocketAddr;
3use std::path::PathBuf;
4use std::str::FromStr;
5use std::sync::Arc;
6
7use hyper::http::header;
8use hyper::service::{make_service_fn, service_fn};
9use hyper::{Body, Method, Request, Response};
10use hyper::{Server, StatusCode};
11use rand::Rng;
12
13use crate::channel::{bounded, Sender};
14use crate::core::cluster::StdResponse;
15use crate::utils::fs::read_binary;
16use crate::utils::http::server::{as_ok_json, page_not_found};
17use crate::utils::thread::async_runtime_multi;
18
19pub(crate) fn web_launch(context: Arc<crate::runtime::context::Context>) -> String {
20    let (tx, rx) = bounded(1);
21
22    std::thread::Builder::new()
23        .name("WebUI".to_string())
24        .spawn(move || {
25            async_runtime_multi("web", 2).block_on(async move {
26                let ip = context.bind_ip.clone();
27                let web_context = Arc::new(WebContext { context });
28                serve_with_rand_port(web_context, ip, tx).await;
29            });
30        })
31        .unwrap();
32
33    let bind_addr: SocketAddr = rx.recv().unwrap();
34    format!("http://{}", bind_addr.to_string())
35}
36
37struct WebContext {
38    context: Arc<crate::runtime::context::Context>,
39}
40
41async fn serve_with_rand_port(
42    web_context: Arc<WebContext>,
43    bind_id: String,
44    bind_addr_tx: Sender<SocketAddr>,
45) {
46    let mut rng = rand::thread_rng();
47    for _ in 0..30 {
48        let port = rng.gen_range(10000..30000);
49        let address = format!("{}:{}", bind_id.as_str(), port);
50        let socket_addr = SocketAddr::from_str(address.as_str()).unwrap();
51
52        let serve_result = serve(web_context.clone(), &socket_addr, bind_addr_tx.clone()).await;
53        match serve_result {
54            Ok(_) => error!("server stop"),
55            Err(e) => info!("try bind failure> {}", e),
56        }
57    }
58
59    error!("no port can be bound");
60}
61
62async fn serve(
63    web_context: Arc<WebContext>,
64    bind_addr: &SocketAddr,
65    bind_addr_tx: Sender<SocketAddr>,
66) -> anyhow::Result<()> {
67    // And a MakeService to handle each connection...
68    let make_service = make_service_fn(move |_conn| {
69        let web_context = web_context.clone();
70        async move {
71            Ok::<_, Infallible>(service_fn(move |req| {
72                let web_context = web_context.clone();
73                route(req, web_context)
74            }))
75        }
76    });
77
78    // Then bind and serve...
79    let server = Server::try_bind(bind_addr)?.serve(make_service);
80
81    bind_addr_tx.send(bind_addr.clone()).unwrap();
82
83    // And run forever...
84    if let Err(e) = server.await {
85        eprintln!("server error: {}", e);
86    }
87
88    Ok(())
89}
90
91async fn route(req: Request<Body>, web_context: Arc<WebContext>) -> anyhow::Result<Response<Body>> {
92    let path = req.uri().path();
93    let method = req.method();
94
95    if path.starts_with("/api/") {
96        if Method::GET.eq(method) {
97            match path {
98                "/api/threads" => get_thread_infos(req, web_context).await,
99                "/api/client/log/enable" => enable_client_log(req, web_context).await,
100                "/api/client/log/disable" => disable_client_log(req, web_context).await,
101                "/api/server/log/enable" => enable_server_log(req, web_context).await,
102                "/api/server/log/disable" => disable_server_log(req, web_context).await,
103                _ => page_not_found().await,
104            }
105        } else {
106            page_not_found().await
107        }
108    } else {
109        if Method::GET.eq(method) {
110            static_file(req, web_context).await
111        } else {
112            page_not_found().await
113        }
114    }
115}
116
117async fn enable_client_log(
118    _req: Request<Body>,
119    _context: Arc<WebContext>,
120) -> anyhow::Result<Response<Body>> {
121    crate::pub_sub::network::client::enable_log();
122    as_ok_json(&StdResponse::ok(Some(true)))
123}
124
125async fn disable_client_log(
126    _req: Request<Body>,
127    _context: Arc<WebContext>,
128) -> anyhow::Result<Response<Body>> {
129    crate::pub_sub::network::client::disable_log();
130    as_ok_json(&StdResponse::ok(Some(false)))
131}
132
133async fn enable_server_log(
134    _req: Request<Body>,
135    _context: Arc<WebContext>,
136) -> anyhow::Result<Response<Body>> {
137    crate::pub_sub::network::server::enable_log();
138    as_ok_json(&StdResponse::ok(Some(true)))
139}
140
141async fn disable_server_log(
142    _req: Request<Body>,
143    _context: Arc<WebContext>,
144) -> anyhow::Result<Response<Body>> {
145    crate::pub_sub::network::server::disable_log();
146    as_ok_json(&StdResponse::ok(Some(false)))
147}
148
149async fn get_thread_infos(
150    _req: Request<Body>,
151    _context: Arc<WebContext>,
152) -> anyhow::Result<Response<Body>> {
153    let c = crate::utils::thread::get_thread_infos();
154    as_ok_json(&StdResponse::ok(Some(c)))
155}
156
157async fn static_file(
158    req: Request<Body>,
159    context: Arc<WebContext>,
160) -> anyhow::Result<Response<Body>> {
161    let path = {
162        let mut path = req.uri().path();
163        if path.is_empty() || "/".eq(path) {
164            path = "/index.html";
165        };
166
167        &path[1..path.len()]
168    };
169
170    let static_file_path = {
171        let path = PathBuf::from_str(path)?;
172
173        let dashboard_path = context.context.dashboard_path.as_str();
174        let base_path = PathBuf::from_str(dashboard_path)?;
175
176        let n = base_path.join(path);
177        n
178    };
179
180    let ext = {
181        let ext_pos = path.rfind(".").ok_or(anyhow!("file ext name not found"))?;
182        &path[ext_pos + 1..path.len()]
183    };
184
185    let context_type = match ext {
186        "html" => "text/html; charset=utf-8",
187        "js" => "application/javascript",
188        "css" => "text/css",
189        "ico" => "image/x-icon",
190        "gif" => "image/gif",
191        "png" => "image/png",
192        "svg" => "image/svg+xml",
193        "woff" => "application/font-woff",
194        _ => "",
195    };
196
197    match read_binary(&static_file_path) {
198        Ok(context) => Response::builder()
199            .header(header::CONTENT_TYPE, context_type)
200            .status(StatusCode::OK)
201            .body(Body::from(context))
202            .map_err(|e| anyhow!(e)),
203        Err(e) => {
204            error!(
205                "static file not found. file path: {:?}, error: {}",
206                static_file_path, e
207            );
208            page_not_found().await
209        }
210    }
211}