diff --git a/Cargo.lock b/Cargo.lock index db3b51c..6b64cef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,7 +208,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", "windows-link", ] @@ -688,6 +690,7 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" name = "hlstate-httpd" version = "0.1.0" dependencies = [ + "chrono", "clap", "rocket", "rocket_dyn_templates", diff --git a/crates/httpd/Cargo.toml b/crates/httpd/Cargo.toml index c67a3ec..6871e7f 100644 --- a/crates/httpd/Cargo.toml +++ b/crates/httpd/Cargo.toml @@ -10,6 +10,7 @@ categories = ["parsing", "text-processing", "value-formatting"] repository = "https://github.com/YGGverse/hlstate-rs" [dependencies] +chrono = "0.4.44" #observer = { git = "https://github.com/YGGverse/xash3d-master.git", package = "xash3d-observer", branch = "ip6-only" } #protocol = { git = "https://github.com/YGGverse/xash3d-master.git", package = "xash3d-protocol", branch = "ip6-only" } #observer = { path = "../../../xash3d-master/observer", package = "xash3d-observer" } @@ -18,4 +19,4 @@ clap = { version = "4.5.54", features = ["derive"] } #mysql = { package = "hlstate-mysql", version = "0.1.0", path = "../mysql" } rocket = { version = "0.5.1", features = ["json"] } rocket_dyn_templates = { version = "0.2.0", features = ["tera"] } -toml = "0.9.10" \ No newline at end of file +toml = "0.9.10" diff --git a/crates/httpd/config.toml b/crates/httpd/config.toml index afef9f8..33b32b6 100644 --- a/crates/httpd/config.toml +++ b/crates/httpd/config.toml @@ -12,5 +12,11 @@ debug = true # Path to `xash3d-query` bin query = "xash3d-query" +# Interval to update master scrape, in seconds +refresh = 60 + # Define at least one master to scrape game servers from -masters = ["[202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]:27010", "[300:dee4:d3c0:953b::2019]:27010"] \ No newline at end of file +masters = [ + "[202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]:27010", + "[300:dee4:d3c0:953b::2019]:27010" +] \ No newline at end of file diff --git a/crates/httpd/src/config.rs b/crates/httpd/src/config.rs index 189f57e..9f7932c 100644 --- a/crates/httpd/src/config.rs +++ b/crates/httpd/src/config.rs @@ -14,4 +14,5 @@ pub struct Config { pub port: u16, pub query: PathBuf, pub title: String, + pub refresh: u64, } diff --git a/crates/httpd/src/main.rs b/crates/httpd/src/main.rs index ac07dbc..c91306d 100644 --- a/crates/httpd/src/main.rs +++ b/crates/httpd/src/main.rs @@ -7,64 +7,82 @@ mod global; mod meta; mod scrape; +use chrono::{DateTime, Utc}; use global::Global; use meta::Meta; -use rocket::{State, http::Status}; +use rocket::{ + State, + http::Status, + tokio::{spawn, sync::RwLock, time::sleep}, +}; use rocket_dyn_templates::{Template, context}; +use std::{collections::HashSet, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration}; + +struct Online { + scrape: scrape::Result, + update: DateTime, +} +type Snap = Arc>>; #[get("/")] -fn index(meta: &State, global: &State) -> Result { - // @TODO: requires library impl - // https://github.com/FWGS/xash3d-master/issues/4 - let scrape = std::process::Command::new(&global.query) - .arg("all") - .arg("-M") - .arg( - global - .masters - .iter() - .map(|a| a.to_string()) - .collect::>() - .join(","), - ) - .arg("-j") - .output() - .map_err(|e| { - error!("Make sure `xash3d-query` is installed: {e}"); - Status::InternalServerError - })?; - if scrape.status.success() { - let result: scrape::Result = rocket::serde::json::serde_json::from_str( - str::from_utf8(&scrape.stdout).map_err(|e| { - error!("stdout parse error: {e}"); - Status::InternalServerError - })?, - ) - .map_err(|e| { - error!("JSON parse error: {e}"); - Status::InternalServerError - })?; - Ok(Template::render( - "index", - context! { - masters: &global.masters, - title: &meta.title, - version: &meta.version, - servers: result.servers, - }, - )) - } else { - error!("Make sure `xash3d-query` is installed!"); - Err(Status::InternalServerError) - } +async fn index( + meta: &State, + global: &State, + online: &State, +) -> Result { + let snap = online.read().await; + Ok(Template::render( + "index", + context! { + masters: &global.masters, + title: &meta.title, + version: &meta.version, + servers: snap.as_ref().map(|s|s.scrape.servers.clone()), + updated: snap.as_ref().map(|s|s.update.to_rfc2822()) + }, + )) } #[launch] -fn rocket() -> _ { +async fn rocket() -> _ { use clap::Parser; let argument = argument::Argument::parse(); let config: config::Config = toml::from_str(&std::fs::read_to_string(argument.config).unwrap()).unwrap(); + let online: Snap = Arc::new(RwLock::new(None)); + spawn({ + let online = online.clone(); + let query = config.query.clone(); + let masters = config.masters.clone(); + async move { + loop { + match scrape(&query, &masters) { + Ok(s) => match str::from_utf8(&s.stdout) { + Ok(r) => { + if s.status.success() { + *online.write().await = + match rocket::serde::json::serde_json::from_str(r) { + Ok(scrape) => Some(Online { + scrape, + update: Utc::now(), + }), + Err(e) => { + error!("Could not decode scrape response: `{e}`"); + None + } + } + } else { + error!("Scrape request failed"); + } + } + Err(e) => error!("Could not decode UTF-8: `{e}`"), + }, + Err(e) => error!("Scrape error: `{e}`"), + } + sleep(Duration::from_secs(config.refresh)).await; + } + } + }); rocket::build() .attach(Template::fairing()) .configure(rocket::Config { @@ -76,6 +94,7 @@ fn rocket() -> _ { rocket::Config::release_default() } }) + .manage(online) .manage(Global { masters: config.masters, query: config.query, @@ -86,3 +105,24 @@ fn rocket() -> _ { }) .mount("/", routes![index]) } + +/// Get servers online using `bin` from given `masters` +fn scrape( + bin: &PathBuf, + masters: &HashSet, +) -> Result { + // @TODO: requires library impl + // https://github.com/FWGS/xash3d-master/issues/4 + std::process::Command::new(bin) + .arg("all") + .arg("-M") + .arg( + masters + .iter() + .map(|a| a.to_string()) + .collect::>() + .join(","), + ) + .arg("-j") + .output() +} diff --git a/crates/httpd/src/scrape.rs b/crates/httpd/src/scrape.rs index 8fd0624..d762910 100644 --- a/crates/httpd/src/scrape.rs +++ b/crates/httpd/src/scrape.rs @@ -12,7 +12,7 @@ pub struct Result { pub servers: Vec, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] #[serde(crate = "rocket::serde")] pub struct Info { pub time: i64, diff --git a/crates/httpd/templates/index.html.tera b/crates/httpd/templates/index.html.tera index 4a9984d..4ee9a4b 100644 --- a/crates/httpd/templates/index.html.tera +++ b/crates/httpd/templates/index.html.tera @@ -2,46 +2,53 @@ {% block content %}

Game

{% if servers %} - - - - - - - - - - - - - - - - - - - - - {% for server in servers %} - - - - - - - - - - - - - - - - - {% endfor %} - -
AddressHostPingProtocolGamedirMapTeamCoopPasswordDedicatedDMMaxOnlineStatus
{{ server.address }}{{ server.host }}{{ server.ping }}{{ server.protocol }}{{ server.gamedir }}{{ server.map }}{{ server.team }}{{ server.coop }}{{ server.password }}{{ server.dedicated }}{{ server.dm }}{{ server.maxcl }}{{ server.numcl }}{{ server.status }}
+
+ + + + {# @TODO subscribe#} + + + + + + + + + + + + + + + + + + {% for server in servers %} + + {# @TODO subscribe#} + + + + + + + + + + + + + + + + {% endfor %} + +
AddressHostPingProtocolGamedirMapTeamCoopPasswordDedicatedDMMaxOnlineStatus
{{ server.address }}{{ server.host }}{{ server.ping }}{{ server.protocol }}{{ server.gamedir }}{{ server.map }}{{ server.team }}{{ server.coop }}{{ server.password }}{{ server.dedicated }}{{ server.dm }}{{ server.maxcl }}{{ server.numcl }}{{ server.status }}
+ {# @TODO subscribe + + #} +
{% else %}

Nobody.

diff --git a/crates/httpd/templates/layout.html.tera b/crates/httpd/templates/layout.html.tera index 89345c6..70034f0 100644 --- a/crates/httpd/templates/layout.html.tera +++ b/crates/httpd/templates/layout.html.tera @@ -9,7 +9,7 @@ padding: 0; font-family: monospace; color-scheme: light dark; - --container-max-width: 1024px; + --container-max-width: 1128px; --color-success: #4bc432; --color-warning: #f37b21; --color-error: #ff6363; @@ -75,6 +75,19 @@ position: relative; } + header > div { + display: inline-block; + } + + header > div:first-child { + float: left; + } + + header > div:last-child { + float: right; + text-align: right; + } + main { display: block; margin: 16px auto; @@ -101,10 +114,7 @@

-
- -
-
+
{{ title }} | Game | @@ -113,18 +123,21 @@ GitHub
+
+ {% if updated %}{{ updated }}{% endif %} +
{% block content %}{% endblock content %}