Compare commits
5 Commits
master
..
2bb515742e
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bb515742e | |||
| 9af62a258b | |||
| bba1e8cf5b | |||
| 98594b7184 | |||
| 4188bcd334 |
@@ -18,15 +18,7 @@ env:
|
||||
jobs:
|
||||
build-deb:
|
||||
name: Build .deb for ${{ matrix.target }}
|
||||
# PINNED, not ubuntu-latest: the binaries link against this runner's
|
||||
# (multiarch) glibc, so the runner sets the MINIMUM glibc the .debs demand
|
||||
# at install time. ubuntu-latest moved to 24.04/glibc 2.39 and the packages
|
||||
# stopped installing on Debian 12/RPi OS bookworm (glibc 2.36). 22.04 links
|
||||
# 2.35, which bookworm satisfies. The "enforce glibc floor" step below
|
||||
# turns any future violation into a red build instead of a fleet-wide apt
|
||||
# failure — if this pin ever has to move past bookworm's glibc, that step
|
||||
# is the contract to renegotiate first.
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -167,22 +159,6 @@ jobs:
|
||||
mkdir -p debs
|
||||
cp target/${{ matrix.target }}/debian/*.deb debs/
|
||||
|
||||
- name: Enforce glibc floor (Debian 12 / RPi OS bookworm fleet)
|
||||
run: |
|
||||
# The fleet's oldest supported glibc. A .deb that demands newer libc6
|
||||
# than this will not install on the Pis — fail HERE, not at apt time.
|
||||
FLOOR="2.36"
|
||||
fail=0
|
||||
for deb in debs/*.deb; do
|
||||
req=$(dpkg-deb -f "$deb" Depends | sed -n 's/.*libc6 (>= \([0-9.]*\)).*/\1/p' | head -1)
|
||||
echo "$deb -> libc6 >= ${req:-none}"
|
||||
if [ -n "$req" ] && [ "$(printf '%s\n' "$req" "$FLOOR" | sort -V | tail -1)" != "$FLOOR" ]; then
|
||||
echo "::error::$deb requires libc6 >= $req, exceeding the fleet floor $FLOOR (bookworm). The build runner's glibc is too new — see the runs-on pin comment."
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
exit $fail
|
||||
|
||||
- name: List generated packages
|
||||
run: ls -lh debs/
|
||||
|
||||
|
||||
+2
-20
@@ -1,26 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
## 1.60.0 — unreleased
|
||||
|
||||
### TUI
|
||||
|
||||
- **`--no-kill` flag and `SOCKTOP_NO_KILL` env var** disable the local
|
||||
process-kill feature regardless of agent locality, for shared terminals and
|
||||
public demos (e.g. the socktop.io webterm). Either one forces the feature
|
||||
off and suppresses the `t` kill hints; the env var covers every socktop
|
||||
invocation under a deployment without touching command lines. `App`'s
|
||||
builder renamed `with_local` → `with_kill_enabled` to match what it now
|
||||
means (locality fact AND policy).
|
||||
|
||||
## 1.60.1 — unreleased
|
||||
|
||||
Identical to 1.60.0 plus rebuilt Debian packages: the 1.60.0 debs were linked
|
||||
against glibc 2.39 (a GitHub runner migration) and would not install on
|
||||
Debian 12 / Raspberry Pi OS bookworm. CI now pins the build environment and
|
||||
gates every package against the fleet's glibc floor. 1.60.0 was never
|
||||
published to crates.io.
|
||||
|
||||
Everything since `v1.50.0`. Applies to all three crates (`socktop`, `socktop_agent`, `socktop_connector`), which move to 1.60.1 together.
|
||||
Everything since `v1.50.0`. Applies to all three crates (`socktop`, `socktop_agent`, `socktop_connector`), which move to 1.60.0 together.
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -2412,7 +2412,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socktop"
|
||||
version = "1.60.2"
|
||||
version = "1.60.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
@@ -2432,7 +2432,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socktop_agent"
|
||||
version = "1.60.2"
|
||||
version = "1.60.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
@@ -2464,7 +2464,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socktop_connector"
|
||||
version = "1.60.2"
|
||||
version = "1.60.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"futures-util",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Witty One Off
|
||||
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
|
||||
|
||||
@@ -1,36 +1,548 @@
|
||||
# socktop
|
||||
|
||||
_socktop_ is a remote system monitor with a rich TUI, talking to an ultra lightweight agent over WebSockets.
|
||||
socktop is a remote system monitor with a rich TUI, inspired by top/btop, talking to a lightweight agent over WebSockets.
|
||||
|
||||
<img src="./docs/socktop_demo_1_60.apng" width="100%">
|
||||
- Linux agent: near-zero CPU when idle (request-driven, no always-on sampler)
|
||||
- TUI: smooth graphs, sortable process table, scrollbars, readable colors
|
||||
|
||||
## Resources
|
||||
[socktop.io](https://www.socktop.io)
|
||||
|
||||
| Resource | Location |
|
||||
| -------- | -------- |
|
||||
| Website and online demo (yes it's real) | [socktop.io](https://www.socktop.io) |
|
||||
| Quick Start guide | [https://socktop.io/assets/docs/installation/quick-start.html](https://socktop.io/assets/docs/installation/quick-start.html) |
|
||||
| Prereqs | [https://socktop.io/assets/docs/installation/prerequisites.html](https://socktop.io/assets/docs/installation/prerequisites.html) |
|
||||
| APT Install | [https://socktop.io/assets/docs/installation/apt.html](https://socktop.io/assets/docs/installation/apt.html) |
|
||||
| Cargo Install | [https://socktop.io/assets/docs/installation/cargo.html](https://socktop.io/assets/docs/installation/cargo.html)
|
||||
| Usage | [https://socktop.io/assets/docs/usage/general.html](https://socktop.io/assets/docs/usage/general.html)
|
||||
| Auth Setup | [https://socktop.io/assets/docs/security/token.html](https://socktop.io/assets/docs/security/token.html) |
|
||||
| TLS Setup | [https://socktop.io/assets/docs/security/tls.html](https://socktop.io/assets/docs/security/tls.html) |
|
||||
| Monitoring Multiple Hosts | [tmux](https://socktop.io/assets/docs/advanced/tmux.html) / [zellij](https://socktop.io/assets/docs/advanced/zellij.html) |
|
||||
<img src="./docs/socktop_demo.apng" width="100%">
|
||||
|
||||
---
|
||||
|
||||
## Platform Support
|
||||
## Features
|
||||
|
||||
Linux (all flavors), ARM/Raspberry Pi (32b/64b), MacOS, Windows, RISC-V (experimental)
|
||||
- Remote monitoring via WebSocket (JSON over WS)
|
||||
- Optional WSS (TLS): agent auto‑generates a self‑signed cert on first run; client pins the cert via --tls-ca/-t
|
||||
- TUI built with ratatui
|
||||
- CPU
|
||||
- Overall sparkline + per-core mini bars
|
||||
- Accurate per-process CPU% (Linux /proc deltas), normalized to 0–100%
|
||||
- Memory/Swap gauges with human units
|
||||
- Disks: per-device usage
|
||||
- Network: per-interface throughput with sparklines and peak markers
|
||||
- Temperatures: CPU (optional)
|
||||
- Top processes (top 50)
|
||||
- PID, name, CPU%, memory, and memory%
|
||||
- Click-to-sort by CPU% or Mem (descending)
|
||||
- Scrollbar and mouse/keyboard scrolling
|
||||
- Total process count shown in the header
|
||||
- Only top-level processes listed (threads hidden) — matches btop/top
|
||||
- Optional GPU metrics (can be disabled)
|
||||
- Optional auth token for the agent
|
||||
- Compact layout for small windows: automatically drops the panes that no longer fit so
|
||||
the CPU graph and per-core bars stay visible (see [Compact mode](#compact-mode))
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
## Prerequisites: Install Rust (rustup)
|
||||
|
||||
Contributions are welcome and you have the freedom to use whatever development tools you would like, as long as there is a human in the loop and all the clippy and unit tests pass you are good to submit a PR. Defects / Bugs just go ahead and fix and file a PR. New features, please create a issue in advance and let me know you are offering to build it. I don't want to be in a position where you worked for a couple of weeks on something and I don't want to merge it.
|
||||
Rust is fast, safe, and cross‑platform. Installing it will make your machine better. Consider yourself privileged.
|
||||
|
||||
### Development
|
||||
Linux/macOS:
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
# load cargo for this shell
|
||||
source "$HOME/.cargo/env"
|
||||
# ensure stable is up to date
|
||||
rustup update stable
|
||||
rustc --version
|
||||
cargo --version
|
||||
# after install you may need to reload your shell, e.g.:
|
||||
exec bash # or: exec zsh / exec fish
|
||||
```
|
||||
|
||||
Windows (for the brave): install from https://rustup.rs with the MSVC toolchain. Yes, you’ll need Visual Studio Build Tools. You chose Windows — enjoy the ride.
|
||||
|
||||
### Raspberry Pi / Ubuntu / PopOS (required for GPU support)
|
||||
|
||||
**Note:** GPU monitoring is only supported on x86_64 and aarch64 (64-bit ARM) platforms. ARMv7 (32-bit) and RISC-V builds do not include GPU support.
|
||||
|
||||
For 64-bit systems with GPU support:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install libdrm-dev libdrm-amdgpu1
|
||||
```
|
||||
|
||||
For ARMv7 (32-bit Raspberry Pi), build with `--no-default-features` to disable GPU support:
|
||||
|
||||
```bash
|
||||
cargo build --release -p socktop_agent --no-default-features
|
||||
```
|
||||
|
||||
_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
|
||||
|
||||
Two components:
|
||||
|
||||
1) Agent (remote): small Rust WS server using sysinfo + /proc. It collects metrics only when the client requests them over the WebSocket (request-driven). No background sampling loop.
|
||||
|
||||
2) Client (local): TUI that connects to ws://HOST:PORT/ws (or wss://HOST:PORT/ws when TLS is enabled) and renders updates.
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
- Build both binaries:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/jasonwitty/socktop.git
|
||||
cd socktop
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
- Start the agent on the target machine (default port 3000):
|
||||
|
||||
```bash
|
||||
./target/release/socktop_agent --port 3000
|
||||
```
|
||||
|
||||
- Connect with the TUI from your local machine:
|
||||
|
||||
```bash
|
||||
./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:
|
||||
|
||||
```bash
|
||||
socktop --demo
|
||||
```
|
||||
|
||||
Or just run `socktop` with no arguments and pick the built‑in `demo` entry from the interactive profile list (if you have saved profiles, `demo` is appended). The demo agent:
|
||||
|
||||
- Runs locally (`ws://127.0.0.1:3231/ws`)
|
||||
- Stops automatically (you'll see "Stopped demo agent on port 3231") when you quit the TUI or press Ctrl-C
|
||||
|
||||
---
|
||||
|
||||
## Install (from crates.io)
|
||||
|
||||
You don’t need to clone this repo to use socktop. Install the published binaries with cargo:
|
||||
|
||||
```bash
|
||||
# TUI (client)
|
||||
cargo install socktop
|
||||
# Agent (server)
|
||||
cargo install socktop_agent
|
||||
```
|
||||
|
||||
This drops socktop and socktop_agent into ~/.cargo/bin (add it to PATH).
|
||||
|
||||
Notes:
|
||||
- After installing Rust via rustup, reload your shell (e.g., exec bash) so cargo is on PATH.
|
||||
- Windows: you can also grab prebuilt EXEs from GitHub Actions artifacts if rustup scares you. It shouldn’t. Be brave.
|
||||
|
||||
System-wide agent (Linux)
|
||||
|
||||
```bash
|
||||
# If you installed with cargo, binaries are in ~/.cargo/bin
|
||||
sudo install -o root -g root -m 0755 "$HOME/.cargo/bin/socktop_agent" /usr/local/bin/socktop_agent
|
||||
|
||||
# Install and enable the systemd service (example unit in docs/)
|
||||
sudo install -o root -g root -m 0644 docs/socktop-agent.service /etc/systemd/system/socktop-agent.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now socktop-agent
|
||||
```
|
||||
|
||||
```bash
|
||||
|
||||
# Enable SSL
|
||||
|
||||
# Stop service
|
||||
sudo systemctl stop socktop-agent
|
||||
|
||||
# Edit service to append SSL option and port
|
||||
sudo micro /etc/systemd/system/socktop-agent.service
|
||||
|
||||
--
|
||||
ExecStart=/usr/local/bin/socktop_agent --enableSSL --port 8443
|
||||
--
|
||||
|
||||
# Reload
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Restart
|
||||
sudo systemctl start socktop-agent
|
||||
|
||||
# check logs for certificate location
|
||||
sudo journalctl -u socktop-agent -f
|
||||
|
||||
--
|
||||
Aug 22 22:25:26 rpi-master socktop_agent[2913998]: socktop_agent: generated self-signed TLS certificate at /var/lib/socktop/.config/socktop_agent/tls/cert.pem
|
||||
--
|
||||
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
Agent (server):
|
||||
|
||||
```bash
|
||||
socktop_agent --port 3000
|
||||
# or env: SOCKTOP_PORT=3000 socktop_agent
|
||||
# optional auth: SOCKTOP_TOKEN=changeme socktop_agent
|
||||
# enable TLS (self‑signed cert, default port 8443; you can also use -p):
|
||||
socktop_agent --enableSSL --port 8443
|
||||
```
|
||||
|
||||
Client (TUI):
|
||||
|
||||
```bash
|
||||
socktop ws://HOST:3000/ws
|
||||
# with token:
|
||||
socktop "ws://HOST:3000/ws?token=changeme"
|
||||
# TLS with pinned server certificate (recommended over the internet):
|
||||
socktop --tls-ca /path/to/cert.pem wss://HOST:8443/ws
|
||||
# (By default hostname/SAN verification is skipped for ease on home networks. To enforce it add --verify-hostname)
|
||||
socktop --verify-hostname --tls-ca /path/to/cert.pem wss://HOST:8443/ws
|
||||
# shorthand:
|
||||
socktop -t /path/to/cert.pem wss://HOST:8443/ws
|
||||
# Note: providing --tls-ca/-t automatically upgrades ws:// to wss:// if you forget
|
||||
# force the small-window layout at any terminal size (normally automatic):
|
||||
socktop --compact ws://HOST:3000/ws
|
||||
```
|
||||
|
||||
Intervals (client-driven):
|
||||
- Fast metrics: ~500 ms
|
||||
- Processes: ~2 s
|
||||
- Disks: ~5 s
|
||||
|
||||
The agent stays idle unless queried. When queried, it collects just what’s needed.
|
||||
|
||||
---
|
||||
|
||||
## Compact mode
|
||||
|
||||
In a short terminal the fixed layout runs out of rows and the CPU graph and per-core bars
|
||||
are the first things to collapse — exactly the panes you are most likely watching. Once
|
||||
the window is too short for the Disks pane to show even one disk, socktop switches to a
|
||||
compact layout:
|
||||
|
||||
- **Disks is dropped.** It is the pane that degrades worst when partially drawn.
|
||||
- **Memory and Swap move side by side** into the row Disks vacated.
|
||||
- **GPU shrinks to a single line** — utilisation and VRAM only, no device name. On a host
|
||||
with no GPU the pane disappears entirely.
|
||||
- **Everything reclaimed goes to the CPU graph and per-core bars**, which stay usable well
|
||||
below the size where they used to vanish.
|
||||
|
||||
The switch is automatic and needs no configuration. Pass `--compact` to pin the compact
|
||||
layout at any window size:
|
||||
|
||||
```bash
|
||||
socktop --compact ws://HOST:3000/ws
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Connection Profiles (Named)
|
||||
|
||||
You can save frequently used connection settings (URL + optional TLS CA path) under a short name and reuse them later.
|
||||
|
||||
Config file location:
|
||||
|
||||
- Linux (XDG): `$XDG_CONFIG_HOME/socktop/profiles.json`
|
||||
- Fallback (when XDG not set): `~/.config/socktop/profiles.json`
|
||||
|
||||
### Creating a profile
|
||||
|
||||
First time you specify a new `--profile/-P` name together with a URL (and optional `--tls-ca`), it is saved automatically:
|
||||
|
||||
```bash
|
||||
socktop --profile prod ws://prod-host:3000/ws
|
||||
# With TLS pinning:
|
||||
socktop --profile prod-tls --tls-ca /path/to/cert.pem wss://prod-host:8443/ws
|
||||
|
||||
You can also set custom intervals (milliseconds):
|
||||
|
||||
```bash
|
||||
socktop --profile prod --metrics-interval-ms 750 --processes-interval-ms 3000 ws://prod-host:3000/ws
|
||||
```
|
||||
```
|
||||
|
||||
If a profile already exists you will be prompted before overwriting:
|
||||
|
||||
```
|
||||
$ socktop --profile prod ws://new-host:3000/ws
|
||||
Overwrite existing profile 'prod'? [y/N]: y
|
||||
```
|
||||
|
||||
To overwrite without an interactive prompt pass `--save`:
|
||||
|
||||
```bash
|
||||
socktop --profile prod --save ws://new-host:3000/ws
|
||||
```
|
||||
|
||||
### Using a saved profile
|
||||
|
||||
Just pass the profile name (no URL needed):
|
||||
|
||||
```bash
|
||||
socktop --profile prod
|
||||
socktop -P prod-tls # short flag
|
||||
```
|
||||
|
||||
The stored URL (and TLS CA path, if any) plus any saved intervals will be used. TLS auto-upgrade still applies if a CA path is stored alongside a ws:// URL.
|
||||
|
||||
### Interactive selection (no args)
|
||||
|
||||
If you run `socktop` with no arguments and at least one profile exists, you will be shown a numbered list to pick from:
|
||||
|
||||
```
|
||||
$ socktop
|
||||
Select profile:
|
||||
1. prod
|
||||
2. prod-tls
|
||||
Enter number (or blank to abort): 2
|
||||
```
|
||||
|
||||
Choosing a number starts the TUI with that profile. A built‑in `demo` option is always appended; selecting it launches a local agent on port 3231 (no TLS) and connects to `ws://127.0.0.1:3231/ws`. Pressing Enter on blank aborts without connecting.
|
||||
|
||||
### JSON format
|
||||
|
||||
An example `profiles.json` (pretty‑printed):
|
||||
|
||||
```json
|
||||
{
|
||||
"profiles": {
|
||||
"prod": { "url": "ws://prod-host:3000/ws" },
|
||||
"prod-tls": {
|
||||
"url": "wss://prod-host:8443/ws",
|
||||
"tls_ca": "/home/user/certs/prod-cert.pem",
|
||||
"metrics_interval_ms": 500,
|
||||
"processes_interval_ms": 2000
|
||||
}
|
||||
},
|
||||
"version": 0
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- The `tls_ca` path is stored as given; if you move or rotate the certificate update the profile by re-running with `--profile NAME --save`.
|
||||
- Deleting a profile: edit the JSON file and remove the entry (TUI does not yet have an in-app delete command).
|
||||
- Profiles are client-side convenience only; they do not affect the agent.
|
||||
- Intervals: `metrics_interval_ms` controls the fast metrics poll (default 500 ms). `processes_interval_ms` controls process list polling (default 2000 ms). Values below 100 ms (metrics) or 200 ms (processes) are clamped.
|
||||
|
||||
---
|
||||
|
||||
## Updating
|
||||
|
||||
Update the agent (systemd):
|
||||
|
||||
```bash
|
||||
# on the server running the agent
|
||||
cargo install socktop_agent --force
|
||||
sudo systemctl stop socktop-agent
|
||||
sudo install -o root -g root -m 0755 "$HOME/.cargo/bin/socktop_agent" /usr/local/bin/socktop_agent
|
||||
# if you changed the unit file:
|
||||
# sudo install -o root -g root -m 0644 docs/socktop-agent.service /etc/systemd/system/socktop-agent.service
|
||||
# sudo systemctl daemon-reload
|
||||
sudo systemctl start socktop-agent
|
||||
sudo systemctl status socktop-agent --no-pager
|
||||
# logs:
|
||||
# journalctl -u socktop-agent -f
|
||||
```
|
||||
|
||||
Update the TUI (client):
|
||||
```bash
|
||||
cargo install socktop --force
|
||||
socktop ws://HOST:3000/ws
|
||||
```
|
||||
|
||||
Tip: If only the binary changed, restart is enough. If the unit file changed, run sudo systemctl daemon-reload.
|
||||
|
||||
---
|
||||
|
||||
## Configuration (agent)
|
||||
|
||||
- Port:
|
||||
- Flag: --port 8080 or -p 8080
|
||||
- Positional: socktop_agent 8080
|
||||
- Env: SOCKTOP_PORT=8080
|
||||
- TLS (self‑signed):
|
||||
- Enable: --enableSSL
|
||||
- Default TLS port: 8443 (override with --port/-p)
|
||||
- Certificate/Key location (created on first TLS run):
|
||||
- Linux (XDG): $XDG_CONFIG_HOME/socktop_agent/tls/{cert.pem,key.pem} (defaults to ~/.config)
|
||||
- The agent prints these paths on creation.
|
||||
- You can set XDG_CONFIG_HOME before first run to control where certs are written.
|
||||
- Additional SANs: set `SOCKTOP_AGENT_EXTRA_SANS` (comma‑separated) before first TLS start to include extra IPs/DNS names in the cert. Example:
|
||||
```bash
|
||||
SOCKTOP_AGENT_EXTRA_SANS="192.168.1.101,myhost.internal" socktop_agent --enableSSL
|
||||
```
|
||||
This prevents client errors like `NotValidForName` when connecting via an IP not present in the default cert SAN list.
|
||||
- Expiry / rotation: the generated cert is valid for ~397 days from creation. If the agent fails to start with an "ExpiredCertificate" error (or your client reports expiry), simply delete the existing cert and key:
|
||||
```bash
|
||||
rm ~/.config/socktop_agent/tls/cert.pem ~/.config/socktop_agent/tls/key.pem
|
||||
# (adjust path if XDG_CONFIG_HOME is set or different user)
|
||||
systemctl restart socktop-agent # if running under systemd
|
||||
```
|
||||
On next TLS start the agent will generate a fresh pair. Only distribute the new cert.pem to clients (never the key).
|
||||
- Auth token (optional): SOCKTOP_TOKEN=changeme
|
||||
- Disable GPU metrics: SOCKTOP_AGENT_GPU=0
|
||||
- Disable CPU temperature: SOCKTOP_AGENT_TEMP=0
|
||||
|
||||
---
|
||||
|
||||
## Keyboard & Mouse
|
||||
|
||||
- Quit: q or Esc
|
||||
- Processes pane:
|
||||
- Click “CPU %” to sort by CPU descending
|
||||
- Click “Mem” to sort by memory descending
|
||||
- Mouse wheel: scroll
|
||||
- Drag scrollbar: scroll
|
||||
- Arrow/PageUp/PageDown/Home/End: scroll
|
||||
|
||||
---
|
||||
|
||||
## Example agent JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"sampled_at_ms": 1786752000123,
|
||||
"cpu_total": 12.4,
|
||||
"cpu_per_core": [11.2, 15.7],
|
||||
"mem_total": 33554432,
|
||||
"mem_used": 18321408,
|
||||
"swap_total": 0,
|
||||
"swap_used": 0,
|
||||
"process_count": 127,
|
||||
"hostname": "myserver",
|
||||
"cpu_temp_c": 42.5,
|
||||
"disks": [{"name":"nvme0n1p2","total":512000000000,"available":320000000000}],
|
||||
"networks": [{"name":"eth0","received":12345678,"transmitted":87654321}],
|
||||
"top_processes": [
|
||||
{"pid":1234,"name":"nginx","cpu_usage":1.2,"mem_bytes":12345678}
|
||||
],
|
||||
"gpus": null
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- process_count is merged into the main metrics on the client when processes are polled.
|
||||
- top_processes are the current top 50 (sorting in the TUI is client-side).
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
Set a token on the agent and pass it as a query param from the client:
|
||||
|
||||
Server:
|
||||
|
||||
```bash
|
||||
SOCKTOP_TOKEN=changeme socktop_agent --port 3000
|
||||
```
|
||||
|
||||
Client:
|
||||
|
||||
```bash
|
||||
socktop "ws://HOST:3000/ws?token=changeme"
|
||||
```
|
||||
|
||||
### TLS / WSS
|
||||
|
||||
For encrypted connections, enable TLS on the agent and pin the server certificate on the client.
|
||||
|
||||
Server (generates self‑signed cert and key on first run):
|
||||
|
||||
```bash
|
||||
socktop_agent --enableSSL --port 8443
|
||||
```
|
||||
|
||||
Client (trust/pin the server cert; copy cert.pem from the agent):
|
||||
|
||||
```bash
|
||||
socktop --tls-ca /path/to/agent/cert.pem wss://HOST:8443/ws
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Do not copy the private key off the server; only the cert.pem is needed by clients.
|
||||
- When --tls-ca/-t is supplied, the client auto‑upgrades ws:// to wss:// to avoid protocol mismatch.
|
||||
- Hostname (SAN) verification is DISABLED by default; instead the client PINS the certificate: the agent must present a cert byte-identical to one in your `--tls-ca` file (expiry is ignored in this mode — you pinned that exact cert). Use `--verify-hostname` to switch to strict chain + SAN validation instead.
|
||||
- You can run multiple clients with different cert paths by passing --tls-ca per invocation.
|
||||
|
||||
---
|
||||
|
||||
## Using tmux to monitor multiple hosts
|
||||
|
||||
You can use tmux to show multiple socktop instances in a single terminal.
|
||||
|
||||

|
||||
monitoring 4 Raspberry Pis using Tmux
|
||||
|
||||
Prerequisites:
|
||||
- Install tmux (Ubuntu/Debian: `sudo apt-get install tmux`)
|
||||
|
||||
Key bindings (defaults):
|
||||
- Split left/right: Ctrl-b %
|
||||
- Split top/bottom: Ctrl-b "
|
||||
- Move between panes: Ctrl-b + Arrow keys
|
||||
- Show pane numbers: Ctrl-b q
|
||||
- Close a pane: Ctrl-b x
|
||||
- Detach from session: Ctrl-b d
|
||||
|
||||
Two panes (left/right)
|
||||
- This creates a session named "socktop", splits it horizontally, and starts two socktops.
|
||||
|
||||
```bash
|
||||
tmux new-session -d -s socktop 'socktop ws://HOST1:3000/ws' \; \
|
||||
split-window -h 'socktop ws://HOST2:3000/ws' \; \
|
||||
select-layout even-horizontal \; \
|
||||
attach
|
||||
```
|
||||
|
||||
Four panes (top-left, top-right, bottom-left, bottom-right)
|
||||
- This creates a 2x2 grid with one socktop per pane.
|
||||
|
||||
```bash
|
||||
tmux new-session -d -s socktop 'socktop ws://HOST1:3000/ws' \; \
|
||||
split-window -h 'socktop ws://HOST2:3000/ws' \; \
|
||||
select-pane -t 0 \; split-window -v 'socktop ws://HOST3:3000/ws' \; \
|
||||
select-pane -t 1 \; split-window -v 'socktop ws://HOST4:3000/ws' \; \
|
||||
select-layout tiled \; \
|
||||
attach
|
||||
```
|
||||
|
||||
Tips:
|
||||
- Replace HOST1..HOST4 (and ports) with your targets.
|
||||
- Reattach later: `tmux attach -t socktop`
|
||||
|
||||
---
|
||||
|
||||
## Platform notes
|
||||
|
||||
- Linux: fully supported (agent and client).
|
||||
- Raspberry Pi:
|
||||
- 64-bit: aarch64-unknown-linux-gnu
|
||||
- 32-bit: armv7-unknown-linux-gnueabihf
|
||||
- Windows:
|
||||
- TUI + agent can build with stable Rust; bring your own MSVC. You’re on Windows; you know the drill.
|
||||
- CPU temperature may be unavailable.
|
||||
- binary exe for both available in build artifacts under actions.
|
||||
- macOS:
|
||||
- TUI works; agent is primarily targeted at Linux. Agent will run just fine on macos for debugging but I have not documented how to run as a service, I may not given the "security" feautures with applications on macos. We will see.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cargo fmt
|
||||
@@ -45,11 +557,35 @@ cargo run -p socktop_agent -- --enableSSL --port 8443
|
||||
A sample pre-commit hook that runs `cargo fmt --all` is provided in `.githooks/pre-commit`.
|
||||
Enable it (one-time):
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
chmod +x .githooks/pre-commit
|
||||
```
|
||||
|
||||
Every commit will then format Rust sources and restage them automatically.
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] Agent authentication (token)
|
||||
- [x] Hide per-thread entries; only show processes
|
||||
- [x] Sort top processes in the TUI
|
||||
- [x] Configurable refresh intervals (client)
|
||||
- [ ] Export metrics to file
|
||||
- [x] TLS / WSS support (self‑signed 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
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
MIT — see LICENSE.
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 64 MiB |
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "socktop"
|
||||
version = "1.60.2"
|
||||
version = "1.60.0"
|
||||
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
|
||||
description = "Remote system monitor over WebSocket, TUI like top"
|
||||
edition = "2024"
|
||||
@@ -11,7 +11,7 @@ repository = "https://github.com/jasonwitty/socktop"
|
||||
|
||||
[dependencies]
|
||||
# socktop connector for agent communication
|
||||
socktop_connector = { version = "1.60.1", path = "../socktop_connector" }
|
||||
socktop_connector = { version = "1.60.0", path = "../socktop_connector" }
|
||||
|
||||
tokio = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
|
||||
+13
-15
@@ -194,10 +194,9 @@ pub struct App {
|
||||
// Security / status flags
|
||||
pub is_tls: bool,
|
||||
pub has_token: bool,
|
||||
// Whether the local process-kill feature (t = SIGTERM, k = SIGKILL) is
|
||||
// available: the connected agent is on this machine AND no policy override
|
||||
// (--no-kill / SOCKTOP_NO_KILL) has disabled it.
|
||||
pub kill_enabled: bool,
|
||||
// Whether the connected agent is on this machine. Gates the local
|
||||
// process-kill feature (t = SIGTERM, k = SIGKILL).
|
||||
pub is_local: bool,
|
||||
// Pending kill awaiting confirmation: (pid, process name). Which signal is
|
||||
// sent depends on the button chosen in the confirmation modal, so it isn't
|
||||
// decided until then.
|
||||
@@ -296,7 +295,7 @@ impl App {
|
||||
verify_hostname: false,
|
||||
is_tls: false,
|
||||
has_token: false,
|
||||
kill_enabled: false,
|
||||
is_local: false,
|
||||
pending_kill: None,
|
||||
force_compact: false,
|
||||
header_title: String::new(),
|
||||
@@ -350,10 +349,9 @@ impl App {
|
||||
}
|
||||
|
||||
/// Enable the local process-kill feature. Only set true when the agent has
|
||||
/// been verified to be on this machine (see [`crate::local`]) and no
|
||||
/// policy override (`--no-kill`, `SOCKTOP_NO_KILL`) forbids it.
|
||||
pub fn with_kill_enabled(mut self, kill_enabled: bool) -> Self {
|
||||
self.kill_enabled = kill_enabled;
|
||||
/// been verified to be on this machine (see [`crate::local`]).
|
||||
pub fn with_local(mut self, is_local: bool) -> Self {
|
||||
self.is_local = is_local;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -377,11 +375,11 @@ impl App {
|
||||
.map(|p| p.name.clone())
|
||||
}
|
||||
|
||||
/// Raise the kill confirmation for `pid`. No-op unless the kill feature is
|
||||
/// enabled — the same gate the keybinding uses, repeated here because this
|
||||
/// Raise the kill confirmation for `pid`. No-op unless the agent is on this
|
||||
/// machine — the same gate the keybinding uses, repeated here because this
|
||||
/// is also reachable from the details modal.
|
||||
fn prompt_kill(&mut self, pid: u32) {
|
||||
if !self.kill_enabled {
|
||||
if !self.is_local {
|
||||
return;
|
||||
}
|
||||
let name = self
|
||||
@@ -1184,7 +1182,7 @@ impl App {
|
||||
// the details modal so it could not be reused there, and one
|
||||
// key for both entry points is one thing to remember.
|
||||
// SIGTERM vs SIGKILL is chosen in the confirmation modal.
|
||||
if self.kill_enabled
|
||||
if self.is_local
|
||||
&& !self.modal_manager.is_active()
|
||||
&& matches!(k.code, KeyCode::Char('t') | KeyCode::Char('T'))
|
||||
&& let Some(pid) = self.selected_process_pid
|
||||
@@ -1950,7 +1948,7 @@ impl App {
|
||||
filtered_indices: &self.procs_filtered,
|
||||
cached_rows: &self.procs_row_cache,
|
||||
peak_cpu: self.procs_row_peak_cpu,
|
||||
kill_enabled: self.kill_enabled,
|
||||
is_local: self.is_local,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1971,7 +1969,7 @@ impl App {
|
||||
},
|
||||
max_mem_bytes: self.max_process_mem_bytes,
|
||||
unsupported: self.process_details_unsupported,
|
||||
kill_enabled: self.kill_enabled,
|
||||
is_local: self.is_local,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+11
-55
@@ -25,18 +25,6 @@ pub(crate) struct ParsedArgs {
|
||||
processes_interval_ms: Option<u64>,
|
||||
verify_hostname: bool,
|
||||
compact: bool,
|
||||
no_kill: bool,
|
||||
}
|
||||
|
||||
/// True when the `SOCKTOP_NO_KILL` environment variable disables the process-kill
|
||||
/// feature. Any value other than empty, `0`, or `false` (case-insensitive) counts
|
||||
/// as set, so a deployment can export `SOCKTOP_NO_KILL=1` once and every socktop
|
||||
/// launched under it — whatever its command line — has the feature off.
|
||||
pub(crate) fn no_kill_from_env() -> bool {
|
||||
match env::var("SOCKTOP_NO_KILL") {
|
||||
Ok(v) => !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false"),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, String> {
|
||||
@@ -52,12 +40,11 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
||||
let mut processes_interval_ms: Option<u64> = None;
|
||||
let mut verify_hostname = false;
|
||||
let mut compact = false;
|
||||
let mut no_kill = false;
|
||||
while let Some(arg) = it.next() {
|
||||
match arg.as_str() {
|
||||
"-h" | "--help" => {
|
||||
return Err(format!(
|
||||
"Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--verify-hostname] [--profile NAME|-P NAME] [--save] [--demo] [--compact] [--no-kill] [--metrics-interval-ms N] [--processes-interval-ms N] [ws://HOST:PORT/ws]\n"
|
||||
"Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--verify-hostname] [--profile NAME|-P NAME] [--save] [--demo] [--compact] [--metrics-interval-ms N] [--processes-interval-ms N] [ws://HOST:PORT/ws]\n"
|
||||
));
|
||||
}
|
||||
"--tls-ca" | "-t" => {
|
||||
@@ -83,12 +70,6 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
||||
// layout switches on its own once the window gets too short.
|
||||
compact = true;
|
||||
}
|
||||
"--no-kill" => {
|
||||
// Disable the local process-kill feature even when the agent is
|
||||
// local. For shared/kiosk deployments; SOCKTOP_NO_KILL=1 in the
|
||||
// environment does the same without touching the command line.
|
||||
no_kill = true;
|
||||
}
|
||||
"--dry-run" => {
|
||||
// intentionally undocumented
|
||||
dry_run = true;
|
||||
@@ -124,20 +105,11 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// An unrecognized option must never fall through to the
|
||||
// positional URL slot: an older binary handed a newer flag
|
||||
// would otherwise "connect" to the flag text — and offer to
|
||||
// save it over a named profile's URL.
|
||||
if arg.starts_with('-') {
|
||||
return Err(format!(
|
||||
"Unknown option '{arg}'. Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--verify-hostname] [--profile NAME|-P NAME] [--save] [--demo] [--compact] [--no-kill] [ws://HOST:PORT/ws]"
|
||||
));
|
||||
}
|
||||
if url.is_none() {
|
||||
url = Some(arg);
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Unexpected argument. Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--verify-hostname] [--profile NAME|-P NAME] [--save] [--demo] [--compact] [--no-kill] [ws://HOST:PORT/ws]"
|
||||
"Unexpected argument. Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--verify-hostname] [--profile NAME|-P NAME] [--save] [--demo] [--compact] [ws://HOST:PORT/ws]"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -154,7 +126,6 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
||||
processes_interval_ms,
|
||||
verify_hostname,
|
||||
compact,
|
||||
no_kill,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -164,11 +135,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(v) => v,
|
||||
Err(msg) => {
|
||||
eprintln!("{msg}");
|
||||
// --help produces the bare usage text and exits cleanly; real
|
||||
// parse errors must be visible to scripts and CI as a failure.
|
||||
if !msg.starts_with("Usage:") {
|
||||
std::process::exit(2);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -180,7 +146,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
if parsed.demo || matches!(parsed.profile.as_deref(), Some("demo")) {
|
||||
return run_demo_mode(parsed.tls_ca.as_deref(), parsed.compact, parsed.no_kill).await;
|
||||
return run_demo_mode(parsed.tls_ca.as_deref(), parsed.compact).await;
|
||||
}
|
||||
|
||||
let profiles_file = load_profiles();
|
||||
@@ -285,12 +251,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if (1..=names.len()).contains(&idx) {
|
||||
let name = &names[idx - 1];
|
||||
if name == "demo" {
|
||||
return run_demo_mode(
|
||||
parsed.tls_ca.as_deref(),
|
||||
parsed.compact,
|
||||
parsed.no_kill,
|
||||
)
|
||||
.await;
|
||||
return run_demo_mode(parsed.tls_ca.as_deref(), parsed.compact).await;
|
||||
}
|
||||
if let Some(entry) = profiles_mut.profiles.get(name) {
|
||||
(
|
||||
@@ -350,8 +311,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
);
|
||||
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]: ") {
|
||||
return run_demo_mode(parsed.tls_ca.as_deref(), parsed.compact, parsed.no_kill)
|
||||
.await;
|
||||
return run_demo_mode(parsed.tls_ca.as_deref(), parsed.compact).await;
|
||||
} else {
|
||||
eprintln!("Aborting. You can run 'socktop --help' for usage information.");
|
||||
return Ok(());
|
||||
@@ -364,16 +324,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let is_tls = url.starts_with("wss://");
|
||||
let has_token = url.contains("token=");
|
||||
// Only enable local process-kill when the agent is verified to be on this
|
||||
// machine — otherwise on-screen PIDs refer to a remote host and acting on
|
||||
// them locally would signal the wrong process (see local::agent_is_local) —
|
||||
// AND neither --no-kill nor SOCKTOP_NO_KILL disables it as a matter of
|
||||
// policy (shared terminals, public demos).
|
||||
let kill_enabled = local::agent_is_local(&url) && !parsed.no_kill && !no_kill_from_env();
|
||||
// machine; otherwise on-screen PIDs refer to a remote host and acting on
|
||||
// them locally would signal the wrong process. See local::agent_is_local.
|
||||
let is_local = local::agent_is_local(&url);
|
||||
let mut app = App::new()
|
||||
.with_intervals(metrics_interval_ms, processes_interval_ms)
|
||||
.with_status(is_tls, has_token)
|
||||
.with_compact(parsed.compact)
|
||||
.with_kill_enabled(kill_enabled);
|
||||
.with_local(is_local);
|
||||
if parsed.dry_run {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -440,7 +398,6 @@ fn gather_intervals(
|
||||
async fn run_demo_mode(
|
||||
_tls_ca: Option<&str>,
|
||||
compact: bool,
|
||||
no_kill: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let port = 3231;
|
||||
let url = format!("ws://127.0.0.1:{port}/ws");
|
||||
@@ -456,11 +413,10 @@ async fn run_demo_mode(
|
||||
};
|
||||
// Demo mode runs the real agent on loopback, so its PIDs are real local
|
||||
// processes — enable the local process-kill feature, gated the same way as
|
||||
// the normal connect path (loopback resolves local, --no-kill and
|
||||
// SOCKTOP_NO_KILL still override).
|
||||
// the normal connect path (loopback resolves local).
|
||||
let mut app = App::new()
|
||||
.with_compact(compact)
|
||||
.with_kill_enabled(local::agent_is_local(&url) && !no_kill && !no_kill_from_env());
|
||||
.with_local(local::agent_is_local(&url));
|
||||
// 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(()) } }
|
||||
}
|
||||
|
||||
@@ -130,8 +130,8 @@ impl ModalManager {
|
||||
])];
|
||||
|
||||
// Kill from here too — same key as the processes pane, and only shown
|
||||
// when the kill feature is enabled (agent local, no policy override).
|
||||
if data.kill_enabled
|
||||
// when the agent is local, since that is the only case where it works.
|
||||
if data.is_local
|
||||
&& let Some(line) = help_text.first_mut()
|
||||
{
|
||||
line.spans.push(Span::styled(
|
||||
|
||||
@@ -19,10 +19,9 @@ pub struct ProcessModalData<'a> {
|
||||
pub history: ProcessHistoryData<'a>,
|
||||
pub max_mem_bytes: u64,
|
||||
pub unsupported: bool,
|
||||
/// Whether the process-kill feature is available (agent local, no policy
|
||||
/// override). Only used to decide whether the `t` kill hint is shown —
|
||||
/// the kill itself is gated in `App`.
|
||||
pub kill_enabled: bool,
|
||||
/// Whether the agent is on this machine. Only used to decide whether the
|
||||
/// `t` kill hint is shown — the kill itself is gated in `App`.
|
||||
pub is_local: bool,
|
||||
}
|
||||
|
||||
/// Parameters for rendering scatter plot
|
||||
|
||||
@@ -87,10 +87,9 @@ pub struct ProcessDisplayParams<'a> {
|
||||
/// Peak cpu_usage from the most recent cache build; used to bold the
|
||||
/// busiest process. -1.0 if no cache.
|
||||
pub peak_cpu: f32,
|
||||
/// The process-kill feature is available (agent local, no policy
|
||||
/// override), so the `t` kill hint applies. Without it the hint would
|
||||
/// advertise a key that deliberately does nothing.
|
||||
pub kill_enabled: bool,
|
||||
/// Agent is on this machine, so the `t` kill hint applies. Without it the
|
||||
/// hint would advertise a key that deliberately does nothing.
|
||||
pub is_local: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
@@ -465,7 +464,7 @@ pub fn draw_top_processes(f: &mut ratatui::Frame<'_>, area: Rect, params: Proces
|
||||
Span::styled(" details", label),
|
||||
Span::styled(" · ", label),
|
||||
];
|
||||
if params.kill_enabled {
|
||||
if params.is_local {
|
||||
hints.push(Span::styled("t", key));
|
||||
hints.push(Span::styled(" kill", label));
|
||||
hints.push(Span::styled(" · ", label));
|
||||
@@ -953,7 +952,7 @@ mod click_tests {
|
||||
filtered_indices: &idxs,
|
||||
cached_rows: &cache,
|
||||
peak_cpu: peak,
|
||||
kill_enabled: false,
|
||||
is_local: false,
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -1067,7 +1066,7 @@ mod tooltip_tests {
|
||||
}
|
||||
|
||||
/// Render the pane with a selection and return the whole buffer as text.
|
||||
fn rendered(name: &str, width: u16, kill_enabled: bool) -> String {
|
||||
fn rendered(name: &str, width: u16, is_local: bool) -> String {
|
||||
let m = metrics(name);
|
||||
let mut cache = Vec::new();
|
||||
let peak = rebuild_row_cache(&m, &mut cache);
|
||||
@@ -1089,7 +1088,7 @@ mod tooltip_tests {
|
||||
filtered_indices: &idxs,
|
||||
cached_rows: &cache,
|
||||
peak_cpu: peak,
|
||||
kill_enabled,
|
||||
is_local,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -106,79 +106,3 @@ fn test_compact_flag_documented_and_accepted() {
|
||||
String::from_utf8_lossy(&out2.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_kill_flag_documented_and_accepted() {
|
||||
let exe = env!("CARGO_BIN_EXE_socktop");
|
||||
let out = Command::new(exe)
|
||||
.args(["--no-kill", "--help"])
|
||||
.output()
|
||||
.expect("run socktop --no-kill --help");
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"socktop --no-kill --help did not succeed"
|
||||
);
|
||||
let text = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(
|
||||
text.contains("--no-kill"),
|
||||
"help text missing --no-kill\n{text}"
|
||||
);
|
||||
|
||||
// The flag must not be mistaken for the positional URL argument.
|
||||
let out2 = Command::new(exe)
|
||||
.args(["--no-kill", "--dry-run", "ws://127.0.0.1:3000/ws"])
|
||||
.output()
|
||||
.expect("run socktop --no-kill --dry-run");
|
||||
assert!(
|
||||
out2.status.success(),
|
||||
"socktop --no-kill with a URL was rejected: {}",
|
||||
String::from_utf8_lossy(&out2.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_kill_env_var_accepted() {
|
||||
// SOCKTOP_NO_KILL must not break startup — the env-only path is how the
|
||||
// webterm deployment disables the kill feature for every invocation.
|
||||
let exe = env!("CARGO_BIN_EXE_socktop");
|
||||
let out = Command::new(exe)
|
||||
.env("SOCKTOP_NO_KILL", "1")
|
||||
.args(["--dry-run", "ws://127.0.0.1:3000/ws"])
|
||||
.output()
|
||||
.expect("run socktop with SOCKTOP_NO_KILL=1");
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"socktop with SOCKTOP_NO_KILL=1 did not succeed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_option_rejected_not_treated_as_url() {
|
||||
// Regression guard for the socktop.io incident (Aug 2026): socktop 1.60.1
|
||||
// parsed the then-unknown --no-kill flag as the positional websocket URL,
|
||||
// which made it prompt to overwrite the 'local' profile's URL with the
|
||||
// literal string "--no-kill". Unknown options must fail loudly instead of
|
||||
// falling through to the URL slot.
|
||||
let exe = env!("CARGO_BIN_EXE_socktop");
|
||||
let out = Command::new(exe)
|
||||
.args(["--not-a-real-flag", "--dry-run", "ws://127.0.0.1:3000/ws"])
|
||||
.output()
|
||||
.expect("run socktop with unknown flag");
|
||||
assert_eq!(
|
||||
out.status.code(),
|
||||
Some(2),
|
||||
"unknown option should exit 2, got: {:?}\nstderr: {}",
|
||||
out.status.code(),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
err.contains("Unknown option '--not-a-real-flag'"),
|
||||
"stderr should name the rejected option\n{err}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "socktop_agent"
|
||||
version = "1.60.2"
|
||||
version = "1.60.0"
|
||||
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
|
||||
description = "Socktop agent daemon. Serves host metrics over WebSocket."
|
||||
edition = "2024"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "socktop_connector"
|
||||
version = "1.60.2"
|
||||
version = "1.60.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "WebSocket connector library for socktop agent communication"
|
||||
|
||||
Reference in New Issue
Block a user