implement state update on background (basics for subscription feature)

This commit is contained in:
yggverse 2026-03-26 20:58:31 +02:00
parent e77d101b70
commit 8daa729bf9
8 changed files with 167 additions and 96 deletions

View file

@ -14,4 +14,5 @@ pub struct Config {
pub port: u16,
pub query: PathBuf,
pub title: String,
pub refresh: u64,
}

View file

@ -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<Utc>,
}
type Snap = Arc<RwLock<Option<Online>>>;
#[get("/")]
fn index(meta: &State<Meta>, global: &State<Global>) -> Result<Template, Status> {
// @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::<Vec<_>>()
.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<Meta>,
global: &State<Global>,
online: &State<Snap>,
) -> Result<Template, Status> {
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<SocketAddr>,
) -> Result<std::process::Output, std::io::Error> {
// @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::<Vec<_>>()
.join(","),
)
.arg("-j")
.output()
}

View file

@ -12,7 +12,7 @@ pub struct Result {
pub servers: Vec<Info>,
}
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(crate = "rocket::serde")]
pub struct Info {
pub time: i64,