Merge pull request #14 from jasonwitty/feature/extract-socktop-connector
Refactor for additional socktop connector library
This commit is contained in:
commit
18b41c1b45
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -42,7 +42,7 @@ jobs:
|
|||||||
kill $AGENT_PID || true
|
kill $AGENT_PID || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
SOCKTOP_WS=ws://127.0.0.1:3000/ws cargo test -p socktop --test ws_probe -- --nocapture
|
SOCKTOP_WS=ws://127.0.0.1:3000/ws cargo test -p socktop_connector --test integration_test -- --nocapture
|
||||||
kill $AGENT_PID || true
|
kill $AGENT_PID || true
|
||||||
|
|
||||||
- name: "Windows: start agent and run WS probe"
|
- name: "Windows: start agent and run WS probe"
|
||||||
@ -79,7 +79,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
$env:SOCKTOP_WS = "ws://127.0.0.1:3000/ws"
|
$env:SOCKTOP_WS = "ws://127.0.0.1:3000/ws"
|
||||||
try {
|
try {
|
||||||
cargo test -p socktop --test ws_probe -- --nocapture
|
cargo test -p socktop_connector --test integration_test -- --nocapture
|
||||||
} finally {
|
} finally {
|
||||||
if ($p -and !$p.HasExited) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue }
|
if ($p -and !$p.HasExited) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue }
|
||||||
}
|
}
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
.vscode/
|
.vscode/
|
||||||
|
/socktop-wasm-test/target
|
||||||
|
|||||||
57
Cargo.lock
generated
57
Cargo.lock
generated
@ -1953,6 +1953,7 @@ dependencies = [
|
|||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki 0.103.4",
|
"rustls-webpki 0.103.4",
|
||||||
"subtle",
|
"subtle",
|
||||||
@ -2168,26 +2169,20 @@ dependencies = [
|
|||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
"crossterm 0.27.0",
|
"crossterm 0.27.0",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
"flate2",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"prost",
|
|
||||||
"prost-build",
|
|
||||||
"protoc-bin-vendored",
|
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"rustls 0.23.31",
|
|
||||||
"rustls-pemfile",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"socktop_connector",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite 0.24.0",
|
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socktop_agent"
|
name = "socktop_agent"
|
||||||
version = "1.40.67"
|
version = "1.40.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
@ -2216,6 +2211,29 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socktop_connector"
|
||||||
|
version = "0.1.6"
|
||||||
|
dependencies = [
|
||||||
|
"flate2",
|
||||||
|
"futures-util",
|
||||||
|
"js-sys",
|
||||||
|
"prost",
|
||||||
|
"prost-build",
|
||||||
|
"protoc-bin-vendored",
|
||||||
|
"rustls 0.23.31",
|
||||||
|
"rustls-pemfile",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
"tokio",
|
||||||
|
"tokio-tungstenite 0.24.0",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -2786,6 +2804,19 @@ dependencies = [
|
|||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-futures"
|
||||||
|
version = "0.4.50"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.100"
|
version = "0.2.100"
|
||||||
@ -2818,6 +2849,16 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-sys"
|
||||||
|
version = "0.3.77"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "which"
|
name = "which"
|
||||||
version = "4.4.2"
|
version = "4.4.2"
|
||||||
|
|||||||
11
Cargo.toml
11
Cargo.toml
@ -2,7 +2,8 @@
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"socktop",
|
"socktop",
|
||||||
"socktop_agent"
|
"socktop_agent",
|
||||||
|
"socktop_connector"
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
@ -26,7 +27,6 @@ sysinfo = "0.37"
|
|||||||
ratatui = "0.28"
|
ratatui = "0.28"
|
||||||
crossterm = "0.27"
|
crossterm = "0.27"
|
||||||
|
|
||||||
|
|
||||||
# web server (remote-agent)
|
# web server (remote-agent)
|
||||||
axum = { version = "0.7", features = ["ws"] }
|
axum = { version = "0.7", features = ["ws"] }
|
||||||
|
|
||||||
@ -34,6 +34,13 @@ axum = { version = "0.7", features = ["ws"] }
|
|||||||
prost = "0.13"
|
prost = "0.13"
|
||||||
dirs-next = "2"
|
dirs-next = "2"
|
||||||
|
|
||||||
|
# compression
|
||||||
|
flate2 = "1.0"
|
||||||
|
|
||||||
|
# TLS
|
||||||
|
rustls = { version = "0.23", features = ["ring"] }
|
||||||
|
rustls-pemfile = "2.1"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
# Favor smaller, simpler binaries with good runtime perf
|
# Favor smaller, simpler binaries with good runtime perf
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
|
|||||||
@ -207,7 +207,7 @@ socktop -t /path/to/cert.pem wss://HOST:8443/ws
|
|||||||
|
|
||||||
Intervals (client-driven):
|
Intervals (client-driven):
|
||||||
- Fast metrics: ~500 ms
|
- Fast metrics: ~500 ms
|
||||||
- Processes: ~2 s (top 50)
|
- Processes: ~2 s
|
||||||
- Disks: ~5 s
|
- Disks: ~5 s
|
||||||
|
|
||||||
The agent stays idle unless queried. When queried, it collects just what’s needed.
|
The agent stays idle unless queried. When queried, it collects just what’s needed.
|
||||||
|
|||||||
@ -3,13 +3,15 @@ name = "socktop"
|
|||||||
version = "1.40.0"
|
version = "1.40.0"
|
||||||
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
|
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
|
||||||
description = "Remote system monitor over WebSocket, TUI like top"
|
description = "Remote system monitor over WebSocket, TUI like top"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
# socktop connector for agent communication
|
||||||
|
socktop_connector = { path = "../socktop_connector" }
|
||||||
|
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tokio-tungstenite = { workspace = true }
|
|
||||||
futures-util = { workspace = true }
|
futures-util = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
@ -17,17 +19,9 @@ url = { workspace = true }
|
|||||||
ratatui = { workspace = true }
|
ratatui = { workspace = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
|
||||||
dirs-next = { workspace = true }
|
dirs-next = { workspace = true }
|
||||||
sysinfo = { workspace = true }
|
sysinfo = { workspace = true }
|
||||||
rustls = "0.23"
|
|
||||||
rustls-pemfile = "2.1"
|
|
||||||
prost = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2.0"
|
assert_cmd = "2.0"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
prost-build = "0.13"
|
|
||||||
protoc-bin-vendored = "3"
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
fn main() {
|
|
||||||
// Vendored protoc for reproducible builds (works on crates.io build machines)
|
|
||||||
let protoc = protoc_bin_vendored::protoc_bin_path().expect("protoc");
|
|
||||||
std::env::set_var("PROTOC", &protoc);
|
|
||||||
|
|
||||||
// Tell Cargo when to re-run
|
|
||||||
println!("cargo:rerun-if-changed=proto/processes.proto");
|
|
||||||
|
|
||||||
let mut cfg = prost_build::Config::new();
|
|
||||||
cfg.out_dir(std::env::var("OUT_DIR").unwrap());
|
|
||||||
// Use in-crate relative path so `cargo package` includes the file
|
|
||||||
cfg.compile_protos(&["proto/processes.proto"], &["proto"]) // paths relative to CARGO_MANIFEST_DIR
|
|
||||||
.expect("compile protos");
|
|
||||||
}
|
|
||||||
@ -9,28 +9,36 @@ use std::{
|
|||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
|
||||||
layout::{Constraint, Direction, Rect},
|
|
||||||
//style::Color, // + add Color
|
//style::Color, // + add Color
|
||||||
Terminal,
|
Terminal,
|
||||||
|
backend::CrosstermBackend,
|
||||||
|
layout::{Constraint, Direction, Rect},
|
||||||
};
|
};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use crate::history::{push_capped, PerCoreHistory};
|
use crate::history::{PerCoreHistory, push_capped};
|
||||||
use crate::types::Metrics;
|
use crate::types::Metrics;
|
||||||
use crate::ui::cpu::{
|
use crate::ui::cpu::{
|
||||||
draw_cpu_avg_graph, draw_per_core_bars, per_core_clamp, per_core_content_area,
|
PerCoreScrollDrag, draw_cpu_avg_graph, draw_per_core_bars, per_core_clamp,
|
||||||
per_core_handle_key, per_core_handle_mouse, per_core_handle_scrollbar_mouse, PerCoreScrollDrag,
|
per_core_content_area, per_core_handle_key, per_core_handle_mouse,
|
||||||
|
per_core_handle_scrollbar_mouse,
|
||||||
};
|
};
|
||||||
use crate::ui::processes::{processes_handle_key, processes_handle_mouse, ProcSortBy};
|
use crate::ui::processes::{ProcSortBy, processes_handle_key, processes_handle_mouse};
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
disks::draw_disks, gpu::draw_gpu, header::draw_header, mem::draw_mem, net::draw_net_spark,
|
disks::draw_disks, gpu::draw_gpu, header::draw_header, mem::draw_mem, net::draw_net_spark,
|
||||||
swap::draw_swap,
|
swap::draw_swap,
|
||||||
};
|
};
|
||||||
use crate::ws::{connect, request_disks, request_metrics, request_processes};
|
use socktop_connector::{
|
||||||
|
AgentRequest, AgentResponse, SocktopConnector, connect_to_socktop_agent,
|
||||||
|
connect_to_socktop_agent_with_tls,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Constants for minimum intervals to ensure reasonable performance
|
||||||
|
const MIN_METRICS_INTERVAL_MS: u64 = 100;
|
||||||
|
const MIN_PROCESSES_INTERVAL_MS: u64 = 200;
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
// Latest metrics + histories
|
// Latest metrics + histories
|
||||||
@ -106,12 +114,12 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_intervals(mut self, metrics_ms: Option<u64>, procs_ms: Option<u64>) -> Self {
|
pub fn with_intervals(mut self, metrics_ms: Option<u64>, procs_ms: Option<u64>) -> Self {
|
||||||
if let Some(m) = metrics_ms {
|
metrics_ms.inspect(|&m| {
|
||||||
self.metrics_interval = Duration::from_millis(m.max(100));
|
self.metrics_interval = Duration::from_millis(m.max(MIN_METRICS_INTERVAL_MS));
|
||||||
}
|
});
|
||||||
if let Some(p) = procs_ms {
|
procs_ms.inspect(|&p| {
|
||||||
self.procs_interval = Duration::from_millis(p.max(200));
|
self.procs_interval = Duration::from_millis(p.max(MIN_PROCESSES_INTERVAL_MS));
|
||||||
}
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,11 +133,15 @@ impl App {
|
|||||||
&mut self,
|
&mut self,
|
||||||
url: &str,
|
url: &str,
|
||||||
tls_ca: Option<&str>,
|
tls_ca: Option<&str>,
|
||||||
|
verify_hostname: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Connect to agent
|
// Connect to agent
|
||||||
//let mut ws = connect(url, tls_ca).await?;
|
|
||||||
self.ws_url = url.to_string();
|
self.ws_url = url.to_string();
|
||||||
let mut ws = connect(url, tls_ca).await?;
|
let mut ws = if let Some(ca_path) = tls_ca {
|
||||||
|
connect_to_socktop_agent_with_tls(url, ca_path, verify_hostname).await?
|
||||||
|
} else {
|
||||||
|
connect_to_socktop_agent(url).await?
|
||||||
|
};
|
||||||
|
|
||||||
// Terminal setup
|
// Terminal setup
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
@ -154,7 +166,7 @@ impl App {
|
|||||||
async fn event_loop<B: ratatui::backend::Backend>(
|
async fn event_loop<B: ratatui::backend::Backend>(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal<B>,
|
terminal: &mut Terminal<B>,
|
||||||
ws: &mut crate::ws::WsStream,
|
ws: &mut SocktopConnector,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
loop {
|
loop {
|
||||||
// Input (non-blocking)
|
// Input (non-blocking)
|
||||||
@ -257,18 +269,17 @@ impl App {
|
|||||||
// Processes table: sort by column on header click
|
// Processes table: sort by column on header click
|
||||||
if let (Some(mm), Some(p_area)) =
|
if let (Some(mm), Some(p_area)) =
|
||||||
(self.last_metrics.as_ref(), self.last_procs_area)
|
(self.last_metrics.as_ref(), self.last_procs_area)
|
||||||
{
|
&& let Some(new_sort) = processes_handle_mouse(
|
||||||
if let Some(new_sort) = processes_handle_mouse(
|
|
||||||
&mut self.procs_scroll_offset,
|
&mut self.procs_scroll_offset,
|
||||||
&mut self.procs_drag,
|
&mut self.procs_drag,
|
||||||
m,
|
m,
|
||||||
p_area,
|
p_area,
|
||||||
mm.top_processes.len(),
|
mm.top_processes.len(),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
self.procs_sort_by = new_sort;
|
self.procs_sort_by = new_sort;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Event::Resize(_, _) => {}
|
Event::Resize(_, _) => {}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -278,27 +289,30 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch and update
|
// Fetch and update
|
||||||
if let Some(m) = request_metrics(ws).await {
|
if let Ok(response) = ws.request(AgentRequest::Metrics).await {
|
||||||
|
if let AgentResponse::Metrics(m) = response {
|
||||||
self.update_with_metrics(m);
|
self.update_with_metrics(m);
|
||||||
|
}
|
||||||
|
|
||||||
// Only poll processes every 2s
|
// Only poll processes every 2s
|
||||||
if self.last_procs_poll.elapsed() >= self.procs_interval {
|
if self.last_procs_poll.elapsed() >= self.procs_interval {
|
||||||
if let Some(procs) = request_processes(ws).await {
|
if let Ok(AgentResponse::Processes(procs)) =
|
||||||
if let Some(mm) = self.last_metrics.as_mut() {
|
ws.request(AgentRequest::Processes).await
|
||||||
|
&& let Some(mm) = self.last_metrics.as_mut()
|
||||||
|
{
|
||||||
mm.top_processes = procs.top_processes;
|
mm.top_processes = procs.top_processes;
|
||||||
mm.process_count = Some(procs.process_count);
|
mm.process_count = Some(procs.process_count);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
self.last_procs_poll = Instant::now();
|
self.last_procs_poll = Instant::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only poll disks every 5s
|
// Only poll disks every 5s
|
||||||
if self.last_disks_poll.elapsed() >= self.disks_interval {
|
if self.last_disks_poll.elapsed() >= self.disks_interval {
|
||||||
if let Some(disks) = request_disks(ws).await {
|
if let Ok(AgentResponse::Disks(disks)) = ws.request(AgentRequest::Disks).await
|
||||||
if let Some(mm) = self.last_metrics.as_mut() {
|
&& let Some(mm) = self.last_metrics.as_mut()
|
||||||
|
{
|
||||||
mm.disks = disks;
|
mm.disks = disks;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
self.last_disks_poll = Instant::now();
|
self.last_disks_poll = Instant::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
//! Library surface for integration tests and reuse.
|
//! Library surface for integration tests and reuse.
|
||||||
|
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod ws;
|
|
||||||
|
// Re-export connector functionality
|
||||||
|
pub use socktop_connector::{SocktopConnector, connect_to_socktop_agent};
|
||||||
|
|||||||
@ -5,10 +5,9 @@ mod history;
|
|||||||
mod profiles;
|
mod profiles;
|
||||||
mod types;
|
mod types;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod ws;
|
|
||||||
|
|
||||||
use app::App;
|
use app::App;
|
||||||
use profiles::{load_profiles, save_profiles, ProfileEntry, ProfileRequest, ResolveProfile};
|
use profiles::{ProfileEntry, ProfileRequest, ResolveProfile, load_profiles, save_profiles};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
|
||||||
@ -39,7 +38,9 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
|||||||
while let Some(arg) = it.next() {
|
while let Some(arg) = it.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"-h" | "--help" => {
|
"-h" | "--help" => {
|
||||||
return Err(format!("Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--verify-hostname] [--profile NAME|-P NAME] [--save] [--demo] [--metrics-interval-ms N] [--processes-interval-ms N] [ws://HOST:PORT/ws]\n"));
|
return Err(format!(
|
||||||
|
"Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--verify-hostname] [--profile NAME|-P NAME] [--save] [--demo] [--metrics-interval-ms N] [--processes-interval-ms N] [ws://HOST:PORT/ws]\n"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
"--tls-ca" | "-t" => {
|
"--tls-ca" | "-t" => {
|
||||||
tls_ca = it.next();
|
tls_ca = it.next();
|
||||||
@ -70,19 +71,19 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
|||||||
processes_interval_ms = it.next().and_then(|v| v.parse().ok());
|
processes_interval_ms = it.next().and_then(|v| v.parse().ok());
|
||||||
}
|
}
|
||||||
_ if arg.starts_with("--tls-ca=") => {
|
_ if arg.starts_with("--tls-ca=") => {
|
||||||
if let Some((_, v)) = arg.split_once('=') {
|
if let Some((_, v)) = arg.split_once('=')
|
||||||
if !v.is_empty() {
|
&& !v.is_empty()
|
||||||
|
{
|
||||||
tls_ca = Some(v.to_string());
|
tls_ca = Some(v.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ if arg.starts_with("--profile=") => {
|
_ if arg.starts_with("--profile=") => {
|
||||||
if let Some((_, v)) = arg.split_once('=') {
|
if let Some((_, v)) = arg.split_once('=')
|
||||||
if !v.is_empty() {
|
&& !v.is_empty()
|
||||||
|
{
|
||||||
profile = Some(v.to_string());
|
profile = Some(v.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ if arg.starts_with("--metrics-interval-ms=") => {
|
_ if arg.starts_with("--metrics-interval-ms=") => {
|
||||||
if let Some((_, v)) = arg.split_once('=') {
|
if let Some((_, v)) = arg.split_once('=') {
|
||||||
metrics_interval_ms = v.parse().ok();
|
metrics_interval_ms = v.parse().ok();
|
||||||
@ -97,7 +98,9 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
|||||||
if url.is_none() {
|
if url.is_none() {
|
||||||
url = Some(arg);
|
url = Some(arg);
|
||||||
} else {
|
} else {
|
||||||
return Err(format!("Unexpected argument. Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--verify-hostname] [--profile NAME|-P NAME] [--save] [--demo] [ws://HOST:PORT/ws]"));
|
return Err(format!(
|
||||||
|
"Unexpected argument. Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--verify-hostname] [--profile NAME|-P NAME] [--save] [--demo] [ws://HOST:PORT/ws]"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,11 +138,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsed.verify_hostname {
|
|
||||||
// Set env var consumed by ws::connect logic
|
|
||||||
std::env::set_var("SOCKTOP_VERIFY_NAME", "1");
|
|
||||||
}
|
|
||||||
|
|
||||||
let profiles_file = load_profiles();
|
let profiles_file = load_profiles();
|
||||||
let req = ProfileRequest {
|
let req = ProfileRequest {
|
||||||
profile_name: parsed.profile.clone(),
|
profile_name: parsed.profile.clone(),
|
||||||
@ -239,7 +237,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
if io::stdin().read_line(&mut line).is_ok() {
|
if io::stdin().read_line(&mut line).is_ok() {
|
||||||
if let Ok(idx) = line.trim().parse::<usize>() {
|
if let Ok(idx) = line.trim().parse::<usize>() {
|
||||||
if idx >= 1 && idx <= names.len() {
|
if (1..=names.len()).contains(&idx) {
|
||||||
let name = &names[idx - 1];
|
let name = &names[idx - 1];
|
||||||
if name == "demo" {
|
if name == "demo" {
|
||||||
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
||||||
@ -297,7 +295,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if profiles_mut.profiles.is_empty() && parsed.url.is_none() {
|
if profiles_mut.profiles.is_empty() && parsed.url.is_none() {
|
||||||
eprintln!("Welcome to socktop!");
|
eprintln!("Welcome to socktop!");
|
||||||
eprintln!("It looks like this is your first time running the application.");
|
eprintln!("It looks like this is your first time running the application.");
|
||||||
eprintln!("You can connect to a socktop_agent instance to monitor system metrics and processes.");
|
eprintln!(
|
||||||
|
"You can connect to a socktop_agent instance to monitor system metrics and processes."
|
||||||
|
);
|
||||||
eprintln!("If you don't have an agent running, you can try the demo mode.");
|
eprintln!("If you don't have an agent running, you can try the demo mode.");
|
||||||
if prompt_yes_no("Would you like to start the demo mode now? [Y/n]: ") {
|
if prompt_yes_no("Would you like to start the demo mode now? [Y/n]: ") {
|
||||||
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
||||||
@ -318,7 +318,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if parsed.dry_run {
|
if parsed.dry_run {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
app.run(&url, tls_ca.as_deref()).await
|
app.run(&url, tls_ca.as_deref(), parsed.verify_hostname)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_yes_no(prompt: &str) -> bool {
|
fn prompt_yes_no(prompt: &str) -> bool {
|
||||||
@ -382,7 +383,8 @@ async fn run_demo_mode(_tls_ca: Option<&str>) -> Result<(), Box<dyn std::error::
|
|||||||
let url = format!("ws://127.0.0.1:{port}/ws");
|
let url = format!("ws://127.0.0.1:{port}/ws");
|
||||||
let child = spawn_demo_agent(port)?;
|
let child = spawn_demo_agent(port)?;
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
tokio::select! { res=app.run(&url,None)=>{ drop(child); res } _=tokio::signal::ctrl_c()=>{ drop(child); Ok(()) } }
|
// Demo mode connects to localhost, so disable hostname verification
|
||||||
|
tokio::select! { res=app.run(&url,None,false)=>{ drop(child); res } _=tokio::signal::ctrl_c()=>{ drop(child); Ok(()) } }
|
||||||
}
|
}
|
||||||
struct DemoGuard {
|
struct DemoGuard {
|
||||||
port: u16,
|
port: u16,
|
||||||
@ -414,8 +416,9 @@ fn spawn_demo_agent(port: u16) -> Result<DemoGuard, Box<dyn std::error::Error>>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
fn find_agent_executable() -> std::path::PathBuf {
|
fn find_agent_executable() -> std::path::PathBuf {
|
||||||
if let Ok(exe) = std::env::current_exe() {
|
if let Ok(exe) = std::env::current_exe()
|
||||||
if let Some(parent) = exe.parent() {
|
&& let Some(parent) = exe.parent()
|
||||||
|
{
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let name = "socktop_agent.exe";
|
let name = "socktop_agent.exe";
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
@ -425,6 +428,5 @@ fn find_agent_executable() -> std::path::PathBuf {
|
|||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
std::path::PathBuf::from("socktop_agent")
|
std::path::PathBuf::from("socktop_agent")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,12 +77,13 @@ impl ProfileRequest {
|
|||||||
pub fn resolve(self, pf: &ProfilesFile) -> ResolveProfile {
|
pub fn resolve(self, pf: &ProfilesFile) -> ResolveProfile {
|
||||||
// Case: only profile name given -> try load
|
// Case: only profile name given -> try load
|
||||||
if self.url.is_none() && self.profile_name.is_some() {
|
if self.url.is_none() && self.profile_name.is_some() {
|
||||||
let name = self.profile_name.unwrap();
|
let Some(name) = self.profile_name else {
|
||||||
if let Some(entry) = pf.profiles.get(&name) {
|
unreachable!("Already checked profile_name.is_some()")
|
||||||
return ResolveProfile::Loaded(entry.url.clone(), entry.tls_ca.clone());
|
};
|
||||||
} else {
|
let Some(entry) = pf.profiles.get(&name) else {
|
||||||
return ResolveProfile::PromptCreate(name);
|
return ResolveProfile::PromptCreate(name);
|
||||||
}
|
};
|
||||||
|
return ResolveProfile::Loaded(entry.url.clone(), entry.tls_ca.clone());
|
||||||
}
|
}
|
||||||
// Both provided -> direct (maybe later saved by caller)
|
// Both provided -> direct (maybe later saved by caller)
|
||||||
if let Some(u) = self.url {
|
if let Some(u) = self.url {
|
||||||
|
|||||||
@ -1,78 +1,4 @@
|
|||||||
//! Types that mirror the agent's JSON schema.
|
//! Types that mirror the agent's JSON schema.
|
||||||
|
|
||||||
use serde::Deserialize;
|
// Re-export commonly used types from socktop_connector
|
||||||
|
pub use socktop_connector::Metrics;
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct ProcessInfo {
|
|
||||||
pub pid: u32,
|
|
||||||
pub name: String,
|
|
||||||
pub cpu_usage: f32,
|
|
||||||
pub mem_bytes: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct DiskInfo {
|
|
||||||
pub name: String,
|
|
||||||
pub total: u64,
|
|
||||||
pub available: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct NetworkInfo {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub name: String,
|
|
||||||
pub received: u64,
|
|
||||||
pub transmitted: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct GpuInfo {
|
|
||||||
pub name: Option<String>,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub vendor: Option<String>,
|
|
||||||
|
|
||||||
// Accept both the new and legacy keys
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
alias = "utilization_gpu_pct",
|
|
||||||
alias = "gpu_util_pct",
|
|
||||||
alias = "gpu_utilization"
|
|
||||||
)]
|
|
||||||
pub utilization: Option<f32>,
|
|
||||||
|
|
||||||
#[serde(default, alias = "mem_used_bytes", alias = "vram_used_bytes")]
|
|
||||||
pub mem_used: Option<u64>,
|
|
||||||
|
|
||||||
#[serde(default, alias = "mem_total_bytes", alias = "vram_total_bytes")]
|
|
||||||
pub mem_total: Option<u64>,
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[serde(default, alias = "temp_c", alias = "temperature_c")]
|
|
||||||
pub temperature: Option<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Metrics {
|
|
||||||
pub cpu_total: f32,
|
|
||||||
pub cpu_per_core: Vec<f32>,
|
|
||||||
pub mem_total: u64,
|
|
||||||
pub mem_used: u64,
|
|
||||||
pub swap_total: u64,
|
|
||||||
pub swap_used: u64,
|
|
||||||
pub hostname: String,
|
|
||||||
pub cpu_temp_c: Option<f32>,
|
|
||||||
pub disks: Vec<DiskInfo>,
|
|
||||||
pub networks: Vec<NetworkInfo>,
|
|
||||||
pub top_processes: Vec<ProcessInfo>,
|
|
||||||
pub gpus: Option<Vec<GpuInfo>>,
|
|
||||||
// New: keep the last reported total process count
|
|
||||||
#[serde(default)]
|
|
||||||
pub process_count: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct ProcessesPayload {
|
|
||||||
pub process_count: usize,
|
|
||||||
pub top_processes: Vec<ProcessInfo>,
|
|
||||||
}
|
|
||||||
|
|||||||
@ -180,8 +180,9 @@ pub fn per_core_handle_scrollbar_mouse(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
MouseEventKind::Drag(MouseButton::Left) => {
|
MouseEventKind::Drag(MouseButton::Left) => {
|
||||||
if let Some(mut d) = drag.take() {
|
if let Some(mut d) = drag.take()
|
||||||
if d.active {
|
&& d.active
|
||||||
|
{
|
||||||
let dy = (mouse.row as i32) - (d.start_y as i32);
|
let dy = (mouse.row as i32) - (d.start_y as i32);
|
||||||
let new_top = (d.start_top as i32 + dy)
|
let new_top = (d.start_top as i32 + dy)
|
||||||
.clamp(0, (track.saturating_sub(thumb_len)) as i32)
|
.clamp(0, (track.saturating_sub(thumb_len)) as i32)
|
||||||
@ -203,7 +204,6 @@ pub fn per_core_handle_scrollbar_mouse(
|
|||||||
*drag = Some(d);
|
*drag = Some(d);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
MouseEventKind::Up(MouseButton::Left) => {
|
MouseEventKind::Up(MouseButton::Left) => {
|
||||||
// End drag
|
// End drag
|
||||||
*drag = None;
|
*drag = None;
|
||||||
|
|||||||
@ -1,210 +0,0 @@
|
|||||||
//! Minimal WebSocket client helpers for requesting metrics from the agent.
|
|
||||||
|
|
||||||
use flate2::bufread::GzDecoder;
|
|
||||||
use futures_util::{SinkExt, StreamExt};
|
|
||||||
use prost::Message as _;
|
|
||||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
|
||||||
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
|
||||||
use rustls::{ClientConfig, RootCertStore};
|
|
||||||
use rustls::{DigitallySignedStruct, SignatureScheme};
|
|
||||||
use rustls_pemfile::Item;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::{fs::File, io::BufReader, sync::Arc};
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
use tokio_tungstenite::{
|
|
||||||
connect_async, connect_async_tls_with_config, tungstenite::client::IntoClientRequest,
|
|
||||||
tungstenite::Message, Connector, MaybeTlsStream, WebSocketStream,
|
|
||||||
};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::types::{DiskInfo, Metrics, ProcessInfo, ProcessesPayload};
|
|
||||||
|
|
||||||
mod pb {
|
|
||||||
// generated by build.rs
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/socktop.rs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
|
|
||||||
|
|
||||||
// Connect to the agent and return the WS stream
|
|
||||||
pub async fn connect(
|
|
||||||
url: &str,
|
|
||||||
tls_ca: Option<&str>,
|
|
||||||
) -> Result<WsStream, Box<dyn std::error::Error>> {
|
|
||||||
let mut u = Url::parse(url)?;
|
|
||||||
if let Some(ca_path) = tls_ca {
|
|
||||||
if u.scheme() == "ws" {
|
|
||||||
let _ = u.set_scheme("wss");
|
|
||||||
}
|
|
||||||
return connect_with_ca(u.as_str(), ca_path).await;
|
|
||||||
}
|
|
||||||
let (ws, _) = connect_async(u.as_str()).await?;
|
|
||||||
Ok(ws)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn connect_with_ca(url: &str, ca_path: &str) -> Result<WsStream, Box<dyn std::error::Error>> {
|
|
||||||
let mut root = RootCertStore::empty();
|
|
||||||
let mut reader = BufReader::new(File::open(ca_path)?);
|
|
||||||
let mut der_certs = Vec::new();
|
|
||||||
while let Ok(Some(item)) = rustls_pemfile::read_one(&mut reader) {
|
|
||||||
if let Item::X509Certificate(der) = item {
|
|
||||||
der_certs.push(der);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
root.add_parsable_certificates(der_certs);
|
|
||||||
|
|
||||||
let mut cfg = ClientConfig::builder()
|
|
||||||
.with_root_certificates(root)
|
|
||||||
.with_no_client_auth();
|
|
||||||
|
|
||||||
let req = url.into_client_request()?;
|
|
||||||
let verify_domain = std::env::var("SOCKTOP_VERIFY_NAME").ok().as_deref() == Some("1");
|
|
||||||
if !verify_domain {
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct NoVerify;
|
|
||||||
impl ServerCertVerifier for NoVerify {
|
|
||||||
fn verify_server_cert(
|
|
||||||
&self,
|
|
||||||
_end_entity: &CertificateDer<'_>,
|
|
||||||
_intermediates: &[CertificateDer<'_>],
|
|
||||||
_server_name: &ServerName,
|
|
||||||
_ocsp_response: &[u8],
|
|
||||||
_now: UnixTime,
|
|
||||||
) -> Result<ServerCertVerified, rustls::Error> {
|
|
||||||
Ok(ServerCertVerified::assertion())
|
|
||||||
}
|
|
||||||
fn verify_tls12_signature(
|
|
||||||
&self,
|
|
||||||
_message: &[u8],
|
|
||||||
_cert: &CertificateDer<'_>,
|
|
||||||
_dss: &DigitallySignedStruct,
|
|
||||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
|
||||||
Ok(HandshakeSignatureValid::assertion())
|
|
||||||
}
|
|
||||||
fn verify_tls13_signature(
|
|
||||||
&self,
|
|
||||||
_message: &[u8],
|
|
||||||
_cert: &CertificateDer<'_>,
|
|
||||||
_dss: &DigitallySignedStruct,
|
|
||||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
|
||||||
Ok(HandshakeSignatureValid::assertion())
|
|
||||||
}
|
|
||||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
|
||||||
// Provide common schemes; not strictly needed for skipping but keeps API happy
|
|
||||||
vec![
|
|
||||||
SignatureScheme::ECDSA_NISTP256_SHA256,
|
|
||||||
SignatureScheme::ED25519,
|
|
||||||
SignatureScheme::RSA_PSS_SHA256,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cfg.dangerous().set_certificate_verifier(Arc::new(NoVerify));
|
|
||||||
eprintln!("socktop: hostname verification disabled (default). Use --verify-hostname to enable strict SAN checking.");
|
|
||||||
}
|
|
||||||
let cfg = Arc::new(cfg);
|
|
||||||
let (ws, _) =
|
|
||||||
connect_async_tls_with_config(req, None, verify_domain, Some(Connector::Rustls(cfg)))
|
|
||||||
.await?;
|
|
||||||
Ok(ws)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a "get_metrics" request and await a single JSON reply
|
|
||||||
pub async fn request_metrics(ws: &mut WsStream) -> Option<Metrics> {
|
|
||||||
if ws.send(Message::Text("get_metrics".into())).await.is_err() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
match ws.next().await {
|
|
||||||
Some(Ok(Message::Binary(b))) => {
|
|
||||||
gunzip_to_string(&b).and_then(|s| serde_json::from_str::<Metrics>(&s).ok())
|
|
||||||
}
|
|
||||||
Some(Ok(Message::Text(json))) => serde_json::from_str::<Metrics>(&json).ok(),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decompress a gzip-compressed binary frame into a String.
|
|
||||||
fn gunzip_to_string(bytes: &[u8]) -> Option<String> {
|
|
||||||
let mut dec = GzDecoder::new(bytes);
|
|
||||||
let mut out = String::new();
|
|
||||||
dec.read_to_string(&mut out).ok()?;
|
|
||||||
Some(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gunzip_to_vec(bytes: &[u8]) -> Option<Vec<u8>> {
|
|
||||||
let mut dec = GzDecoder::new(bytes);
|
|
||||||
let mut out = Vec::new();
|
|
||||||
dec.read_to_end(&mut out).ok()?;
|
|
||||||
Some(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_gzip(bytes: &[u8]) -> bool {
|
|
||||||
bytes.len() >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b
|
|
||||||
}
|
|
||||||
// Suppress dead_code until these are wired into the app
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum Payload {
|
|
||||||
Metrics(Metrics),
|
|
||||||
Disks(Vec<DiskInfo>),
|
|
||||||
Processes(ProcessesPayload),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a "get_disks" request and await a JSON Vec<DiskInfo>
|
|
||||||
pub async fn request_disks(ws: &mut WsStream) -> Option<Vec<DiskInfo>> {
|
|
||||||
if ws.send(Message::Text("get_disks".into())).await.is_err() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
match ws.next().await {
|
|
||||||
Some(Ok(Message::Binary(b))) => {
|
|
||||||
gunzip_to_string(&b).and_then(|s| serde_json::from_str::<Vec<DiskInfo>>(&s).ok())
|
|
||||||
}
|
|
||||||
Some(Ok(Message::Text(json))) => serde_json::from_str::<Vec<DiskInfo>>(&json).ok(),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a "get_processes" request and await a ProcessesPayload decoded from protobuf (binary, may be gzipped)
|
|
||||||
pub async fn request_processes(ws: &mut WsStream) -> Option<ProcessesPayload> {
|
|
||||||
if ws
|
|
||||||
.send(Message::Text("get_processes".into()))
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
match ws.next().await {
|
|
||||||
Some(Ok(Message::Binary(b))) => {
|
|
||||||
let gz = is_gzip(&b);
|
|
||||||
let data = if gz { gunzip_to_vec(&b)? } else { b };
|
|
||||||
match pb::Processes::decode(data.as_slice()) {
|
|
||||||
Ok(pb) => {
|
|
||||||
let rows: Vec<ProcessInfo> = pb
|
|
||||||
.rows
|
|
||||||
.into_iter()
|
|
||||||
.map(|p: pb::Process| ProcessInfo {
|
|
||||||
pid: p.pid,
|
|
||||||
name: p.name,
|
|
||||||
cpu_usage: p.cpu_usage,
|
|
||||||
mem_bytes: p.mem_bytes,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
Some(ProcessesPayload {
|
|
||||||
process_count: pb.process_count as usize,
|
|
||||||
top_processes: rows,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
if std::env::var("SOCKTOP_DEBUG").ok().as_deref() == Some("1") {
|
|
||||||
eprintln!("protobuf decode failed: {e}");
|
|
||||||
}
|
|
||||||
// Fallback: maybe it's JSON (bytes already decompressed if gz)
|
|
||||||
match String::from_utf8(data) {
|
|
||||||
Ok(s) => serde_json::from_str::<ProcessesPayload>(&s).ok(),
|
|
||||||
Err(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(Ok(Message::Text(json))) => serde_json::from_str::<ProcessesPayload>(&json).ok(),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -60,7 +60,9 @@ fn test_profile_created_on_first_use() {
|
|||||||
let _guard = ENV_LOCK.lock().unwrap();
|
let _guard = ENV_LOCK.lock().unwrap();
|
||||||
// Isolate config in a temp dir
|
// Isolate config in a temp dir
|
||||||
let td = tempfile::tempdir().unwrap();
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
unsafe {
|
||||||
std::env::set_var("XDG_CONFIG_HOME", td.path());
|
std::env::set_var("XDG_CONFIG_HOME", td.path());
|
||||||
|
}
|
||||||
// Ensure directory exists fresh
|
// Ensure directory exists fresh
|
||||||
std::fs::create_dir_all(td.path().join("socktop")).unwrap();
|
std::fs::create_dir_all(td.path().join("socktop")).unwrap();
|
||||||
let _ = fs::remove_file(profiles_path());
|
let _ = fs::remove_file(profiles_path());
|
||||||
@ -78,7 +80,9 @@ fn test_profile_created_on_first_use() {
|
|||||||
fn test_profile_overwrite_only_when_changed() {
|
fn test_profile_overwrite_only_when_changed() {
|
||||||
let _guard = ENV_LOCK.lock().unwrap();
|
let _guard = ENV_LOCK.lock().unwrap();
|
||||||
let td = tempfile::tempdir().unwrap();
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
unsafe {
|
||||||
std::env::set_var("XDG_CONFIG_HOME", td.path());
|
std::env::set_var("XDG_CONFIG_HOME", td.path());
|
||||||
|
}
|
||||||
std::fs::create_dir_all(td.path().join("socktop")).unwrap();
|
std::fs::create_dir_all(td.path().join("socktop")).unwrap();
|
||||||
let _ = fs::remove_file(profiles_path());
|
let _ = fs::remove_file(profiles_path());
|
||||||
// Initial create
|
// Initial create
|
||||||
@ -101,7 +105,9 @@ fn test_profile_overwrite_only_when_changed() {
|
|||||||
fn test_profile_tls_ca_persisted() {
|
fn test_profile_tls_ca_persisted() {
|
||||||
let _guard = ENV_LOCK.lock().unwrap();
|
let _guard = ENV_LOCK.lock().unwrap();
|
||||||
let td = tempfile::tempdir().unwrap();
|
let td = tempfile::tempdir().unwrap();
|
||||||
|
unsafe {
|
||||||
std::env::set_var("XDG_CONFIG_HOME", td.path());
|
std::env::set_var("XDG_CONFIG_HOME", td.path());
|
||||||
|
}
|
||||||
std::fs::create_dir_all(td.path().join("socktop")).unwrap();
|
std::fs::create_dir_all(td.path().join("socktop")).unwrap();
|
||||||
let _ = fs::remove_file(profiles_path());
|
let _ = fs::remove_file(profiles_path());
|
||||||
let (_ok, _out) = run_socktop(&[
|
let (_ok, _out) = run_socktop(&[
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
use socktop::ws::{connect, request_metrics, request_processes};
|
|
||||||
|
|
||||||
// Integration probe: only runs when SOCKTOP_WS is set to an agent WebSocket URL.
|
|
||||||
// Example: SOCKTOP_WS=ws://127.0.0.1:3000/ws cargo test -p socktop --test ws_probe -- --nocapture
|
|
||||||
#[tokio::test]
|
|
||||||
async fn probe_ws_endpoints() {
|
|
||||||
// Gate the test to avoid CI failures when no agent is running.
|
|
||||||
let url = match std::env::var("SOCKTOP_WS") {
|
|
||||||
Ok(v) if !v.is_empty() => v,
|
|
||||||
_ => {
|
|
||||||
eprintln!(
|
|
||||||
"skipping ws_probe: set SOCKTOP_WS=ws://host:port/ws to run this integration test"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optional pinned CA for WSS/self-signed setups
|
|
||||||
let tls_ca = std::env::var("SOCKTOP_TLS_CA").ok();
|
|
||||||
let mut ws = connect(&url, tls_ca.as_deref()).await.expect("connect ws");
|
|
||||||
|
|
||||||
// Should get fast metrics quickly
|
|
||||||
let m = request_metrics(&mut ws).await;
|
|
||||||
assert!(m.is_some(), "expected Metrics payload within timeout");
|
|
||||||
|
|
||||||
// Processes may be gzipped and a bit slower, but should arrive
|
|
||||||
let p = request_processes(&mut ws).await;
|
|
||||||
assert!(p.is_some(), "expected Processes payload within timeout");
|
|
||||||
}
|
|
||||||
@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "socktop_agent"
|
name = "socktop_agent"
|
||||||
version = "1.40.67"
|
version = "1.40.70"
|
||||||
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
|
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
|
||||||
description = "Remote system monitor over WebSocket, TUI like top"
|
description = "Socktop agent daemon. Serves host metrics over WebSocket."
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,10 @@ Environment toggles:
|
|||||||
- SOCKTOP_AGENT_PROCESSES_TTL_MS=1000
|
- SOCKTOP_AGENT_PROCESSES_TTL_MS=1000
|
||||||
- SOCKTOP_AGENT_DISKS_TTL_MS=1000
|
- SOCKTOP_AGENT_DISKS_TTL_MS=1000
|
||||||
|
|
||||||
|
*NOTE ON ENV vars*
|
||||||
|
|
||||||
|
Generally these have been added for debugging purposes. you do not need to configure them, default values are tuned and GPU will deisable itself after the first poll if not available.
|
||||||
|
|
||||||
Systemd unit example & full docs:
|
Systemd unit example & full docs:
|
||||||
https://github.com/jasonwitty/socktop
|
https://github.com/jasonwitty/socktop
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
// Vendored protoc for reproducible builds
|
// Vendored protoc for reproducible builds
|
||||||
let protoc = protoc_bin_vendored::protoc_bin_path().expect("protoc");
|
let protoc = protoc_bin_vendored::protoc_bin_path().expect("protoc");
|
||||||
std::env::set_var("PROTOC", &protoc);
|
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=proto/processes.proto");
|
println!("cargo:rerun-if-changed=proto/processes.proto");
|
||||||
|
|
||||||
// Compile protobuf definitions for processes
|
// Compile protobuf definitions for processes
|
||||||
let mut cfg = prost_build::Config::new();
|
let mut cfg = prost_build::Config::new();
|
||||||
cfg.out_dir(std::env::var("OUT_DIR").unwrap());
|
cfg.out_dir(std::env::var("OUT_DIR").unwrap());
|
||||||
|
cfg.protoc_executable(protoc); // Use the vendored protoc directly
|
||||||
// Use local path (ensures file is inside published crate tarball)
|
// Use local path (ensures file is inside published crate tarball)
|
||||||
cfg.compile_protos(&["proto/processes.proto"], &["proto"]) // relative to CARGO_MANIFEST_DIR
|
cfg.compile_protos(&["proto/processes.proto"], &["proto"]) // relative to CARGO_MANIFEST_DIR
|
||||||
.expect("compile protos");
|
.expect("compile protos");
|
||||||
|
|||||||
@ -8,7 +8,7 @@ mod state;
|
|||||||
mod types;
|
mod types;
|
||||||
mod ws;
|
mod ws;
|
||||||
|
|
||||||
use axum::{http::StatusCode, routing::get, Router};
|
use axum::{Router, http::StatusCode, routing::get};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
|||||||
@ -74,13 +74,13 @@ fn cached_temp() -> Option<f32> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_temp(v: Option<f32>) {
|
fn set_temp(v: Option<f32>) {
|
||||||
if let Some(lock) = TEMP.get() {
|
if let Some(lock) = TEMP.get()
|
||||||
if let Ok(mut c) = lock.lock() {
|
&& let Ok(mut c) = lock.lock()
|
||||||
|
{
|
||||||
c.v = v;
|
c.v = v;
|
||||||
c.at = Some(Instant::now());
|
c.at = Some(Instant::now());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn cached_gpus() -> Option<Vec<crate::gpu::GpuMetrics>> {
|
fn cached_gpus() -> Option<Vec<crate::gpu::GpuMetrics>> {
|
||||||
if !gpu_enabled() {
|
if !gpu_enabled() {
|
||||||
@ -98,13 +98,13 @@ fn cached_gpus() -> Option<Vec<crate::gpu::GpuMetrics>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_gpus(v: Option<Vec<crate::gpu::GpuMetrics>>) {
|
fn set_gpus(v: Option<Vec<crate::gpu::GpuMetrics>>) {
|
||||||
if let Some(lock) = GPUC.get() {
|
if let Some(lock) = GPUC.get()
|
||||||
if let Ok(mut c) = lock.lock() {
|
&& let Ok(mut c) = lock.lock()
|
||||||
|
{
|
||||||
c.v = v.clone();
|
c.v = v.clone();
|
||||||
c.at = Some(Instant::now());
|
c.at = Some(Instant::now());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Collect only fast-changing metrics (CPU/mem/net + optional temps/gpus).
|
// Collect only fast-changing metrics (CPU/mem/net + optional temps/gpus).
|
||||||
pub async fn collect_fast_metrics(state: &AppState) -> Metrics {
|
pub async fn collect_fast_metrics(state: &AppState) -> Metrics {
|
||||||
@ -116,12 +116,12 @@ pub async fn collect_fast_metrics(state: &AppState) -> Metrics {
|
|||||||
let ttl = StdDuration::from_millis(ttl_ms);
|
let ttl = StdDuration::from_millis(ttl_ms);
|
||||||
{
|
{
|
||||||
let cache = state.cache_metrics.lock().await;
|
let cache = state.cache_metrics.lock().await;
|
||||||
if cache.is_fresh(ttl) {
|
if cache.is_fresh(ttl)
|
||||||
if let Some(c) = cache.get() {
|
&& let Some(c) = cache.get()
|
||||||
|
{
|
||||||
return c.clone();
|
return c.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let mut sys = state.sys.lock().await;
|
let mut sys = state.sys.lock().await;
|
||||||
if let Err(e) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
if let Err(e) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
sys.refresh_cpu_usage();
|
sys.refresh_cpu_usage();
|
||||||
@ -278,12 +278,12 @@ pub async fn collect_disks(state: &AppState) -> Vec<DiskInfo> {
|
|||||||
let ttl = StdDuration::from_millis(ttl_ms);
|
let ttl = StdDuration::from_millis(ttl_ms);
|
||||||
{
|
{
|
||||||
let cache = state.cache_disks.lock().await;
|
let cache = state.cache_disks.lock().await;
|
||||||
if cache.is_fresh(ttl) {
|
if cache.is_fresh(ttl)
|
||||||
if let Some(v) = cache.get() {
|
&& let Some(v) = cache.get()
|
||||||
|
{
|
||||||
return v.clone();
|
return v.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let mut disks_list = state.disks.lock().await;
|
let mut disks_list = state.disks.lock().await;
|
||||||
disks_list.refresh(false); // don't drop missing disks
|
disks_list.refresh(false); // don't drop missing disks
|
||||||
let disks: Vec<DiskInfo> = disks_list
|
let disks: Vec<DiskInfo> = disks_list
|
||||||
@ -347,12 +347,12 @@ pub async fn collect_processes_all(state: &AppState) -> ProcessesPayload {
|
|||||||
let ttl = StdDuration::from_millis(ttl_ms);
|
let ttl = StdDuration::from_millis(ttl_ms);
|
||||||
{
|
{
|
||||||
let cache = state.cache_processes.lock().await;
|
let cache = state.cache_processes.lock().await;
|
||||||
if cache.is_fresh(ttl) {
|
if cache.is_fresh(ttl)
|
||||||
if let Some(c) = cache.get() {
|
&& let Some(c) = cache.get()
|
||||||
|
{
|
||||||
return c.clone();
|
return c.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Reuse shared System to avoid reallocation; refresh processes fully.
|
// Reuse shared System to avoid reallocation; refresh processes fully.
|
||||||
let mut sys_guard = state.sys.lock().await;
|
let mut sys_guard = state.sys.lock().await;
|
||||||
let sys = &mut *sys_guard;
|
let sys = &mut *sys_guard;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
//! Shared agent state: sysinfo handles and hot JSON cache.
|
//! Shared agent state: sysinfo handles and hot JSON cache.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicUsize};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use sysinfo::{Components, Disks, Networks, System};
|
use sysinfo::{Components, Disks, Networks, System};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use axum::{
|
|||||||
extract::{Query, State, WebSocketUpgrade},
|
extract::{Query, State, WebSocketUpgrade},
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
use flate2::{write::GzEncoder, Compression};
|
use flate2::{Compression, write::GzEncoder};
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@ -40,13 +40,13 @@ pub async fn ws_handler(
|
|||||||
Query(q): Query<HashMap<String, String>>,
|
Query(q): Query<HashMap<String, String>>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
// optional auth
|
// optional auth
|
||||||
if let Some(expected) = state.auth_token.as_ref() {
|
if let Some(expected) = state.auth_token.as_ref()
|
||||||
if q.get("token") != Some(expected) {
|
&& q.get("token") != Some(expected)
|
||||||
|
{
|
||||||
return ws.on_upgrade(|socket| async move {
|
return ws.on_upgrade(|socket| async move {
|
||||||
let _ = socket.close().await;
|
let _ = socket.close().await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
ws.on_upgrade(move |socket| handle_socket(socket, state))
|
ws.on_upgrade(move |socket| handle_socket(socket, state))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
60
socktop_connector/Cargo.toml
Normal file
60
socktop_connector/Cargo.toml
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
[package]
|
||||||
|
name = "socktop_connector"
|
||||||
|
version = "0.1.6"
|
||||||
|
edition = "2024"
|
||||||
|
license = "MIT"
|
||||||
|
description = "WebSocket connector library for socktop agent communication"
|
||||||
|
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
|
||||||
|
repository = "https://github.com/jasonwitty/socktop"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["monitoring", "websocket", "metrics", "system"]
|
||||||
|
categories = ["network-programming", "development-tools"]
|
||||||
|
documentation = "https://docs.rs/socktop_connector"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
# docs.rs specific metadata
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# WebSocket client - only for non-WASM targets
|
||||||
|
tokio-tungstenite = { workspace = true, optional = true }
|
||||||
|
tokio = { workspace = true, optional = true }
|
||||||
|
futures-util = { workspace = true, optional = true }
|
||||||
|
url = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
# WASM WebSocket support
|
||||||
|
wasm-bindgen = { version = "0.2", optional = true }
|
||||||
|
wasm-bindgen-futures = { version = "0.4", optional = true }
|
||||||
|
js-sys = { version = "0.3", optional = true }
|
||||||
|
web-sys = { version = "0.3", features = ["WebSocket", "MessageEvent", "ErrorEvent", "CloseEvent", "BinaryType", "Window", "console"], optional = true }
|
||||||
|
|
||||||
|
# TLS support
|
||||||
|
rustls = { version = "0.23", features = ["ring"], optional = true }
|
||||||
|
rustls-pemfile = { version = "2.1", optional = true }
|
||||||
|
|
||||||
|
# Serialization - always available
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
|
||||||
|
# Compression - used in both networking and WASM modes
|
||||||
|
flate2 = "1.0"
|
||||||
|
|
||||||
|
# Protobuf - always available
|
||||||
|
prost = { workspace = true }
|
||||||
|
|
||||||
|
# Error handling - always available
|
||||||
|
thiserror = "2.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
prost-build = "0.13"
|
||||||
|
protoc-bin-vendored = "3.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["networking", "tls"]
|
||||||
|
networking = ["tokio-tungstenite", "tokio", "futures-util", "url"]
|
||||||
|
tls = ["networking", "rustls", "rustls-pemfile"]
|
||||||
|
wasm = ["wasm-bindgen", "wasm-bindgen-futures", "js-sys", "web-sys"] # WASM-compatible networking with compression
|
||||||
21
socktop_connector/LICENSE
Normal file
21
socktop_connector/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Jason Witty
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
486
socktop_connector/README.md
Normal file
486
socktop_connector/README.md
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
# socktop_connector
|
||||||
|
|
||||||
|
A WebSocket connector library for communicating with socktop agents.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`socktop_connector` provides a high-level, type-safe interface for connecting to socktop agents over WebSocket connections. It handles connection management, TLS certificate pinning, compression, and protocol buffer decoding automatically.
|
||||||
|
|
||||||
|
The library is designed for professional use with structured error handling that allows you to pattern match on specific error types, making it easy to implement robust error recovery and monitoring strategies.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **WebSocket Communication**: Support for both `ws://` and `wss://` connections
|
||||||
|
- **TLS Security**: Certificate pinning for secure connections with self-signed certificates
|
||||||
|
- **Hostname Verification**: Configurable hostname verification for TLS connections
|
||||||
|
- **Type Safety**: Strongly typed requests and responses
|
||||||
|
- **Automatic Compression**: Handles gzip compression/decompression transparently
|
||||||
|
- **Protocol Buffer Support**: Decodes binary process data automatically
|
||||||
|
- **Error Handling**: Comprehensive error handling with structured error types for pattern matching
|
||||||
|
|
||||||
|
## Connection Types
|
||||||
|
|
||||||
|
### Non-TLS Connections (`ws://`)
|
||||||
|
Use `connect_to_socktop_agent()` for unencrypted WebSocket connections.
|
||||||
|
|
||||||
|
### TLS Connections (`wss://`)
|
||||||
|
Use `connect_to_socktop_agent_with_tls()` for encrypted connections with certificate pinning. You can control hostname verification with the `verify_hostname` parameter.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
Add this to your `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
socktop_connector = "0.1.5"
|
||||||
|
tokio = { version = "1", features = ["rt", "rt-multi-thread", "net", "time", "macros"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use socktop_connector::{connect_to_socktop_agent, AgentRequest, AgentResponse};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Connect to a socktop agent (non-TLS connections are always unverified)
|
||||||
|
let mut connector = connect_to_socktop_agent("ws://localhost:3000/ws").await?;
|
||||||
|
|
||||||
|
// Request metrics
|
||||||
|
match connector.request(AgentRequest::Metrics).await? {
|
||||||
|
AgentResponse::Metrics(metrics) => {
|
||||||
|
println!("CPU: {}%, Memory: {}/{}MB",
|
||||||
|
metrics.cpu_total,
|
||||||
|
metrics.mem_used / 1024 / 1024,
|
||||||
|
metrics.mem_total / 1024 / 1024
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request process list
|
||||||
|
match connector.request(AgentRequest::Processes).await? {
|
||||||
|
AgentResponse::Processes(processes) => {
|
||||||
|
println!("Total processes: {}", processes.process_count);
|
||||||
|
for process in processes.top_processes.iter().take(5) {
|
||||||
|
println!(" {} (PID: {}) - CPU: {}%",
|
||||||
|
process.name, process.pid, process.cpu_usage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling with Pattern Matching
|
||||||
|
|
||||||
|
Take advantage of structured error types for robust error handling:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use socktop_connector::{connect_to_socktop_agent, ConnectorError, AgentRequest};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Handle connection errors specifically
|
||||||
|
let mut connector = match connect_to_socktop_agent("ws://localhost:3000/ws").await {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(ConnectorError::WebSocketError(e)) => {
|
||||||
|
eprintln!("Failed to connect to WebSocket: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(ConnectorError::UrlError(e)) => {
|
||||||
|
eprintln!("Invalid URL provided: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Connection failed: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle request errors specifically
|
||||||
|
match connector.request(AgentRequest::Metrics).await {
|
||||||
|
Ok(response) => println!("Success: {:?}", response),
|
||||||
|
Err(ConnectorError::JsonError(e)) => {
|
||||||
|
eprintln!("Failed to parse server response: {}", e);
|
||||||
|
}
|
||||||
|
Err(ConnectorError::WebSocketError(e)) => {
|
||||||
|
eprintln!("Communication error: {}", e);
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Request failed: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TLS with Certificate Pinning
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use socktop_connector::{connect_to_socktop_agent_with_tls, AgentRequest};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Connect with TLS certificate pinning and hostname verification
|
||||||
|
let mut connector = connect_to_socktop_agent_with_tls(
|
||||||
|
"wss://remote-host:8443/ws",
|
||||||
|
"/path/to/cert.pem",
|
||||||
|
false // Enable hostname verification
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
let response = connector.request(AgentRequest::Disks).await?;
|
||||||
|
println!("Got disk info: {:?}", response);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use socktop_connector::{ConnectorConfig, SocktopConnector, AgentRequest};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Create a custom configuration
|
||||||
|
let config = ConnectorConfig::new("wss://remote-host:8443/ws")
|
||||||
|
.with_tls_ca("/path/to/cert.pem")
|
||||||
|
.with_hostname_verification(false);
|
||||||
|
|
||||||
|
// Create and connect
|
||||||
|
let mut connector = SocktopConnector::new(config);
|
||||||
|
connector.connect().await?;
|
||||||
|
|
||||||
|
// Make requests
|
||||||
|
let response = connector.request(AgentRequest::Metrics).await?;
|
||||||
|
|
||||||
|
// Clean disconnect
|
||||||
|
connector.disconnect().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket Protocol Configuration
|
||||||
|
|
||||||
|
For version compatibility (if applies), you can configure WebSocket protocol version and sub-protocols:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use socktop_connector::{ConnectorConfig, SocktopConnector, connect_to_socktop_agent_with_config};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Method 1: Using the convenience function
|
||||||
|
let connector = connect_to_socktop_agent_with_config(
|
||||||
|
"ws://localhost:3000/ws",
|
||||||
|
Some(vec!["socktop".to_string(), "v1".to_string()]), // Sub-protocols
|
||||||
|
Some("13".to_string()), // WebSocket version (13 is standard)
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// Method 2: Using ConnectorConfig builder
|
||||||
|
let config = ConnectorConfig::new("ws://localhost:3000/ws")
|
||||||
|
.with_protocols(vec!["socktop".to_string()])
|
||||||
|
.with_version("13");
|
||||||
|
|
||||||
|
let mut connector = SocktopConnector::new(config);
|
||||||
|
connector.connect().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** WebSocket version 13 is the current standard and is used by default. The sub-protocols feature is useful for protocol negotiation with servers that support multiple protocols.
|
||||||
|
|
||||||
|
## Continuous Updates
|
||||||
|
|
||||||
|
The socktop agent provides real-time system metrics. Each request returns the current snapshot, but you can implement continuous monitoring by making requests in a loop:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use socktop_connector::{connect_to_socktop_agent, AgentRequest, AgentResponse, ConnectorError};
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut connector = connect_to_socktop_agent("ws://localhost:3000/ws").await?;
|
||||||
|
|
||||||
|
// Monitor system metrics every 2 seconds
|
||||||
|
loop {
|
||||||
|
match connector.request(AgentRequest::Metrics).await {
|
||||||
|
Ok(AgentResponse::Metrics(metrics)) => {
|
||||||
|
// Calculate total network activity across all interfaces
|
||||||
|
let total_rx: u64 = metrics.networks.iter().map(|n| n.received).sum();
|
||||||
|
let total_tx: u64 = metrics.networks.iter().map(|n| n.transmitted).sum();
|
||||||
|
|
||||||
|
println!("CPU: {:.1}%, Memory: {:.1}%, Network: ↓{} ↑{}",
|
||||||
|
metrics.cpu_total,
|
||||||
|
(metrics.mem_used as f64 / metrics.mem_total as f64) * 100.0,
|
||||||
|
format_bytes(total_rx),
|
||||||
|
format_bytes(total_tx)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error getting metrics: {}", e);
|
||||||
|
|
||||||
|
// You can pattern match on specific error types for different handling
|
||||||
|
match e {
|
||||||
|
socktop_connector::ConnectorError::WebSocketError(_) => {
|
||||||
|
eprintln!("Connection lost, attempting to reconnect...");
|
||||||
|
// Implement reconnection logic here
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
socktop_connector::ConnectorError::JsonError(_) => {
|
||||||
|
eprintln!("Data parsing error, continuing...");
|
||||||
|
// Continue with next iteration for transient parsing errors
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
eprintln!("Other error, stopping monitoring");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_secs(2)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_bytes(bytes: u64) -> String {
|
||||||
|
const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
|
||||||
|
let mut size = bytes as f64;
|
||||||
|
let mut unit_index = 0;
|
||||||
|
|
||||||
|
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
|
||||||
|
size /= 1024.0;
|
||||||
|
unit_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("{:.1}{}", size, UNITS[unit_index])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Understanding Data Freshness
|
||||||
|
|
||||||
|
The socktop agent implements intelligent caching to avoid overwhelming the system:
|
||||||
|
|
||||||
|
- **Metrics**: Cached for ~250ms by default (cheap / fast-changing data like CPU, memory)
|
||||||
|
- **Processes**: Cached for ~1500ms by default (exppensive / moderately changing data)
|
||||||
|
- **Disks**: Cached for ~1000ms by default (cheap / slowly changing data)
|
||||||
|
|
||||||
|
These values have been generally tuned in advance. You should not need to override them. The reason for this cache is for the use case that multiple clients are requesting data. In general a single client should never really hit a cached response since the polling rates are slower that the cache intervals. Cache intervals have been tuned based on how much work the agent has to do in the case of reloading fresh data.
|
||||||
|
|
||||||
|
|
||||||
|
This means:
|
||||||
|
|
||||||
|
1. **Multiple rapid requests** for the same data type will return cached results
|
||||||
|
2. **Different data types** have independent cache timers
|
||||||
|
3. **Fresh data** is automatically retrieved when cache expires
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use socktop_connector::{connect_to_socktop_agent, AgentRequest, AgentResponse};
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut connector = connect_to_socktop_agent("ws://localhost:3000/ws").await?;
|
||||||
|
|
||||||
|
// This demonstrates cache behavior
|
||||||
|
println!("Requesting metrics twice quickly...");
|
||||||
|
|
||||||
|
// First request - fresh data from system
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
connector.request(AgentRequest::Metrics).await?;
|
||||||
|
println!("First request took: {:?}", start.elapsed());
|
||||||
|
|
||||||
|
// Second request immediately - cached data
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
connector.request(AgentRequest::Metrics).await?;
|
||||||
|
println!("Second request took: {:?}", start.elapsed()); // Much faster!
|
||||||
|
|
||||||
|
// Wait for cache to expire, then request again
|
||||||
|
sleep(Duration::from_millis(300)).await;
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
connector.request(AgentRequest::Metrics).await?;
|
||||||
|
println!("Third request (after cache expiry): {:?}", start.elapsed());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The WebSocket connection remains open between requests, providing efficient real-time monitoring without connection overhead.
|
||||||
|
|
||||||
|
## Request Types
|
||||||
|
|
||||||
|
The library supports three types of requests:
|
||||||
|
|
||||||
|
- `AgentRequest::Metrics` - Get current system metrics (CPU, memory, network, etc.)
|
||||||
|
- `AgentRequest::Disks` - Get disk usage information
|
||||||
|
- `AgentRequest::Processes` - Get running process information
|
||||||
|
|
||||||
|
## Response Types
|
||||||
|
|
||||||
|
Responses are automatically parsed into strongly-typed structures:
|
||||||
|
|
||||||
|
- `AgentResponse::Metrics(Metrics)` - System metrics with CPU, memory, network data
|
||||||
|
- `AgentResponse::Disks(Vec<DiskInfo>)` - List of disk usage information
|
||||||
|
- `AgentResponse::Processes(ProcessesPayload)` - Process list with CPU and memory usage
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
The library provides flexible configuration through the `ConnectorConfig` builder:
|
||||||
|
|
||||||
|
- `with_tls_ca(path)` - Enable TLS with certificate pinning
|
||||||
|
- `with_hostname_verification(bool)` - Control hostname verification for TLS connections
|
||||||
|
- `true` (recommended): Verify the server hostname matches the certificate
|
||||||
|
- `false`: Skip hostname verification (useful for localhost or IP-based connections)
|
||||||
|
- `with_protocols(Vec<String>)` - Set WebSocket sub-protocols for protocol negotiation
|
||||||
|
- `with_version(String)` - Set WebSocket protocol version (default is "13", the current standard)
|
||||||
|
|
||||||
|
**Note**: Hostname verification only applies to TLS connections (`wss://`). Non-TLS connections (`ws://`) don't use certificates, so hostname verification is not applicable.
|
||||||
|
|
||||||
|
## WASM Compatibility (experimental)
|
||||||
|
|
||||||
|
`socktop_connector` provides **full WebSocket support** for WebAssembly (WASM) environments, including complete networking functionality with automatic compression and protobuf decoding.
|
||||||
|
|
||||||
|
### Quick Setup
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
socktop_connector = { version = "0.1.5", default-features = false, features = ["wasm"] }
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
### What Works
|
||||||
|
- ✅ Full WebSocket connectivity (`ws://` connections)
|
||||||
|
- ✅ All request types (`Metrics`, `Disks`, `Processes`)
|
||||||
|
- ✅ Automatic gzip decompression for metrics and disks
|
||||||
|
- ✅ Automatic protobuf decoding for process data
|
||||||
|
- ✅ All types (`ConnectorConfig`, `AgentRequest`, `AgentResponse`)
|
||||||
|
- ✅ JSON serialization/deserialization
|
||||||
|
- ✅ Protocol and version configuration
|
||||||
|
|
||||||
|
### What Doesn't Work
|
||||||
|
- ❌ TLS connections (`wss://`) - use `ws://` only
|
||||||
|
- ❌ TLS certificate handling
|
||||||
|
|
||||||
|
### Basic WASM Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use socktop_connector::{ConnectorConfig, SocktopConnector, AgentRequest};
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn test_connection() {
|
||||||
|
let config = ConnectorConfig::new("ws://localhost:3000/ws");
|
||||||
|
let mut connector = SocktopConnector::new(config);
|
||||||
|
|
||||||
|
match connector.connect().await {
|
||||||
|
Ok(()) => {
|
||||||
|
// Request metrics with automatic gzip decompression
|
||||||
|
let response = connector.request(AgentRequest::Metrics).await.unwrap();
|
||||||
|
console_log!("Got metrics: {:?}", response);
|
||||||
|
|
||||||
|
// Request processes with automatic protobuf decoding
|
||||||
|
let response = connector.request(AgentRequest::Processes).await.unwrap();
|
||||||
|
console_log!("Got processes: {:?}", response);
|
||||||
|
}
|
||||||
|
Err(e) => console_log!("Connection failed: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete WASM Guide
|
||||||
|
|
||||||
|
For detailed implementation examples, complete code samples, and a working test environment, see the **[WASM Compatibility Guide](../socktop_wasm_test/README.md)** in the `socktop_wasm_test/` directory.
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- **Production TLS**: You can enable hostname verification (`verify_hostname: true`) for production systems, This will add an additional level of production of verifying the hostname against the certificate. Generally this is to stop a man in the middle attack, but since it will be the client who is fooled and not the server, the risk and likelyhood of this use case is rather low. Which is why this is disabled by default.
|
||||||
|
- **Certificate Pinning**: Use `with_tls_ca()` for self-signed certificates, the socktop agent will generate certificates on start. see main readme for more details.
|
||||||
|
- **Non-TLS**: Use only for development or trusted networks
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Currently no environment variables are used. All configuration is done through the API.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The library uses structured error types via `thiserror` for comprehensive error handling. You can pattern match on specific error types:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use socktop_connector::{connect_to_socktop_agent, ConnectorError, AgentRequest};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
match connect_to_socktop_agent("invalid://url").await {
|
||||||
|
Ok(mut connector) => {
|
||||||
|
// Handle successful connection
|
||||||
|
match connector.request(AgentRequest::Metrics).await {
|
||||||
|
Ok(response) => println!("Got response: {:?}", response),
|
||||||
|
Err(ConnectorError::WebSocketError(e)) => {
|
||||||
|
eprintln!("WebSocket communication failed: {}", e);
|
||||||
|
}
|
||||||
|
Err(ConnectorError::JsonError(e)) => {
|
||||||
|
eprintln!("Failed to parse response: {}", e);
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Other error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(ConnectorError::UrlError(e)) => {
|
||||||
|
eprintln!("Invalid URL: {}", e);
|
||||||
|
}
|
||||||
|
Err(ConnectorError::WebSocketError(e)) => {
|
||||||
|
eprintln!("Failed to connect: {}", e);
|
||||||
|
}
|
||||||
|
Err(ConnectorError::TlsError(msg)) => {
|
||||||
|
eprintln!("TLS error: {}", msg);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Connection failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Types
|
||||||
|
|
||||||
|
The `ConnectorError` enum provides specific variants for different error conditions:
|
||||||
|
|
||||||
|
- `ConnectorError::WebSocketError` - WebSocket connection or communication errors
|
||||||
|
- `ConnectorError::TlsError` - TLS-related errors (certificate validation, etc.)
|
||||||
|
- `ConnectorError::UrlError` - URL parsing errors
|
||||||
|
- `ConnectorError::JsonError` - JSON serialization/deserialization errors
|
||||||
|
- `ConnectorError::ProtocolError` - Protocol-level errors
|
||||||
|
- `ConnectorError::CompressionError` - Gzip compression/decompression errors
|
||||||
|
- `ConnectorError::IoError` - I/O errors
|
||||||
|
- `ConnectorError::Other` - Other errors with descriptive messages
|
||||||
|
|
||||||
|
All errors implement `std::error::Error` so they work seamlessly with `Box<dyn std::error::Error>`, `anyhow`, and other error handling crates.
|
||||||
|
|
||||||
|
### Migration from Generic Errors
|
||||||
|
|
||||||
|
If you were previously using the library with generic error handling, your existing code will continue to work:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// This continues to work as before
|
||||||
|
async fn my_function() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut connector = connect_to_socktop_agent("ws://localhost:3000/ws").await?;
|
||||||
|
let response = connector.request(AgentRequest::Metrics).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// But now you can also use structured error handling for better control
|
||||||
|
async fn improved_function() -> Result<(), ConnectorError> {
|
||||||
|
let mut connector = connect_to_socktop_agent("ws://localhost:3000/ws").await?;
|
||||||
|
let response = connector.request(AgentRequest::Metrics).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see the LICENSE file for details.
|
||||||
10
socktop_connector/build.rs
Normal file
10
socktop_connector/build.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Set the protoc binary path to use the vendored version for CI compatibility
|
||||||
|
// SAFETY: We're only setting PROTOC in a build script environment, which is safe
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("PROTOC", protoc_bin_vendored::protoc_bin_path()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
prost_build::compile_protos(&["processes.proto"], &["."])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
38
socktop_connector/examples/wasm_example.rs
Normal file
38
socktop_connector/examples/wasm_example.rs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
//! Example of using socktop_connector in a WASM environment.
|
||||||
|
//!
|
||||||
|
//! This example demonstrates how to use the connector without TLS dependencies
|
||||||
|
//! for WebAssembly builds.
|
||||||
|
|
||||||
|
use socktop_connector::{AgentRequest, ConnectorConfig, connect_to_socktop_agent};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("WASM-compatible socktop connector example");
|
||||||
|
|
||||||
|
// For WASM builds, use ws:// (not wss://) to avoid TLS dependencies
|
||||||
|
let url = "ws://localhost:3000/ws";
|
||||||
|
|
||||||
|
// Method 1: Simple connection (recommended for most use cases)
|
||||||
|
let mut connector = connect_to_socktop_agent(url).await?;
|
||||||
|
|
||||||
|
// Method 2: With custom WebSocket configuration
|
||||||
|
let config = ConnectorConfig::new(url)
|
||||||
|
.with_protocols(vec!["socktop".to_string()])
|
||||||
|
.with_version("13".to_string());
|
||||||
|
|
||||||
|
let mut connector_custom = socktop_connector::SocktopConnector::new(config);
|
||||||
|
connector_custom.connect().await?;
|
||||||
|
|
||||||
|
// Make a request to get metrics
|
||||||
|
match connector.request(AgentRequest::Metrics).await {
|
||||||
|
Ok(response) => {
|
||||||
|
println!("Successfully received response: {response:?}");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Request failed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("WASM example completed successfully!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
15
socktop_connector/processes.proto
Normal file
15
socktop_connector/processes.proto
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
package socktop;
|
||||||
|
|
||||||
|
// All running processes. Sorting is done client-side.
|
||||||
|
message Processes {
|
||||||
|
uint64 process_count = 1; // total processes in the system
|
||||||
|
repeated Process rows = 2; // all processes
|
||||||
|
}
|
||||||
|
|
||||||
|
message Process {
|
||||||
|
uint32 pid = 1;
|
||||||
|
string name = 2;
|
||||||
|
float cpu_usage = 3; // 0..100
|
||||||
|
uint64 mem_bytes = 4; // RSS bytes
|
||||||
|
}
|
||||||
48
socktop_connector/src/config.rs
Normal file
48
socktop_connector/src/config.rs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
//! Configuration for socktop WebSocket connections.
|
||||||
|
|
||||||
|
/// Configuration for connecting to a socktop agent.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ConnectorConfig {
|
||||||
|
pub url: String,
|
||||||
|
pub tls_ca_path: Option<String>,
|
||||||
|
pub verify_hostname: bool,
|
||||||
|
pub ws_protocols: Option<Vec<String>>,
|
||||||
|
pub ws_version: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectorConfig {
|
||||||
|
/// Create a new connector configuration with the given URL.
|
||||||
|
pub fn new(url: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
url: url.into(),
|
||||||
|
tls_ca_path: None,
|
||||||
|
verify_hostname: false,
|
||||||
|
ws_protocols: None,
|
||||||
|
ws_version: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the path to a custom TLS CA certificate file.
|
||||||
|
pub fn with_tls_ca(mut self, ca_path: impl Into<String>) -> Self {
|
||||||
|
self.tls_ca_path = Some(ca_path.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable or disable hostname verification for TLS connections.
|
||||||
|
pub fn with_hostname_verification(mut self, verify: bool) -> Self {
|
||||||
|
self.verify_hostname = verify;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set WebSocket sub-protocols to negotiate.
|
||||||
|
pub fn with_protocols(mut self, protocols: Vec<String>) -> Self {
|
||||||
|
self.ws_protocols = Some(protocols);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set WebSocket protocol version (default is "13").
|
||||||
|
pub fn with_version(mut self, version: impl Into<String>) -> Self {
|
||||||
|
self.ws_version = Some(version.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
1096
socktop_connector/src/connector.rs
Normal file
1096
socktop_connector/src/connector.rs
Normal file
File diff suppressed because it is too large
Load Diff
261
socktop_connector/src/connector_impl.rs
Normal file
261
socktop_connector/src/connector_impl.rs
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
//! Modular SocktopConnector implementation using networking and WASM modules.
|
||||||
|
|
||||||
|
use crate::config::ConnectorConfig;
|
||||||
|
use crate::error::{ConnectorError, Result};
|
||||||
|
use crate::{AgentRequest, AgentResponse};
|
||||||
|
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
use crate::networking::{
|
||||||
|
WsStream, connect_to_agent, request_disks, request_metrics, request_processes,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(all(feature = "wasm", not(feature = "networking")))]
|
||||||
|
use crate::wasm::{connect_to_agent, send_request_and_wait};
|
||||||
|
|
||||||
|
#[cfg(all(feature = "wasm", not(feature = "networking")))]
|
||||||
|
use crate::{DiskInfo, Metrics, ProcessesPayload};
|
||||||
|
|
||||||
|
#[cfg(all(feature = "wasm", not(feature = "networking")))]
|
||||||
|
use web_sys::WebSocket;
|
||||||
|
|
||||||
|
/// Main connector for communicating with socktop agents
|
||||||
|
pub struct SocktopConnector {
|
||||||
|
pub config: ConnectorConfig,
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
stream: Option<WsStream>,
|
||||||
|
#[cfg(all(feature = "wasm", not(feature = "networking")))]
|
||||||
|
websocket: Option<WebSocket>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SocktopConnector {
|
||||||
|
/// Create a new connector with the given configuration
|
||||||
|
pub fn new(config: ConnectorConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
stream: None,
|
||||||
|
#[cfg(all(feature = "wasm", not(feature = "networking")))]
|
||||||
|
websocket: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
impl SocktopConnector {
|
||||||
|
/// Connect to the agent
|
||||||
|
pub async fn connect(&mut self) -> Result<()> {
|
||||||
|
let stream = connect_to_agent(&self.config).await?;
|
||||||
|
self.stream = Some(stream);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a request to the agent and get the response
|
||||||
|
pub async fn request(&mut self, request: AgentRequest) -> Result<AgentResponse> {
|
||||||
|
let stream = self.stream.as_mut().ok_or(ConnectorError::NotConnected)?;
|
||||||
|
|
||||||
|
match request {
|
||||||
|
AgentRequest::Metrics => {
|
||||||
|
let metrics = request_metrics(stream)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| ConnectorError::invalid_response("Failed to get metrics"))?;
|
||||||
|
Ok(AgentResponse::Metrics(metrics))
|
||||||
|
}
|
||||||
|
AgentRequest::Disks => {
|
||||||
|
let disks = request_disks(stream)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| ConnectorError::invalid_response("Failed to get disks"))?;
|
||||||
|
Ok(AgentResponse::Disks(disks))
|
||||||
|
}
|
||||||
|
AgentRequest::Processes => {
|
||||||
|
let processes = request_processes(stream)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| ConnectorError::invalid_response("Failed to get processes"))?;
|
||||||
|
Ok(AgentResponse::Processes(processes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the connector is connected
|
||||||
|
pub fn is_connected(&self) -> bool {
|
||||||
|
self.stream.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disconnect from the agent
|
||||||
|
pub async fn disconnect(&mut self) -> Result<()> {
|
||||||
|
if let Some(mut stream) = self.stream.take() {
|
||||||
|
let _ = stream.close(None).await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WASM WebSocket implementation
|
||||||
|
#[cfg(all(feature = "wasm", not(feature = "networking")))]
|
||||||
|
impl SocktopConnector {
|
||||||
|
/// Connect to the agent using WASM WebSocket
|
||||||
|
pub async fn connect(&mut self) -> Result<()> {
|
||||||
|
let websocket = connect_to_agent(&self.config).await?;
|
||||||
|
self.websocket = Some(websocket);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a request to the agent and get the response
|
||||||
|
pub async fn request(&mut self, request: AgentRequest) -> Result<AgentResponse> {
|
||||||
|
let ws = self
|
||||||
|
.websocket
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(ConnectorError::NotConnected)?;
|
||||||
|
|
||||||
|
send_request_and_wait(ws, request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the connector is connected
|
||||||
|
pub fn is_connected(&self) -> bool {
|
||||||
|
use crate::utils::WEBSOCKET_OPEN;
|
||||||
|
self.websocket
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|ws| ws.ready_state() == WEBSOCKET_OPEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disconnect from the agent
|
||||||
|
pub async fn disconnect(&mut self) -> Result<()> {
|
||||||
|
if let Some(ws) = self.websocket.take() {
|
||||||
|
let _ = ws.close();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request metrics from the agent
|
||||||
|
pub async fn get_metrics(&mut self) -> Result<Metrics> {
|
||||||
|
match self.request(AgentRequest::Metrics).await? {
|
||||||
|
AgentResponse::Metrics(metrics) => Ok(metrics),
|
||||||
|
_ => Err(ConnectorError::protocol_error(
|
||||||
|
"Unexpected response type for metrics",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request disk information from the agent
|
||||||
|
pub async fn get_disks(&mut self) -> Result<Vec<DiskInfo>> {
|
||||||
|
match self.request(AgentRequest::Disks).await? {
|
||||||
|
AgentResponse::Disks(disks) => Ok(disks),
|
||||||
|
_ => Err(ConnectorError::protocol_error(
|
||||||
|
"Unexpected response type for disks",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request process information from the agent
|
||||||
|
pub async fn get_processes(&mut self) -> Result<ProcessesPayload> {
|
||||||
|
match self.request(AgentRequest::Processes).await? {
|
||||||
|
AgentResponse::Processes(processes) => Ok(processes),
|
||||||
|
_ => Err(ConnectorError::protocol_error(
|
||||||
|
"Unexpected response type for processes",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stub implementations when neither networking nor wasm is enabled
|
||||||
|
#[cfg(not(any(feature = "networking", feature = "wasm")))]
|
||||||
|
impl SocktopConnector {
|
||||||
|
/// Connect to the socktop agent endpoint.
|
||||||
|
///
|
||||||
|
/// Note: Networking functionality is disabled. Enable the "networking" feature to use this function.
|
||||||
|
pub async fn connect(&mut self) -> Result<()> {
|
||||||
|
Err(ConnectorError::protocol_error(
|
||||||
|
"Networking functionality disabled. Enable the 'networking' feature to connect to agents.",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a request to the agent and await a response.
|
||||||
|
///
|
||||||
|
/// Note: Networking functionality is disabled. Enable the "networking" feature to use this function.
|
||||||
|
pub async fn request(&mut self, _request: AgentRequest) -> Result<AgentResponse> {
|
||||||
|
Err(ConnectorError::protocol_error(
|
||||||
|
"Networking functionality disabled. Enable the 'networking' feature to send requests.",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the connection to the agent.
|
||||||
|
///
|
||||||
|
/// Note: Networking functionality is disabled. This is a no-op when networking is disabled.
|
||||||
|
pub async fn disconnect(&mut self) -> Result<()> {
|
||||||
|
Ok(()) // No-op when networking is disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience function to create a connector and connect in one step.
|
||||||
|
///
|
||||||
|
/// This function is for non-TLS WebSocket connections (`ws://`). Since there's no
|
||||||
|
/// certificate involved, hostname verification is not applicable.
|
||||||
|
///
|
||||||
|
/// For TLS connections with certificate pinning, use `connect_to_socktop_agent_with_tls()`.
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
pub async fn connect_to_socktop_agent(url: impl Into<String>) -> Result<SocktopConnector> {
|
||||||
|
let config = ConnectorConfig::new(url);
|
||||||
|
let mut connector = SocktopConnector::new(config);
|
||||||
|
connector.connect().await?;
|
||||||
|
Ok(connector)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience function to create a connector with TLS and connect in one step.
|
||||||
|
///
|
||||||
|
/// This function enables TLS with certificate pinning using the provided CA certificate.
|
||||||
|
/// The `verify_hostname` parameter controls whether the server's hostname is verified
|
||||||
|
/// against the certificate (recommended for production, can be disabled for testing).
|
||||||
|
#[cfg(feature = "tls")]
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
|
||||||
|
pub async fn connect_to_socktop_agent_with_tls(
|
||||||
|
url: impl Into<String>,
|
||||||
|
ca_path: impl Into<String>,
|
||||||
|
verify_hostname: bool,
|
||||||
|
) -> Result<SocktopConnector> {
|
||||||
|
let config = ConnectorConfig::new(url)
|
||||||
|
.with_tls_ca(ca_path)
|
||||||
|
.with_hostname_verification(verify_hostname);
|
||||||
|
let mut connector = SocktopConnector::new(config);
|
||||||
|
connector.connect().await?;
|
||||||
|
Ok(connector)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience function to create a connector with custom WebSocket protocol configuration.
|
||||||
|
///
|
||||||
|
/// This function allows you to specify WebSocket protocol version and sub-protocols.
|
||||||
|
/// Most users should use the simpler `connect_to_socktop_agent()` function instead.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// use socktop_connector::connect_to_socktop_agent_with_config;
|
||||||
|
///
|
||||||
|
/// # #[tokio::main]
|
||||||
|
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let connector = connect_to_socktop_agent_with_config(
|
||||||
|
/// "ws://localhost:3000/ws",
|
||||||
|
/// Some(vec!["socktop".to_string()]), // WebSocket sub-protocols
|
||||||
|
/// Some("13".to_string()), // WebSocket version (13 is standard)
|
||||||
|
/// ).await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
pub async fn connect_to_socktop_agent_with_config(
|
||||||
|
url: impl Into<String>,
|
||||||
|
protocols: Option<Vec<String>>,
|
||||||
|
version: Option<String>,
|
||||||
|
) -> Result<SocktopConnector> {
|
||||||
|
let mut config = ConnectorConfig::new(url);
|
||||||
|
|
||||||
|
if let Some(protocols) = protocols {
|
||||||
|
config = config.with_protocols(protocols);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(version) = version {
|
||||||
|
config = config.with_version(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut connector = SocktopConnector::new(config);
|
||||||
|
connector.connect().await?;
|
||||||
|
Ok(connector)
|
||||||
|
}
|
||||||
155
socktop_connector/src/error.rs
Normal file
155
socktop_connector/src/error.rs
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
//! Error types for socktop_connector
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Errors that can occur when using socktop_connector
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ConnectorError {
|
||||||
|
/// WebSocket connection failed
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
#[error("WebSocket connection failed: {source}")]
|
||||||
|
ConnectionFailed {
|
||||||
|
source: Box<tokio_tungstenite::tungstenite::Error>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// URL parsing error
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
#[error("Invalid URL: {url}")]
|
||||||
|
InvalidUrl {
|
||||||
|
url: String,
|
||||||
|
#[source]
|
||||||
|
source: url::ParseError,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// TLS certificate error
|
||||||
|
#[error("TLS certificate error: {message}")]
|
||||||
|
TlsError {
|
||||||
|
message: String,
|
||||||
|
#[source]
|
||||||
|
source: Box<dyn std::error::Error + Send + Sync>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Certificate file not found or invalid
|
||||||
|
#[error("Certificate file error at '{path}': {message}")]
|
||||||
|
CertificateError { path: String, message: String },
|
||||||
|
|
||||||
|
/// Invalid server response format
|
||||||
|
#[error("Invalid response from server: {message}")]
|
||||||
|
InvalidResponse { message: String },
|
||||||
|
|
||||||
|
/// JSON parsing error
|
||||||
|
#[error("JSON parsing error: {source}")]
|
||||||
|
JsonError {
|
||||||
|
#[from]
|
||||||
|
source: serde_json::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Request/response protocol error
|
||||||
|
#[error("Protocol error: {message}")]
|
||||||
|
ProtocolError { message: String },
|
||||||
|
|
||||||
|
/// Connection is not established
|
||||||
|
#[error("Not connected to server")]
|
||||||
|
NotConnected,
|
||||||
|
|
||||||
|
/// Connection was closed unexpectedly
|
||||||
|
#[error("Connection closed: {reason}")]
|
||||||
|
ConnectionClosed { reason: String },
|
||||||
|
|
||||||
|
/// IO error (network, file system, etc.)
|
||||||
|
#[error("IO error: {source}")]
|
||||||
|
IoError {
|
||||||
|
#[from]
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Compression/decompression error
|
||||||
|
#[error("Compression error: {message}")]
|
||||||
|
CompressionError { message: String },
|
||||||
|
|
||||||
|
/// Protocol Buffer parsing error
|
||||||
|
#[error("Protocol buffer error: {source}")]
|
||||||
|
ProtobufError {
|
||||||
|
#[from]
|
||||||
|
source: prost::DecodeError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result type alias for connector operations
|
||||||
|
pub type Result<T> = std::result::Result<T, ConnectorError>;
|
||||||
|
|
||||||
|
impl ConnectorError {
|
||||||
|
/// Create a TLS error with context
|
||||||
|
pub fn tls_error(
|
||||||
|
message: impl Into<String>,
|
||||||
|
source: impl std::error::Error + Send + Sync + 'static,
|
||||||
|
) -> Self {
|
||||||
|
Self::TlsError {
|
||||||
|
message: message.into(),
|
||||||
|
source: Box::new(source),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a certificate error
|
||||||
|
pub fn certificate_error(path: impl Into<String>, message: impl Into<String>) -> Self {
|
||||||
|
Self::CertificateError {
|
||||||
|
path: path.into(),
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a protocol error
|
||||||
|
pub fn protocol_error(message: impl Into<String>) -> Self {
|
||||||
|
Self::ProtocolError {
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an invalid response error
|
||||||
|
pub fn invalid_response(message: impl Into<String>) -> Self {
|
||||||
|
Self::InvalidResponse {
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a connection closed error
|
||||||
|
pub fn connection_closed(reason: impl Into<String>) -> Self {
|
||||||
|
Self::ConnectionClosed {
|
||||||
|
reason: reason.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a compression error
|
||||||
|
pub fn compression_error(message: impl Into<String>) -> Self {
|
||||||
|
Self::CompressionError {
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a serialization error (wraps JSON error)
|
||||||
|
pub fn serialization_error(message: impl Into<String>) -> Self {
|
||||||
|
Self::ProtocolError {
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
impl From<url::ParseError> for ConnectorError {
|
||||||
|
fn from(source: url::ParseError) -> Self {
|
||||||
|
Self::InvalidUrl {
|
||||||
|
url: "unknown".to_string(), // We don't have the URL in the error context
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual From implementation for boxed tungstenite errors
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
impl From<tokio_tungstenite::tungstenite::Error> for ConnectorError {
|
||||||
|
fn from(source: tokio_tungstenite::tungstenite::Error) -> Self {
|
||||||
|
Self::ConnectionFailed {
|
||||||
|
source: Box::new(source),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
182
socktop_connector/src/lib.rs
Normal file
182
socktop_connector/src/lib.rs
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
//! WebSocket connector library for socktop agents.
|
||||||
|
//!
|
||||||
|
//! This library provides a high-level interface for connecting to socktop agents
|
||||||
|
//! over WebSocket connections with support for TLS and certificate pinning.
|
||||||
|
//!
|
||||||
|
//! # Quick Start
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! use socktop_connector::{connect_to_socktop_agent, AgentRequest, AgentResponse};
|
||||||
|
//!
|
||||||
|
//! #[tokio::main]
|
||||||
|
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! let mut connector = connect_to_socktop_agent("ws://localhost:3000/ws").await?;
|
||||||
|
//!
|
||||||
|
//! // Get comprehensive system metrics
|
||||||
|
//! if let Ok(AgentResponse::Metrics(metrics)) = connector.request(AgentRequest::Metrics).await {
|
||||||
|
//! println!("Hostname: {}", metrics.hostname);
|
||||||
|
//! println!("CPU Usage: {:.1}%", metrics.cpu_total);
|
||||||
|
//!
|
||||||
|
//! // CPU temperature if available
|
||||||
|
//! if let Some(temp) = metrics.cpu_temp_c {
|
||||||
|
//! println!("CPU Temperature: {:.1}°C", temp);
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // Memory usage
|
||||||
|
//! println!("Memory: {:.1} GB / {:.1} GB",
|
||||||
|
//! metrics.mem_used as f64 / 1_000_000_000.0,
|
||||||
|
//! metrics.mem_total as f64 / 1_000_000_000.0);
|
||||||
|
//!
|
||||||
|
//! // Per-core CPU usage
|
||||||
|
//! for (i, usage) in metrics.cpu_per_core.iter().enumerate() {
|
||||||
|
//! println!("Core {}: {:.1}%", i, usage);
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // GPU information
|
||||||
|
//! if let Some(gpus) = &metrics.gpus {
|
||||||
|
//! for gpu in gpus {
|
||||||
|
//! if let Some(name) = &gpu.name {
|
||||||
|
//! println!("GPU {}: {:.1}% usage", name, gpu.utilization.unwrap_or(0.0));
|
||||||
|
//! if let Some(temp) = gpu.temp {
|
||||||
|
//! println!(" Temperature: {:.1}°C", temp);
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // Get process information
|
||||||
|
//! if let Ok(AgentResponse::Processes(processes)) = connector.request(AgentRequest::Processes).await {
|
||||||
|
//! println!("Running processes: {}", processes.process_count);
|
||||||
|
//! for proc in &processes.top_processes {
|
||||||
|
//! println!(" PID {}: {} ({:.1}% CPU, {:.1} MB RAM)",
|
||||||
|
//! proc.pid, proc.name, proc.cpu_usage, proc.mem_bytes as f64 / 1_000_000.0);
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // Get disk information
|
||||||
|
//! if let Ok(AgentResponse::Disks(disks)) = connector.request(AgentRequest::Disks).await {
|
||||||
|
//! for disk in disks {
|
||||||
|
//! let used_gb = (disk.total - disk.available) as f64 / 1_000_000_000.0;
|
||||||
|
//! let total_gb = disk.total as f64 / 1_000_000_000.0;
|
||||||
|
//! println!("Disk {}: {:.1} GB / {:.1} GB", disk.name, used_gb, total_gb);
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! Ok(())
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! # TLS Support
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! use socktop_connector::connect_to_socktop_agent_with_tls;
|
||||||
|
//!
|
||||||
|
//! # #[tokio::main]
|
||||||
|
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! let connector = connect_to_socktop_agent_with_tls(
|
||||||
|
//! "wss://secure-host:3000/ws",
|
||||||
|
//! "/path/to/ca.pem",
|
||||||
|
//! false // Enable hostname verification
|
||||||
|
//! ).await?;
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! # Continuous Monitoring
|
||||||
|
//!
|
||||||
|
//! For real-time system monitoring, you can make requests in a loop. The agent
|
||||||
|
//! implements intelligent caching to avoid overwhelming the system:
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! use socktop_connector::{connect_to_socktop_agent, AgentRequest, AgentResponse};
|
||||||
|
//! use tokio::time::{sleep, Duration};
|
||||||
|
//!
|
||||||
|
//! #[tokio::main]
|
||||||
|
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! let mut connector = connect_to_socktop_agent("ws://localhost:3000/ws").await?;
|
||||||
|
//!
|
||||||
|
//! // Monitor system metrics every 2 seconds
|
||||||
|
//! loop {
|
||||||
|
//! match connector.request(AgentRequest::Metrics).await {
|
||||||
|
//! Ok(AgentResponse::Metrics(metrics)) => {
|
||||||
|
//! // Calculate total network activity across all interfaces
|
||||||
|
//! let total_rx: u64 = metrics.networks.iter().map(|n| n.received).sum();
|
||||||
|
//! let total_tx: u64 = metrics.networks.iter().map(|n| n.transmitted).sum();
|
||||||
|
//!
|
||||||
|
//! println!("CPU: {:.1}%, Memory: {:.1}%, Network: ↓{} ↑{}",
|
||||||
|
//! metrics.cpu_total,
|
||||||
|
//! (metrics.mem_used as f64 / metrics.mem_total as f64) * 100.0,
|
||||||
|
//! format_bytes(total_rx),
|
||||||
|
//! format_bytes(total_tx)
|
||||||
|
//! );
|
||||||
|
//! }
|
||||||
|
//! Err(e) => {
|
||||||
|
//! eprintln!("Connection error: {}", e);
|
||||||
|
//! break;
|
||||||
|
//! }
|
||||||
|
//! _ => unreachable!(),
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! sleep(Duration::from_secs(2)).await;
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! Ok(())
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn format_bytes(bytes: u64) -> String {
|
||||||
|
//! const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
|
||||||
|
//! let mut size = bytes as f64;
|
||||||
|
//! let mut unit_index = 0;
|
||||||
|
//!
|
||||||
|
//! while size >= 1024.0 && unit_index < UNITS.len() - 1 {
|
||||||
|
//! size /= 1024.0;
|
||||||
|
//! unit_index += 1;
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! format!("{:.1}{}", size, UNITS[unit_index])
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
|
// Core modules
|
||||||
|
pub mod config;
|
||||||
|
pub mod error;
|
||||||
|
pub mod types;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
// Implementation modules
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
pub mod networking;
|
||||||
|
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
pub mod wasm;
|
||||||
|
|
||||||
|
// Main connector implementation
|
||||||
|
pub mod connector_impl;
|
||||||
|
|
||||||
|
// Re-export the main types
|
||||||
|
pub use config::ConnectorConfig;
|
||||||
|
pub use connector_impl::SocktopConnector;
|
||||||
|
pub use error::{ConnectorError, Result};
|
||||||
|
pub use types::{
|
||||||
|
AgentRequest, AgentResponse, DiskInfo, GpuInfo, Metrics, NetworkInfo, ProcessInfo,
|
||||||
|
ProcessesPayload,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-export convenience functions
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
pub use connector_impl::{connect_to_socktop_agent, connect_to_socktop_agent_with_config};
|
||||||
|
|
||||||
|
#[cfg(all(feature = "tls", feature = "networking"))]
|
||||||
|
pub use connector_impl::connect_to_socktop_agent_with_tls;
|
||||||
|
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
pub use networking::WsStream;
|
||||||
|
|
||||||
|
// Protobuf types for internal use
|
||||||
|
#[cfg(any(feature = "networking", feature = "wasm"))]
|
||||||
|
pub mod pb {
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/socktop.rs"));
|
||||||
|
}
|
||||||
185
socktop_connector/src/networking/connection.rs
Normal file
185
socktop_connector/src/networking/connection.rs
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
//! WebSocket connection handling for native (non-WASM) environments.
|
||||||
|
|
||||||
|
use crate::config::ConnectorConfig;
|
||||||
|
use crate::error::{ConnectorError, Result};
|
||||||
|
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||||
|
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, connect_async};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[cfg(feature = "tls")]
|
||||||
|
use {
|
||||||
|
rustls::{self, ClientConfig},
|
||||||
|
rustls::{
|
||||||
|
DigitallySignedStruct, RootCertStore, SignatureScheme,
|
||||||
|
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||||
|
crypto::ring,
|
||||||
|
pki_types::{CertificateDer, ServerName, UnixTime},
|
||||||
|
},
|
||||||
|
rustls_pemfile::Item,
|
||||||
|
std::fs::File,
|
||||||
|
tokio_tungstenite::Connector,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type WsStream = WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>;
|
||||||
|
|
||||||
|
/// Connect to the agent and return the WS stream
|
||||||
|
pub async fn connect_to_agent(config: &ConnectorConfig) -> Result<WsStream> {
|
||||||
|
#[cfg(feature = "tls")]
|
||||||
|
ensure_crypto_provider();
|
||||||
|
|
||||||
|
let mut u = Url::parse(&config.url)?;
|
||||||
|
if let Some(ca_path) = &config.tls_ca_path {
|
||||||
|
if u.scheme() == "ws" {
|
||||||
|
let _ = u.set_scheme("wss");
|
||||||
|
}
|
||||||
|
return connect_with_ca_and_config(u.as_str(), ca_path, config).await;
|
||||||
|
}
|
||||||
|
// No TLS - hostname verification is not applicable
|
||||||
|
connect_without_ca_and_config(u.as_str(), config).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_without_ca_and_config(url: &str, config: &ConnectorConfig) -> Result<WsStream> {
|
||||||
|
let mut req = url.into_client_request()?;
|
||||||
|
|
||||||
|
// Apply WebSocket protocol configuration
|
||||||
|
if let Some(version) = &config.ws_version {
|
||||||
|
req.headers_mut().insert(
|
||||||
|
"Sec-WebSocket-Version",
|
||||||
|
version
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ConnectorError::protocol_error("Invalid WebSocket version"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(protocols) = &config.ws_protocols {
|
||||||
|
let protocols_str = protocols.join(", ");
|
||||||
|
req.headers_mut().insert(
|
||||||
|
"Sec-WebSocket-Protocol",
|
||||||
|
protocols_str
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ConnectorError::protocol_error("Invalid WebSocket protocols"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (ws, _) = connect_async(req).await?;
|
||||||
|
Ok(ws)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tls")]
|
||||||
|
async fn connect_with_ca_and_config(
|
||||||
|
url: &str,
|
||||||
|
ca_path: &str,
|
||||||
|
config: &ConnectorConfig,
|
||||||
|
) -> Result<WsStream> {
|
||||||
|
// Initialize the crypto provider for rustls
|
||||||
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
|
||||||
|
let mut root = RootCertStore::empty();
|
||||||
|
let mut reader = BufReader::new(File::open(ca_path)?);
|
||||||
|
let mut der_certs = Vec::new();
|
||||||
|
while let Ok(Some(item)) = rustls_pemfile::read_one(&mut reader) {
|
||||||
|
if let Item::X509Certificate(der) = item {
|
||||||
|
der_certs.push(der);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root.add_parsable_certificates(der_certs);
|
||||||
|
|
||||||
|
let mut cfg = ClientConfig::builder()
|
||||||
|
.with_root_certificates(root)
|
||||||
|
.with_no_client_auth();
|
||||||
|
|
||||||
|
let mut req = url.into_client_request()?;
|
||||||
|
|
||||||
|
// Apply WebSocket protocol configuration
|
||||||
|
if let Some(version) = &config.ws_version {
|
||||||
|
req.headers_mut().insert(
|
||||||
|
"Sec-WebSocket-Version",
|
||||||
|
version
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ConnectorError::protocol_error("Invalid WebSocket version"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(protocols) = &config.ws_protocols {
|
||||||
|
let protocols_str = protocols.join(", ");
|
||||||
|
req.headers_mut().insert(
|
||||||
|
"Sec-WebSocket-Protocol",
|
||||||
|
protocols_str
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ConnectorError::protocol_error("Invalid WebSocket protocols"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.verify_hostname {
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct NoVerify;
|
||||||
|
impl ServerCertVerifier for NoVerify {
|
||||||
|
fn verify_server_cert(
|
||||||
|
&self,
|
||||||
|
_end_entity: &CertificateDer<'_>,
|
||||||
|
_intermediates: &[CertificateDer<'_>],
|
||||||
|
_server_name: &ServerName,
|
||||||
|
_ocsp_response: &[u8],
|
||||||
|
_now: UnixTime,
|
||||||
|
) -> std::result::Result<ServerCertVerified, rustls::Error> {
|
||||||
|
Ok(ServerCertVerified::assertion())
|
||||||
|
}
|
||||||
|
fn verify_tls12_signature(
|
||||||
|
&self,
|
||||||
|
_message: &[u8],
|
||||||
|
_cert: &CertificateDer<'_>,
|
||||||
|
_dss: &DigitallySignedStruct,
|
||||||
|
) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
|
||||||
|
Ok(HandshakeSignatureValid::assertion())
|
||||||
|
}
|
||||||
|
fn verify_tls13_signature(
|
||||||
|
&self,
|
||||||
|
_message: &[u8],
|
||||||
|
_cert: &CertificateDer<'_>,
|
||||||
|
_dss: &DigitallySignedStruct,
|
||||||
|
) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
|
||||||
|
Ok(HandshakeSignatureValid::assertion())
|
||||||
|
}
|
||||||
|
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||||
|
vec![
|
||||||
|
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||||
|
SignatureScheme::ED25519,
|
||||||
|
SignatureScheme::RSA_PSS_SHA256,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.dangerous().set_certificate_verifier(Arc::new(NoVerify));
|
||||||
|
eprintln!(
|
||||||
|
"socktop_connector: hostname verification disabled (default). Set SOCKTOP_VERIFY_NAME=1 to enable strict SAN checking."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let cfg = Arc::new(cfg);
|
||||||
|
let (ws, _) = tokio_tungstenite::connect_async_tls_with_config(
|
||||||
|
req,
|
||||||
|
None,
|
||||||
|
config.verify_hostname,
|
||||||
|
Some(Connector::Rustls(cfg)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(ws)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "tls"))]
|
||||||
|
async fn connect_with_ca_and_config(
|
||||||
|
_url: &str,
|
||||||
|
_ca_path: &str,
|
||||||
|
_config: &ConnectorConfig,
|
||||||
|
) -> Result<WsStream> {
|
||||||
|
Err(ConnectorError::tls_error(
|
||||||
|
"TLS support not compiled in",
|
||||||
|
std::io::Error::new(std::io::ErrorKind::Unsupported, "TLS not available"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tls")]
|
||||||
|
fn ensure_crypto_provider() {
|
||||||
|
let _ = ring::default_provider().install_default();
|
||||||
|
}
|
||||||
7
socktop_connector/src/networking/mod.rs
Normal file
7
socktop_connector/src/networking/mod.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
//! Networking module for native WebSocket connections.
|
||||||
|
|
||||||
|
pub mod connection;
|
||||||
|
pub mod requests;
|
||||||
|
|
||||||
|
pub use connection::*;
|
||||||
|
pub use requests::*;
|
||||||
84
socktop_connector/src/networking/requests.rs
Normal file
84
socktop_connector/src/networking/requests.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
//! WebSocket request handlers for native (non-WASM) environments.
|
||||||
|
|
||||||
|
use crate::networking::WsStream;
|
||||||
|
use crate::utils::{gunzip_to_string, gunzip_to_vec, is_gzip};
|
||||||
|
use crate::{DiskInfo, Metrics, ProcessInfo, ProcessesPayload, pb};
|
||||||
|
|
||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use prost::Message as ProstMessage;
|
||||||
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
|
||||||
|
/// Send a "get_metrics" request and await a single JSON reply
|
||||||
|
pub async fn request_metrics(ws: &mut WsStream) -> Option<Metrics> {
|
||||||
|
if ws.send(Message::Text("get_metrics".into())).await.is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match ws.next().await {
|
||||||
|
Some(Ok(Message::Binary(b))) => gunzip_to_string(&b)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str::<Metrics>(&s).ok()),
|
||||||
|
Some(Ok(Message::Text(json))) => serde_json::from_str::<Metrics>(&json).ok(),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a "get_disks" request and await a JSON Vec<DiskInfo>
|
||||||
|
pub async fn request_disks(ws: &mut WsStream) -> Option<Vec<DiskInfo>> {
|
||||||
|
if ws.send(Message::Text("get_disks".into())).await.is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match ws.next().await {
|
||||||
|
Some(Ok(Message::Binary(b))) => gunzip_to_string(&b)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str::<Vec<DiskInfo>>(&s).ok()),
|
||||||
|
Some(Ok(Message::Text(json))) => serde_json::from_str::<Vec<DiskInfo>>(&json).ok(),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a "get_processes" request and await a ProcessesPayload decoded from protobuf (binary, may be gzipped)
|
||||||
|
pub async fn request_processes(ws: &mut WsStream) -> Option<ProcessesPayload> {
|
||||||
|
if ws
|
||||||
|
.send(Message::Text("get_processes".into()))
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match ws.next().await {
|
||||||
|
Some(Ok(Message::Binary(b))) => {
|
||||||
|
let gz = is_gzip(&b);
|
||||||
|
let data = if gz { gunzip_to_vec(&b).ok()? } else { b };
|
||||||
|
match pb::Processes::decode(data.as_slice()) {
|
||||||
|
Ok(pb) => {
|
||||||
|
let rows: Vec<ProcessInfo> = pb
|
||||||
|
.rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|p: pb::Process| ProcessInfo {
|
||||||
|
pid: p.pid,
|
||||||
|
name: p.name,
|
||||||
|
cpu_usage: p.cpu_usage,
|
||||||
|
mem_bytes: p.mem_bytes,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Some(ProcessesPayload {
|
||||||
|
process_count: pb.process_count as usize,
|
||||||
|
top_processes: rows,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if std::env::var("SOCKTOP_DEBUG").ok().as_deref() == Some("1") {
|
||||||
|
eprintln!("protobuf decode failed: {e}");
|
||||||
|
}
|
||||||
|
// Fallback: maybe it's JSON (bytes already decompressed if gz)
|
||||||
|
match String::from_utf8(data) {
|
||||||
|
Ok(s) => serde_json::from_str::<ProcessesPayload>(&s).ok(),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Ok(Message::Text(json))) => serde_json::from_str::<ProcessesPayload>(&json).ok(),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
109
socktop_connector/src/types.rs
Normal file
109
socktop_connector/src/types.rs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
//! Types that represent data from the socktop agent.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct ProcessInfo {
|
||||||
|
pub pid: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub cpu_usage: f32,
|
||||||
|
pub mem_bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct DiskInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub total: u64,
|
||||||
|
pub available: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct NetworkInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub received: u64,
|
||||||
|
pub transmitted: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct GpuInfo {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub vendor: Option<String>,
|
||||||
|
|
||||||
|
// Accept both the new and legacy keys
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
alias = "utilization_gpu_pct",
|
||||||
|
alias = "gpu_util_pct",
|
||||||
|
alias = "gpu_utilization"
|
||||||
|
)]
|
||||||
|
pub utilization: Option<f32>,
|
||||||
|
|
||||||
|
#[serde(default, alias = "mem_used_bytes", alias = "vram_used_bytes")]
|
||||||
|
pub mem_used: Option<u64>,
|
||||||
|
|
||||||
|
#[serde(default, alias = "mem_total_bytes", alias = "vram_total_bytes")]
|
||||||
|
pub mem_total: Option<u64>,
|
||||||
|
|
||||||
|
#[serde(default, alias = "temp_c", alias = "temperature_c")]
|
||||||
|
pub temp: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Metrics {
|
||||||
|
pub cpu_total: f32,
|
||||||
|
pub cpu_per_core: Vec<f32>,
|
||||||
|
pub mem_total: u64,
|
||||||
|
pub mem_used: u64,
|
||||||
|
pub swap_total: u64,
|
||||||
|
pub swap_used: u64,
|
||||||
|
pub hostname: String,
|
||||||
|
pub cpu_temp_c: Option<f32>,
|
||||||
|
pub disks: Vec<DiskInfo>,
|
||||||
|
pub networks: Vec<NetworkInfo>,
|
||||||
|
pub top_processes: Vec<ProcessInfo>,
|
||||||
|
pub gpus: Option<Vec<GpuInfo>>,
|
||||||
|
// New: keep the last reported total process count
|
||||||
|
#[serde(default)]
|
||||||
|
pub process_count: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct ProcessesPayload {
|
||||||
|
pub process_count: usize,
|
||||||
|
pub top_processes: Vec<ProcessInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request types that can be sent to the agent
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum AgentRequest {
|
||||||
|
#[serde(rename = "metrics")]
|
||||||
|
Metrics,
|
||||||
|
#[serde(rename = "disks")]
|
||||||
|
Disks,
|
||||||
|
#[serde(rename = "processes")]
|
||||||
|
Processes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentRequest {
|
||||||
|
/// Convert to the legacy string format used by the agent
|
||||||
|
pub fn to_legacy_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
AgentRequest::Metrics => "get_metrics".to_string(),
|
||||||
|
AgentRequest::Disks => "get_disks".to_string(),
|
||||||
|
AgentRequest::Processes => "get_processes".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response types that can be received from the agent
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum AgentResponse {
|
||||||
|
#[serde(rename = "metrics")]
|
||||||
|
Metrics(Metrics),
|
||||||
|
#[serde(rename = "disks")]
|
||||||
|
Disks(Vec<DiskInfo>),
|
||||||
|
#[serde(rename = "processes")]
|
||||||
|
Processes(ProcessesPayload),
|
||||||
|
}
|
||||||
67
socktop_connector/src/utils.rs
Normal file
67
socktop_connector/src/utils.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
//! Shared utilities for both networking and WASM implementations.
|
||||||
|
|
||||||
|
#[cfg(any(feature = "networking", feature = "wasm"))]
|
||||||
|
use flate2::read::GzDecoder;
|
||||||
|
#[cfg(any(feature = "networking", feature = "wasm"))]
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use crate::error::{ConnectorError, Result};
|
||||||
|
|
||||||
|
// WebSocket state constants
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub const WEBSOCKET_CONNECTING: u16 = 0;
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub const WEBSOCKET_OPEN: u16 = 1;
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub const WEBSOCKET_CLOSING: u16 = 2;
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub const WEBSOCKET_CLOSED: u16 = 3;
|
||||||
|
|
||||||
|
// Gzip magic header constants
|
||||||
|
pub const GZIP_MAGIC_1: u8 = 0x1f;
|
||||||
|
pub const GZIP_MAGIC_2: u8 = 0x8b;
|
||||||
|
|
||||||
|
/// Unified gzip decompression to string for both networking and WASM
|
||||||
|
#[cfg(any(feature = "networking", feature = "wasm"))]
|
||||||
|
pub fn gunzip_to_string(bytes: &[u8]) -> Result<String> {
|
||||||
|
let mut decoder = GzDecoder::new(bytes);
|
||||||
|
let mut decompressed = String::new();
|
||||||
|
decoder
|
||||||
|
.read_to_string(&mut decompressed)
|
||||||
|
.map_err(|e| ConnectorError::protocol_error(format!("Gzip decompression failed: {e}")))?;
|
||||||
|
Ok(decompressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unified gzip decompression to bytes for both networking and WASM
|
||||||
|
#[cfg(any(feature = "networking", feature = "wasm"))]
|
||||||
|
pub fn gunzip_to_vec(bytes: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let mut decoder = GzDecoder::new(bytes);
|
||||||
|
let mut decompressed = Vec::new();
|
||||||
|
decoder
|
||||||
|
.read_to_end(&mut decompressed)
|
||||||
|
.map_err(|e| ConnectorError::protocol_error(format!("Gzip decompression failed: {e}")))?;
|
||||||
|
Ok(decompressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unified gzip detection for both networking and WASM
|
||||||
|
#[cfg(any(feature = "networking", feature = "wasm"))]
|
||||||
|
pub fn is_gzip(bytes: &[u8]) -> bool {
|
||||||
|
bytes.len() >= 2 && bytes[0] == GZIP_MAGIC_1 && bytes[1] == GZIP_MAGIC_2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unified debug logging for both networking and WASM modes
|
||||||
|
#[cfg(any(feature = "networking", feature = "wasm"))]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn log_debug(message: &str) {
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
if std::env::var("SOCKTOP_DEBUG").ok().as_deref() == Some("1") {
|
||||||
|
eprintln!("{message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "wasm", not(feature = "networking")))]
|
||||||
|
eprintln!("{message}");
|
||||||
|
}
|
||||||
66
socktop_connector/src/wasm/connection.rs
Normal file
66
socktop_connector/src/wasm/connection.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
//! WebSocket connection handling for WASM environments.
|
||||||
|
|
||||||
|
use crate::config::ConnectorConfig;
|
||||||
|
use crate::error::{ConnectorError, Result};
|
||||||
|
use crate::utils::{WEBSOCKET_CLOSED, WEBSOCKET_CLOSING, WEBSOCKET_OPEN};
|
||||||
|
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use web_sys::WebSocket;
|
||||||
|
|
||||||
|
/// Connect to the agent using WASM WebSocket
|
||||||
|
pub async fn connect_to_agent(config: &ConnectorConfig) -> Result<WebSocket> {
|
||||||
|
let websocket = WebSocket::new(&config.url).map_err(|e| {
|
||||||
|
ConnectorError::protocol_error(format!("Failed to create WebSocket: {e:?}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Set binary type for proper message handling
|
||||||
|
websocket.set_binary_type(web_sys::BinaryType::Arraybuffer);
|
||||||
|
|
||||||
|
// Wait for connection to be ready with proper async delays
|
||||||
|
let start_time = js_sys::Date::now();
|
||||||
|
let timeout_ms = 10000.0; // 10 second timeout (increased from 5)
|
||||||
|
|
||||||
|
// Poll connection status until ready or timeout
|
||||||
|
loop {
|
||||||
|
let ready_state = websocket.ready_state();
|
||||||
|
|
||||||
|
if ready_state == WEBSOCKET_OPEN {
|
||||||
|
// OPEN - connection is ready
|
||||||
|
break;
|
||||||
|
} else if ready_state == WEBSOCKET_CLOSED {
|
||||||
|
// CLOSED
|
||||||
|
return Err(ConnectorError::protocol_error(
|
||||||
|
"WebSocket connection closed",
|
||||||
|
));
|
||||||
|
} else if ready_state == WEBSOCKET_CLOSING {
|
||||||
|
// CLOSING
|
||||||
|
return Err(ConnectorError::protocol_error("WebSocket is closing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check timeout
|
||||||
|
let now = js_sys::Date::now();
|
||||||
|
if now - start_time > timeout_ms {
|
||||||
|
return Err(ConnectorError::protocol_error(
|
||||||
|
"WebSocket connection timeout",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proper async delay using setTimeout Promise
|
||||||
|
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
||||||
|
let closure = Closure::once(move || resolve.call0(&JsValue::UNDEFINED));
|
||||||
|
web_sys::window()
|
||||||
|
.unwrap()
|
||||||
|
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||||
|
closure.as_ref().unchecked_ref(),
|
||||||
|
100, // 100ms delay between polls
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
closure.forget();
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(websocket)
|
||||||
|
}
|
||||||
7
socktop_connector/src/wasm/mod.rs
Normal file
7
socktop_connector/src/wasm/mod.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
//! WASM module for browser WebSocket connections.
|
||||||
|
|
||||||
|
pub mod connection;
|
||||||
|
pub mod requests;
|
||||||
|
|
||||||
|
pub use connection::*;
|
||||||
|
pub use requests::*;
|
||||||
398
socktop_connector/src/wasm/requests.rs
Normal file
398
socktop_connector/src/wasm/requests.rs
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
//! WebSocket request handlers for WASM environments.
|
||||||
|
|
||||||
|
use crate::error::{ConnectorError, Result};
|
||||||
|
use crate::pb::Processes;
|
||||||
|
use crate::utils::{gunzip_to_string, gunzip_to_vec, is_gzip, log_debug};
|
||||||
|
use crate::{AgentRequest, AgentResponse, DiskInfo, Metrics, ProcessInfo, ProcessesPayload};
|
||||||
|
|
||||||
|
use prost::Message as ProstMessage;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use web_sys::WebSocket;
|
||||||
|
|
||||||
|
/// Send a request and wait for response with binary data handling
|
||||||
|
pub async fn send_request_and_wait(
|
||||||
|
websocket: &WebSocket,
|
||||||
|
request: AgentRequest,
|
||||||
|
) -> Result<AgentResponse> {
|
||||||
|
// Use the legacy string format that the agent expects
|
||||||
|
let request_string = request.to_legacy_string();
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
websocket
|
||||||
|
.send_with_str(&request_string)
|
||||||
|
.map_err(|e| ConnectorError::protocol_error(format!("Failed to send message: {e:?}")))?;
|
||||||
|
|
||||||
|
// Wait for response using JavaScript Promise
|
||||||
|
let (response, binary_data) = wait_for_response_with_binary(websocket).await?;
|
||||||
|
|
||||||
|
// Parse the response based on the request type
|
||||||
|
match request {
|
||||||
|
AgentRequest::Metrics => {
|
||||||
|
// Check if this is binary data (protobuf from agent)
|
||||||
|
if response.starts_with("BINARY_DATA:") {
|
||||||
|
// Extract the byte count
|
||||||
|
let byte_count: usize = response
|
||||||
|
.strip_prefix("BINARY_DATA:")
|
||||||
|
.unwrap_or("0")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
// For now, return a placeholder metrics response indicating binary data received
|
||||||
|
// TODO: Implement proper protobuf decoding for binary data
|
||||||
|
let placeholder_metrics = Metrics {
|
||||||
|
cpu_total: 0.0,
|
||||||
|
cpu_per_core: vec![0.0],
|
||||||
|
mem_total: 0,
|
||||||
|
mem_used: 0,
|
||||||
|
swap_total: 0,
|
||||||
|
swap_used: 0,
|
||||||
|
hostname: format!("Binary protobuf data ({byte_count} bytes)"),
|
||||||
|
cpu_temp_c: None,
|
||||||
|
disks: vec![],
|
||||||
|
networks: vec![],
|
||||||
|
top_processes: vec![],
|
||||||
|
gpus: None,
|
||||||
|
process_count: None,
|
||||||
|
};
|
||||||
|
Ok(AgentResponse::Metrics(placeholder_metrics))
|
||||||
|
} else {
|
||||||
|
// Try to parse as JSON (fallback)
|
||||||
|
let metrics: Metrics = serde_json::from_str(&response).map_err(|e| {
|
||||||
|
ConnectorError::serialization_error(format!("Failed to parse metrics: {e}"))
|
||||||
|
})?;
|
||||||
|
Ok(AgentResponse::Metrics(metrics))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AgentRequest::Disks => {
|
||||||
|
let disks: Vec<DiskInfo> = serde_json::from_str(&response).map_err(|e| {
|
||||||
|
ConnectorError::serialization_error(format!("Failed to parse disks: {e}"))
|
||||||
|
})?;
|
||||||
|
Ok(AgentResponse::Disks(disks))
|
||||||
|
}
|
||||||
|
AgentRequest::Processes => {
|
||||||
|
log_debug(&format!(
|
||||||
|
"🔍 Processing process request - response: {}",
|
||||||
|
if response.len() > 100 {
|
||||||
|
format!("{}...", &response[..100])
|
||||||
|
} else {
|
||||||
|
response.clone()
|
||||||
|
}
|
||||||
|
));
|
||||||
|
log_debug(&format!(
|
||||||
|
"🔍 Binary data available: {}",
|
||||||
|
binary_data.is_some()
|
||||||
|
));
|
||||||
|
if let Some(ref data) = binary_data {
|
||||||
|
log_debug(&format!("🔍 Binary data size: {} bytes", data.len()));
|
||||||
|
// Check if it's gzipped data and decompress it first
|
||||||
|
if is_gzip(data) {
|
||||||
|
log_debug("🔍 Process data is gzipped, decompressing...");
|
||||||
|
match gunzip_to_vec(data) {
|
||||||
|
Ok(decompressed_bytes) => {
|
||||||
|
log_debug(&format!(
|
||||||
|
"🔍 Successfully decompressed {} bytes, now decoding protobuf...",
|
||||||
|
decompressed_bytes.len()
|
||||||
|
));
|
||||||
|
// Now decode the decompressed bytes as protobuf
|
||||||
|
match <Processes as ProstMessage>::decode(decompressed_bytes.as_slice())
|
||||||
|
{
|
||||||
|
Ok(protobuf_processes) => {
|
||||||
|
log_debug(&format!(
|
||||||
|
"✅ Successfully decoded {} processes from gzipped protobuf",
|
||||||
|
protobuf_processes.rows.len()
|
||||||
|
));
|
||||||
|
|
||||||
|
// Convert protobuf processes to ProcessInfo structs
|
||||||
|
let processes: Vec<ProcessInfo> = protobuf_processes
|
||||||
|
.rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| ProcessInfo {
|
||||||
|
pid: p.pid,
|
||||||
|
name: p.name,
|
||||||
|
cpu_usage: p.cpu_usage,
|
||||||
|
mem_bytes: p.mem_bytes,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let processes_payload = ProcessesPayload {
|
||||||
|
top_processes: processes,
|
||||||
|
process_count: protobuf_processes.process_count as usize,
|
||||||
|
};
|
||||||
|
return Ok(AgentResponse::Processes(processes_payload));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_debug(&format!(
|
||||||
|
"❌ Failed to decode decompressed protobuf: {e}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_debug(&format!(
|
||||||
|
"❌ Failed to decompress gzipped process data: {e}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is binary data (protobuf from agent)
|
||||||
|
if response.starts_with("BINARY_DATA:") {
|
||||||
|
// Extract the binary data size and decode protobuf
|
||||||
|
let byte_count_str = response.strip_prefix("BINARY_DATA:").unwrap_or("0");
|
||||||
|
let _byte_count: usize = byte_count_str.parse().unwrap_or(0);
|
||||||
|
|
||||||
|
// Check if we have the actual binary data
|
||||||
|
if let Some(binary_bytes) = binary_data {
|
||||||
|
log_debug(&format!(
|
||||||
|
"🔧 Decoding {} bytes of protobuf process data",
|
||||||
|
binary_bytes.len()
|
||||||
|
));
|
||||||
|
|
||||||
|
// Try to decode the protobuf data using the prost Message trait
|
||||||
|
match <Processes as ProstMessage>::decode(&binary_bytes[..]) {
|
||||||
|
Ok(protobuf_processes) => {
|
||||||
|
log_debug(&format!(
|
||||||
|
"✅ Successfully decoded {} processes from protobuf",
|
||||||
|
protobuf_processes.rows.len()
|
||||||
|
));
|
||||||
|
|
||||||
|
// Convert protobuf processes to ProcessInfo structs
|
||||||
|
let processes: Vec<ProcessInfo> = protobuf_processes
|
||||||
|
.rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| ProcessInfo {
|
||||||
|
pid: p.pid,
|
||||||
|
name: p.name,
|
||||||
|
cpu_usage: p.cpu_usage,
|
||||||
|
mem_bytes: p.mem_bytes,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let processes_payload = ProcessesPayload {
|
||||||
|
top_processes: processes,
|
||||||
|
process_count: protobuf_processes.process_count as usize,
|
||||||
|
};
|
||||||
|
Ok(AgentResponse::Processes(processes_payload))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_debug(&format!("❌ Failed to decode protobuf: {e}"));
|
||||||
|
// Fallback to empty processes
|
||||||
|
let processes = ProcessesPayload {
|
||||||
|
top_processes: vec![],
|
||||||
|
process_count: 0,
|
||||||
|
};
|
||||||
|
Ok(AgentResponse::Processes(processes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_debug(
|
||||||
|
"❌ Binary data indicator received but no actual binary data preserved",
|
||||||
|
);
|
||||||
|
let processes = ProcessesPayload {
|
||||||
|
top_processes: vec![],
|
||||||
|
process_count: 0,
|
||||||
|
};
|
||||||
|
Ok(AgentResponse::Processes(processes))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Try to parse as JSON (fallback)
|
||||||
|
let processes: ProcessesPayload = serde_json::from_str(&response).map_err(|e| {
|
||||||
|
ConnectorError::serialization_error(format!("Failed to parse processes: {e}"))
|
||||||
|
})?;
|
||||||
|
Ok(AgentResponse::Processes(processes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_response_with_binary(websocket: &WebSocket) -> Result<(String, Option<Vec<u8>>)> {
|
||||||
|
let start_time = js_sys::Date::now();
|
||||||
|
let timeout_ms = 10000.0; // 10 second timeout
|
||||||
|
|
||||||
|
// Store the response in a shared location
|
||||||
|
let response_cell = Rc::new(RefCell::new(None::<String>));
|
||||||
|
let binary_data_cell = Rc::new(RefCell::new(None::<Vec<u8>>));
|
||||||
|
let error_cell = Rc::new(RefCell::new(None::<String>));
|
||||||
|
|
||||||
|
// Use a unique request ID to avoid message collision
|
||||||
|
let _request_id = js_sys::Math::random();
|
||||||
|
let response_received = Rc::new(RefCell::new(false));
|
||||||
|
|
||||||
|
// Set up the message handler that only processes if we haven't gotten a response yet
|
||||||
|
{
|
||||||
|
let response_cell = response_cell.clone();
|
||||||
|
let binary_data_cell = binary_data_cell.clone();
|
||||||
|
let response_received = response_received.clone();
|
||||||
|
let onmessage_callback = Closure::wrap(Box::new(move |e: web_sys::MessageEvent| {
|
||||||
|
// Only process if we haven't already received a response for this request
|
||||||
|
if !*response_received.borrow() {
|
||||||
|
// Handle text messages (JSON responses for metrics/disks)
|
||||||
|
if let Ok(data) = e.data().dyn_into::<js_sys::JsString>() {
|
||||||
|
let message = data.as_string().unwrap_or_default();
|
||||||
|
if !message.is_empty() {
|
||||||
|
// Debug: Log what we received (truncated)
|
||||||
|
let preview = if message.len() > 100 {
|
||||||
|
format!("{}...", &message[..100])
|
||||||
|
} else {
|
||||||
|
message.clone()
|
||||||
|
};
|
||||||
|
log_debug(&format!("🔍 Received text: {preview}"));
|
||||||
|
|
||||||
|
*response_cell.borrow_mut() = Some(message);
|
||||||
|
*response_received.borrow_mut() = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle binary messages (could be JSON as text bytes or actual protobuf)
|
||||||
|
else if let Ok(array_buffer) = e.data().dyn_into::<js_sys::ArrayBuffer>() {
|
||||||
|
let uint8_array = js_sys::Uint8Array::new(&array_buffer);
|
||||||
|
let length = uint8_array.length() as usize;
|
||||||
|
let mut bytes = vec![0u8; length];
|
||||||
|
uint8_array.copy_to(&mut bytes);
|
||||||
|
|
||||||
|
log_debug(&format!("🔍 Received binary data: {length} bytes"));
|
||||||
|
|
||||||
|
// Debug: Log the first few bytes to see what we're dealing with
|
||||||
|
let first_bytes = if bytes.len() >= 4 {
|
||||||
|
format!(
|
||||||
|
"0x{:02x} 0x{:02x} 0x{:02x} 0x{:02x}",
|
||||||
|
bytes[0], bytes[1], bytes[2], bytes[3]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("Only {} bytes available", bytes.len())
|
||||||
|
};
|
||||||
|
log_debug(&format!("🔍 First bytes: {first_bytes}"));
|
||||||
|
|
||||||
|
// Try to decode as UTF-8 text first (in case it's JSON sent as binary)
|
||||||
|
match String::from_utf8(bytes.clone()) {
|
||||||
|
Ok(text) => {
|
||||||
|
// If it decodes to valid UTF-8, check if it looks like JSON
|
||||||
|
let trimmed = text.trim();
|
||||||
|
if (trimmed.starts_with('{') && trimmed.ends_with('}'))
|
||||||
|
|| (trimmed.starts_with('[') && trimmed.ends_with(']'))
|
||||||
|
{
|
||||||
|
log_debug(&format!(
|
||||||
|
"🔍 Binary data is actually JSON text: {}",
|
||||||
|
if text.len() > 100 {
|
||||||
|
format!("{}...", &text[..100])
|
||||||
|
} else {
|
||||||
|
text.clone()
|
||||||
|
}
|
||||||
|
));
|
||||||
|
*response_cell.borrow_mut() = Some(text);
|
||||||
|
*response_received.borrow_mut() = true;
|
||||||
|
} else {
|
||||||
|
log_debug(&format!(
|
||||||
|
"🔍 Binary data is UTF-8 text but not JSON: {}",
|
||||||
|
if text.len() > 100 {
|
||||||
|
format!("{}...", &text[..100])
|
||||||
|
} else {
|
||||||
|
text.clone()
|
||||||
|
}
|
||||||
|
));
|
||||||
|
*response_cell.borrow_mut() = Some(text);
|
||||||
|
*response_received.borrow_mut() = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// If it's not valid UTF-8, check if it's gzipped data
|
||||||
|
if is_gzip(&bytes) {
|
||||||
|
log_debug(&format!(
|
||||||
|
"🔍 Binary data appears to be gzipped ({length} bytes)"
|
||||||
|
));
|
||||||
|
// Try to decompress using unified gzip decompression
|
||||||
|
match gunzip_to_string(&bytes) {
|
||||||
|
Ok(decompressed_text) => {
|
||||||
|
log_debug(&format!(
|
||||||
|
"🔍 Gzipped data decompressed to text: {}",
|
||||||
|
if decompressed_text.len() > 100 {
|
||||||
|
format!("{}...", &decompressed_text[..100])
|
||||||
|
} else {
|
||||||
|
decompressed_text.clone()
|
||||||
|
}
|
||||||
|
));
|
||||||
|
*response_cell.borrow_mut() = Some(decompressed_text);
|
||||||
|
*response_received.borrow_mut() = true;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_debug(&format!("🔍 Failed to decompress gzip: {e}"));
|
||||||
|
// Fallback: treat as actual binary protobuf data
|
||||||
|
*binary_data_cell.borrow_mut() = Some(bytes.clone());
|
||||||
|
*response_cell.borrow_mut() =
|
||||||
|
Some(format!("BINARY_DATA:{length}"));
|
||||||
|
*response_received.borrow_mut() = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If it's not valid UTF-8 and not gzipped, it's likely actual binary protobuf data
|
||||||
|
log_debug(&format!(
|
||||||
|
"🔍 Binary data is actual protobuf ({length} bytes)"
|
||||||
|
));
|
||||||
|
*binary_data_cell.borrow_mut() = Some(bytes);
|
||||||
|
*response_cell.borrow_mut() = Some(format!("BINARY_DATA:{length}"));
|
||||||
|
*response_received.borrow_mut() = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Log what type of data we got
|
||||||
|
log_debug(&format!("🔍 Received unknown data type: {:?}", e.data()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) as Box<dyn FnMut(_)>);
|
||||||
|
websocket.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
|
||||||
|
onmessage_callback.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the error handler
|
||||||
|
{
|
||||||
|
let error_cell = error_cell.clone();
|
||||||
|
let response_received = response_received.clone();
|
||||||
|
let onerror_callback = Closure::wrap(Box::new(move |_e: web_sys::ErrorEvent| {
|
||||||
|
if !*response_received.borrow() {
|
||||||
|
*error_cell.borrow_mut() = Some("WebSocket error occurred".to_string());
|
||||||
|
*response_received.borrow_mut() = true;
|
||||||
|
}
|
||||||
|
}) as Box<dyn FnMut(_)>);
|
||||||
|
websocket.set_onerror(Some(onerror_callback.as_ref().unchecked_ref()));
|
||||||
|
onerror_callback.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll for response with proper async delays
|
||||||
|
loop {
|
||||||
|
// Check for response
|
||||||
|
if *response_received.borrow() {
|
||||||
|
if let Some(response) = response_cell.borrow().as_ref() {
|
||||||
|
let binary_data = binary_data_cell.borrow().clone();
|
||||||
|
return Ok((response.clone(), binary_data));
|
||||||
|
}
|
||||||
|
if let Some(error) = error_cell.borrow().as_ref() {
|
||||||
|
return Err(ConnectorError::protocol_error(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check timeout
|
||||||
|
let now = js_sys::Date::now();
|
||||||
|
if now - start_time > timeout_ms {
|
||||||
|
*response_received.borrow_mut() = true; // Mark as done to prevent future processing
|
||||||
|
return Err(ConnectorError::protocol_error("WebSocket response timeout"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 50ms before checking again
|
||||||
|
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
||||||
|
let closure = Closure::once(move || resolve.call0(&JsValue::UNDEFINED));
|
||||||
|
web_sys::window()
|
||||||
|
.unwrap()
|
||||||
|
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||||
|
closure.as_ref().unchecked_ref(),
|
||||||
|
50,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
closure.forget();
|
||||||
|
});
|
||||||
|
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
51
socktop_connector/tests/integration_test.rs
Normal file
51
socktop_connector/tests/integration_test.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use socktop_connector::{
|
||||||
|
AgentRequest, AgentResponse, connect_to_socktop_agent, connect_to_socktop_agent_with_tls,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Integration probe: only runs when SOCKTOP_WS is set to an agent WebSocket URL.
|
||||||
|
// Example: SOCKTOP_WS=ws://127.0.0.1:3000/ws cargo test -p socktop_connector --test integration_test -- --nocapture
|
||||||
|
#[tokio::test]
|
||||||
|
async fn probe_ws_endpoints() {
|
||||||
|
// Gate the test to avoid CI failures when no agent is running.
|
||||||
|
let url = match std::env::var("SOCKTOP_WS") {
|
||||||
|
Ok(v) if !v.is_empty() => v,
|
||||||
|
_ => {
|
||||||
|
eprintln!(
|
||||||
|
"skipping ws_probe: set SOCKTOP_WS=ws://host:port/ws to run this integration test"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optional pinned CA for WSS/self-signed setups
|
||||||
|
let tls_ca = std::env::var("SOCKTOP_TLS_CA").ok();
|
||||||
|
|
||||||
|
let mut connector = if let Some(ca_path) = tls_ca {
|
||||||
|
connect_to_socktop_agent_with_tls(&url, ca_path, true)
|
||||||
|
.await
|
||||||
|
.expect("connect ws with TLS")
|
||||||
|
} else {
|
||||||
|
connect_to_socktop_agent(&url).await.expect("connect ws")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Should get fast metrics quickly
|
||||||
|
let response = connector.request(AgentRequest::Metrics).await;
|
||||||
|
assert!(response.is_ok(), "expected Metrics payload within timeout");
|
||||||
|
if let Ok(AgentResponse::Metrics(_)) = response {
|
||||||
|
// Success
|
||||||
|
} else {
|
||||||
|
panic!("expected Metrics response");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processes may be gzipped and a bit slower, but should arrive
|
||||||
|
let response = connector.request(AgentRequest::Processes).await;
|
||||||
|
assert!(
|
||||||
|
response.is_ok(),
|
||||||
|
"expected Processes payload within timeout"
|
||||||
|
);
|
||||||
|
if let Ok(AgentResponse::Processes(_)) = response {
|
||||||
|
// Success
|
||||||
|
} else {
|
||||||
|
panic!("expected Processes response");
|
||||||
|
}
|
||||||
|
}
|
||||||
15
socktop_wasm_test/.gitignore
vendored
Normal file
15
socktop_wasm_test/.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Build artifacts
|
||||||
|
/target/
|
||||||
|
/pkg/
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*~
|
||||||
|
*.bak
|
||||||
741
socktop_wasm_test/Cargo.lock
generated
Normal file
741
socktop_wasm_test/Cargo.lock
generated
Normal file
@ -0,0 +1,741 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.99"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console_error_panic_hook"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fixedbitset"
|
||||||
|
version = "0.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"libc",
|
||||||
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasi 0.14.4+wasi-0.2.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.78"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.175"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "multimap"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "petgraph"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
|
||||||
|
dependencies = [
|
||||||
|
"fixedbitset",
|
||||||
|
"indexmap",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"prost-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-build"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"itertools",
|
||||||
|
"log",
|
||||||
|
"multimap",
|
||||||
|
"once_cell",
|
||||||
|
"petgraph",
|
||||||
|
"prettyplease",
|
||||||
|
"prost",
|
||||||
|
"prost-types",
|
||||||
|
"regex",
|
||||||
|
"syn",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-derive"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"itertools",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-types"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
||||||
|
dependencies = [
|
||||||
|
"prost",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa"
|
||||||
|
dependencies = [
|
||||||
|
"protoc-bin-vendored-linux-aarch_64",
|
||||||
|
"protoc-bin-vendored-linux-ppcle_64",
|
||||||
|
"protoc-bin-vendored-linux-s390_64",
|
||||||
|
"protoc-bin-vendored-linux-x86_32",
|
||||||
|
"protoc-bin-vendored-linux-x86_64",
|
||||||
|
"protoc-bin-vendored-macos-aarch_64",
|
||||||
|
"protoc-bin-vendored-macos-x86_64",
|
||||||
|
"protoc-bin-vendored-win32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-linux-aarch_64"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-linux-ppcle_64"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-linux-s390_64"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-linux-x86_32"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-linux-x86_64"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-macos-aarch_64"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-macos-x86_64"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-win32"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.143"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socktop_connector"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a63dadaa5105df11b0684759a829012257d48e72a469cc554c0cf4394605f5a"
|
||||||
|
dependencies = [
|
||||||
|
"flate2",
|
||||||
|
"js-sys",
|
||||||
|
"prost",
|
||||||
|
"prost-build",
|
||||||
|
"protoc-bin-vendored",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socktop_wasm_test"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"socktop_connector",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.3.3",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.14.4+wasi-0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-backend"
|
||||||
|
version = "0.2.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"log",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-futures"
|
||||||
|
version = "0.4.51"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-backend",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-sys"
|
||||||
|
version = "0.3.78"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.53.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.45.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
|
||||||
36
socktop_wasm_test/Cargo.toml
Normal file
36
socktop_wasm_test/Cargo.toml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
[package]
|
||||||
|
name = "socktop_wasm_test"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# Make this a standalone package, not part of the parent workspace
|
||||||
|
[workspace]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Use WASM features for WebSocket connectivity (published version)
|
||||||
|
socktop_connector = { version = "0.1.5", default-features = false, features = ["wasm"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
|
console_error_panic_hook = "0.1"
|
||||||
|
js-sys = "0.3"
|
||||||
|
|
||||||
|
[dependencies.web-sys]
|
||||||
|
version = "0.3"
|
||||||
|
features = [
|
||||||
|
"console",
|
||||||
|
"WebSocket",
|
||||||
|
"MessageEvent",
|
||||||
|
"ErrorEvent",
|
||||||
|
"CloseEvent",
|
||||||
|
"BinaryType",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Enable JS feature for WASM random number generation
|
||||||
|
[dependencies.getrandom]
|
||||||
|
version = "0.2"
|
||||||
|
features = ["js"]
|
||||||
150
socktop_wasm_test/README.md
Normal file
150
socktop_wasm_test/README.md
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# WASM Compatibility Guide for socktop_connector
|
||||||
|
|
||||||
|
This directory contains a complete WebAssembly (WASM) compatibility test and implementation guide for the `socktop_connector` library.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`socktop_connector` provides **full WebSocket networking support** for WebAssembly environments. The library includes complete connectivity functionality with automatic compression and protobuf decoding, making it easy to connect to socktop agents directly from browser applications.
|
||||||
|
|
||||||
|
## What Works in WASM
|
||||||
|
|
||||||
|
- ✅ **Full WebSocket connections** (`ws://` connections)
|
||||||
|
- ✅ **All request types** (`AgentRequest::Metrics`, `AgentRequest::Disks`, `AgentRequest::Processes`)
|
||||||
|
- ✅ **Automatic data processing**: Gzip decompression for metrics/disks, protobuf decoding for processes
|
||||||
|
- ✅ Configuration types (`ConnectorConfig`)
|
||||||
|
- ✅ Request/Response types (`AgentRequest`, `AgentResponse`)
|
||||||
|
- ✅ JSON serialization/deserialization of all types
|
||||||
|
- ✅ Protocol and version configuration builders
|
||||||
|
- ✅ All type-safe validation and error handling
|
||||||
|
|
||||||
|
## What Doesn't Work in WASM
|
||||||
|
|
||||||
|
- ❌ TLS connections (`wss://`) - use `ws://` only
|
||||||
|
- ❌ TLS certificate handling (use non-TLS endpoints)
|
||||||
|
|
||||||
|
## Quick Start - WASM Test Page
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Please note that the test assumes you have and agent runnign on your local host at port 3000. If you would like to use an alternate configuration please update lib.rs prior to build.
|
||||||
|
|
||||||
|
# Build the WASM package
|
||||||
|
wasm-pack build --target web --out-dir pkg
|
||||||
|
|
||||||
|
# Serve the test page
|
||||||
|
basic-http-server . --addr 127.0.0.1:8000
|
||||||
|
|
||||||
|
# Open http://127.0.0.1:8000 in your browser
|
||||||
|
# Check the browser console for test results
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="./screenshot_09092025_134458.jpg" width="85%">
|
||||||
|
|
||||||
|
|
||||||
|
## WASM Dependencies
|
||||||
|
|
||||||
|
The test uses the WASM-compatible networking features:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
socktop_connector = { version = "0.1.5", default-features = false, features = ["wasm"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
console_error_panic_hook = "0.1"
|
||||||
|
|
||||||
|
[dependencies.web-sys]
|
||||||
|
version = "0.3"
|
||||||
|
features = ["console"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key**: Use `features = ["wasm"]` to enable full WebSocket networking support in WASM builds.
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### 1. Use socktop_connector Types for Configuration
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use socktop_connector::{ConnectorConfig, AgentRequest, AgentResponse};
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn create_config() -> String {
|
||||||
|
// Use socktop_connector types for type-safe configuration
|
||||||
|
let config = ConnectorConfig::new("ws://localhost:3000/ws")
|
||||||
|
.with_protocols(vec!["socktop".to_string(), "v1".to_string()])
|
||||||
|
.with_version("13".to_string());
|
||||||
|
|
||||||
|
// Return JSON for use with browser WebSocket API
|
||||||
|
serde_json::to_string(&config).unwrap_or_default()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Type-Safe Requests
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn create_metrics_request() -> String {
|
||||||
|
let request = AgentRequest::Metrics;
|
||||||
|
serde_json::to_string(&request).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn create_processes_request() -> String {
|
||||||
|
let request = AgentRequest::Processes;
|
||||||
|
serde_json::to_string(&request).unwrap_or_default()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Parse Responses with Type Safety
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn parse_metrics_response(json: &str) -> Option<String> {
|
||||||
|
match serde_json::from_str::<AgentResponse>(json) {
|
||||||
|
Ok(AgentResponse::Metrics(metrics)) => {
|
||||||
|
Some(format!("CPU: {}%, Memory: {}MB",
|
||||||
|
metrics.cpu_total,
|
||||||
|
metrics.mem_used / 1024 / 1024))
|
||||||
|
}
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Browser Integration
|
||||||
|
|
||||||
|
Then in JavaScript:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import init, {
|
||||||
|
create_config,
|
||||||
|
create_metrics_request,
|
||||||
|
parse_metrics_response
|
||||||
|
} from './pkg/socktop_wasm_test.js';
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
await init();
|
||||||
|
|
||||||
|
// Use type-safe configuration
|
||||||
|
const configJson = create_config();
|
||||||
|
const config = JSON.parse(configJson);
|
||||||
|
|
||||||
|
// Create WebSocket with proper protocols
|
||||||
|
const ws = new WebSocket(config.url, config.ws_protocols);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
// Send type-safe requests
|
||||||
|
ws.send(create_metrics_request());
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
// Handle responses with type safety
|
||||||
|
const result = parse_metrics_response(event.data);
|
||||||
|
if (result) {
|
||||||
|
console.log(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
|
```
|
||||||
154
socktop_wasm_test/index.html
Normal file
154
socktop_wasm_test/index.html
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Socktop Connector WASM Test</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: monospace; padding: 20px; background-color: #f5f5f5; }
|
||||||
|
.container { max-width: 800px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.log { margin: 5px 0; padding: 5px; border-radius: 4px; }
|
||||||
|
.success { color: #0a7c0a; background-color: #e8f5e8; }
|
||||||
|
.warning { color: #b8860b; background-color: #fdf6e3; }
|
||||||
|
.error { color: #d2322d; background-color: #f9e6e6; }
|
||||||
|
.info { color: #0969da; background-color: #e6f3ff; }
|
||||||
|
button {
|
||||||
|
background: #0969da;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
button:hover { background: #0757c7; }
|
||||||
|
button:disabled { background: #ccc; cursor: not-allowed; }
|
||||||
|
.server-input {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 8px;
|
||||||
|
width: 300px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.input-group { margin: 15px 0; }
|
||||||
|
.input-group label { display: block; margin-bottom: 5px; font-weight: bold; }
|
||||||
|
#output {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 200px;
|
||||||
|
background: #fafafa;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
.status { font-weight: bold; margin: 10px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🦀 Socktop Connector WASM Test</h1>
|
||||||
|
|
||||||
|
<div class="status">
|
||||||
|
<p><strong>Test Purpose:</strong> Verify socktop_connector works in WebAssembly without TLS dependencies</p>
|
||||||
|
<p><strong>Status:</strong> <span id="status">Loading WASM module...</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="server-url">Server URL:</label>
|
||||||
|
<input type="text" id="server-url" class="server-input" value="ws://localhost:3000/ws"
|
||||||
|
placeholder="ws://localhost:3000/ws">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="test-btn" disabled>Run WASM Test</button>
|
||||||
|
<button id="clear-btn">Clear Output</button>
|
||||||
|
|
||||||
|
<h3>Output:</h3>
|
||||||
|
<div id="output"></div>
|
||||||
|
|
||||||
|
<h3>ICON LEGEND:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>✅ <strong>Success:</strong> No rustls/TLS errors, connector loads in WASM</li>
|
||||||
|
<li>⚠️ <strong>Expected:</strong> Connection failures without running socktop_agent</li>
|
||||||
|
<li>❌ <strong>Failure:</strong> Build errors or TLS dependency issues</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p><small>💡 <strong>Tip:</strong> start socktop_agent with: <code>socktop_agent --port 3000</code></small></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import init, { test_socktop_connector } from './pkg/socktop_wasm_test.js';
|
||||||
|
|
||||||
|
const output = document.getElementById('output');
|
||||||
|
const testBtn = document.getElementById('test-btn');
|
||||||
|
const clearBtn = document.getElementById('clear-btn');
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
|
||||||
|
// Capture console output and display it on page
|
||||||
|
const originalLog = console.log;
|
||||||
|
const originalError = console.error;
|
||||||
|
|
||||||
|
function addLog(text, type = 'info') {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = `log ${type}`;
|
||||||
|
div.textContent = new Date().toLocaleTimeString() + ' - ' + text;
|
||||||
|
output.appendChild(div);
|
||||||
|
output.scrollTop = output.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log = function(...args) {
|
||||||
|
originalLog.apply(console, args);
|
||||||
|
const text = args.join(' ');
|
||||||
|
let type = 'info';
|
||||||
|
if (text.includes('✅')) {
|
||||||
|
type = 'success';
|
||||||
|
} else if (text.includes('⚠️')) {
|
||||||
|
type = 'warning';
|
||||||
|
} else if (text.includes('❌')) {
|
||||||
|
type = 'error';
|
||||||
|
}
|
||||||
|
addLog(text, type);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.error = function(...args) {
|
||||||
|
originalError.apply(console, args);
|
||||||
|
addLog('ERROR: ' + args.join(' '), 'error');
|
||||||
|
};
|
||||||
|
|
||||||
|
clearBtn.onclick = () => {
|
||||||
|
output.innerHTML = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
await init();
|
||||||
|
addLog('WASM module initialized successfully!', 'success');
|
||||||
|
status.textContent = 'Ready to test';
|
||||||
|
testBtn.disabled = false;
|
||||||
|
|
||||||
|
testBtn.onclick = () => {
|
||||||
|
testBtn.disabled = true;
|
||||||
|
const serverUrl = document.getElementById('server-url').value.trim();
|
||||||
|
addLog('=== Starting WASM Test ===', 'info');
|
||||||
|
addLog(`🌐 Using server: ${serverUrl}`, 'info');
|
||||||
|
try {
|
||||||
|
test_socktop_connector(serverUrl || undefined);
|
||||||
|
setTimeout(() => {
|
||||||
|
testBtn.disabled = false;
|
||||||
|
}, 2000);
|
||||||
|
} catch (e) {
|
||||||
|
addLog('Test execution failed: ' + e.message, 'error');
|
||||||
|
testBtn.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
addLog('Failed to initialize WASM: ' + e.message, 'error');
|
||||||
|
status.textContent = 'Failed to load WASM module';
|
||||||
|
console.error('WASM initialization error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
socktop_wasm_test/screenshot_09092025_134458.jpg
Normal file
BIN
socktop_wasm_test/screenshot_09092025_134458.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 214 KiB |
188
socktop_wasm_test/src/lib.rs
Normal file
188
socktop_wasm_test/src/lib.rs
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use socktop_connector::{ConnectorConfig, AgentRequest, SocktopConnector};
|
||||||
|
|
||||||
|
// Import the `console.log` function from the Web API
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern "C" {
|
||||||
|
#[wasm_bindgen(js_namespace = console)]
|
||||||
|
fn log(s: &str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define a macro for easier console logging
|
||||||
|
macro_rules! console_log {
|
||||||
|
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the main entry point called from JavaScript
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn test_socktop_connector(server_url: Option<String>) {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
// Use provided URL or default
|
||||||
|
let url = server_url.unwrap_or_else(|| "ws://localhost:3000/ws".to_string());
|
||||||
|
|
||||||
|
console_log!("🦀 Starting WASM connector test...");
|
||||||
|
console_log!("🌐 Connecting to: {}", url);
|
||||||
|
|
||||||
|
// Test 1: Create configuration
|
||||||
|
let config = ConnectorConfig::new(&url);
|
||||||
|
console_log!("✅ Config created: {}", config.url);
|
||||||
|
|
||||||
|
// Test 2: Test configuration methods
|
||||||
|
let config_with_protocols = config
|
||||||
|
.clone()
|
||||||
|
.with_protocols(vec!["socktop".to_string(), "v1".to_string()]);
|
||||||
|
console_log!("✅ Config with protocols: {:?}", config_with_protocols.ws_protocols);
|
||||||
|
|
||||||
|
let config_with_version = config_with_protocols.with_version("13".to_string());
|
||||||
|
console_log!("✅ Config with version: {:?}", config_with_version.ws_version);
|
||||||
|
|
||||||
|
// Test 3: Create request types
|
||||||
|
let _metrics_request = AgentRequest::Metrics;
|
||||||
|
let _disks_request = AgentRequest::Disks;
|
||||||
|
let _processes_request = AgentRequest::Processes;
|
||||||
|
console_log!("✅ AgentRequest types created");
|
||||||
|
|
||||||
|
// Test 4: Test serialization
|
||||||
|
if let Ok(json) = serde_json::to_string(&AgentRequest::Metrics) {
|
||||||
|
console_log!("✅ Serialization works: {}", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 5: WebSocket connection test
|
||||||
|
console_log!("🌐 Testing WebSocket connection...");
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
test_websocket_connection(config_with_version).await;
|
||||||
|
|
||||||
|
console_log!("");
|
||||||
|
console_log!("🎉 socktop_connector WASM Test Results:");
|
||||||
|
console_log!("✅ ConnectorConfig API works in WASM");
|
||||||
|
console_log!("✅ AgentRequest types work in WASM");
|
||||||
|
console_log!("✅ SocktopConnector compiles for WASM");
|
||||||
|
console_log!("✅ Connection stays alive with regular requests");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_websocket_connection(config: ConnectorConfig) {
|
||||||
|
console_log!("📡 Connecting to agent...");
|
||||||
|
|
||||||
|
let mut connector = SocktopConnector::new(config);
|
||||||
|
|
||||||
|
match connector.connect().await {
|
||||||
|
Ok(()) => {
|
||||||
|
console_log!("✅ Connected!");
|
||||||
|
|
||||||
|
// Test continuous monitoring (5 rounds)
|
||||||
|
for round in 1..=5 {
|
||||||
|
console_log!("🔄 Round {}/5 - Requesting metrics...", round);
|
||||||
|
|
||||||
|
// Request metrics (mimicking TUI behavior)
|
||||||
|
match connector.request(AgentRequest::Metrics).await {
|
||||||
|
Ok(response) => {
|
||||||
|
match &response {
|
||||||
|
socktop_connector::AgentResponse::Metrics(metrics) => {
|
||||||
|
console_log!("✅ Round {} - CPU: {:.1}%, Mem: {:.1}%, Host: {}",
|
||||||
|
round,
|
||||||
|
metrics.cpu_total,
|
||||||
|
(metrics.mem_used as f64 / metrics.mem_total as f64) * 100.0,
|
||||||
|
metrics.hostname
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show JSON summary for each round (clean, collapsible)
|
||||||
|
if let Ok(json_str) = serde_json::to_string_pretty(&response) {
|
||||||
|
console_log!("📊 Round {} JSON ({} chars):", round, json_str.len());
|
||||||
|
console_log!("{}", json_str);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => console_log!("📊 Round {} - Received non-metrics response", round),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
console_log!("❌ Round {} failed: {}", round, e);
|
||||||
|
console_log!("🔍 Error details: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every other round, also test disks and processes
|
||||||
|
if round % 2 == 0 {
|
||||||
|
console_log!("💾 Round {} - Requesting disk info...", round);
|
||||||
|
match connector.request(AgentRequest::Disks).await {
|
||||||
|
Ok(response) => {
|
||||||
|
match &response {
|
||||||
|
socktop_connector::AgentResponse::Disks(disks) => {
|
||||||
|
console_log!("✅ Round {} - Got {} disks", round, disks.len());
|
||||||
|
for disk in disks.iter().take(3) { // Show first 3 disks
|
||||||
|
let used_gb = (disk.total - disk.available) / 1024 / 1024 / 1024;
|
||||||
|
let total_gb = disk.total / 1024 / 1024 / 1024;
|
||||||
|
console_log!(" 💿 {}: {}/{} GB used", disk.name, used_gb, total_gb);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => console_log!("❌ Round {} - Unexpected disk response type", round),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => console_log!("❌ Round {} - Disk request failed: {}", round, e),
|
||||||
|
}
|
||||||
|
|
||||||
|
console_log!("⚙️ Round {} - Requesting process info...", round);
|
||||||
|
match connector.request(AgentRequest::Processes).await {
|
||||||
|
Ok(response) => {
|
||||||
|
match &response {
|
||||||
|
socktop_connector::AgentResponse::Processes(processes) => {
|
||||||
|
console_log!("✅ Round {} - Process count: {}, Top processes: {}",
|
||||||
|
round,
|
||||||
|
processes.process_count,
|
||||||
|
processes.top_processes.len()
|
||||||
|
);
|
||||||
|
if processes.top_processes.is_empty() {
|
||||||
|
console_log!("ℹ️ No top processes in response (process_count: {})", processes.process_count);
|
||||||
|
} else {
|
||||||
|
for process in processes.top_processes.iter().take(3) { // Show top 3 processes
|
||||||
|
console_log!(" ⚙️ {}: {:.1}% CPU, {} MB",
|
||||||
|
process.name,
|
||||||
|
process.cpu_usage,
|
||||||
|
process.mem_bytes / 1024 / 1024
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => console_log!("❌ Round {} - Unexpected process response type", round),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => console_log!("❌ Round {} - Process request failed: {}", round, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 1 second between rounds
|
||||||
|
if round < 5 {
|
||||||
|
console_log!("⏱️ Waiting 1 second...");
|
||||||
|
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
||||||
|
let closure = wasm_bindgen::closure::Closure::once(move || resolve.call0(&wasm_bindgen::JsValue::UNDEFINED));
|
||||||
|
web_sys::window()
|
||||||
|
.unwrap()
|
||||||
|
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||||
|
closure.as_ref().unchecked_ref(),
|
||||||
|
1000, // 1 second delay
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
closure.forget();
|
||||||
|
});
|
||||||
|
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console_log!("");
|
||||||
|
console_log!("🎉 Test completed successfully!");
|
||||||
|
|
||||||
|
// Clean disconnect
|
||||||
|
match connector.disconnect().await {
|
||||||
|
Ok(()) => console_log!("✅ Disconnected"),
|
||||||
|
Err(e) => console_log!("⚠️ Disconnect error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
console_log!("❌ Connection failed: {}", e);
|
||||||
|
console_log!("💡 Make sure socktop_agent is running on localhost:3000");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
146
socktop_wasm_test/test.html
Normal file
146
socktop_wasm_test/test.html
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>socktop_connector WASM Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.log {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
height: 500px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #007cba;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #005a87;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🦀 socktop_connector WASM Test</h1>
|
||||||
|
|
||||||
|
<p>This test demonstrates that <code>socktop_connector</code> works properly in WebAssembly with real WebSocket connections.</p>
|
||||||
|
|
||||||
|
<div style="margin: 20px 0; padding: 15px; background: #f9f9f9; border-radius: 4px;">
|
||||||
|
<label for="serverUrl" style="display: block; font-weight: bold; margin-bottom: 5px;">
|
||||||
|
🌐 WebSocket Server URL:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="serverUrl"
|
||||||
|
value="ws://localhost:3000/ws"
|
||||||
|
style="width: 300px; padding: 8px; margin-right: 10px; border: 1px solid #ccc; border-radius: 4px;"
|
||||||
|
placeholder="ws://your-server:port/ws"
|
||||||
|
/>
|
||||||
|
<small style="color: #666;">Edit if your socktop_agent is running on a different host/port</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button onclick="runTest()">Run Test</button>
|
||||||
|
<button onclick="clearLog()">Clear Log</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Test Output:</h2>
|
||||||
|
<div id="log" class="log">Click "Run Test" to start...\n</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import init, { test_socktop_connector } from './pkg/socktop_wasm_test.js';
|
||||||
|
|
||||||
|
const logElement = document.getElementById('log');
|
||||||
|
let wasmInitialized = false;
|
||||||
|
|
||||||
|
// Override console.log to capture output
|
||||||
|
const originalConsoleLog = console.log;
|
||||||
|
console.log = function(...args) {
|
||||||
|
originalConsoleLog.apply(console, arguments);
|
||||||
|
logElement.textContent += args.join(' ') + '\n';
|
||||||
|
logElement.scrollTop = logElement.scrollHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize WASM module on page load
|
||||||
|
async function initializeWasm() {
|
||||||
|
try {
|
||||||
|
console.log('🔄 Loading WASM module...');
|
||||||
|
await init();
|
||||||
|
wasmInitialized = true;
|
||||||
|
console.log('✅ WASM module initialized successfully!');
|
||||||
|
updateButtonState();
|
||||||
|
} catch (error) {
|
||||||
|
console.log('❌ Failed to initialize WASM module:', error);
|
||||||
|
console.log('💡 Make sure the build was successful and pkg/ directory exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateButtonState() {
|
||||||
|
const button = document.querySelector('button');
|
||||||
|
if (wasmInitialized) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = 'Run Test';
|
||||||
|
button.style.cursor = 'pointer';
|
||||||
|
button.title = 'Click to run the WASM test';
|
||||||
|
} else {
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = 'Loading WASM...';
|
||||||
|
button.style.cursor = 'not-allowed';
|
||||||
|
button.title = 'WASM module is loading...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.runTest = async function() {
|
||||||
|
if (!wasmInitialized) {
|
||||||
|
console.log('❌ WASM module not initialized yet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the server URL from the input field
|
||||||
|
const serverUrl = document.getElementById('serverUrl').value.trim();
|
||||||
|
if (!serverUrl) {
|
||||||
|
console.log('❌ Server URL cannot be empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('');
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
console.log('🚀 Starting socktop_connector WASM test...');
|
||||||
|
console.log('🌐 Server URL: ' + serverUrl);
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
test_socktop_connector(serverUrl);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('❌ Error running test:', error);
|
||||||
|
console.log('🔍 Error details:', error.stack);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.clearLog = function() {
|
||||||
|
logElement.textContent = 'Click "Run Test" to start...\n';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
initializeWasm();
|
||||||
|
|
||||||
|
// Update button state initially
|
||||||
|
updateButtonState();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
0
test_thiserror.rs
Normal file
0
test_thiserror.rs
Normal file
19
zellij_socktop_plugin/Cargo.toml
Normal file
19
zellij_socktop_plugin/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "zellij_socktop_plugin"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
zellij-tile = "0.40.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
socktop_connector = { version = "0.1.5", default-features = false, features = ["wasm"] }
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
version = "0.4"
|
||||||
|
default-features = false
|
||||||
|
features = ["clock", "std", "wasmbind"]
|
||||||
101
zellij_socktop_plugin/README.md
Normal file
101
zellij_socktop_plugin/README.md
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# Zellij Socktop Plugin
|
||||||
|
|
||||||
|
A Zellij plugin that displays real-time system metrics from a socktop agent.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Build the plugin:**
|
||||||
|
```bash
|
||||||
|
cargo build --target wasm32-wasi --release
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install in Zellij:**
|
||||||
|
```bash
|
||||||
|
# Copy the WASM file to Zellij plugins directory
|
||||||
|
mkdir -p ~/.config/zellij/plugins
|
||||||
|
cp target/wasm32-wasi/release/zellij_socktop_plugin.wasm ~/.config/zellij/plugins/socktop.wasm
|
||||||
|
cp plugin.yaml ~/.config/zellij/plugins/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Use in Zellij layout:**
|
||||||
|
```yaml
|
||||||
|
# ~/.config/zellij/layouts/socktop.yaml
|
||||||
|
template:
|
||||||
|
direction: Horizontal
|
||||||
|
parts:
|
||||||
|
- direction: Vertical
|
||||||
|
borderless: true
|
||||||
|
split_size:
|
||||||
|
Fixed: 1
|
||||||
|
run:
|
||||||
|
plugin:
|
||||||
|
location: "file:~/.config/zellij/plugins/socktop.wasm"
|
||||||
|
configuration:
|
||||||
|
server_url: "ws://localhost:3000/ws"
|
||||||
|
- direction: Vertical
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Launch Zellij with the layout:**
|
||||||
|
```bash
|
||||||
|
zellij --layout socktop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin Features
|
||||||
|
|
||||||
|
- **Real-time Metrics**: Displays CPU and memory usage
|
||||||
|
- **Auto-refresh**: Updates every 2 seconds
|
||||||
|
- **Reconnection**: Press 'r' to reconnect to socktop agent
|
||||||
|
- **Configurable**: Set custom server URL in plugin config
|
||||||
|
- **Error Handling**: Shows connection status and errors
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
- `server_url`: WebSocket URL for socktop agent (default: `ws://localhost:3000/ws`)
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
- **`r`** - Reconnect to socktop agent
|
||||||
|
- Plugin updates automatically every 2 seconds
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
This is a scaffold implementation. To make it fully functional:
|
||||||
|
|
||||||
|
1. **Async Operations**: Zellij plugins have limitations with async operations. You may need to:
|
||||||
|
- Use a different async runtime or approach
|
||||||
|
- Handle WebSocket connections in a background thread
|
||||||
|
- Use message passing between threads
|
||||||
|
|
||||||
|
2. **Error Handling**: Add more robust error handling for:
|
||||||
|
- Network connectivity issues
|
||||||
|
- Invalid server URLs
|
||||||
|
- Agent unavailability
|
||||||
|
|
||||||
|
3. **UI Improvements**:
|
||||||
|
- Add more detailed metrics display
|
||||||
|
- Implement scrolling for large datasets
|
||||||
|
- Add color coding for status indicators
|
||||||
|
|
||||||
|
4. **Performance**:
|
||||||
|
- Implement caching to reduce agent requests
|
||||||
|
- Add configurable update intervals
|
||||||
|
- Optimize WASM binary size
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `zellij-tile`: Zellij plugin framework
|
||||||
|
- `socktop_connector`: WebSocket connector with WASM support
|
||||||
|
- `serde`: JSON serialization
|
||||||
|
- `chrono`: Time handling (WASM-compatible)
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add WASM target
|
||||||
|
rustup target add wasm32-wasi
|
||||||
|
|
||||||
|
# Build for WASM
|
||||||
|
cargo build --target wasm32-wasi --release
|
||||||
|
|
||||||
|
# The plugin will be at: target/wasm32-wasi/release/zellij_socktop_plugin.wasm
|
||||||
|
```
|
||||||
11
zellij_socktop_plugin/plugin.yaml
Normal file
11
zellij_socktop_plugin/plugin.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
name: "socktop"
|
||||||
|
version: "0.1.0"
|
||||||
|
authors: ["Your Name <your@email.com>"]
|
||||||
|
plugin: true
|
||||||
|
permissions:
|
||||||
|
- ReadApplicationState
|
||||||
|
configuration:
|
||||||
|
server_url:
|
||||||
|
type: "string"
|
||||||
|
default: "ws://localhost:3000/ws"
|
||||||
|
description: "WebSocket URL for socktop agent"
|
||||||
180
zellij_socktop_plugin/src/lib.rs
Normal file
180
zellij_socktop_plugin/src/lib.rs
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
use zellij_tile::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use socktop_connector::{ConnectorConfig, AgentRequest, SocktopConnector, AgentResponse};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct State {
|
||||||
|
connector: Option<SocktopConnector>,
|
||||||
|
metrics_data: Option<String>,
|
||||||
|
connection_status: String,
|
||||||
|
error_message: Option<String>,
|
||||||
|
update_counter: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
static mut STATE: State = State {
|
||||||
|
connector: None,
|
||||||
|
metrics_data: None,
|
||||||
|
connection_status: String::new(),
|
||||||
|
error_message: None,
|
||||||
|
update_counter: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
register_plugin!(State);
|
||||||
|
|
||||||
|
impl ZellijPlugin for State {
|
||||||
|
fn load(&mut self, configuration: BTreeMap<String, String>) {
|
||||||
|
// Get server URL from plugin config or use default
|
||||||
|
let server_url = configuration
|
||||||
|
.get("server_url")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "ws://localhost:3000/ws".to_string());
|
||||||
|
|
||||||
|
// Initialize connector configuration
|
||||||
|
let config = ConnectorConfig::new(&server_url);
|
||||||
|
let connector = SocktopConnector::new(config);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
STATE.connector = Some(connector);
|
||||||
|
STATE.connection_status = "Connecting...".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up periodic updates
|
||||||
|
set_timeout(1.0); // Update every second
|
||||||
|
|
||||||
|
// Start initial connection
|
||||||
|
self.connect_to_socktop();
|
||||||
|
|
||||||
|
request_permission(&[
|
||||||
|
PermissionType::ReadApplicationState,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, event: Event) -> bool {
|
||||||
|
match event {
|
||||||
|
Event::Timer(_) => {
|
||||||
|
unsafe {
|
||||||
|
STATE.update_counter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request metrics every update cycle
|
||||||
|
self.fetch_metrics();
|
||||||
|
|
||||||
|
// Set next timer
|
||||||
|
set_timeout(2.0); // Update every 2 seconds
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Event::Key(key) => {
|
||||||
|
match key {
|
||||||
|
Key::Char('r') => {
|
||||||
|
// Reconnect on 'r' key press
|
||||||
|
self.connect_to_socktop();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, rows: usize, cols: usize) {
|
||||||
|
unsafe {
|
||||||
|
let mut output = Vec::new();
|
||||||
|
|
||||||
|
// Header
|
||||||
|
output.push("╭─ Socktop Metrics Plugin ─╮".to_string());
|
||||||
|
output.push(format!("│ Status: {:<18} │", STATE.connection_status));
|
||||||
|
output.push("├──────────────────────────╯".to_string());
|
||||||
|
|
||||||
|
// Metrics display
|
||||||
|
if let Some(ref metrics) = STATE.metrics_data {
|
||||||
|
output.push("│ System Metrics:".to_string());
|
||||||
|
output.push(format!("│ {}", metrics));
|
||||||
|
} else if let Some(ref error) = STATE.error_message {
|
||||||
|
output.push("│ Error:".to_string());
|
||||||
|
output.push(format!("│ {}", error));
|
||||||
|
} else {
|
||||||
|
output.push("│ Waiting for data...".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
output.push("│".to_string());
|
||||||
|
output.push(format!("│ Updates: {} │ Press 'r' to reconnect", STATE.update_counter));
|
||||||
|
output.push("╰──────────────────────────╯".to_string());
|
||||||
|
|
||||||
|
// Print lines within terminal bounds
|
||||||
|
for (i, line) in output.iter().enumerate() {
|
||||||
|
if i < rows {
|
||||||
|
println!("{}", line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
fn connect_to_socktop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
if let Some(ref mut connector) = STATE.connector {
|
||||||
|
STATE.connection_status = "Connecting...".to_string();
|
||||||
|
STATE.error_message = None;
|
||||||
|
|
||||||
|
// In a real implementation, you'd use async/await here
|
||||||
|
// For this scaffold, we'll simulate the connection
|
||||||
|
// Note: Zellij plugins have limitations with async operations
|
||||||
|
STATE.connection_status = "Connected".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_metrics(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
if let Some(ref mut connector) = STATE.connector {
|
||||||
|
// Try to get real metrics from socktop agent
|
||||||
|
match self.try_get_metrics(connector) {
|
||||||
|
Ok(metrics_text) => {
|
||||||
|
STATE.metrics_data = Some(metrics_text);
|
||||||
|
STATE.connection_status = "Active".to_string();
|
||||||
|
STATE.error_message = None;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
STATE.error_message = Some(error);
|
||||||
|
STATE.connection_status = "Error".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
STATE.error_message = Some("No connector available".to_string());
|
||||||
|
STATE.connection_status = "Disconnected".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_get_metrics(&mut self, connector: &mut SocktopConnector) -> Result<String, String> {
|
||||||
|
// Note: This is synchronous for simplicity. In a real plugin you might need
|
||||||
|
// to handle async operations differently depending on Zellij's threading model.
|
||||||
|
|
||||||
|
// For now, we'll use a blocking approach or return a placeholder
|
||||||
|
// that indicates we're trying to connect
|
||||||
|
|
||||||
|
// Attempt connection if not connected
|
||||||
|
if let Err(e) = futures::executor::block_on(connector.connect()) {
|
||||||
|
return Err(format!("Connection failed: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request metrics
|
||||||
|
match futures::executor::block_on(connector.request(AgentRequest::Metrics)) {
|
||||||
|
Ok(AgentResponse::Metrics(metrics)) => {
|
||||||
|
Ok(format!(
|
||||||
|
"CPU: {:.1}% | Mem: {:.1}% | Host: {} | Load: {:.2}",
|
||||||
|
metrics.cpu_total,
|
||||||
|
(metrics.mem_used as f64 / metrics.mem_total as f64) * 100.0,
|
||||||
|
metrics.hostname,
|
||||||
|
metrics.load_avg_1m
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Ok(_) => Err("Unexpected response type".to_string()),
|
||||||
|
Err(e) => Err(format!("Request failed: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user