Compare commits

..

36 Commits

Author SHA1 Message Date
jason 4e378b882a add license file
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
2025-09-03 23:10:48 -07:00
jason 0c5a1d7553 update readme 2025-08-30 01:59:25 -07:00
jason 0bd709d2a7 slipsteam note for rpi users on kernel version 2025-08-29 11:28:59 -07:00
jason 31f5f9ce76 re: Accessibility: Cross-compile guide 2025-08-29 11:23:41 -07:00
jason df2308e6e9 code optimizations to reduce cpu usage of agent on all platforms and additional unit test. 2025-08-28 16:03:05 -07:00
jason 7592709a43 clamp then divide by cores for more accurate statistics 2025-08-28 13:11:48 -07:00
jason 61fe1cc38e socktop_agent: bump version to 1.40.65 2025-08-28 12:27:48 -07:00
jason eed346abb6 scripts: add publish_socktop_agent.sh job 2025-08-28 12:07:03 -07:00
jason ab3bb33711 socktop_agent: bump version to 1.40.64 2025-08-28 12:03:45 -07:00
jason 7caf2f4bfb remove ununused var 2025-08-27 17:15:35 -07:00
jason b249c7ba99 Update metrics.rs 2025-08-27 16:55:12 -07:00
jason f0858525e8 fix for macos defect where processes less than .01% were being filtered 2025-08-27 16:55:09 -07:00
jason 2fe005ed90 ProcessesToUpdate::All enum 2025-08-27 16:20:55 -07:00
jason ca6a5cbdfa use ProcessesToUpdate::All ENUM 2025-08-27 16:20:41 -07:00
jason 56301d61fd fixes for non linux compilation issues. 2025-08-27 16:11:17 -07:00
jason 55e5c708fe MACOS / NON LINUX metrics optimizations. 2025-08-27 16:00:29 -07:00
jason 2d17cf1598 additional optimizations for macos 2025-08-27 15:05:38 -07:00
jason 353c08c35e increment version and macos performance 2025-08-26 12:14:32 -07:00
jason f13ea45360 increment version 2025-08-26 10:57:01 -07:00
jason 8ce00a5dad non linux optimizations for macbook 2025-08-26 10:14:14 -07:00
jason f37b8d9ff4 chore(agent): fix clippy unused mut on non-linux process list 2025-08-26 00:22:29 -07:00
jason 322981ada7 cargo fmt and version bump 2025-08-26 00:20:52 -07:00
jason 3394beab67 chore: make pre-commit resilient when cargo absent 2025-08-26 00:18:10 -07:00
jason c9ebea92f5 perf(agent,non-linux): enable CPU core normalization by default; tighter scaling threshold 2025-08-25 23:50:26 -07:00
jason e2dc5e8ac9 perf(agent,non-linux): add per-process CPU scaling heuristic to reduce overreporting 2025-08-25 23:47:31 -07:00
jason beddba0072 perf(agent,non-linux): reduce process collection overhead; configurable CPU sample delay 2025-08-25 23:44:28 -07:00
jason cacc4cba9f cargo fmt 2025-08-25 23:24:50 -07:00
jason 66270c16b7 perf(agent): windows/mac cpu collection tweak; add optional normalization; silence linux dead_code 2025-08-25 22:59:07 -07:00
jason 00d5777d05 Merge branch 'master' of https://github.com/jasonwitty/socktop 2025-08-25 22:35:02 -07:00
jason f62b5274d2 optimize non linux metrics collection 2025-08-25 22:35:01 -07:00
jason bbbe35111a Update README.md 2025-08-25 01:33:40 -07:00
jason a4356b5ece update readme with animated demo 2025-08-24 20:35:56 -07:00
jason b6e656738b chore(release): bump to 1.40.0
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
2025-08-24 18:56:40 -07:00
jason f83cb07d57 Release candidate 1.4
increment version, support version flag on socktop
2025-08-24 18:03:45 -07:00
jason 7697c7dc2b docs: add per-crate README.md and link via Cargo.toml readme field 2025-08-24 17:56:22 -07:00
jason 1043fffc8d Merge pull request #5 from jasonwitty/feature/housekeeping
Feature/housekeeping
2025-08-24 17:46:26 -07:00
17 changed files with 1116 additions and 94 deletions
Regular → Executable
+13 -2
View File
@@ -1,11 +1,22 @@
#!/usr/bin/env bash
# This repository uses a custom hooks directory (.githooks). To enable this pre-commit hook run:
# git config core.hooksPath .githooks
# Ensure this file is executable: chmod +x .githooks/pre-commit
set -euo pipefail
echo "[pre-commit] Running cargo fmt --all" >&2
if ! command -v cargo >/dev/null 2>&1; then
echo "[pre-commit] cargo not found in PATH" >&2
exit 1
# Try loading rustup environment (common install path)
if [ -f "$HOME/.cargo/env" ]; then
# shellcheck source=/dev/null
. "$HOME/.cargo/env"
fi
fi
if ! command -v cargo >/dev/null 2>&1; then
echo "[pre-commit] cargo not found in PATH; skipping fmt (install Rust or adjust PATH)." >&2
exit 0
fi
cargo fmt --all
Generated
+37 -5
View File
@@ -158,7 +158,7 @@ dependencies = [
"sha1",
"sync_wrapper",
"tokio",
"tokio-tungstenite",
"tokio-tungstenite 0.24.0",
"tower 0.5.2",
"tower-layer",
"tower-service",
@@ -2162,7 +2162,7 @@ dependencies = [
[[package]]
name = "socktop"
version = "0.1.3"
version = "1.40.0"
dependencies = [
"anyhow",
"assert_cmd",
@@ -2181,13 +2181,13 @@ dependencies = [
"sysinfo",
"tempfile",
"tokio",
"tokio-tungstenite",
"tokio-tungstenite 0.24.0",
"url",
]
[[package]]
name = "socktop_agent"
version = "0.1.3"
version = "1.40.67"
dependencies = [
"anyhow",
"assert_cmd",
@@ -2210,6 +2210,7 @@ dependencies = [
"tempfile",
"time",
"tokio",
"tokio-tungstenite 0.21.0",
"tonic-build",
"tracing",
"tracing-subscriber",
@@ -2463,6 +2464,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-tungstenite"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite 0.21.0",
]
[[package]]
name = "tokio-tungstenite"
version = "0.24.0"
@@ -2475,7 +2488,7 @@ dependencies = [
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.2",
"tungstenite",
"tungstenite 0.24.0",
]
[[package]]
@@ -2608,6 +2621,25 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "tungstenite"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
dependencies = [
"byteorder",
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"rand",
"sha1",
"thiserror 1.0.69",
"url",
"utf-8",
]
[[package]]
name = "tungstenite"
version = "0.24.0"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Witty One Off
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.
+13 -2
View File
@@ -5,7 +5,7 @@ socktop is a remote system monitor with a rich TUI, inspired by top/btop, talkin
- Linux agent: near-zero CPU when idle (request-driven, no always-on sampler)
- TUI: smooth graphs, sortable process table, scrollbars, readable colors
![socktop screenshot](./docs/14900ks_arch_alacritty_gpu_active_v2.jpg)
<img src="./docs/socktop_demo.apng" width="100%">
---
@@ -60,6 +60,8 @@ sudo apt-get update
sudo apt-get install libdrm-dev libdrm-amdgpu1
```
_Additional note for Raspberry Pi users. Please update your system to use the newest kernel available through app, kernel version 6.6+ will use considerably less overall CPU to run the agent. For example on a rpi4 the kernel < 6.6 the agent will consume .8 cpu but on the same hardware on > 6.6 the agent will consume only .2 cpu. (these numbers indicate continuous polling at web socket endpoints, when not in use the usage is 0)_
---
## Architecture
@@ -94,6 +96,12 @@ cargo build --release
./target/release/socktop ws://REMOTE_HOST:3000/ws
```
### Cross-compiling for Raspberry Pi
For Raspberry Pi and other ARM devices, you can cross-compile the agent from a more powerful machine:
- [Cross-compilation guide](./docs/cross-compiling.md) - Instructions for cross-compiling from Linux, macOS, or Windows hosts
### Quick demo (no agent setup)
Spin up a temporary local agent on port 3231 and connect automatically:
@@ -525,10 +533,13 @@ Every commit will then format Rust sources and restage them automatically.
- [x] Agent authentication (token)
- [x] Hide per-thread entries; only show processes
- [x] Sort top processes in the TUI
- [ ] Configurable refresh intervals (client)
- [x] Configurable refresh intervals (client)
- [ ] Export metrics to file
- [x] TLS / WSS support (selfsigned server cert + client pinning)
- [x] Split processes/disks to separate WS calls with independent cadences (already logical on client; formalize API)
- [ ] Outage notifications and reconnect.
- [ ] Per process detailed statistics pane
- [ ] cleanup of Disks section, properly display physical disks / partitions, remove duplicate entries
---
+204
View File
@@ -0,0 +1,204 @@
# Cross-Compiling socktop_agent for Raspberry Pi
This guide explains how to cross-compile the socktop_agent on various host systems and deploy it to a Raspberry Pi. Cross-compiling is particularly useful for older or resource-constrained Pi models where native compilation might be slow.
## Cross-Compilation Host Setup
Choose your host operating system:
- [Debian/Ubuntu](#debianubuntu-based-systems)
- [Arch Linux](#arch-linux-based-systems)
- [macOS](#macos)
- [Windows](#windows)
## Debian/Ubuntu Based Systems
### Prerequisites
Install the cross-compilation toolchain for your target Raspberry Pi architecture:
```bash
# For 64-bit Raspberry Pi (aarch64)
sudo apt update
sudo apt install gcc-aarch64-linux-gnu libc6-dev-arm64-cross libdrm-dev:arm64
# For 32-bit Raspberry Pi (armv7)
sudo apt update
sudo apt install gcc-arm-linux-gnueabihf libc6-dev-armhf-cross libdrm-dev:armhf
```
### Setup Rust Cross-Compilation Targets
```bash
# For 64-bit Raspberry Pi
rustup target add aarch64-unknown-linux-gnu
# For 32-bit Raspberry Pi
rustup target add armv7-unknown-linux-gnueabihf
```
### Configure Cargo for Cross-Compilation
Create or edit `~/.cargo/config.toml`:
```toml
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
```
## Arch Linux Based Systems
### Prerequisites
Install the cross-compilation toolchain using pacman and AUR:
```bash
# Install base dependencies
sudo pacman -S base-devel
# For 64-bit Raspberry Pi (aarch64)
sudo pacman -S aarch64-linux-gnu-gcc
# Install libdrm for aarch64 using an AUR helper (e.g., yay, paru)
yay -S aarch64-linux-gnu-libdrm
# For 32-bit Raspberry Pi (armv7)
sudo pacman -S arm-linux-gnueabihf-gcc
# Install libdrm for armv7 using an AUR helper
yay -S arm-linux-gnueabihf-libdrm
```
### Setup Rust Cross-Compilation Targets
```bash
# For 64-bit Raspberry Pi
rustup target add aarch64-unknown-linux-gnu
# For 32-bit Raspberry Pi
rustup target add armv7-unknown-linux-gnueabihf
```
### Configure Cargo for Cross-Compilation
Create or edit `~/.cargo/config.toml`:
```toml
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
```
## macOS
The recommended approach for cross-compiling from macOS is to use Docker:
```bash
# Install Docker
brew install --cask docker
# Pull a cross-compilation Docker image
docker pull messense/rust-musl-cross:armv7-musleabihf # For 32-bit Pi
docker pull messense/rust-musl-cross:aarch64-musl # For 64-bit Pi
```
### Using Docker for Cross-Compilation
```bash
# Navigate to your socktop project directory
cd path/to/socktop
# For 64-bit Raspberry Pi
docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:aarch64-musl cargo build --release --target aarch64-unknown-linux-musl -p socktop_agent
# For 32-bit Raspberry Pi
docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:armv7-musleabihf cargo build --release --target armv7-unknown-linux-musleabihf -p socktop_agent
```
The compiled binaries will be available in your local target directory.
## Windows
The recommended approach for Windows is to use Windows Subsystem for Linux (WSL2):
1. Install WSL2 with a Debian/Ubuntu distribution by following the [official Microsoft documentation](https://docs.microsoft.com/en-us/windows/wsl/install).
2. Once WSL2 is set up with a Debian/Ubuntu distribution, open your WSL terminal and follow the [Debian/Ubuntu instructions](#debianubuntu-based-systems) above.
## Cross-Compile the Agent
After setting up your environment, build the socktop_agent for your target Raspberry Pi:
```bash
# For 64-bit Raspberry Pi
cargo build --release --target aarch64-unknown-linux-gnu -p socktop_agent
# For 32-bit Raspberry Pi
cargo build --release --target armv7-unknown-linux-gnueabihf -p socktop_agent
```
## Transfer the Binary to Your Raspberry Pi
Use SCP to transfer the compiled binary to your Raspberry Pi:
```bash
# For 64-bit Raspberry Pi
scp target/aarch64-unknown-linux-gnu/release/socktop_agent pi@raspberry-pi-ip:~/
# For 32-bit Raspberry Pi
scp target/armv7-unknown-linux-gnueabihf/release/socktop_agent pi@raspberry-pi-ip:~/
```
Replace `raspberry-pi-ip` with your Raspberry Pi's IP address and `pi` with your username.
## Install Dependencies on the Raspberry Pi
SSH into your Raspberry Pi and install the required dependencies:
```bash
ssh pi@raspberry-pi-ip
# For Raspberry Pi OS (Debian-based)
sudo apt update
sudo apt install libdrm-dev libdrm-amdgpu1
# For Arch Linux ARM
sudo pacman -Syu
sudo pacman -S libdrm
```
## Make the Binary Executable and Install
```bash
chmod +x ~/socktop_agent
# Optional: Install system-wide
sudo install -o root -g root -m 0755 ~/socktop_agent /usr/local/bin/socktop_agent
# Optional: Set up as a systemd service
sudo install -o root -g root -m 0644 ~/socktop-agent.service /etc/systemd/system/socktop-agent.service
sudo systemctl daemon-reload
sudo systemctl enable --now socktop-agent
```
## Troubleshooting
If you encounter issues with the cross-compiled binary:
1. **Incorrect Architecture**: Ensure you've chosen the correct target for your Raspberry Pi model:
- For Raspberry Pi 2: use `armv7-unknown-linux-gnueabihf`
- For Raspberry Pi 3/4/5 in 64-bit mode: use `aarch64-unknown-linux-gnu`
- For Raspberry Pi 3/4/5 in 32-bit mode: use `armv7-unknown-linux-gnueabihf`
2. **Dependency Issues**: Check for missing libraries:
```bash
ldd ~/socktop_agent
```
3. **Run with Backtrace**: Get detailed error information:
```bash
RUST_BACKTRACE=1 ~/socktop_agent
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 MiB

+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
# Cross-check Windows build from Linux using the GNU (MinGW) toolchain.
# - Ensures target `x86_64-pc-windows-gnu` is installed
# - Verifies MinGW cross-compiler is available (x86_64-w64-mingw32-gcc)
# - Runs cargo clippy with warnings-as-errors for the Windows target
# - Builds release binaries for the Windows target
echo "[socktop] Windows cross-check: clippy + build (GNU target)"
have() { command -v "$1" >/dev/null 2>&1; }
if ! have rustup; then
echo "error: rustup not found. Install Rust via rustup first (see README)." >&2
exit 1
fi
if ! rustup target list --installed | grep -q '^x86_64-pc-windows-gnu$'; then
echo "+ rustup target add x86_64-pc-windows-gnu"
rustup target add x86_64-pc-windows-gnu
fi
if ! have x86_64-w64-mingw32-gcc; then
echo "error: Missing MinGW cross-compiler (x86_64-w64-mingw32-gcc)." >&2
if have pacman; then
echo "Arch Linux: sudo pacman -S --needed mingw-w64-gcc" >&2
elif have apt-get; then
echo "Debian/Ubuntu: sudo apt-get install -y mingw-w64" >&2
elif have dnf; then
echo "Fedora: sudo dnf install -y mingw64-gcc" >&2
else
echo "Install the mingw-w64 toolchain for your distro, then re-run." >&2
fi
exit 1
fi
CARGO_FLAGS=(--workspace --all-targets --all-features --target x86_64-pc-windows-gnu)
echo "+ cargo clippy ${CARGO_FLAGS[*]} -- -D warnings"
cargo clippy "${CARGO_FLAGS[@]}" -- -D warnings
echo "+ cargo build --release ${CARGO_FLAGS[*]}"
cargo build --release "${CARGO_FLAGS[@]}"
echo "✅ Windows clippy and build completed successfully."
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
# Publish job: "publish new socktop agent version"
# Usage: ./scripts/publish_socktop_agent.sh <new_version>
if [[ ${1:-} == "" ]]; then
echo "Usage: $0 <new_version>" >&2
exit 1
fi
NEW_VERSION="$1"
ROOT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
CRATE_DIR="$ROOT_DIR/socktop_agent"
echo "==> Formatting socktop_agent"
(cd "$ROOT_DIR" && cargo fmt -p socktop_agent)
echo "==> Running tests for socktop_agent"
(cd "$ROOT_DIR" && cargo test -p socktop_agent)
echo "==> Running clippy (warnings as errors) for socktop_agent"
(cd "$ROOT_DIR" && cargo clippy -p socktop_agent -- -D warnings)
echo "==> Building release for socktop_agent"
(cd "$ROOT_DIR" && cargo build -p socktop_agent --release)
echo "==> Bumping version to $NEW_VERSION in socktop_agent/Cargo.toml"
sed -i.bak -E "s/^version = \"[0-9]+\.[0-9]+\.[0-9]+\"/version = \"$NEW_VERSION\"/" "$CRATE_DIR/Cargo.toml"
rm -f "$CRATE_DIR/Cargo.toml.bak"
echo "==> Committing version bump"
(cd "$ROOT_DIR" && git add -A && git commit -m "socktop_agent: bump version to $NEW_VERSION")
CURRENT_BRANCH=$(cd "$ROOT_DIR" && git rev-parse --abbrev-ref HEAD)
echo "==> Pushing to origin $CURRENT_BRANCH"
(cd "$ROOT_DIR" && git push origin "$CURRENT_BRANCH")
echo "==> Publishing socktop_agent $NEW_VERSION to crates.io"
(cd "$ROOT_DIR" && cargo publish -p socktop_agent)
echo "==> Done: socktop_agent $NEW_VERSION published"
+2 -1
View File
@@ -1,10 +1,11 @@
[package]
name = "socktop"
version = "0.1.3"
version = "1.40.0"
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
description = "Remote system monitor over WebSocket, TUI like top"
edition = "2021"
license = "MIT"
readme = "README.md"
[dependencies]
tokio = { workspace = true }
+26
View File
@@ -0,0 +1,26 @@
# socktop (client)
Minimal TUI client for the socktop remote monitoring agent.
Features:
- Connects to a socktop_agent over WebSocket / secure WebSocket
- Displays CPU, memory, swap, disks, network, processes, (optional) GPU metrics
- Selfsigned TLS cert pinning via --tls-ca
- Profile management with saved intervals
- Low CPU usage (request-driven updates)
Quick start:
```
cargo install socktop
socktop ws://HOST:3000/ws
```
With TLS (copy agent cert first):
```
socktop --tls-ca cert.pem wss://HOST:8443/ws
```
Demo mode (spawns a local agent automatically on first run prompt):
```
socktop --demo
```
Full documentation, screenshots, and advanced usage:
https://github.com/jasonwitty/socktop
+10
View File
@@ -124,19 +124,29 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
return Ok(());
}
};
//support version flag (print and exit)
if env::args().any(|a| a == "--version" || a == "-V") {
println!("socktop {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
if parsed.demo || matches!(parsed.profile.as_deref(), Some("demo")) {
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 req = ProfileRequest {
profile_name: parsed.profile.clone(),
url: parsed.url.clone(),
tls_ca: parsed.tls_ca.clone(),
};
let resolved = req.resolve(&profiles_file);
let mut profiles_mut = profiles_file.clone();
let (url, tls_ca, metrics_interval_ms, processes_interval_ms): (
+4 -2
View File
@@ -1,10 +1,11 @@
[package]
name = "socktop_agent"
version = "0.1.3"
version = "1.40.67"
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
description = "Remote system monitor over WebSocket, TUI like top"
edition = "2021"
license = "MIT"
readme = "README.md"
[dependencies]
tokio = { version = "1", features = ["full"] }
@@ -34,4 +35,5 @@ tonic-build = { version = "0.12", default-features = false, optional = true }
protoc-bin-vendored = "3"
[dev-dependencies]
assert_cmd = "2.0"
tempfile = "3.10"
tempfile = "3.10"
tokio-tungstenite = "0.21"
+392
View File
@@ -0,0 +1,392 @@
# socktop_agent (server)
Lightweight ondemand metrics WebSocket server for the socktop TUI.
Highlights:
- Collects system metrics only when requested (keeps idle CPU <1%)
- Optional TLS (selfsigned cert autogenerated & pinned by client)
- JSON for fast metrics / disks; protobuf (optionally gzipped) for processes
- Accurate perprocess CPU% on Linux via /proc jiffies delta
- Optional GPU & temperature metrics (disable via env vars)
- Simple token auth (?token=...) support
Run (no TLS):
```
cargo install socktop_agent
socktop_agent --port 3000
```
Enable TLS:
```
SOCKTOP_ENABLE_SSL=1 socktop_agent --port 8443
# cert/key stored under $XDG_DATA_HOME/socktop_agent/tls
```
Environment toggles:
- SOCKTOP_AGENT_GPU=0 (disable GPU collection)
- SOCKTOP_AGENT_TEMP=0 (disable temperature)
- SOCKTOP_TOKEN=secret (require token param from client)
- SOCKTOP_AGENT_METRICS_TTL_MS=250 (cache fast metrics window)
- SOCKTOP_AGENT_PROCESSES_TTL_MS=1000
- SOCKTOP_AGENT_DISKS_TTL_MS=1000
Systemd unit example & full docs:
https://github.com/jasonwitty/socktop
## WebSocket API Integration Guide
The socktop_agent exposes a WebSocket API that can be directly integrated with your own applications. This allows you to build custom monitoring dashboards or analysis tools using the agent's metrics.
### WebSocket Endpoint
```
ws://HOST:PORT/ws # Without TLS
wss://HOST:PORT/ws # With TLS
```
With authentication token (if configured):
```
ws://HOST:PORT/ws?token=YOUR_TOKEN
wss://HOST:PORT/ws?token=YOUR_TOKEN
```
### Communication Protocol
All communication uses JSON format for requests and responses, except for the process list which uses Protocol Buffers (protobuf) format with optional gzip compression.
#### Request Types
Send a JSON message with a `type` field to request specific metrics:
```json
{"type": "metrics"} // Request fast-changing metrics (CPU, memory, network)
{"type": "disks"} // Request disk information
{"type": "processes"} // Request process list (returns protobuf)
```
#### Response Formats
1. **Fast Metrics** (JSON):
```json
{
"cpu_total": 12.4,
"cpu_per_core": [11.2, 15.7],
"mem_total": 33554432,
"mem_used": 18321408,
"swap_total": 0,
"swap_used": 0,
"hostname": "myserver",
"cpu_temp_c": 42.5,
"networks": [{"name":"eth0","received":12345678,"transmitted":87654321}],
"gpus": [{"name":"nvidia-0","usage":56.7,"memory_total":8589934592,"memory_used":1073741824,"temp_c":65.0}]
}
```
2. **Disks** (JSON):
```json
[
{"name":"nvme0n1p2","total":512000000000,"available":320000000000},
{"name":"sda1","total":1000000000000,"available":750000000000}
]
```
3. **Processes** (Protocol Buffers):
Processes are returned in Protocol Buffers format, optionally gzip-compressed for large process lists. The protobuf schema is:
```protobuf
syntax = "proto3";
message Process {
uint32 pid = 1;
string name = 2;
float cpu_usage = 3;
uint64 mem_bytes = 4;
}
message ProcessList {
uint32 process_count = 1;
repeated Process processes = 2;
}
```
### Example Integration (JavaScript/Node.js)
```javascript
const WebSocket = require('ws');
// Connect to the agent
const ws = new WebSocket('ws://localhost:3000/ws');
ws.on('open', function open() {
console.log('Connected to socktop_agent');
// Request metrics immediately on connection
ws.send(JSON.stringify({type: 'metrics'}));
// Set up regular polling
setInterval(() => {
ws.send(JSON.stringify({type: 'metrics'}));
}, 1000);
// Request processes every 3 seconds
setInterval(() => {
ws.send(JSON.stringify({type: 'processes'}));
}, 3000);
});
ws.on('message', function incoming(data) {
// Check if the response is JSON or binary (protobuf)
try {
const jsonData = JSON.parse(data);
console.log('Received JSON data:', jsonData);
} catch (e) {
console.log('Received binary data (protobuf), length:', data.length);
// Process binary protobuf data with a library like protobufjs
}
});
ws.on('close', function close() {
console.log('Disconnected from socktop_agent');
});
```
### Example Integration (Python)
```python
import json
import asyncio
import websockets
async def monitor_system():
uri = "ws://localhost:3000/ws"
async with websockets.connect(uri) as websocket:
print("Connected to socktop_agent")
# Request initial metrics
await websocket.send(json.dumps({"type": "metrics"}))
# Set up regular polling
while True:
# Request metrics
await websocket.send(json.dumps({"type": "metrics"}))
# Receive and process response
response = await websocket.recv()
# Check if response is JSON or binary (protobuf)
try:
data = json.loads(response)
print(f"CPU: {data['cpu_total']}%, Memory: {data['mem_used']/data['mem_total']*100:.1f}%")
except json.JSONDecodeError:
print(f"Received binary data, length: {len(response)}")
# Process binary protobuf data with a library like protobuf
# Wait before next poll
await asyncio.sleep(1)
asyncio.run(monitor_system())
```
### Notes for Integration
1. **Error Handling**: The WebSocket connection may close unexpectedly; implement reconnection logic in your client.
2. **Rate Limiting**: Avoid excessive polling that could impact the system being monitored. Recommended intervals:
- Metrics: 500ms or slower
- Processes: 2000ms or slower
- Disks: 5000ms or slower
3. **Authentication**: If the agent is configured with a token, always include it in the WebSocket URL.
4. **Protocol Buffers Handling**: For processing the binary process list data, use a Protocol Buffers library for your language and the schema provided in the `proto/processes.proto` file.
5. **Compression**: Process lists may be gzip-compressed. Check if the response starts with the gzip magic bytes (`0x1f, 0x8b`) and decompress if necessary.
## LLM Integration Guide
If you're using an LLM to generate code for integrating with socktop_agent, this section provides structured information to help the model understand the API better.
### API Schema
```yaml
# WebSocket API Schema for socktop_agent
endpoint: ws://HOST:PORT/ws or wss://HOST:PORT/ws (with TLS)
authentication:
type: query parameter
parameter: token
example: ws://HOST:PORT/ws?token=YOUR_TOKEN
requests:
- type: metrics
format: JSON
example: {"type": "metrics"}
description: Fast-changing metrics (CPU, memory, network)
- type: disks
format: JSON
example: {"type": "disks"}
description: Disk information
- type: processes
format: JSON
example: {"type": "processes"}
description: Process list (returns protobuf)
responses:
- request_type: metrics
format: JSON
schema:
cpu_total: float # percentage of total CPU usage
cpu_per_core: [float] # array of per-core CPU usage percentages
mem_total: uint64 # total memory in bytes
mem_used: uint64 # used memory in bytes
swap_total: uint64 # total swap in bytes
swap_used: uint64 # used swap in bytes
hostname: string # system hostname
cpu_temp_c: float? # CPU temperature in Celsius (optional)
networks: [
{
name: string # network interface name
received: uint64 # total bytes received
transmitted: uint64 # total bytes transmitted
}
]
gpus: [
{
name: string # GPU device name
usage: float # GPU usage percentage
memory_total: uint64 # total GPU memory in bytes
memory_used: uint64 # used GPU memory in bytes
temp_c: float # GPU temperature in Celsius
}
]?
- request_type: disks
format: JSON
schema:
[
{
name: string # disk name
total: uint64 # total space in bytes
available: uint64 # available space in bytes
}
]
- request_type: processes
format: Protocol Buffers (optionally gzip-compressed)
schema: See protobuf definition below
```
### Protobuf Schema (processes.proto)
```protobuf
syntax = "proto3";
message Process {
uint32 pid = 1;
string name = 2;
float cpu_usage = 3;
uint64 mem_bytes = 4;
}
message ProcessList {
uint32 process_count = 1;
repeated Process processes = 2;
}
```
### Step-by-Step Integration Pseudocode
```
1. Establish WebSocket connection to ws://HOST:PORT/ws
- Add token if required: ws://HOST:PORT/ws?token=YOUR_TOKEN
2. For regular metrics updates:
- Send: {"type": "metrics"}
- Parse JSON response
- Extract CPU, memory, network info
3. For disk information:
- Send: {"type": "disks"}
- Parse JSON response
- Extract disk usage data
4. For process list:
- Send: {"type": "processes"}
- Check if response is binary
- If starts with 0x1f, 0x8b bytes:
- Decompress using gzip
- Parse binary data using protobuf schema
- Extract process information
5. Implement reconnection logic:
- On connection close/error
- Use exponential backoff
6. Respect rate limits:
- metrics: ≥ 500ms interval
- disks: ≥ 5000ms interval
- processes: ≥ 2000ms interval
```
### Common Implementation Patterns
**Pattern 1: Periodic Polling**
```javascript
// Set up separate timers for different metric types
const metricsInterval = setInterval(() => ws.send(JSON.stringify({type: 'metrics'})), 500);
const disksInterval = setInterval(() => ws.send(JSON.stringify({type: 'disks'})), 5000);
const processesInterval = setInterval(() => ws.send(JSON.stringify({type: 'processes'})), 2000);
// Clean up on disconnect
ws.on('close', () => {
clearInterval(metricsInterval);
clearInterval(disksInterval);
clearInterval(processesInterval);
});
```
**Pattern 2: Processing Binary Protobuf Data**
```javascript
// Using protobufjs
const root = protobuf.loadSync('processes.proto');
const ProcessList = root.lookupType('ProcessList');
ws.on('message', function(data) {
if (typeof data !== 'string') {
// Check for gzip compression
if (data[0] === 0x1f && data[1] === 0x8b) {
data = gunzipSync(data); // Use appropriate decompression library
}
// Decode protobuf
const processes = ProcessList.decode(new Uint8Array(data));
console.log(`Total processes: ${processes.process_count}`);
processes.processes.forEach(p => {
console.log(`PID: ${p.pid}, Name: ${p.name}, CPU: ${p.cpu_usage}%`);
});
}
});
```
**Pattern 3: Reconnection Logic**
```javascript
function connect() {
const ws = new WebSocket('ws://localhost:3000/ws');
ws.on('open', () => {
console.log('Connected');
// Start polling
});
ws.on('close', () => {
console.log('Connection lost, reconnecting...');
setTimeout(connect, 1000); // Reconnect after 1 second
});
// Handle other events...
}
connect();
```
-2
View File
@@ -29,8 +29,6 @@ fn arg_value(name: &str) -> Option<String> {
None
}
// (tests moved to end of file to satisfy clippy::items_after_test_module)
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
+153 -59
View File
@@ -16,6 +16,7 @@ use std::time::{Duration, Instant};
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate};
use tracing::warn;
// NOTE: CPU normalization env removed; non-Linux now always reports per-process share (0..100) as given by sysinfo.
// Runtime toggles (read once)
fn gpu_enabled() -> bool {
static ON: OnceCell<bool> = OnceCell::new();
@@ -48,6 +49,15 @@ struct GpuCache {
}
static GPUC: OnceCell<Mutex<GpuCache>> = OnceCell::new();
// Static caches for unchanging data
static HOSTNAME: OnceCell<String> = OnceCell::new();
struct NetworkNameCache {
names: Vec<String>,
infos: Vec<NetworkInfo>,
}
static NETWORK_CACHE: OnceCell<Mutex<NetworkNameCache>> = OnceCell::new();
static CPU_VEC: OnceCell<Mutex<Vec<f32>>> = OnceCell::new();
fn cached_temp() -> Option<f32> {
if !temp_enabled() {
return None;
@@ -107,8 +117,8 @@ pub async fn collect_fast_metrics(state: &AppState) -> Metrics {
{
let cache = state.cache_metrics.lock().await;
if cache.is_fresh(ttl) {
if let Some(c) = cache.take_clone() {
return c;
if let Some(c) = cache.get() {
return c.clone();
}
}
}
@@ -120,9 +130,19 @@ pub async fn collect_fast_metrics(state: &AppState) -> Metrics {
warn!("sysinfo selective refresh panicked: {e:?}");
}
let hostname = state.hostname.clone();
// Get or initialize hostname once
let hostname = HOSTNAME.get_or_init(|| state.hostname.clone()).clone();
// Reuse CPU vector to avoid allocation
let cpu_total = sys.global_cpu_usage();
let cpu_per_core: Vec<f32> = sys.cpus().iter().map(|c| c.cpu_usage()).collect();
let cpu_per_core = {
let vec_lock = CPU_VEC.get_or_init(|| Mutex::new(Vec::with_capacity(32)));
let mut vec = vec_lock.lock().unwrap();
vec.clear();
vec.extend(sys.cpus().iter().map(|c| c.cpu_usage()));
vec.clone() // Still need to clone but the allocation is reused
};
let mem_total = sys.total_memory();
let mem_used = mem_total.saturating_sub(sys.available_memory());
let swap_total = sys.total_swap();
@@ -155,17 +175,38 @@ pub async fn collect_fast_metrics(state: &AppState) -> Metrics {
None
};
// Networks
let networks: Vec<NetworkInfo> = {
// Networks with reusable name cache
let networks = {
let mut nets = state.networks.lock().await;
nets.refresh(false);
nets.iter()
.map(|(name, data)| NetworkInfo {
name: name.to_string(),
// Get or initialize network cache
let cache = NETWORK_CACHE.get_or_init(|| {
Mutex::new(NetworkNameCache {
names: Vec::new(),
infos: Vec::with_capacity(4), // Most systems have few network interfaces
})
});
let mut cache = cache.lock().unwrap();
// Collect current network names
let current_names: Vec<_> = nets.keys().map(|name| name.to_string()).collect();
// Update cached network names if they changed
if cache.names != current_names {
cache.names = current_names;
}
// Reuse NetworkInfo objects
cache.infos.clear();
for (name, data) in nets.iter() {
cache.infos.push(NetworkInfo {
name: name.to_string(), // We'll still clone but avoid Vec reallocation
received: data.total_received(),
transmitted: data.total_transmitted(),
})
.collect()
});
}
cache.infos.clone()
};
// GPUs: if we already determined none exist, short-circuit (no repeated probing)
@@ -238,8 +279,8 @@ pub async fn collect_disks(state: &AppState) -> Vec<DiskInfo> {
{
let cache = state.cache_disks.lock().await;
if cache.is_fresh(ttl) {
if let Some(v) = cache.take_clone() {
return v;
if let Some(v) = cache.get() {
return v.clone();
}
}
}
@@ -301,13 +342,14 @@ pub async fn collect_processes_all(state: &AppState) -> ProcessesPayload {
let ttl_ms: u64 = std::env::var("SOCKTOP_AGENT_PROCESSES_TTL_MS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(1_000);
// Higher default (1500ms) on non-Linux only; keep 1500 here for Linux correctness (more frequent updates).
.unwrap_or(1_500);
let ttl = StdDuration::from_millis(ttl_ms);
{
let cache = state.cache_processes.lock().await;
if cache.is_fresh(ttl) {
if let Some(v) = cache.take_clone() {
return v;
if let Some(c) = cache.get() {
return c.clone();
}
}
}
@@ -399,59 +441,111 @@ pub async fn collect_processes_all(state: &AppState) -> ProcessesPayload {
payload
}
/// Collect all processes (non-Linux): use sysinfo's internal CPU% by doing a double refresh.
/// Collect all processes (non-Linux): optimized for reduced allocations and selective updates.
#[cfg(not(target_os = "linux"))]
pub async fn collect_processes_all(state: &AppState) -> ProcessesPayload {
use tokio::time::sleep;
let ttl_ms: u64 = std::env::var("SOCKTOP_AGENT_PROCESSES_TTL_MS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(1_000);
let ttl = StdDuration::from_millis(ttl_ms);
// Serve from cache if fresh
{
let cache = state.cache_processes.lock().await;
if cache.is_fresh(ttl) {
if let Some(v) = cache.take_clone() {
return v;
if cache.is_fresh(StdDuration::from_millis(2_000)) {
// Use fixed TTL for cache check
if let Some(c) = cache.get() {
return c.clone();
}
}
}
{
// Single efficient refresh with optimized CPU collection
let (total_count, procs) = {
let mut sys = state.sys.lock().await;
sys.refresh_processes_specifics(
ProcessesToUpdate::All,
false,
ProcessRefreshKind::everything().without_tasks(),
);
}
// Release lock during sleep interval
sleep(Duration::from_millis(250)).await;
{
let mut sys = state.sys.lock().await;
sys.refresh_processes_specifics(
ProcessesToUpdate::All,
false,
ProcessRefreshKind::everything().without_tasks(),
);
let kind = ProcessRefreshKind::nothing().with_memory();
// Optimize refresh strategy based on system load
//if load > 5.0 {
//JW too complicated. simplify to remove strange behavior
// For active systems, get accurate CPU metrics
sys.refresh_processes_specifics(ProcessesToUpdate::All, false, kind.with_cpu());
// } else {
// // For idle systems, just get basic process info
// sys.refresh_processes_specifics(ProcessesToUpdate::All, false, kind);
// sys.refresh_cpu_usage();
// }
let total_count = sys.processes().len();
let procs: Vec<ProcessInfo> = sys
.processes()
.values()
.map(|p| ProcessInfo {
pid: p.pid().as_u32(),
name: p.name().to_string_lossy().into_owned(),
cpu_usage: p.cpu_usage(),
let cpu_count = sys.cpus().len() as f32;
// Reuse allocations via process cache
let mut proc_cache = state.proc_cache.lock().await;
proc_cache.reusable_vec.clear();
// Collect all processes, will sort by CPU later
for p in sys.processes().values() {
let pid = p.pid().as_u32();
// Reuse cached name if available
let name = if let Some(cached) = proc_cache.names.get(&pid) {
cached.clone()
} else {
let new_name = p.name().to_string_lossy().into_owned();
proc_cache.names.insert(pid, new_name.clone());
new_name
};
// Convert to percentage of total CPU capacity
// e.g., 100% on 2 cores of 8 core system = 25% total CPU
let raw = p.cpu_usage(); // This is per-core percentage
let total_cpu = raw.clamp(0.0, 100.0) / cpu_count;
proc_cache.reusable_vec.push(ProcessInfo {
pid,
name,
cpu_usage: total_cpu,
mem_bytes: p.memory(),
})
.collect();
let payload = ProcessesPayload {
process_count: total_count,
top_processes: procs,
};
{
let mut cache = state.cache_processes.lock().await;
cache.set(payload.clone());
});
}
payload
//JW no need to sort here; client does the sorting
// // Sort by CPU usage
// proc_cache.reusable_vec.sort_by(|a, b| {
// b.cpu_usage
// .partial_cmp(&a.cpu_usage)
// .unwrap_or(std::cmp::Ordering::Equal)
// });
// Clean up old process names cache when it grows too large
let cache_cleanup_threshold = std::env::var("SOCKTOP_AGENT_NAME_CACHE_CLEANUP_THRESHOLD")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(1000); // Default: most modern systems have 400-700 processes
if total_count > proc_cache.names.len() + cache_cleanup_threshold {
let now = std::time::Instant::now();
proc_cache
.names
.retain(|pid, _| sys.processes().contains_key(&sysinfo::Pid::from_u32(*pid)));
tracing::debug!(
"Cleaned up {} stale process names in {}ms",
proc_cache.names.capacity() - proc_cache.names.len(),
now.elapsed().as_millis()
);
}
// Get all processes, take ownership of the vec (will be replaced with empty vec)
(total_count, std::mem::take(&mut proc_cache.reusable_vec))
};
let payload = ProcessesPayload {
process_count: total_count,
top_processes: procs,
};
{
let mut cache = state.cache_processes.lock().await;
cache.set(payload.clone());
}
payload
}
+24 -6
View File
@@ -1,6 +1,5 @@
//! Shared agent state: sysinfo handles and hot JSON cache.
#[cfg(target_os = "linux")]
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicUsize};
use std::sync::Arc;
@@ -20,6 +19,22 @@ pub struct ProcCpuTracker {
pub last_per_pid: HashMap<u32, u64>,
}
#[cfg(not(target_os = "linux"))]
pub struct ProcessCache {
pub names: HashMap<u32, String>,
pub reusable_vec: Vec<crate::types::ProcessInfo>,
}
#[cfg(not(target_os = "linux"))]
impl Default for ProcessCache {
fn default() -> Self {
Self {
names: HashMap::with_capacity(1000), // Pre-allocate for typical modern system process count
reusable_vec: Vec::with_capacity(1000),
}
}
}
#[derive(Clone)]
pub struct AppState {
pub sys: SharedSystem,
@@ -32,6 +47,10 @@ pub struct AppState {
#[cfg(target_os = "linux")]
pub proc_cpu: Arc<Mutex<ProcCpuTracker>>,
// Process name caching and vector reuse for non-Linux to reduce allocations
#[cfg(not(target_os = "linux"))]
pub proc_cache: Arc<Mutex<ProcessCache>>,
// Connection tracking (to allow future idle sleeps if desired)
pub client_count: Arc<AtomicUsize>,
@@ -66,11 +85,8 @@ impl<T> CacheEntry<T> {
self.value = Some(v);
self.at = Some(Instant::now());
}
pub fn take_clone(&self) -> Option<T>
where
T: Clone,
{
self.value.clone()
pub fn get(&self) -> Option<&T> {
self.value.as_ref()
}
}
@@ -89,6 +105,8 @@ impl AppState {
hostname: System::host_name().unwrap_or_else(|| "unknown".into()),
#[cfg(target_os = "linux")]
proc_cpu: Arc::new(Mutex::new(ProcCpuTracker::default())),
#[cfg(not(target_os = "linux"))]
proc_cache: Arc::new(Mutex::new(ProcessCache::default())),
client_count: Arc::new(AtomicUsize::new(0)),
auth_token: std::env::var("SOCKTOP_TOKEN")
.ok()
+127 -15
View File
@@ -7,13 +7,33 @@ use axum::{
};
use flate2::{write::GzEncoder, Compression};
use futures_util::StreamExt;
use once_cell::sync::OnceCell;
use std::collections::HashMap;
use std::io::Write;
use tokio::sync::Mutex;
use crate::metrics::{collect_disks, collect_fast_metrics, collect_processes_all};
use crate::proto::pb;
use crate::state::AppState;
// Compression threshold based on typical payload size
const COMPRESSION_THRESHOLD: usize = 768;
// Reusable buffer for compression to avoid allocations
struct CompressionCache {
processes_vec: Vec<pb::Process>,
}
impl CompressionCache {
fn new() -> Self {
Self {
processes_vec: Vec::with_capacity(512), // Typical process count
}
}
}
static COMPRESSION_CACHE: OnceCell<Mutex<CompressionCache>> = OnceCell::new();
pub async fn ws_handler(
ws: WebSocketUpgrade,
State(state): State<AppState>,
@@ -46,38 +66,50 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) {
}
Message::Text(ref text) if text == "get_processes" => {
let payload = collect_processes_all(&state).await;
// Map to protobuf message
let rows: Vec<pb::Process> = payload
.top_processes
.into_iter()
.map(|p| pb::Process {
// Get cached buffers
let cache = COMPRESSION_CACHE.get_or_init(|| Mutex::new(CompressionCache::new()));
let mut cache = cache.lock().await;
// Reuse process vector to build the list
cache.processes_vec.clear();
cache
.processes_vec
.extend(payload.top_processes.into_iter().map(|p| pb::Process {
pid: p.pid,
name: p.name,
cpu_usage: p.cpu_usage,
mem_bytes: p.mem_bytes,
})
.collect();
}));
let pb = pb::Processes {
process_count: payload.process_count as u64,
rows,
rows: std::mem::take(&mut cache.processes_vec),
};
let mut buf = Vec::with_capacity(8 * 1024);
if prost::Message::encode(&pb, &mut buf).is_err() {
let _ = socket.send(Message::Close(None)).await;
} else {
// compress if large
if buf.len() <= 768 {
if buf.len() <= COMPRESSION_THRESHOLD {
let _ = socket.send(Message::Binary(buf)).await;
} else {
let mut enc = GzEncoder::new(Vec::new(), Compression::fast());
if enc.write_all(&buf).is_ok() {
let bin = enc.finish().unwrap_or(buf);
let _ = socket.send(Message::Binary(bin)).await;
} else {
let _ = socket.send(Message::Binary(buf)).await;
// Create a new encoder for each message to ensure proper gzip headers
let mut encoder =
GzEncoder::new(Vec::with_capacity(buf.len()), Compression::fast());
match encoder.write_all(&buf).and_then(|_| encoder.finish()) {
Ok(compressed) => {
let _ = socket.send(Message::Binary(compressed)).await;
}
Err(_) => {
let _ = socket.send(Message::Binary(buf)).await;
}
}
}
}
drop(cache); // Explicit drop to release mutex early
}
Message::Close(_) => break,
_ => {}
@@ -91,7 +123,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) {
// Small, cheap gzip for larger payloads; send text for small.
async fn send_json<T: serde::Serialize>(ws: &mut WebSocket, value: &T) -> Result<(), axum::Error> {
let json = serde_json::to_string(value).expect("serialize");
if json.len() <= 768 {
if json.len() <= COMPRESSION_THRESHOLD {
return ws.send(Message::Text(json)).await;
}
let mut enc = GzEncoder::new(Vec::new(), Compression::fast());
@@ -99,3 +131,83 @@ async fn send_json<T: serde::Serialize>(ws: &mut WebSocket, value: &T) -> Result
let bin = enc.finish().unwrap_or_else(|_| json.into_bytes());
ws.send(Message::Binary(bin)).await
}
#[cfg(test)]
mod tests {
use super::*;
use prost::Message as ProstMessage;
use sysinfo::System;
#[tokio::test]
async fn test_process_list_not_empty() {
// Initialize system data first to ensure we have processes
let mut sys = System::new_all();
sys.refresh_all();
// Create state and put the refreshed system in it
let state = AppState::new();
{
let mut sys_lock = state.sys.lock().await;
*sys_lock = sys;
}
// Get processes directly using the collection function
let processes = collect_processes_all(&state).await;
// Convert to protobuf message format
let cache = COMPRESSION_CACHE.get_or_init(|| Mutex::new(CompressionCache::new()));
let mut cache = cache.lock().await;
// Reuse process vector to build the list
cache.processes_vec.clear();
cache
.processes_vec
.extend(processes.top_processes.into_iter().map(|p| pb::Process {
pid: p.pid,
name: p.name,
cpu_usage: p.cpu_usage,
mem_bytes: p.mem_bytes,
}));
// Create the protobuf message
let pb = pb::Processes {
process_count: processes.process_count as u64,
rows: cache.processes_vec.clone(),
};
// Test protobuf encoding/decoding
let mut buf = Vec::new();
prost::Message::encode(&pb, &mut buf).expect("Failed to encode protobuf");
let decoded = pb::Processes::decode(buf.as_slice()).expect("Failed to decode protobuf");
// Print debug info
println!("Process count: {}", pb.process_count);
println!("Process vector length: {}", pb.rows.len());
println!("Encoded size: {} bytes", buf.len());
println!("Decoded process count: {}", decoded.rows.len());
// Print first few processes if available
for (i, process) in pb.rows.iter().take(5).enumerate() {
println!(
"Process {}: {} (PID: {}) CPU: {:.1}% MEM: {} bytes",
i + 1,
process.name,
process.pid,
process.cpu_usage,
process.mem_bytes
);
}
// Validate
assert!(!pb.rows.is_empty(), "Process list should not be empty");
assert!(
pb.process_count > 0,
"Process count should be greater than 0"
);
assert_eq!(
pb.process_count as usize,
pb.rows.len(),
"Process count mismatch with actual rows"
);
}
}