Compare commits
2 Commits
feature/ma
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3024816525 | |||
| 1d7bc42d59 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -2060,7 +2060,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socktop_agent"
|
name = "socktop_agent"
|
||||||
version = "1.50.1"
|
version = "1.50.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
|
|||||||
@ -1,121 +0,0 @@
|
|||||||
# Process Details Race Condition Fix
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
The `collect_process_metrics()` function was calling:
|
|
||||||
```rust
|
|
||||||
system.refresh_processes_specifics(ProcessesToUpdate::All, ...)
|
|
||||||
```
|
|
||||||
|
|
||||||
This caused several issues:
|
|
||||||
1. **Race Condition**: Refreshing ALL processes invalidated CPU baselines for main metrics collection
|
|
||||||
2. **Thread Pollution**: Main process list included threads (not desired in main UI)
|
|
||||||
3. **CPU Waste**: Refreshing ~500-1000+ processes when we only need 1
|
|
||||||
4. **Memory Waste**: Storing thread data unnecessarily
|
|
||||||
|
|
||||||
## Solution: Lightweight Child Process Enumeration
|
|
||||||
|
|
||||||
### Key Changes
|
|
||||||
|
|
||||||
#### 1. Targeted Process Refresh
|
|
||||||
```rust
|
|
||||||
// OLD: Refreshed ALL processes (expensive, causes race condition)
|
|
||||||
system.refresh_processes_specifics(ProcessesToUpdate::All, ...)
|
|
||||||
|
|
||||||
// NEW: Only refresh the specific process we care about
|
|
||||||
system.refresh_processes_specifics(
|
|
||||||
ProcessesToUpdate::Some(&[sysinfo::Pid::from_u32(pid)]),
|
|
||||||
...
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Direct /proc Access for Children (Linux)
|
|
||||||
Instead of iterating through all sysinfo processes, we now:
|
|
||||||
- Scan `/proc/` directory directly
|
|
||||||
- Read `/proc/{pid}/stat` to check parent PID
|
|
||||||
- Extract process details from `/proc/{pid}/` files
|
|
||||||
- Fall back to sysinfo for non-Linux platforms
|
|
||||||
|
|
||||||
### Performance Impact
|
|
||||||
|
|
||||||
| Metric | Before | After | Improvement |
|
|
||||||
|--------|--------|-------|-------------|
|
|
||||||
| CPU per request | ~15-20ms | ~1-3ms | **~85% reduction** |
|
|
||||||
| Processes refreshed | All (~500-1000+) | 1 | **99.9% reduction** |
|
|
||||||
| Memory overhead | All processes + threads | Single process | **~95% reduction** |
|
|
||||||
| Race condition risk | High | None | **100% eliminated** |
|
|
||||||
|
|
||||||
### Implementation Details
|
|
||||||
|
|
||||||
#### Linux Implementation
|
|
||||||
**`enumerate_child_processes_lightweight()`**
|
|
||||||
- Scans `/proc/` directory for child processes
|
|
||||||
- Uses `read_parent_pid_from_proc()` to filter by parent
|
|
||||||
- Calls `collect_process_info_from_proc()` to extract details
|
|
||||||
- Reads from:
|
|
||||||
- `/proc/{pid}/stat` - Process state, parent PID, start time
|
|
||||||
- `/proc/{pid}/status` - UID, GID, threads, memory, state
|
|
||||||
- `/proc/{pid}/cmdline` - Command line
|
|
||||||
- `/proc/{pid}/io` - I/O statistics (if available)
|
|
||||||
- `/proc/{pid}/cwd` - Working directory (symlink)
|
|
||||||
- `/proc/{pid}/exe` - Executable path (symlink)
|
|
||||||
|
|
||||||
#### Non-Linux Fallback
|
|
||||||
- Uses sysinfo's process iteration (less efficient but functional)
|
|
||||||
- Maintains cross-platform compatibility
|
|
||||||
- Same API, just different implementation
|
|
||||||
|
|
||||||
### Testing Instructions
|
|
||||||
|
|
||||||
1. **Start the agent:**
|
|
||||||
```bash
|
|
||||||
cargo run --bin socktop_agent --release -- --port 8123
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Connect with the client:**
|
|
||||||
```bash
|
|
||||||
cargo run --bin socktop --release -- ws://localhost:8123/ws
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Test process details:**
|
|
||||||
- Navigate to a process with the arrow keys
|
|
||||||
- Press Enter to open process details modal
|
|
||||||
- Verify child processes are shown correctly
|
|
||||||
- Check that the main UI still shows only top-level processes (no threads)
|
|
||||||
|
|
||||||
4. **Verify no race condition:**
|
|
||||||
- Open process details modal
|
|
||||||
- Watch main UI CPU percentages
|
|
||||||
- They should remain stable and accurate
|
|
||||||
- No sudden spikes or drops in CPU percentages
|
|
||||||
|
|
||||||
### Code Locations
|
|
||||||
|
|
||||||
- **Main fix:** `socktop_agent/src/metrics.rs`
|
|
||||||
- `collect_process_metrics()` - Modified to use targeted refresh
|
|
||||||
- `enumerate_child_processes_lightweight()` - New function for Linux
|
|
||||||
- `read_parent_pid_from_proc()` - Helper to read parent PID
|
|
||||||
- `collect_process_info_from_proc()` - Helper to read process details
|
|
||||||
|
|
||||||
### Benefits
|
|
||||||
|
|
||||||
1. **Lightweight**: Minimal CPU and memory usage
|
|
||||||
2. **No Race Conditions**: Doesn't interfere with main metrics collection
|
|
||||||
3. **Clean Separation**: Main UI never sees threads
|
|
||||||
4. **Cross-Platform**: Works on Linux (optimized) and other platforms (fallback)
|
|
||||||
5. **Maintainable**: Clear, well-documented code
|
|
||||||
|
|
||||||
### Future Enhancements
|
|
||||||
|
|
||||||
Potential optimizations if needed:
|
|
||||||
- Cache `/proc` file descriptors for frequently accessed processes
|
|
||||||
- Batch read multiple `/proc` files in parallel
|
|
||||||
- Add support for thread enumeration (currently not needed)
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
✅ Compiles without errors
|
|
||||||
✅ No race conditions
|
|
||||||
✅ Child processes correctly enumerated
|
|
||||||
✅ Main UI remains clean (no threads)
|
|
||||||
✅ Significantly reduced CPU usage
|
|
||||||
✅ Cross-platform compatible
|
|
||||||
@ -1,150 +0,0 @@
|
|||||||
# Thread Support Implementation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Added per-thread CPU metrics collection and visualization to the process details modal. Threads and child processes are now clearly distinguished in both the scatter plot and the table view.
|
|
||||||
|
|
||||||
## Changes Made
|
|
||||||
|
|
||||||
### 1. Data Structures
|
|
||||||
|
|
||||||
#### `socktop_connector/src/types.rs` & `socktop_agent/src/types.rs`
|
|
||||||
- **New:** `ThreadInfo` struct
|
|
||||||
- `tid: u32` - Thread ID
|
|
||||||
- `name: String` - Thread name from `/proc/{pid}/task/{tid}/comm`
|
|
||||||
- `cpu_time_user: u64` - User CPU time in microseconds
|
|
||||||
- `cpu_time_system: u64` - System CPU time in microseconds
|
|
||||||
- `status: String` - Thread status (Running, Sleeping, etc.)
|
|
||||||
|
|
||||||
- **Updated:** `DetailedProcessInfo` struct
|
|
||||||
- Added `threads: Vec<ThreadInfo>` field
|
|
||||||
|
|
||||||
### 2. Agent - Thread Collection
|
|
||||||
|
|
||||||
#### `socktop_agent/src/metrics.rs`
|
|
||||||
|
|
||||||
**New Function: `collect_thread_info(pid: u32)` (Linux only)**
|
|
||||||
- Reads `/proc/{pid}/task/` directory to enumerate all threads
|
|
||||||
- For each thread:
|
|
||||||
- Reads thread name from `/proc/{pid}/task/{tid}/comm`
|
|
||||||
- Parses `/proc/{pid}/task/{tid}/stat` for CPU times and status
|
|
||||||
- Converts clock ticks (100 Hz) to microseconds: `ticks * 10,000`
|
|
||||||
- Extracts utime (field 13) and stime (field 14) from stat file
|
|
||||||
|
|
||||||
**Updated: `collect_process_metrics()`**
|
|
||||||
- Calls `collect_thread_info(pid)` to collect thread data
|
|
||||||
- Includes threads in the `DetailedProcessInfo` response
|
|
||||||
|
|
||||||
**Updated: `collect_process_info_from_proc()`**
|
|
||||||
- Added `threads: Vec::new()` to child process info (not collected recursively)
|
|
||||||
|
|
||||||
**Updated: `enumerate_child_processes_lightweight()` (non-Linux)**
|
|
||||||
- Added `threads: Vec::new()` for cross-platform compatibility
|
|
||||||
|
|
||||||
### 3. Client - UI Visualization
|
|
||||||
|
|
||||||
#### `socktop/src/ui/modal.rs`
|
|
||||||
|
|
||||||
**Updated: `render_cpu_scatter_plot()`**
|
|
||||||
- Title changed to "Thread & Process CPU Time Distribution"
|
|
||||||
- Includes threads in max value scaling calculation
|
|
||||||
- Plots threads with hollow circle marker `○`
|
|
||||||
- Plots child processes with filled circle marker `•`
|
|
||||||
- Uses different markers for overlapping items:
|
|
||||||
- `○` - Single thread
|
|
||||||
- `◎` - Multiple threads at same point
|
|
||||||
- `•` - Single child process
|
|
||||||
- `◉` - Multiple items (threads/processes) at same point
|
|
||||||
|
|
||||||
**Updated: Legend**
|
|
||||||
- Now shows: `● Main Process ○ Thread • Child Process ◉ Multiple`
|
|
||||||
|
|
||||||
**Updated: `render_thread_table()`**
|
|
||||||
- Title now shows counts: `"Threads (N) & Children (M)"`
|
|
||||||
- Table format:
|
|
||||||
```
|
|
||||||
Type TID/PID Name/Status
|
|
||||||
─────────────────────────────
|
|
||||||
[T] 12345 thread-name
|
|
||||||
[P] 12346 child-process
|
|
||||||
```
|
|
||||||
- `[T]` prefix in cyan for threads
|
|
||||||
- `[P]` prefix in green for child processes
|
|
||||||
- Displays up to 10 items total
|
|
||||||
- Threads listed first, then child processes
|
|
||||||
|
|
||||||
## Platform Support
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
- **Full support** for per-thread metrics
|
|
||||||
- Reads directly from `/proc/{pid}/task/*/` for efficiency
|
|
||||||
- No additional dependencies required
|
|
||||||
|
|
||||||
### Non-Linux
|
|
||||||
- Returns empty thread list
|
|
||||||
- Falls back gracefully
|
|
||||||
- Child process enumeration still works via sysinfo
|
|
||||||
|
|
||||||
## Performance
|
|
||||||
|
|
||||||
- **Thread enumeration**: ~0.5-2ms for typical processes
|
|
||||||
- **No additional locks**: Thread data collected outside sysinfo mutex
|
|
||||||
- **Minimal overhead**: Only collected when process details modal is open
|
|
||||||
- **No race conditions**: Doesn't interfere with main metrics collection
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
Perfect for visualizing:
|
|
||||||
- Multi-threaded applications (web servers, databases, compilers)
|
|
||||||
- Thread pool behavior
|
|
||||||
- Worker thread distribution
|
|
||||||
- Identifying busy vs idle threads
|
|
||||||
- Comparing thread CPU usage patterns
|
|
||||||
|
|
||||||
## Example Output
|
|
||||||
|
|
||||||
For a process with 8 threads and 2 child processes:
|
|
||||||
|
|
||||||
**Scatter Plot:**
|
|
||||||
- Main process shown as `●`
|
|
||||||
- 8 threads shown as `○` distributed based on their CPU times
|
|
||||||
- 2 child processes shown as `•`
|
|
||||||
- X-axis: User CPU time
|
|
||||||
- Y-axis: System CPU time
|
|
||||||
|
|
||||||
**Table:**
|
|
||||||
```
|
|
||||||
Threads (8) & Children (2)
|
|
||||||
Type TID/PID Name/Status
|
|
||||||
─────────────────────────────
|
|
||||||
[T] 12345 web-worker-1
|
|
||||||
[T] 12346 web-worker-2
|
|
||||||
[T] 12347 io-handler
|
|
||||||
...
|
|
||||||
[P] 12355 nginx: cache
|
|
||||||
[P] 12356 nginx: worker
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Test with multi-threaded applications:
|
|
||||||
```bash
|
|
||||||
# Terminal 1: Start agent
|
|
||||||
cargo run --release --bin socktop_agent -- --port 3000
|
|
||||||
|
|
||||||
# Terminal 2: Start client
|
|
||||||
cargo run --release --bin socktop -- ws://localhost:3000/ws
|
|
||||||
|
|
||||||
# Navigate to a multi-threaded process (e.g., Firefox, Chrome, Node.js)
|
|
||||||
# Press Enter to open process details
|
|
||||||
# Scatter plot will show thread distribution
|
|
||||||
# Table will show threads marked with [T] and children with [P]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
Potential improvements:
|
|
||||||
- Per-thread memory usage (requires parsing `/proc/{pid}/task/{tid}/statm`)
|
|
||||||
- Thread-level I/O statistics
|
|
||||||
- Thread CPU percentage (requires delta calculation with caching)
|
|
||||||
- Sorting threads by CPU time in the table
|
|
||||||
- Thread state filtering (show only running/sleeping threads)
|
|
||||||
@ -1,320 +0,0 @@
|
|||||||
# Auto-Generated Man Pages
|
|
||||||
|
|
||||||
This document explains how man pages are automatically generated from the CLI definitions using `clap` and `clap_mangen`.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Starting from version 1.50.0+, socktop uses **clap** for CLI parsing and **clap_mangen** to automatically generate man pages at build time. This approach has several advantages:
|
|
||||||
|
|
||||||
✅ Man pages are always in sync with the actual CLI
|
|
||||||
✅ Single source of truth (CLI definitions)
|
|
||||||
✅ No manual maintenance of separate man page files
|
|
||||||
✅ Generated during `cargo build` automatically
|
|
||||||
✅ Can be installed alongside binaries
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
### 1. CLI Definitions
|
|
||||||
|
|
||||||
Both `socktop` and `socktop_agent` use clap's derive macros to define their CLI:
|
|
||||||
|
|
||||||
- **`socktop/src/cli.rs`** - Client CLI definition
|
|
||||||
- **`socktop_agent/src/cli.rs`** - Agent CLI definition
|
|
||||||
|
|
||||||
These files use clap's attributes to specify:
|
|
||||||
- Arguments and options
|
|
||||||
- Help text and descriptions
|
|
||||||
- Value names and types
|
|
||||||
- Environment variable support
|
|
||||||
- Hidden options (for testing)
|
|
||||||
|
|
||||||
### 2. Build-Time Generation
|
|
||||||
|
|
||||||
Each crate has a `build.rs` script that:
|
|
||||||
1. Includes the CLI definition file
|
|
||||||
2. Uses `clap_mangen` to generate the man page
|
|
||||||
3. Saves it to `$OUT_DIR/man/*.1`
|
|
||||||
|
|
||||||
The generation happens automatically during:
|
|
||||||
```bash
|
|
||||||
cargo build
|
|
||||||
cargo build --release
|
|
||||||
cargo install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Generated Man Pages Location
|
|
||||||
|
|
||||||
After building, man pages are located at:
|
|
||||||
```
|
|
||||||
target/debug/build/socktop-*/out/man/socktop.1
|
|
||||||
target/debug/build/socktop_agent-*/out/man/socktop_agent.1
|
|
||||||
|
|
||||||
# Or for release builds:
|
|
||||||
target/release/build/socktop-*/out/man/socktop.1
|
|
||||||
target/release/build/socktop_agent-*/out/man/socktop_agent.1
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation Options
|
|
||||||
|
|
||||||
### Option 1: Use the Installation Script (Recommended)
|
|
||||||
|
|
||||||
The `scripts/install-with-man.sh` script builds the binaries, extracts the generated man pages, and installs everything:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# User installation (no sudo)
|
|
||||||
./scripts/install-with-man.sh
|
|
||||||
|
|
||||||
# System-wide installation (requires sudo)
|
|
||||||
sudo ./scripts/install-with-man.sh --system
|
|
||||||
|
|
||||||
# Only install man pages (after building)
|
|
||||||
./scripts/install-with-man.sh --man-only
|
|
||||||
```
|
|
||||||
|
|
||||||
This script:
|
|
||||||
- Builds the project in release mode
|
|
||||||
- Extracts generated man pages from `OUT_DIR`
|
|
||||||
- Installs binaries to `~/.cargo/bin` or `/usr/local/bin`
|
|
||||||
- Installs man pages to `~/.local/share/man/man1` or `/usr/local/share/man/man1`
|
|
||||||
|
|
||||||
### Option 2: Manual Installation After Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build the project
|
|
||||||
cargo build --release
|
|
||||||
|
|
||||||
# Find generated man pages
|
|
||||||
SOCKTOP_MAN=$(find target/release/build/socktop-*/out/man/socktop.1 | head -1)
|
|
||||||
AGENT_MAN=$(find target/release/build/socktop_agent-*/out/man/socktop_agent.1 | head -1)
|
|
||||||
|
|
||||||
# Install to user directory
|
|
||||||
mkdir -p ~/.local/share/man/man1
|
|
||||||
cp "$SOCKTOP_MAN" ~/.local/share/man/man1/
|
|
||||||
cp "$AGENT_MAN" ~/.local/share/man/man1/
|
|
||||||
|
|
||||||
# Or install system-wide
|
|
||||||
sudo mkdir -p /usr/local/share/man/man1
|
|
||||||
sudo cp "$SOCKTOP_MAN" /usr/local/share/man/man1/
|
|
||||||
sudo cp "$AGENT_MAN" /usr/local/share/man/man1/
|
|
||||||
sudo mandb # Update man database
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 3: View Without Installing
|
|
||||||
|
|
||||||
You can view the generated man pages directly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# After building
|
|
||||||
man -l $(find target/release/build/socktop-*/out/man/socktop.1 | head -1)
|
|
||||||
man -l $(find target/release/build/socktop_agent-*/out/man/socktop_agent.1 | head -1)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Viewing Installed Man Pages
|
|
||||||
|
|
||||||
After installation:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
man socktop
|
|
||||||
man socktop_agent
|
|
||||||
```
|
|
||||||
|
|
||||||
If `man socktop` doesn't work after user installation, add to your shell rc:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For bash
|
|
||||||
echo 'export MANPATH="$HOME/.local/share/man:$MANPATH"' >> ~/.bashrc
|
|
||||||
source ~/.bashrc
|
|
||||||
|
|
||||||
# For zsh
|
|
||||||
echo 'export MANPATH="$HOME/.local/share/man:$MANPATH"' >> ~/.zshrc
|
|
||||||
source ~/.zshrc
|
|
||||||
```
|
|
||||||
|
|
||||||
## Updating CLI and Man Pages
|
|
||||||
|
|
||||||
When you need to update the CLI or man pages:
|
|
||||||
|
|
||||||
1. **Edit the CLI definition** in `src/cli.rs`:
|
|
||||||
```rust
|
|
||||||
/// Your new option description
|
|
||||||
#[arg(short = 'x', long = "example")]
|
|
||||||
pub example: bool,
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Rebuild** to regenerate man pages:
|
|
||||||
```bash
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Reinstall** man pages:
|
|
||||||
```bash
|
|
||||||
./scripts/install-with-man.sh --man-only
|
|
||||||
```
|
|
||||||
|
|
||||||
The man pages will automatically reflect your changes!
|
|
||||||
|
|
||||||
## CLI Definition Format
|
|
||||||
|
|
||||||
### Basic Structure
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(
|
|
||||||
name = "myapp",
|
|
||||||
version,
|
|
||||||
author,
|
|
||||||
about = "Short description",
|
|
||||||
long_about = "Longer description that appears in man page and --help"
|
|
||||||
)]
|
|
||||||
pub struct Cli {
|
|
||||||
/// Short description of this option
|
|
||||||
///
|
|
||||||
/// Longer description that appears in the man page.
|
|
||||||
/// Can span multiple lines.
|
|
||||||
#[arg(short = 't', long = "thing", value_name = "VALUE")]
|
|
||||||
pub thing: Option<String>,
|
|
||||||
|
|
||||||
/// Boolean flag
|
|
||||||
#[arg(long)]
|
|
||||||
pub flag: bool,
|
|
||||||
|
|
||||||
/// Hidden option (won't appear in man page or --help)
|
|
||||||
#[arg(long, hide = true)]
|
|
||||||
pub secret: bool,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Variable Support
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// Port to listen on
|
|
||||||
///
|
|
||||||
/// Can also be set via MYAPP_PORT environment variable.
|
|
||||||
#[arg(short = 'p', long = "port", env = "MYAPP_PORT")]
|
|
||||||
pub port: Option<u16>,
|
|
||||||
```
|
|
||||||
|
|
||||||
### Value Parsing
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// Custom parser
|
|
||||||
#[arg(long, value_parser = parse_custom)]
|
|
||||||
pub custom: Option<String>,
|
|
||||||
|
|
||||||
fn parse_custom(s: &str) -> Result<String, String> {
|
|
||||||
// Custom validation logic
|
|
||||||
Ok(s.to_string())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Advantages Over Manual Man Pages
|
|
||||||
|
|
||||||
| Feature | Auto-Generated | Manual |
|
|
||||||
|---------|---------------|--------|
|
|
||||||
| Always in sync with CLI | ✅ Yes | ❌ Manual updates required |
|
|
||||||
| Single source of truth | ✅ Yes | ❌ Duplicated info |
|
|
||||||
| Maintenance effort | ✅ Low | ❌ High |
|
|
||||||
| Consistency | ✅ Guaranteed | ❌ Can drift |
|
|
||||||
| Generated at build time | ✅ Yes | ❌ Separate process |
|
|
||||||
| Works with `--help` | ✅ Same source | ❌ Separate |
|
|
||||||
| Rich formatting | ⚠️ Good | ✅ Full control |
|
|
||||||
|
|
||||||
## Comparison with Manual Man Pages
|
|
||||||
|
|
||||||
The project also includes manually written man pages in `docs/man/` for comparison and as templates. These are more detailed and include additional sections like:
|
|
||||||
|
|
||||||
- EXAMPLES with complex scenarios
|
|
||||||
- SECURITY CONSIDERATIONS
|
|
||||||
- PLATFORM NOTES
|
|
||||||
- Systemd integration guides
|
|
||||||
- Troubleshooting tips
|
|
||||||
|
|
||||||
The auto-generated man pages from clap are excellent for:
|
|
||||||
- Options and arguments
|
|
||||||
- Basic descriptions
|
|
||||||
- Version and author info
|
|
||||||
- Environment variables
|
|
||||||
|
|
||||||
But may be limited for:
|
|
||||||
- Complex examples
|
|
||||||
- Extensive narrative documentation
|
|
||||||
- Custom formatting
|
|
||||||
- Additional reference sections
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Write good doc comments** in `cli.rs` - they become man page content
|
|
||||||
2. **Use `long_about`** for detailed descriptions
|
|
||||||
3. **Specify `value_name`** for clarity (e.g., `<PORT>`, `<URL>`)
|
|
||||||
4. **Document environment variables** in the option description
|
|
||||||
5. **Use `hide = true`** for internal/test options
|
|
||||||
6. **Keep descriptions concise** but informative
|
|
||||||
7. **Rebuild after CLI changes** to update man pages
|
|
||||||
|
|
||||||
## Testing Man Page Generation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clean build to ensure regeneration
|
|
||||||
cargo clean
|
|
||||||
|
|
||||||
# Build and check for man page warning
|
|
||||||
cargo build --release 2>&1 | grep "Man page generated"
|
|
||||||
|
|
||||||
# View the generated man page
|
|
||||||
man -l $(find target/release/build/socktop-*/out/man/socktop.1 | head -1)
|
|
||||||
|
|
||||||
# Check for errors
|
|
||||||
lexgrog $(find target/release/build/socktop-*/out/man/socktop.1 | head -1)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Man page not generated
|
|
||||||
|
|
||||||
**Solution:** Check that `build.rs` ran successfully:
|
|
||||||
```bash
|
|
||||||
cargo clean
|
|
||||||
cargo build -vv 2>&1 | grep build.rs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Can't find generated man page
|
|
||||||
|
|
||||||
**Solution:** Look in the correct build output:
|
|
||||||
```bash
|
|
||||||
find target -name "socktop.1" -type f
|
|
||||||
```
|
|
||||||
|
|
||||||
### Man page content is outdated
|
|
||||||
|
|
||||||
**Solution:** Clean and rebuild:
|
|
||||||
```bash
|
|
||||||
cargo clean
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
### MANPATH not working
|
|
||||||
|
|
||||||
**Solution:** Verify the path is correct:
|
|
||||||
```bash
|
|
||||||
echo $MANPATH
|
|
||||||
man -w # Show current man paths
|
|
||||||
```
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
Potential improvements:
|
|
||||||
|
|
||||||
- [ ] Add more detailed examples section using clap's `after_help`
|
|
||||||
- [ ] Generate shell completions alongside man pages
|
|
||||||
- [ ] Create a custom man page template with additional sections
|
|
||||||
- [ ] Package man pages in release artifacts
|
|
||||||
- [ ] Auto-install man pages during `cargo install`
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [clap documentation](https://docs.rs/clap/)
|
|
||||||
- [clap_mangen documentation](https://docs.rs/clap_mangen/)
|
|
||||||
- [Manual man pages](man/README.md) - The original manually written versions
|
|
||||||
- [Quick Reference](QUICK_REFERENCE.md) - Command cheat sheet
|
|
||||||
@ -1,380 +0,0 @@
|
|||||||
# Migration to Clap for Auto-Generated Man Pages
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Socktop has been migrated from manual argument parsing to **clap** (Command Line Argument Parser) with automatic man page generation via **clap_mangen**. This provides several benefits:
|
|
||||||
|
|
||||||
✅ **Auto-generated man pages** - Always in sync with CLI
|
|
||||||
✅ **Better help output** - Rich, formatted `--help` text
|
|
||||||
✅ **Type safety** - Compile-time checking of arguments
|
|
||||||
✅ **Environment variable support** - Built-in env var integration
|
|
||||||
✅ **Shell completions** - Easy to add bash/zsh/fish completions
|
|
||||||
✅ **Single source of truth** - CLI definitions generate everything
|
|
||||||
|
|
||||||
## What Changed
|
|
||||||
|
|
||||||
### Before (Manual Parsing)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Old approach: Manual argument parsing
|
|
||||||
fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, String> {
|
|
||||||
let mut it = args.into_iter();
|
|
||||||
let prog = it.next().unwrap_or_else(|| "socktop".into());
|
|
||||||
let mut url: Option<String> = None;
|
|
||||||
let mut tls_ca: Option<String> = None;
|
|
||||||
// ... lots of manual parsing code ...
|
|
||||||
while let Some(arg) = it.next() {
|
|
||||||
match arg.as_str() {
|
|
||||||
"-h" | "--help" => {
|
|
||||||
return Err(format!("Usage: {prog} ..."));
|
|
||||||
}
|
|
||||||
"--tls-ca" | "-t" => {
|
|
||||||
tls_ca = it.next();
|
|
||||||
}
|
|
||||||
// ... more matches ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(ParsedArgs { url, tls_ca, ... })
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problems:**
|
|
||||||
- Manual parsing is error-prone
|
|
||||||
- Help text gets out of sync
|
|
||||||
- No automatic man page generation
|
|
||||||
- Duplicated logic for `--flag` and `--flag=value`
|
|
||||||
- No environment variable support
|
|
||||||
- Hard to test
|
|
||||||
|
|
||||||
### After (Clap Derive)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// New approach: Clap derive macros
|
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(
|
|
||||||
name = "socktop",
|
|
||||||
version,
|
|
||||||
author,
|
|
||||||
about = "Remote system monitor with a rich TUI over WebSocket",
|
|
||||||
long_about = "socktop is a remote system monitor..."
|
|
||||||
)]
|
|
||||||
pub struct Cli {
|
|
||||||
/// WebSocket URL to connect to
|
|
||||||
#[arg(value_name = "URL")]
|
|
||||||
pub url: Option<String>,
|
|
||||||
|
|
||||||
/// Path to TLS certificate PEM file for WSS connections
|
|
||||||
#[arg(short = 't', long = "tls-ca", value_name = "CERT_PEM")]
|
|
||||||
pub tls_ca: Option<String>,
|
|
||||||
|
|
||||||
// ... more fields ...
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage:
|
|
||||||
let cli = Cli::parse();
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits:**
|
|
||||||
- Declarative and concise
|
|
||||||
- Auto-generated help and man pages
|
|
||||||
- Type-safe argument parsing
|
|
||||||
- Automatic support for `--flag` and `--flag=value`
|
|
||||||
- Built-in env var support with `env` attribute
|
|
||||||
- Easy to test
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
### Core Changes
|
|
||||||
|
|
||||||
1. **`socktop/Cargo.toml`** - Added clap dependencies
|
|
||||||
2. **`socktop/src/cli.rs`** - NEW: CLI definition using clap derive
|
|
||||||
3. **`socktop/src/main.rs`** - Updated to use `Cli::parse_args()`
|
|
||||||
4. **`socktop/build.rs`** - NEW: Auto-generates man pages at build time
|
|
||||||
|
|
||||||
5. **`socktop_agent/Cargo.toml`** - Added clap dependencies
|
|
||||||
6. **`socktop_agent/src/cli.rs`** - NEW: CLI definition using clap derive
|
|
||||||
7. **`socktop_agent/src/main.rs`** - Updated to use `Cli::parse_args()`
|
|
||||||
8. **`socktop_agent/build.rs`** - Updated to auto-generate man pages
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
9. **`docs/AUTO_MAN_PAGES.md`** - Comprehensive guide to auto-generated man pages
|
|
||||||
10. **`docs/CLAP_MIGRATION.md`** - This file
|
|
||||||
11. **`README.md`** - Updated Man Pages section
|
|
||||||
12. **`scripts/install-with-man.sh`** - NEW: Installation script that includes man pages
|
|
||||||
|
|
||||||
## Man Page Generation
|
|
||||||
|
|
||||||
### How It Works
|
|
||||||
|
|
||||||
1. **Build Time** - When you run `cargo build`, the `build.rs` script:
|
|
||||||
- Includes the CLI definition from `src/cli.rs`
|
|
||||||
- Creates a clap `Command` instance
|
|
||||||
- Uses `clap_mangen` to generate a man page
|
|
||||||
- Saves it to `$OUT_DIR/man/*.1`
|
|
||||||
|
|
||||||
2. **Location** - Generated man pages are at:
|
|
||||||
```
|
|
||||||
target/release/build/socktop-*/out/man/socktop.1
|
|
||||||
target/release/build/socktop_agent-*/out/man/socktop_agent.1
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Installation** - Use the installation script:
|
|
||||||
```bash
|
|
||||||
./scripts/install-with-man.sh # User install
|
|
||||||
sudo ./scripts/install-with-man.sh --system # System install
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Viewing** - After installation:
|
|
||||||
```bash
|
|
||||||
man socktop
|
|
||||||
man socktop_agent
|
|
||||||
```
|
|
||||||
|
|
||||||
### Man Page Content
|
|
||||||
|
|
||||||
The man pages include:
|
|
||||||
- **NAME** - From `about` attribute
|
|
||||||
- **SYNOPSIS** - Auto-generated from arguments
|
|
||||||
- **DESCRIPTION** - From `long_about` attribute
|
|
||||||
- **OPTIONS** - From field doc comments and `#[arg(...)]` attributes
|
|
||||||
- **VERSION** - From Cargo.toml
|
|
||||||
- **AUTHORS** - From Cargo.toml
|
|
||||||
|
|
||||||
## CLI Definition Format
|
|
||||||
|
|
||||||
### Structure
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(
|
|
||||||
name = "myapp",
|
|
||||||
version, // Uses Cargo.toml version
|
|
||||||
author, // Uses Cargo.toml authors
|
|
||||||
about = "Short description",
|
|
||||||
long_about = "Longer description for man page and --help"
|
|
||||||
)]
|
|
||||||
pub struct Cli {
|
|
||||||
/// Short description
|
|
||||||
///
|
|
||||||
/// Longer description that appears in the man page.
|
|
||||||
/// Multiple paragraphs supported.
|
|
||||||
#[arg(short = 't', long = "thing", value_name = "VALUE")]
|
|
||||||
pub thing: Option<String>,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Common Attributes
|
|
||||||
|
|
||||||
| Attribute | Purpose | Example |
|
|
||||||
|-----------|---------|---------|
|
|
||||||
| `short = 'x'` | Short flag | `-x` |
|
|
||||||
| `long = "example"` | Long flag | `--example` |
|
|
||||||
| `value_name = "FOO"` | Display name | `--thing <FOO>` |
|
|
||||||
| `env = "VAR"` | Environment variable | `env = "MY_VAR"` |
|
|
||||||
| `default_value = "x"` | Default value | Default: "x" |
|
|
||||||
| `hide = true` | Hide from help/man | For internal options |
|
|
||||||
| `value_parser = func` | Custom parser | Validation |
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// Port to listen on
|
|
||||||
///
|
|
||||||
/// Can be set via SOCKTOP_PORT environment variable.
|
|
||||||
#[arg(short = 'p', long = "port", env = "SOCKTOP_PORT")]
|
|
||||||
pub port: Option<u16>,
|
|
||||||
```
|
|
||||||
|
|
||||||
This automatically:
|
|
||||||
- Checks the environment variable
|
|
||||||
- Shows `[env: SOCKTOP_PORT=]` in help
|
|
||||||
- Documents it in the man page
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
|
|
||||||
Both CLI modules include comprehensive unit tests:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_basic_parsing() {
|
|
||||||
let cli = Cli::try_parse_from(&["socktop", "ws://localhost:8080/ws"]).unwrap();
|
|
||||||
assert_eq!(cli.url, Some("ws://localhost:8080/ws".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_tls_options() {
|
|
||||||
let cli = Cli::try_parse_from(&[
|
|
||||||
"socktop",
|
|
||||||
"-t", "/path/to/cert.pem",
|
|
||||||
"--verify-hostname",
|
|
||||||
"wss://example.com:8443/ws",
|
|
||||||
]).unwrap();
|
|
||||||
assert_eq!(cli.tls_ca, Some("/path/to/cert.pem".to_string()));
|
|
||||||
assert!(cli.verify_hostname);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Run tests with:
|
|
||||||
```bash
|
|
||||||
cargo test --package socktop cli::
|
|
||||||
cargo test --package socktop_agent cli::
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual Testing
|
|
||||||
|
|
||||||
Test the help output:
|
|
||||||
```bash
|
|
||||||
cargo run --package socktop -- --help
|
|
||||||
cargo run --package socktop_agent -- --help
|
|
||||||
```
|
|
||||||
|
|
||||||
Test argument parsing:
|
|
||||||
```bash
|
|
||||||
cargo run --package socktop -- ws://localhost:8080/ws
|
|
||||||
cargo run --package socktop -- -t cert.pem wss://localhost:8443/ws
|
|
||||||
cargo run --package socktop_agent -- --port 8080
|
|
||||||
cargo run --package socktop_agent -- --enableSSL
|
|
||||||
```
|
|
||||||
|
|
||||||
Test environment variables:
|
|
||||||
```bash
|
|
||||||
SOCKTOP_PORT=9000 cargo run --package socktop_agent
|
|
||||||
SOCKTOP_ENABLE_SSL=1 cargo run --package socktop_agent
|
|
||||||
```
|
|
||||||
|
|
||||||
## Backwards Compatibility
|
|
||||||
|
|
||||||
### Command-Line Interface
|
|
||||||
|
|
||||||
✅ **Fully compatible** - All existing command-line arguments work exactly the same:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Still works
|
|
||||||
socktop -t cert.pem wss://host:8443/ws
|
|
||||||
socktop --profile myprofile
|
|
||||||
socktop --demo
|
|
||||||
|
|
||||||
socktop_agent --port 8080
|
|
||||||
socktop_agent --enableSSL
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
✅ **Fully compatible** - All environment variables still work:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
SOCKTOP_PORT=8080 socktop_agent
|
|
||||||
SOCKTOP_ENABLE_SSL=1 socktop_agent
|
|
||||||
```
|
|
||||||
|
|
||||||
### Breaking Changes
|
|
||||||
|
|
||||||
❌ **None** - This is a drop-in replacement for the old parser.
|
|
||||||
|
|
||||||
## Comparison: Manual vs Clap
|
|
||||||
|
|
||||||
| Feature | Manual Parsing | Clap |
|
|
||||||
|---------|---------------|------|
|
|
||||||
| Code lines | ~120 lines | ~50 lines |
|
|
||||||
| Man pages | Separate files | Auto-generated |
|
|
||||||
| Help text | Hardcoded strings | Auto-generated |
|
|
||||||
| Type safety | Runtime errors | Compile-time |
|
|
||||||
| Env vars | Manual `std::env::var` | Built-in `env` attribute |
|
|
||||||
| Testing | Hard to test | Easy with `try_parse_from` |
|
|
||||||
| Maintenance | High | Low |
|
|
||||||
| Consistency | Can drift | Always in sync |
|
|
||||||
| Completions | Manual | Auto-generate |
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
Now that we're using clap, we can easily add:
|
|
||||||
|
|
||||||
### Shell Completions
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// In build.rs
|
|
||||||
use clap_complete::{generate_to, shells::*};
|
|
||||||
|
|
||||||
let cmd = Cli::command();
|
|
||||||
generate_to(Bash, &mut cmd, "socktop", &out_dir)?;
|
|
||||||
generate_to(Zsh, &mut cmd, "socktop", &out_dir)?;
|
|
||||||
generate_to(Fish, &mut cmd, "socktop", &out_dir)?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Subcommands
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(Parser)]
|
|
||||||
enum Commands {
|
|
||||||
/// Connect to an agent
|
|
||||||
Connect {
|
|
||||||
#[arg(value_name = "URL")]
|
|
||||||
url: String,
|
|
||||||
},
|
|
||||||
/// List saved profiles
|
|
||||||
Profiles,
|
|
||||||
/// Run demo mode
|
|
||||||
Demo,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Better Validation
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[arg(value_parser = clap::value_parser!(u16).range(1..=65535))]
|
|
||||||
pub port: Option<u16>,
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Help Sections
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[command(
|
|
||||||
after_help = "EXAMPLES:\n socktop ws://localhost:8080/ws\n socktop --demo"
|
|
||||||
)]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Checklist
|
|
||||||
|
|
||||||
If migrating other Rust projects to clap:
|
|
||||||
|
|
||||||
- [ ] Add clap and clap_mangen dependencies
|
|
||||||
- [ ] Create `src/cli.rs` with derive macros
|
|
||||||
- [ ] Update `main.rs` to use `Cli::parse()`
|
|
||||||
- [ ] Create/update `build.rs` for man page generation
|
|
||||||
- [ ] Write unit tests for CLI parsing
|
|
||||||
- [ ] Test all existing command-line arguments
|
|
||||||
- [ ] Test environment variables
|
|
||||||
- [ ] Update documentation
|
|
||||||
- [ ] Create installation scripts for man pages
|
|
||||||
- [ ] Consider adding shell completions
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [Clap Documentation](https://docs.rs/clap/)
|
|
||||||
- [Clap Derive Tutorial](https://docs.rs/clap/latest/clap/_derive/index.html)
|
|
||||||
- [Clap Mangen](https://docs.rs/clap_mangen/)
|
|
||||||
- [Auto-Generated Man Pages Guide](AUTO_MAN_PAGES.md)
|
|
||||||
- [Manual Man Pages](man/README.md)
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The migration to clap provides:
|
|
||||||
- Better developer experience
|
|
||||||
- Auto-generated, always-in-sync documentation
|
|
||||||
- Reduced maintenance burden
|
|
||||||
- Professional-quality help output and man pages
|
|
||||||
- Foundation for future enhancements (completions, subcommands)
|
|
||||||
|
|
||||||
All with **zero breaking changes** to the existing CLI.
|
|
||||||
@ -1,200 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Install socktop binaries and man pages
|
|
||||||
# This script builds the binaries, generates man pages, and installs everything
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
|
|
||||||
# Color output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
print_header() {
|
|
||||||
echo -e "${BLUE}==>${NC} ${1}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}✓${NC} ${1}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}✗${NC} ${1}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}!${NC} ${1}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
SYSTEM_INSTALL=false
|
|
||||||
MAN_ONLY=false
|
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--system)
|
|
||||||
SYSTEM_INSTALL=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--man-only)
|
|
||||||
MAN_ONLY=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--help|-h)
|
|
||||||
echo "Usage: $0 [OPTIONS]"
|
|
||||||
echo ""
|
|
||||||
echo "Build and install socktop binaries and man pages"
|
|
||||||
echo ""
|
|
||||||
echo "Options:"
|
|
||||||
echo " --system Install system-wide (requires sudo)"
|
|
||||||
echo " --man-only Only install man pages (skip binary build)"
|
|
||||||
echo " --help Show this help message"
|
|
||||||
echo ""
|
|
||||||
echo "Examples:"
|
|
||||||
echo " $0 # Build and install for current user"
|
|
||||||
echo " sudo $0 --system # Build and install system-wide"
|
|
||||||
echo " $0 --man-only # Only install man pages"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
print_error "Unknown option: $1"
|
|
||||||
echo "Run '$0 --help' for usage information"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
cd "$PROJECT_ROOT"
|
|
||||||
|
|
||||||
# Step 1: Build binaries (unless --man-only)
|
|
||||||
if [ "$MAN_ONLY" = false ]; then
|
|
||||||
print_header "Building binaries..."
|
|
||||||
cargo build --release
|
|
||||||
print_success "Binaries built successfully"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 2: Extract generated man pages from OUT_DIR
|
|
||||||
print_header "Extracting generated man pages..."
|
|
||||||
|
|
||||||
# Find the build output directory
|
|
||||||
SOCKTOP_OUT_DIR=$(find target/release/build/socktop-*/out -type d -name "man" 2>/dev/null | head -1)
|
|
||||||
AGENT_OUT_DIR=$(find target/release/build/socktop_agent-*/out -type d -name "man" 2>/dev/null | head -1)
|
|
||||||
|
|
||||||
if [ -z "$SOCKTOP_OUT_DIR" ] || [ -z "$AGENT_OUT_DIR" ]; then
|
|
||||||
print_error "Generated man pages not found. Building to generate them..."
|
|
||||||
cargo build --release
|
|
||||||
SOCKTOP_OUT_DIR=$(find target/release/build/socktop-*/out -type d -name "man" 2>/dev/null | head -1)
|
|
||||||
AGENT_OUT_DIR=$(find target/release/build/socktop_agent-*/out -type d -name "man" 2>/dev/null | head -1)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$SOCKTOP_OUT_DIR" ] || [ ! -f "$SOCKTOP_OUT_DIR/socktop.1" ]; then
|
|
||||||
print_error "Failed to find generated socktop.1 man page"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$AGENT_OUT_DIR" ] || [ ! -f "$AGENT_OUT_DIR/socktop_agent.1" ]; then
|
|
||||||
print_error "Failed to find generated socktop_agent.1 man page"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_success "Found generated man pages"
|
|
||||||
|
|
||||||
# Step 3: Determine installation directories
|
|
||||||
if [ "$SYSTEM_INSTALL" = true ]; then
|
|
||||||
if [ "$EUID" -ne 0 ]; then
|
|
||||||
print_error "System-wide installation requires root privileges"
|
|
||||||
echo "Please run with sudo: sudo $0 --system"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
BIN_DIR="/usr/local/bin"
|
|
||||||
MAN_DIR="/usr/local/share/man/man1"
|
|
||||||
else
|
|
||||||
BIN_DIR="$HOME/.cargo/bin"
|
|
||||||
MAN_DIR="$HOME/.local/share/man/man1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 4: Install binaries (unless --man-only)
|
|
||||||
if [ "$MAN_ONLY" = false ]; then
|
|
||||||
print_header "Installing binaries to $BIN_DIR..."
|
|
||||||
|
|
||||||
if [ "$SYSTEM_INSTALL" = true ]; then
|
|
||||||
install -m 755 target/release/socktop "$BIN_DIR/socktop"
|
|
||||||
install -m 755 target/release/socktop_agent "$BIN_DIR/socktop_agent"
|
|
||||||
else
|
|
||||||
# For user install, cargo already puts binaries in ~/.cargo/bin
|
|
||||||
# But we can copy from release if needed
|
|
||||||
if [ ! -f "$BIN_DIR/socktop" ]; then
|
|
||||||
cp target/release/socktop "$BIN_DIR/"
|
|
||||||
chmod 755 "$BIN_DIR/socktop"
|
|
||||||
fi
|
|
||||||
if [ ! -f "$BIN_DIR/socktop_agent" ]; then
|
|
||||||
cp target/release/socktop_agent "$BIN_DIR/"
|
|
||||||
chmod 755 "$BIN_DIR/socktop_agent"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_success "Binaries installed to $BIN_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 5: Install man pages
|
|
||||||
print_header "Installing man pages to $MAN_DIR..."
|
|
||||||
|
|
||||||
mkdir -p "$MAN_DIR"
|
|
||||||
|
|
||||||
if [ "$SYSTEM_INSTALL" = true ]; then
|
|
||||||
install -m 644 "$SOCKTOP_OUT_DIR/socktop.1" "$MAN_DIR/socktop.1"
|
|
||||||
install -m 644 "$AGENT_OUT_DIR/socktop_agent.1" "$MAN_DIR/socktop_agent.1"
|
|
||||||
else
|
|
||||||
cp "$SOCKTOP_OUT_DIR/socktop.1" "$MAN_DIR/socktop.1"
|
|
||||||
cp "$AGENT_OUT_DIR/socktop_agent.1" "$MAN_DIR/socktop_agent.1"
|
|
||||||
chmod 644 "$MAN_DIR/socktop.1"
|
|
||||||
chmod 644 "$MAN_DIR/socktop_agent.1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_success "Man pages installed to $MAN_DIR"
|
|
||||||
|
|
||||||
# Update man database if available
|
|
||||||
if [ "$SYSTEM_INSTALL" = true ]; then
|
|
||||||
if command -v mandb &>/dev/null; then
|
|
||||||
print_header "Updating man database..."
|
|
||||||
mandb 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Final summary
|
|
||||||
echo ""
|
|
||||||
print_success "Installation complete!"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [ "$MAN_ONLY" = false ]; then
|
|
||||||
echo "Binaries installed:"
|
|
||||||
echo " socktop -> $BIN_DIR/socktop"
|
|
||||||
echo " socktop_agent -> $BIN_DIR/socktop_agent"
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Man pages installed:"
|
|
||||||
echo " socktop(1) -> $MAN_DIR/socktop.1"
|
|
||||||
echo " socktop_agent(1) -> $MAN_DIR/socktop_agent.1"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "Try it out:"
|
|
||||||
if [ "$MAN_ONLY" = false ]; then
|
|
||||||
echo " socktop --help"
|
|
||||||
echo " socktop_agent --help"
|
|
||||||
fi
|
|
||||||
echo " man socktop"
|
|
||||||
echo " man socktop_agent"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check if MANPATH needs updating for user install
|
|
||||||
if [ "$SYSTEM_INSTALL" = false ]; then
|
|
||||||
if ! man -w socktop &>/dev/null 2>&1; then
|
|
||||||
print_warning "If 'man socktop' doesn't work, add to your shell rc file:"
|
|
||||||
echo " export MANPATH=\"\$HOME/.local/share/man:\$MANPATH\""
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
@ -8,9 +8,6 @@ license = "MIT"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# CLI parsing and man page generation
|
|
||||||
clap = { version = "4.5", features = ["derive", "cargo", "wrap_help"] }
|
|
||||||
|
|
||||||
# socktop connector for agent communication
|
# socktop connector for agent communication
|
||||||
socktop_connector = "1.50.0"
|
socktop_connector = "1.50.0"
|
||||||
|
|
||||||
@ -25,10 +22,6 @@ anyhow = { workspace = true }
|
|||||||
dirs-next = { workspace = true }
|
dirs-next = { workspace = true }
|
||||||
sysinfo = { workspace = true }
|
sysinfo = { workspace = true }
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
clap = { version = "4.5", features = ["derive", "cargo"] }
|
|
||||||
clap_mangen = "0.2"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2.0"
|
assert_cmd = "2.0"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
use clap::CommandFactory;
|
|
||||||
use clap_mangen::Man;
|
|
||||||
use std::fs;
|
|
||||||
use std::io::Result;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
include!("src/cli.rs");
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
println!("cargo:rerun-if-changed=src/cli.rs");
|
|
||||||
|
|
||||||
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
|
||||||
let man_dir = out_dir.join("man");
|
|
||||||
fs::create_dir_all(&man_dir)?;
|
|
||||||
|
|
||||||
// Generate man page for socktop
|
|
||||||
let cmd = Cli::command();
|
|
||||||
let man = Man::new(cmd);
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
man.render(&mut buffer)?;
|
|
||||||
|
|
||||||
fs::write(man_dir.join("socktop.1"), buffer)?;
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"cargo:warning=Man page generated at {:?}",
|
|
||||||
man_dir.join("socktop.1")
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
// CLI argument definitions using clap derive macros.
|
|
||||||
// This file is also included by build.rs for man page generation.
|
|
||||||
|
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(
|
|
||||||
name = "socktop",
|
|
||||||
version,
|
|
||||||
author,
|
|
||||||
about = "Remote system monitor with a rich TUI over WebSocket",
|
|
||||||
long_about = "socktop is a remote system monitor with a rich terminal user interface (TUI), \
|
|
||||||
inspired by top/btop. It connects to a lightweight socktop_agent over WebSockets \
|
|
||||||
to display real-time system metrics including CPU usage, memory, swap, disk usage, \
|
|
||||||
network throughput, temperatures, GPU metrics, and a sortable process table.\n\n\
|
|
||||||
The agent is request-driven with near-zero CPU usage when idle."
|
|
||||||
)]
|
|
||||||
pub struct Cli {
|
|
||||||
/// WebSocket URL to connect to (e.g., ws://192.168.1.100:8080/ws or wss://host:8443/ws)
|
|
||||||
#[arg(value_name = "URL")]
|
|
||||||
pub url: Option<String>,
|
|
||||||
|
|
||||||
/// Path to TLS certificate PEM file for WSS connections
|
|
||||||
///
|
|
||||||
/// The certificate is pinned for security. The agent auto-generates
|
|
||||||
/// a self-signed certificate on first run.
|
|
||||||
#[arg(short = 't', long = "tls-ca", value_name = "CERT_PEM")]
|
|
||||||
pub tls_ca: Option<String>,
|
|
||||||
|
|
||||||
/// Enable hostname (SAN) verification for TLS connections
|
|
||||||
///
|
|
||||||
/// By default, hostname verification is skipped for easier home network usage,
|
|
||||||
/// but the certificate is still pinned.
|
|
||||||
#[arg(long)]
|
|
||||||
pub verify_hostname: bool,
|
|
||||||
|
|
||||||
/// Use a named connection profile
|
|
||||||
///
|
|
||||||
/// Profiles are stored in ~/.config/socktop/profiles.json and can contain
|
|
||||||
/// URL, TLS settings, and polling intervals.
|
|
||||||
#[arg(short = 'P', long = "profile", value_name = "NAME")]
|
|
||||||
pub profile: Option<String>,
|
|
||||||
|
|
||||||
/// Save the current connection as a named profile
|
|
||||||
///
|
|
||||||
/// Use with --profile to specify the profile name.
|
|
||||||
#[arg(long)]
|
|
||||||
pub save: bool,
|
|
||||||
|
|
||||||
/// Run in demo mode using mock data without connecting to an agent
|
|
||||||
///
|
|
||||||
/// Useful for testing the UI without a running agent.
|
|
||||||
#[arg(long)]
|
|
||||||
pub demo: bool,
|
|
||||||
|
|
||||||
/// Set the metrics polling interval in milliseconds
|
|
||||||
///
|
|
||||||
/// Default is typically 1000ms. Lower values increase update frequency
|
|
||||||
/// but also CPU usage.
|
|
||||||
#[arg(long, value_name = "MS")]
|
|
||||||
pub metrics_interval_ms: Option<u64>,
|
|
||||||
|
|
||||||
/// Set the process list polling interval in milliseconds
|
|
||||||
///
|
|
||||||
/// Can be different from metrics interval to reduce overhead.
|
|
||||||
#[arg(long, value_name = "MS")]
|
|
||||||
pub processes_interval_ms: Option<u64>,
|
|
||||||
|
|
||||||
/// Hidden test helper: skip connecting
|
|
||||||
#[arg(long, hide = true)]
|
|
||||||
pub dry_run: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cli {
|
|
||||||
/// Parse CLI arguments from environment
|
|
||||||
pub fn parse_args() -> Self {
|
|
||||||
Cli::parse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_basic_parsing() {
|
|
||||||
let cli = Cli::try_parse_from(&["socktop", "ws://localhost:8080/ws"]).unwrap();
|
|
||||||
assert_eq!(cli.url, Some("ws://localhost:8080/ws".to_string()));
|
|
||||||
assert!(!cli.demo);
|
|
||||||
assert!(!cli.save);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_tls_options() {
|
|
||||||
let cli = Cli::try_parse_from(&[
|
|
||||||
"socktop",
|
|
||||||
"-t",
|
|
||||||
"/path/to/cert.pem",
|
|
||||||
"--verify-hostname",
|
|
||||||
"wss://example.com:8443/ws",
|
|
||||||
])
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(cli.tls_ca, Some("/path/to/cert.pem".to_string()));
|
|
||||||
assert!(cli.verify_hostname);
|
|
||||||
assert_eq!(cli.url, Some("wss://example.com:8443/ws".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_profile_options() {
|
|
||||||
let cli = Cli::try_parse_from(&["socktop", "-P", "myprofile", "--save"]).unwrap();
|
|
||||||
assert_eq!(cli.profile, Some("myprofile".to_string()));
|
|
||||||
assert!(cli.save);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_intervals() {
|
|
||||||
let cli = Cli::try_parse_from(&[
|
|
||||||
"socktop",
|
|
||||||
"--metrics-interval-ms",
|
|
||||||
"500",
|
|
||||||
"--processes-interval-ms",
|
|
||||||
"2000",
|
|
||||||
"ws://localhost:8080/ws",
|
|
||||||
])
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(cli.metrics_interval_ms, Some(500));
|
|
||||||
assert_eq!(cli.processes_interval_ms, Some(2000));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_demo_mode() {
|
|
||||||
let cli = Cli::try_parse_from(&["socktop", "--demo"]).unwrap();
|
|
||||||
assert!(cli.demo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +1,139 @@
|
|||||||
//! Entry point for the socktop TUI. Parses args and runs the App.
|
//! Entry point for the socktop TUI. Parses args and runs the App.
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod cli;
|
|
||||||
mod history;
|
mod history;
|
||||||
mod profiles;
|
mod profiles;
|
||||||
mod retry;
|
mod retry;
|
||||||
mod types;
|
mod types;
|
||||||
mod ui;
|
mod ui; // pure retry timing logic
|
||||||
|
|
||||||
use app::App;
|
use app::App;
|
||||||
use cli::Cli;
|
|
||||||
use profiles::{ProfileEntry, ProfileRequest, ResolveProfile, load_profiles, save_profiles};
|
use profiles::{ProfileEntry, ProfileRequest, ResolveProfile, load_profiles, save_profiles};
|
||||||
|
use std::env;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
pub(crate) struct ParsedArgs {
|
||||||
|
url: Option<String>,
|
||||||
|
tls_ca: Option<String>,
|
||||||
|
profile: Option<String>,
|
||||||
|
save: bool,
|
||||||
|
demo: bool,
|
||||||
|
dry_run: bool, // hidden test helper: skip connecting
|
||||||
|
metrics_interval_ms: Option<u64>,
|
||||||
|
processes_interval_ms: Option<u64>,
|
||||||
|
verify_hostname: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, String> {
|
||||||
|
let mut it = args.into_iter();
|
||||||
|
let prog = it.next().unwrap_or_else(|| "socktop".into());
|
||||||
|
let mut url: Option<String> = None;
|
||||||
|
let mut tls_ca: Option<String> = None;
|
||||||
|
let mut profile: Option<String> = None;
|
||||||
|
let mut save = false;
|
||||||
|
let mut demo = false;
|
||||||
|
let mut dry_run = false;
|
||||||
|
let mut metrics_interval_ms: Option<u64> = None;
|
||||||
|
let mut processes_interval_ms: Option<u64> = None;
|
||||||
|
let mut verify_hostname = 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] [--metrics-interval-ms N] [--processes-interval-ms N] [ws://HOST:PORT/ws]\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"--tls-ca" | "-t" => {
|
||||||
|
tls_ca = it.next();
|
||||||
|
}
|
||||||
|
"--verify-hostname" => {
|
||||||
|
// opt-in hostname (SAN) verification
|
||||||
|
// default behavior is to skip it for easier home network usage
|
||||||
|
// (still pins the provided certificate)
|
||||||
|
verify_hostname = true;
|
||||||
|
}
|
||||||
|
"--profile" | "-P" => {
|
||||||
|
profile = it.next();
|
||||||
|
}
|
||||||
|
"--save" => {
|
||||||
|
save = true;
|
||||||
|
}
|
||||||
|
"--demo" => {
|
||||||
|
demo = true;
|
||||||
|
}
|
||||||
|
"--dry-run" => {
|
||||||
|
// intentionally undocumented
|
||||||
|
dry_run = true;
|
||||||
|
}
|
||||||
|
"--metrics-interval-ms" => {
|
||||||
|
metrics_interval_ms = it.next().and_then(|v| v.parse().ok());
|
||||||
|
}
|
||||||
|
"--processes-interval-ms" => {
|
||||||
|
processes_interval_ms = it.next().and_then(|v| v.parse().ok());
|
||||||
|
}
|
||||||
|
_ if arg.starts_with("--tls-ca=") => {
|
||||||
|
if let Some((_, v)) = arg.split_once('=')
|
||||||
|
&& !v.is_empty()
|
||||||
|
{
|
||||||
|
tls_ca = Some(v.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ if arg.starts_with("--profile=") => {
|
||||||
|
if let Some((_, v)) = arg.split_once('=')
|
||||||
|
&& !v.is_empty()
|
||||||
|
{
|
||||||
|
profile = Some(v.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ if arg.starts_with("--metrics-interval-ms=") => {
|
||||||
|
if let Some((_, v)) = arg.split_once('=') {
|
||||||
|
metrics_interval_ms = v.parse().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ if arg.starts_with("--processes-interval-ms=") => {
|
||||||
|
if let Some((_, v)) = arg.split_once('=') {
|
||||||
|
processes_interval_ms = v.parse().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
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] [ws://HOST:PORT/ws]"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ParsedArgs {
|
||||||
|
url,
|
||||||
|
tls_ca,
|
||||||
|
profile,
|
||||||
|
save,
|
||||||
|
demo,
|
||||||
|
dry_run,
|
||||||
|
metrics_interval_ms,
|
||||||
|
processes_interval_ms,
|
||||||
|
verify_hostname,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let parsed = Cli::parse_args();
|
let parsed = match parse_args(env::args()) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(msg) => {
|
||||||
|
eprintln!("{msg}");
|
||||||
|
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")) {
|
if parsed.demo || matches!(parsed.profile.as_deref(), Some("demo")) {
|
||||||
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "socktop_agent"
|
name = "socktop_agent"
|
||||||
version = "1.50.1"
|
version = "1.50.2"
|
||||||
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
|
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
|
||||||
description = "Socktop agent daemon. Serves host metrics over WebSocket."
|
description = "Socktop agent daemon. Serves host metrics over WebSocket."
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
@ -8,9 +8,6 @@ license = "MIT"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# CLI parsing and man page generation
|
|
||||||
clap = { version = "4.5", features = ["derive", "cargo", "wrap_help", "env"] }
|
|
||||||
|
|
||||||
# Tokio: Use minimal features instead of "full" to reduce binary size
|
# Tokio: Use minimal features instead of "full" to reduce binary size
|
||||||
# Only include: rt-multi-thread (async runtime), net (WebSocket), sync (Mutex/RwLock), macros (#[tokio::test])
|
# Only include: rt-multi-thread (async runtime), net (WebSocket), sync (Mutex/RwLock), macros (#[tokio::test])
|
||||||
# Excluded: io, fs, process, signal, time (not needed for this workload)
|
# Excluded: io, fs, process, signal, time (not needed for this workload)
|
||||||
@ -27,7 +24,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = tr
|
|||||||
gfxinfo = "0.1.2"
|
gfxinfo = "0.1.2"
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
axum-server = { version = "0.7", features = ["tls-rustls"] }
|
axum-server = { version = "0.7", features = ["tls-rustls"] }
|
||||||
rustls = "0.23"
|
rustls = { version = "0.23", features = ["aws-lc-rs"] }
|
||||||
rustls-pemfile = "2.1"
|
rustls-pemfile = "2.1"
|
||||||
rcgen = "0.13"
|
rcgen = "0.13"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
@ -40,8 +37,6 @@ default = []
|
|||||||
logging = ["tracing", "tracing-subscriber"]
|
logging = ["tracing", "tracing-subscriber"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
clap = { version = "4.5", features = ["derive", "cargo", "env"] }
|
|
||||||
clap_mangen = "0.2"
|
|
||||||
prost-build = "0.13"
|
prost-build = "0.13"
|
||||||
tonic-build = { version = "0.12", default-features = false, optional = true }
|
tonic-build = { version = "0.12", default-features = false, optional = true }
|
||||||
protoc-bin-vendored = "3"
|
protoc-bin-vendored = "3"
|
||||||
|
|||||||
@ -1,16 +1,8 @@
|
|||||||
use clap::CommandFactory;
|
|
||||||
use clap_mangen::Man;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
include!("src/cli.rs");
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Vendored protoc for reproducible builds
|
// Vendored protoc for reproducible builds
|
||||||
let protoc = protoc_bin_vendored::protoc_bin_path().expect("protoc");
|
let protoc = protoc_bin_vendored::protoc_bin_path().expect("protoc");
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=proto/processes.proto");
|
println!("cargo:rerun-if-changed=proto/processes.proto");
|
||||||
println!("cargo:rerun-if-changed=src/cli.rs");
|
|
||||||
|
|
||||||
// Compile protobuf definitions for processes
|
// Compile protobuf definitions for processes
|
||||||
let mut cfg = prost_build::Config::new();
|
let mut cfg = prost_build::Config::new();
|
||||||
@ -19,28 +11,4 @@ fn main() {
|
|||||||
// Use local path (ensures file is inside published crate tarball)
|
// Use local path (ensures file is inside published crate tarball)
|
||||||
cfg.compile_protos(&["proto/processes.proto"], &["proto"]) // relative to CARGO_MANIFEST_DIR
|
cfg.compile_protos(&["proto/processes.proto"], &["proto"]) // relative to CARGO_MANIFEST_DIR
|
||||||
.expect("compile protos");
|
.expect("compile protos");
|
||||||
|
|
||||||
// Generate man page
|
|
||||||
generate_man_page().expect("man page generation failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_man_page() -> std::io::Result<()> {
|
|
||||||
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
|
||||||
let man_dir = out_dir.join("man");
|
|
||||||
fs::create_dir_all(&man_dir)?;
|
|
||||||
|
|
||||||
// Generate man page for socktop_agent
|
|
||||||
let cmd = Cli::command();
|
|
||||||
let man = Man::new(cmd);
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
man.render(&mut buffer)?;
|
|
||||||
|
|
||||||
fs::write(man_dir.join("socktop_agent.1"), buffer)?;
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"cargo:warning=Man page generated at {:?}",
|
|
||||||
man_dir.join("socktop_agent.1")
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,105 +0,0 @@
|
|||||||
// CLI argument definitions for socktop_agent using clap derive macros.
|
|
||||||
// This file is also included by build.rs for man page generation.
|
|
||||||
|
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(
|
|
||||||
name = "socktop_agent",
|
|
||||||
version,
|
|
||||||
author,
|
|
||||||
about = "Lightweight WebSocket server for remote system monitoring",
|
|
||||||
long_about = "socktop_agent is a lightweight Rust-based WebSocket server that provides system \
|
|
||||||
metrics on demand. It serves metrics to socktop clients over WebSocket connections \
|
|
||||||
at the /ws endpoint.\n\n\
|
|
||||||
The agent is request-driven with near-zero CPU usage when idle. It collects metrics \
|
|
||||||
only when clients request them over WebSocket, eliminating the need for background \
|
|
||||||
sampling loops. This design results in minimal resource consumption, making it ideal \
|
|
||||||
for resource-constrained systems like Raspberry Pi.\n\n\
|
|
||||||
Metrics include: CPU (overall and per-core), memory, swap, disk usage, network \
|
|
||||||
throughput, CPU temperatures, top processes, and optional GPU metrics."
|
|
||||||
)]
|
|
||||||
pub struct Cli {
|
|
||||||
/// Port number to listen on
|
|
||||||
///
|
|
||||||
/// Default is 3000 for non-TLS mode and 8443 for TLS mode.
|
|
||||||
/// Can also be set via SOCKTOP_PORT environment variable.
|
|
||||||
#[arg(short = 'p', long = "port", value_name = "PORT", env = "SOCKTOP_PORT")]
|
|
||||||
pub port: Option<u16>,
|
|
||||||
|
|
||||||
/// Enable TLS (secure WebSocket) mode
|
|
||||||
///
|
|
||||||
/// The agent will listen on wss:// instead of ws://.
|
|
||||||
/// On first run with TLS enabled, the agent automatically generates
|
|
||||||
/// a self-signed certificate and private key.
|
|
||||||
/// Can also be enabled via SOCKTOP_ENABLE_SSL=1 environment variable.
|
|
||||||
#[arg(long = "enableSSL", env = "SOCKTOP_ENABLE_SSL", value_parser = parse_bool_env)]
|
|
||||||
pub enable_ssl: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse boolean from environment variable (accepts "1" or "true")
|
|
||||||
fn parse_bool_env(s: &str) -> Result<bool, String> {
|
|
||||||
match s {
|
|
||||||
"1" | "true" | "TRUE" | "True" => Ok(true),
|
|
||||||
"0" | "false" | "FALSE" | "False" => Ok(false),
|
|
||||||
_ => Err(format!("Invalid boolean value: {}", s)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cli {
|
|
||||||
/// Parse CLI arguments from environment
|
|
||||||
pub fn parse_args() -> Self {
|
|
||||||
Cli::parse()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the port to listen on, with appropriate defaults
|
|
||||||
pub fn get_port(&self) -> u16 {
|
|
||||||
if let Some(port) = self.port {
|
|
||||||
port
|
|
||||||
} else if self.enable_ssl {
|
|
||||||
8443
|
|
||||||
} else {
|
|
||||||
3000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_default() {
|
|
||||||
let cli = Cli::try_parse_from(&["socktop_agent"]).unwrap();
|
|
||||||
assert_eq!(cli.port, None);
|
|
||||||
assert!(!cli.enable_ssl);
|
|
||||||
assert_eq!(cli.get_port(), 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_custom_port() {
|
|
||||||
let cli = Cli::try_parse_from(&["socktop_agent", "--port", "8080"]).unwrap();
|
|
||||||
assert_eq!(cli.port, Some(8080));
|
|
||||||
assert_eq!(cli.get_port(), 8080);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_short_port() {
|
|
||||||
let cli = Cli::try_parse_from(&["socktop_agent", "-p", "9000"]).unwrap();
|
|
||||||
assert_eq!(cli.port, Some(9000));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_enable_ssl() {
|
|
||||||
let cli = Cli::try_parse_from(&["socktop_agent", "--enableSSL"]).unwrap();
|
|
||||||
assert!(cli.enable_ssl);
|
|
||||||
assert_eq!(cli.get_port(), 8443); // Default TLS port
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ssl_with_custom_port() {
|
|
||||||
let cli = Cli::try_parse_from(&["socktop_agent", "--enableSSL", "-p", "9443"]).unwrap();
|
|
||||||
assert!(cli.enable_ssl);
|
|
||||||
assert_eq!(cli.get_port(), 9443);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
//! socktop agent entrypoint: sets up sysinfo handles and serves a WebSocket endpoint at /ws.
|
//! socktop agent entrypoint: sets up sysinfo handles and serves a WebSocket endpoint at /ws.
|
||||||
|
|
||||||
mod cli;
|
|
||||||
mod gpu;
|
mod gpu;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
mod proto;
|
mod proto;
|
||||||
@ -15,10 +14,28 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
mod tls;
|
mod tls;
|
||||||
|
|
||||||
use cli::Cli;
|
|
||||||
use state::AppState;
|
use state::AppState;
|
||||||
|
|
||||||
|
fn arg_flag(name: &str) -> bool {
|
||||||
|
std::env::args().any(|a| a == name)
|
||||||
|
}
|
||||||
|
fn arg_value(name: &str) -> Option<String> {
|
||||||
|
let mut it = std::env::args();
|
||||||
|
while let Some(a) = it.next() {
|
||||||
|
if a == name {
|
||||||
|
return it.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
|
// Install rustls crypto provider before any TLS operations
|
||||||
|
// This is required when using axum-server's tls-rustls feature
|
||||||
|
rustls::crypto::aws_lc_rs::default_provider()
|
||||||
|
.install_default()
|
||||||
|
.ok(); // Ignore error if already installed
|
||||||
|
|
||||||
#[cfg(feature = "logging")]
|
#[cfg(feature = "logging")]
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
@ -59,8 +76,11 @@ fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn async_main() -> anyhow::Result<()> {
|
async fn async_main() -> anyhow::Result<()> {
|
||||||
// Parse CLI arguments
|
// Version flag (print and exit). Keep before heavy initialization.
|
||||||
let cli = Cli::parse_args();
|
if arg_flag("--version") || arg_flag("-V") {
|
||||||
|
println!("socktop_agent {}", env!("CARGO_PKG_VERSION"));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let state = AppState::new();
|
let state = AppState::new();
|
||||||
|
|
||||||
@ -76,8 +96,15 @@ async fn async_main() -> anyhow::Result<()> {
|
|||||||
.route("/healthz", get(healthz))
|
.route("/healthz", get(healthz))
|
||||||
.with_state(state.clone());
|
.with_state(state.clone());
|
||||||
|
|
||||||
if cli.enable_ssl {
|
let enable_ssl =
|
||||||
let port = cli.get_port();
|
arg_flag("--enableSSL") || std::env::var("SOCKTOP_ENABLE_SSL").ok().as_deref() == Some("1");
|
||||||
|
if enable_ssl {
|
||||||
|
// Port can be overridden by --port or SOCKTOP_PORT; default to 8443 when SSL
|
||||||
|
let port = arg_value("--port")
|
||||||
|
.or_else(|| arg_value("-p"))
|
||||||
|
.or_else(|| std::env::var("SOCKTOP_PORT").ok())
|
||||||
|
.and_then(|s| s.parse::<u16>().ok())
|
||||||
|
.unwrap_or(8443);
|
||||||
|
|
||||||
let (cert_path, key_path) = tls::ensure_self_signed_cert()?;
|
let (cert_path, key_path) = tls::ensure_self_signed_cert()?;
|
||||||
let cfg = axum_server::tls_rustls::RustlsConfig::from_pem_file(cert_path, key_path).await?;
|
let cfg = axum_server::tls_rustls::RustlsConfig::from_pem_file(cert_path, key_path).await?;
|
||||||
@ -91,7 +118,11 @@ async fn async_main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Non-TLS HTTP/WS path
|
// Non-TLS HTTP/WS path
|
||||||
let port = cli.get_port();
|
let port = arg_value("--port")
|
||||||
|
.or_else(|| arg_value("-p"))
|
||||||
|
.or_else(|| std::env::var("SOCKTOP_PORT").ok())
|
||||||
|
.and_then(|s| s.parse::<u16>().ok())
|
||||||
|
.unwrap_or(3000);
|
||||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||||
println!("socktop_agent: Listening on ws://{addr}/ws");
|
println!("socktop_agent: Listening on ws://{addr}/ws");
|
||||||
axum_server::bind(addr)
|
axum_server::bind(addr)
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
use assert_cmd::prelude::*;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@ -17,7 +16,7 @@ fn generates_self_signed_cert_and_key_in_xdg_path() {
|
|||||||
let xdg = tmpdir.path().to_path_buf();
|
let xdg = tmpdir.path().to_path_buf();
|
||||||
|
|
||||||
// Run the agent once with --enableSSL, short timeout so it exits quickly when killed
|
// Run the agent once with --enableSSL, short timeout so it exits quickly when killed
|
||||||
let mut cmd = Command::cargo_bin("socktop_agent").expect("binary exists");
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("socktop_agent"));
|
||||||
// Bind to an ephemeral port (-p 0) to avoid conflicts/flakes
|
// Bind to an ephemeral port (-p 0) to avoid conflicts/flakes
|
||||||
cmd.env("XDG_CONFIG_HOME", &xdg)
|
cmd.env("XDG_CONFIG_HOME", &xdg)
|
||||||
.arg("--enableSSL")
|
.arg("--enableSSL")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user