diff --git a/Cargo.lock b/Cargo.lock index 7a9203b3034c463d3b3ecf63abf018b62b4a973e..dfb1c2887ade847bbd8b8a26ea998dd5173c2a1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -605,12 +605,6 @@ dependencies = [ "libc", ] -[[package]] -name = "data-encoding" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" - [[package]] name = "deranged" version = "0.3.11" @@ -1792,7 +1786,6 @@ dependencies = [ "regex", "reqwest", "rocket", - "rocket_ws", "ron", "ryu", "serde", @@ -2204,16 +2197,6 @@ dependencies = [ "uncased", ] -[[package]] -name = "rocket_ws" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f1877668c937b701177c349f21383c556cd3bb4ba8fa1d07fa96ccb3a8782e" -dependencies = [ - "rocket", - "tokio-tungstenite", -] - [[package]] name = "ron" version = "0.8.1" @@ -2462,17 +2445,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.8" @@ -2886,18 +2858,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", -] - [[package]] name = "tokio-util" version = "0.7.10" @@ -3019,25 +2979,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http 1.1.0", - "httparse", - "log", - "rand", - "sha1", - "thiserror", - "url", - "utf-8", -] - [[package]] name = "typenum" version = "1.17.0" @@ -3229,12 +3170,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8parse" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index a055005e46723ec0cb7834a3c5998e8542cb44d1..5614e740185aabcc3c199bc002fc207e46d98d71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,6 @@ chrono = { version = "0.4.31", features = ["serde"] } serde = { version = "1.0.193", features = ["derive"] } nix = { version = "0.27.1", features = ["process", "signal"] } utoipa = { version = "4.1.0", features = ["serde_yaml", "non_strict_integers"] } -rocket_ws = "0.1.1" [dependencies.reqwest] version = "0.11.23" diff --git a/lib/psutil.cc b/lib/psutil.cc index e001c841df653316034e769ad4b9abd2bd277130..f1e77f71c97affcfdcb5dc3181c7c1797cc77ce6 100644 --- a/lib/psutil.cc +++ b/lib/psutil.cc @@ -19,8 +19,7 @@ double get_process_cpu_usage_percentage(int64_t pid) { struct proc_taskinfo pti; int ret = proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti)); if (ret <= 0) { - std::cerr << "Error: Unable to get process info" << std::endl; - return -1.0; + return 0; } return (pti.pti_total_user + pti.pti_total_system) / 100000000.0; // Convert nanoseconds to seconds #else diff --git a/src/daemon/api/mod.rs b/src/daemon/api/mod.rs index dd09acd65b87b9c1843ef7fdbb712a780ab84556..a35eb2df68c1ff4eadf0636dc55a4cf7df286f53 100644 --- a/src/daemon/api/mod.rs +++ b/src/daemon/api/mod.rs @@ -199,6 +199,7 @@ pub async fn start(webui: bool) { routes::logs_handler, routes::logs_raw_handler, routes::metrics_handler, + routes::stream_info, routes::stream_metrics, routes::prometheus_handler, routes::create_handler, diff --git a/src/daemon/api/routes.rs b/src/daemon/api/routes.rs index e76b3d7f318b065ee59c7f59100e2b7ca160aacd..fe741410ea7d6d62b3887c8693ff8dfe2411a516 100644 --- a/src/daemon/api/routes.rs +++ b/src/daemon/api/routes.rs @@ -6,16 +6,15 @@ use macros_rs::{fmtstr, string, ternary, then}; use prometheus::{Encoder, TextEncoder}; use psutil::process::{MemoryInfo, Process}; use reqwest::header::HeaderValue; -use rocket_ws::{Channel, Message, WebSocket}; use serde::Deserialize; use tera::Context; use utoipa::ToSchema; use rocket::{ - futures::{SinkExt, StreamExt}, get, http::{ContentType, Status}, post, + response::stream::{Event, EventStream}, serde::{json::Json, Serialize}, State, }; @@ -42,6 +41,8 @@ use std::{ fs::{self, File}, io::{self, BufRead, BufReader}, path::PathBuf, + thread::sleep, + time::Duration, }; pub(crate) struct Token; @@ -846,18 +847,65 @@ pub async fn get_metrics() -> MetricsRoot { )] pub async fn metrics_handler(_t: Token) -> Json { Json(get_metrics().await) } -#[get("/daemon/stream_metrics")] -pub async fn stream_metrics(ws: WebSocket, _t: Token) -> Channel<'static> { - ws.channel(move |mut stream| { - Box::pin(async move { - while let Some(message) = stream.next().await { - let response = get_metrics().await; - let json = serde_json::to_string(&response); +#[get("/live/daemon//metrics")] +pub async fn stream_metrics(server: String, _t: Token) -> EventStream![] { + EventStream! { + + if let Some(servers) = config::servers().servers { + let (address, (client, headers)) = match servers.get(&server) { + Some(server) => (&server.address, client(&server.token).await), + None => loop { + let response = get_metrics().await; + yield Event::data(serde_json::to_string(&response).unwrap()); + sleep(Duration::from_millis(1500)) + }, + }; - let _ = stream.send(Message::from(json.unwrap())).await; + loop { + match client.get(fmtstr!("{address}/daemon/metrics")).headers(headers.clone()).send().await { + Ok(data) => { + if data.status() != 200 { + break yield Event::data(data.text().await.unwrap()); + } else { + yield Event::data(data.text().await.unwrap()); + sleep(Duration::from_millis(1500)); + } + } + Err(err) => break yield Event::data(format!("{{\"error\": \"{err}\"}}")), + } } + }; + } +} - Ok(()) - }) - }) +#[get("/live/process//")] +pub async fn stream_info(server: String, id: usize, _t: Token) -> EventStream![] { + EventStream! { + let runner = Runner::new(); + + if let Some(servers) = config::servers().servers { + let (address, (client, headers)) = match servers.get(&server) { + Some(server) => (&server.address, client(&server.token).await), + None => loop { + let item = runner.refresh().get(id); + yield Event::data(serde_json::to_string(&item.fetch()).unwrap()); + sleep(Duration::from_millis(1500)) + }, + }; + + loop { + match client.get(fmtstr!("{address}/process/{id}/info")).headers(headers.clone()).send().await { + Ok(data) => { + if data.status() != 200 { + break yield Event::data(data.text().await.unwrap()); + } else { + yield Event::data(data.text().await.unwrap()); + sleep(Duration::from_millis(1500)); + } + } + Err(err) => break yield Event::data(format!("{{\"error\": \"{err}\"}}")), + } + } + }; + } } diff --git a/src/process/mod.rs b/src/process/mod.rs index ec14055ec34ef0cad3f296e9c0fee4cb41591706..23e8e463a613e68a221cf9be275a7c147327b6f3 100644 --- a/src/process/mod.rs +++ b/src/process/mod.rs @@ -194,6 +194,8 @@ fn kill_children(children: Vec) { impl Runner { pub fn new() -> Self { dump::read() } + pub fn refresh(&self) -> Self { Runner::new() } + pub fn connect(name: String, Server { address, token }: Server, verbose: bool) -> Option { let remote_config = match config::from(&address, token.as_deref()) { Ok(config) => config, diff --git a/src/webui/src/components/base.astro b/src/webui/src/components/base.astro index d172f177d7f5377090132b0f75e320d28143bf8c..60a722689a5a8ac89d548c84f26f1498f78b9e70 100644 --- a/src/webui/src/components/base.astro +++ b/src/webui/src/components/base.astro @@ -3,7 +3,6 @@ import '@/styles.css' import banner from '@/public/banner.png?url' import favicon from '@/public/favicon.svg?url' import { ViewTransitions } from "astro:transitions"; -import ProgressBar from 'astro-vtbot/components/ProgressBar.astro'; interface Props { title: string; diff --git a/src/webui/src/components/react/view.tsx b/src/webui/src/components/react/view.tsx index 08837eaec36c8328e81ae2fc6fd715930571c5b4..f8f00b1feb26d3daa715358973a9fa6fca578a2f 100644 --- a/src/webui/src/components/react/view.tsx +++ b/src/webui/src/components/react/view.tsx @@ -1,12 +1,11 @@ import { api } from '@/api'; import { matchSorter } from 'match-sorter'; import Rename from '@/components/react/rename'; +import { classNames } from '@/helpers'; import { useEffect, useState, useRef, Fragment } from 'react'; import { EllipsisVerticalIcon, CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'; import { Menu, MenuItem, MenuItems, MenuButton, Transition, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'; -const classNames = (...classes: Array) => classes.filter(Boolean).join(' '); - const formatMemory = (bytes: number): [number, string] => { const units = ['b', 'kb', 'mb', 'gb']; let size = bytes; @@ -233,7 +232,8 @@ const LogViewer = (props: { server: string | null; base: string; id: number }) = const View = (props: { id: string; base: string }) => { const [item, setItem] = useState(); const [loaded, setLoaded] = useState(false); - const server = new URLSearchParams(window.location.search).get('server'); + const [live, setLive] = useState(null); + const server = new URLSearchParams(window.location.search).get('server') ?? 'local'; const badge = { online: 'bg-emerald-400/10 text-emerald-400', @@ -241,28 +241,37 @@ const View = (props: { id: string; base: string }) => { crashed: 'bg-amber-400/10 text-amber-400' }; - const fetch = () => { - api - .get(`${props.base}/process/${props.id}/info`) - .json() - .then((res) => setItem(res)) - .finally(() => setLoaded(true)); - }; + useEffect(() => { + let retryTimeout; + let hasRun = false; + + const openConnection = () => { + const source = new EventSource(`${props.base}/live/process/${server}/${props.id}`); + setLive(source); + + source.onmessage = (event) => { + setItem(JSON.parse(event.data)); + setLoaded(true); + }; + + source.onerror = (error) => { + source.close(); + retryTimeout = setTimeout(() => { + openConnection(); + }, 5000); + }; + }; - const fetchRemote = () => { - api - .get(`${props.base}/remote/${server}/info/${props.id}`) - .json() - .then((res) => setItem(res)) - .finally(() => setLoaded(true)); - }; + openConnection(); - const isRunning = (status: string): bool => (status == 'stopped' ? false : status == 'crashed' ? false : true); - const action = (id: number, name: string) => api.post(`${props.base}/process/${id}/action`, { json: { method: name } }).then(() => fetch()); + return () => { + live && live.close(); + clearTimeout(retryTimeout); + }; + }, [props.base, server, props.id]); - useEffect(() => { - server != null ? fetchRemote() : fetch(); - }, []); + const isRunning = (status: string): bool => (status == 'stopped' ? false : status == 'crashed' ? false : true); + const action = (id: number, name: string) => api.post(`${props.base}/process/${id}/action`, { json: { method: name } }); if (!loaded) { return ; @@ -280,11 +289,17 @@ const View = (props: { id: string; base: string }) => { return ( + + + {server != 'local' ? server : 'Internal'} +

- {server != null ? `${server}/${item.info.name}` : item.info.name} + {item.info.name}

@@ -338,9 +353,7 @@ const View = (props: { id: string; base: string }) => { )} - - {({ focus }) => } - + {({ focus }) => } {({ _ }) => ( ) => classes.filter(Boolean).join(' ');