Add http interface

This commit is contained in:
2026-03-31 21:46:07 +01:00
parent d05390ab7a
commit 9f71890880
5 changed files with 547 additions and 101 deletions
Generated
+359 -54
View File
@@ -64,6 +64,86 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "async-stream"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "axum"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"axum-core",
"bytes",
"form_urlencoded",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"serde_core",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "bitflags"
version = "2.11.0"
@@ -71,13 +151,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "block2"
version = "0.6.2"
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2",
]
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cfg-if"
@@ -85,12 +162,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clap"
version = "4.6.0"
@@ -147,26 +218,13 @@ dependencies = [
]
[[package]]
name = "ctrlc"
version = "3.5.2"
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"dispatch2",
"nix",
"windows-sys",
]
[[package]]
name = "dispatch2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags",
"block2",
"libc",
"objc2",
"windows-sys",
]
[[package]]
@@ -179,18 +237,135 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures-channel"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "http"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-core",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"bytes",
"http",
"http-body",
"hyper",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "inotify"
version = "0.11.1"
@@ -231,12 +406,30 @@ version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -259,31 +452,10 @@ dependencies = [
]
[[package]]
name = "nix"
version = "0.31.2"
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "objc2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
@@ -291,12 +463,24 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -315,6 +499,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "serde"
version = "1.0.228"
@@ -358,12 +548,57 @@ dependencies = [
"zmij",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
dependencies = [
"itoa",
"serde",
"serde_core",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.3"
@@ -379,12 +614,15 @@ name = "srvmon"
version = "0.1.0"
dependencies = [
"anyhow",
"async-stream",
"axum",
"clap",
"ctrlc",
"flate2",
"futures-core",
"inotify",
"serde",
"serde_json",
"tokio",
]
[[package]]
@@ -404,6 +642,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "tokio"
version = "1.50.0"
@@ -413,10 +657,71 @@ dependencies = [
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
+4 -1
View File
@@ -5,9 +5,12 @@ edition = "2024"
[dependencies]
anyhow = "1.0.102"
async-stream = "0.3.6"
axum = "0.8.8"
clap = { version = "4.6.0", features = ["derive"] }
ctrlc = "3.5.2"
flate2 = "1.1.9"
futures-core = "0.3.32"
inotify = "0.11.1"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros", "signal", "net", "sync"] }
+33 -21
View File
@@ -13,17 +13,7 @@ fn format_count(n: u64) -> String {
}
}
pub fn enter_alt_screen() {
print!("\x1b[?1049h\x1b[?25l");
io::stdout().flush().unwrap();
}
pub fn leave_alt_screen() {
print!("\x1b[?25h\x1b[?1049l");
io::stdout().flush().unwrap();
}
pub fn render(latest: &[Run], top: &[TopEntry], lines: usize) {
fn build_lines(latest: &[Run], top: &[TopEntry], lines: usize) -> (usize, String, Vec<(String, String)>) {
let latest_slice = &latest[..latest.len().min(lines)];
let top_slice = &top[..top.len().min(lines)];
@@ -43,16 +33,9 @@ pub fn render(latest: &[Run], top: &[TopEntry], lines: usize) {
let left_w = if ln_w > 0 { ln_w + 1 + lc_w } else { 6 };
let left_w = left_w.max(6);
let mut out = String::new();
out.push_str("\x1b[H");
out.push_str(&format!(
"\x1b[1m{:<lw$} {}\x1b[0m\x1b[K\n",
"Latest",
"Top",
lw = left_w
));
let header = format!("{:<lw$} Top", "Latest", lw = left_w);
let mut rows = Vec::new();
for i in 0..lines {
let left = if i < latest_slice.len() {
format!(
@@ -76,9 +59,38 @@ pub fn render(latest: &[Run], top: &[TopEntry], lines: usize) {
} else {
String::new()
};
out.push_str(&format!("{} {}\x1b[K\n", left, right));
rows.push((left, right));
}
(left_w, header, rows)
}
pub fn render_plain(latest: &[Run], top: &[TopEntry], lines: usize) -> String {
let (_left_w, header, rows) = build_lines(latest, top, lines);
let mut out = format!("{header}\n");
for (left, right) in &rows {
out.push_str(&format!("{left} {right}\n"));
}
out
}
pub fn enter_alt_screen() {
print!("\x1b[?1049h\x1b[?25l");
io::stdout().flush().unwrap();
}
pub fn leave_alt_screen() {
print!("\x1b[?25h\x1b[?1049l");
io::stdout().flush().unwrap();
}
pub fn render(latest: &[Run], top: &[TopEntry], lines: usize) {
let (_left_w, header, rows) = build_lines(latest, top, lines);
let mut out = String::from("\x1b[H");
out.push_str(&format!("\x1b[1m{header}\x1b[0m\x1b[K\n"));
for (left, right) in &rows {
out.push_str(&format!("{left} {right}\x1b[K\n"));
}
out.push_str("\x1b[J");
print!("{out}");
io::stdout().flush().unwrap();
+97 -25
View File
@@ -1,17 +1,21 @@
mod display;
mod latest;
mod logs;
mod server;
mod state;
mod top;
use std::collections::HashMap;
use std::ffi::OsString;
use std::io;
use std::path::PathBuf;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{bail, Result};
use clap::Parser;
use inotify::{Inotify, WatchDescriptor, WatchMask};
use tokio::sync::watch;
#[derive(Parser)]
#[command(name = "srvmon", about = "Monitor Caddy access logs")]
@@ -23,24 +27,27 @@ struct Cli {
/// Number of lines to display per column
#[arg(short, long, default_value_t = 8)]
lines: usize,
/// Disable terminal display
#[arg(long)]
no_tui: bool,
/// Disable HTTP server
#[arg(long)]
no_http: bool,
/// HTTP server bind address
#[arg(short, long, default_value = "127.0.0.1")]
bind: IpAddr,
/// HTTP server port
#[arg(short, long, default_value_t = 8080)]
port: u16,
}
fn main() -> Result<()> {
let cli = Cli::parse();
let dir = &cli.dir;
if !dir.is_dir() {
bail!("{} is not a directory", dir.display());
}
ctrlc::set_handler(|| {
display::leave_alt_screen();
std::process::exit(0);
})?;
fn startup(dir: &Path) -> Result<(state::State, Vec<latest::Run>)> {
let mut state = state::State::load(dir);
// Process compressed logs
let gz_new: Vec<_> = logs::discover(dir, ".access.log.gz")?
.into_iter()
.filter(|f| !state.processed_gz.contains(&f.name))
@@ -56,7 +63,6 @@ fn main() -> Result<()> {
eprintln!(" done");
}
// Process active log files
let log_files = logs::discover(dir, ".access.log")?;
let mut all_recent = Vec::new();
@@ -78,20 +84,28 @@ fn main() -> Result<()> {
eprintln!(" done");
}
let mut latest_runs = latest::build(&mut all_recent);
let latest_runs = latest::build(&mut all_recent);
state.save(dir)?;
display::enter_alt_screen();
display::render(&latest_runs, &top::sorted(&state.counts), cli.lines);
Ok((state, latest_runs))
}
// Set up inotify
fn watch_loop(
dir: PathBuf,
lines: usize,
tui: bool,
http: bool,
tx: Arc<watch::Sender<String>>,
mut state: state::State,
mut latest_runs: Vec<latest::Run>,
) -> Result<()> {
let mut inotify = Inotify::init()?;
let dir_wd = inotify
.watches()
.add(dir, WatchMask::CREATE | WatchMask::MOVED_TO)?;
.add(&dir, WatchMask::CREATE | WatchMask::MOVED_TO)?;
let mut wd_map: HashMap<WatchDescriptor, logs::LogFile> = HashMap::new();
for f in logs::discover(dir, ".access.log")? {
for f in logs::discover(&dir, ".access.log")? {
let wd = inotify.watches().add(&f.path, WatchMask::MODIFY)?;
wd_map.insert(wd, f);
}
@@ -137,7 +151,7 @@ fn main() -> Result<()> {
state.processed_gz.insert(name_str.into_owned());
changed = true;
}
Err(_) => {} // file not ready yet, will catch on next event
Err(_) => {}
}
}
}
@@ -163,8 +177,66 @@ fn main() -> Result<()> {
new_entries
.sort_by(|a, b| a.ts.partial_cmp(&b.ts).unwrap_or(std::cmp::Ordering::Equal));
latest::push(&mut latest_runs, &new_entries);
state.save(dir)?;
display::render(&latest_runs, &top::sorted(&state.counts), cli.lines);
state.save(&dir)?;
let top = top::sorted(&state.counts);
if tui {
display::render(&latest_runs, &top, lines);
}
if http {
let _ = tx.send(display::render_plain(&latest_runs, &top, lines));
}
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let dir = &cli.dir;
if !dir.is_dir() {
bail!("{} is not a directory", dir.display());
}
let (state, latest_runs) = startup(dir)?;
let top = top::sorted(&state.counts);
let initial_plain = display::render_plain(&latest_runs, &top, cli.lines);
if !cli.no_tui {
display::enter_alt_screen();
display::render(&latest_runs, &top, cli.lines);
}
let (tx, _) = watch::channel(initial_plain);
let tx = Arc::new(tx);
if !cli.no_http {
let tx_server = Arc::clone(&tx);
tokio::spawn(async move {
if let Err(e) = server::run(tx_server, cli.bind, cli.port).await {
eprintln!("HTTP server error: {e}");
std::process::exit(1);
}
});
}
let tui = !cli.no_tui;
let http = !cli.no_http;
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
if tui {
display::leave_alt_screen();
}
std::process::exit(0);
});
let dir = cli.dir.clone();
let lines = cli.lines;
tokio::task::spawn_blocking(move || {
watch_loop(dir, lines, tui, http, tx, state, latest_runs)
})
.await??;
Ok(())
}
+54
View File
@@ -0,0 +1,54 @@
use std::convert::Infallible;
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use axum::response::sse::{Event, Sse};
use axum::response::Html;
use axum::routing::get;
use axum::Router;
use tokio::sync::watch;
const INDEX_HTML: &str = r#"<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>srvmon</title></head>
<body style="background:#1e1e1e;color:#d4d4d4;font-family:monospace;margin:2em">
<pre><code id="o"></code></pre>
<script>
const o = document.getElementById('o');
const es = new EventSource('/events');
es.onmessage = e => { o.textContent = e.data; };
</script>
</body>
</html>"#;
async fn index() -> Html<&'static str> {
Html(INDEX_HTML)
}
async fn events(
axum::extract::State(tx): axum::extract::State<Arc<watch::Sender<String>>>,
) -> Sse<impl futures_core::Stream<Item = Result<Event, Infallible>>> {
let mut rx = tx.subscribe();
let stream = async_stream::stream! {
let current = rx.borrow().clone();
yield Ok(Event::default().data(current));
while rx.changed().await.is_ok() {
let data = rx.borrow().clone();
yield Ok(Event::default().data(data));
}
};
Sse::new(stream)
}
pub async fn run(tx: Arc<watch::Sender<String>>, bind: IpAddr, port: u16) -> anyhow::Result<()> {
let app = Router::new()
.route("/", get(index))
.route("/events", get(events))
.with_state(tx);
let addr = SocketAddr::from((bind, port));
eprintln!("HTTP server listening on {addr}");
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}