Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92c503ad84 | |||
| f5d267c08a | |||
| 4eddb19d59 | |||
| 745a681de7 | |||
| a9366d069d | |||
| bf1b4f70c3 | |||
| f73e198a66 | |||
| f7b095eb4a | |||
| 180186e2cc | |||
| cc167f71d3 | |||
| dd1dbdf29b | |||
| 524af0d123 | |||
| 619c288e9e | |||
| e04e344e2c | |||
| 8c531f9981 | |||
| 7fe302a3e2 | |||
| 31c2b59fd4 | |||
| f9462a1633 | |||
| 512913e897 | |||
| 64b641368c | |||
| f4b6faffaa | |||
| 4b9d11dc9e | |||
| c80f8cc363 | |||
| ed3d43ff7a | |||
| 350611b3b1 |
@@ -0,0 +1,420 @@
|
||||
name: Build Debian Packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- feature/debian-packaging
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build-deb:
|
||||
name: Build .deb for ${{ matrix.target }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
arch: amd64
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
arch: arm64
|
||||
- target: armv7-unknown-linux-gnueabihf
|
||||
arch: armhf
|
||||
- target: riscv64gc-unknown-linux-gnu
|
||||
arch: riscv64
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Install cargo-deb
|
||||
run: cargo install cargo-deb
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dpkg-dev
|
||||
|
||||
- name: Install cross-compilation tools (ARM64)
|
||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
run: |
|
||||
sudo dpkg --add-architecture arm64
|
||||
# Disable all existing sources and create new ones with proper arch specifications
|
||||
sudo mv /etc/apt/sources.list /etc/apt/sources.list.backup
|
||||
sudo mv /etc/apt/sources.list.d /etc/apt/sources.list.d.backup || true
|
||||
sudo mkdir -p /etc/apt/sources.list.d
|
||||
# Clear APT cache and lists
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
sudo mkdir -p /var/lib/apt/lists/partial
|
||||
# Create new sources.list with both amd64 and arm64
|
||||
cat << EOF | sudo tee /etc/apt/sources.list
|
||||
deb [arch=amd64] http://archive.ubuntu.com/ubuntu $(lsb_release -sc) main universe restricted multiverse
|
||||
deb [arch=amd64] http://archive.ubuntu.com/ubuntu $(lsb_release -sc)-updates main universe restricted multiverse
|
||||
deb [arch=amd64] http://archive.ubuntu.com/ubuntu $(lsb_release -sc)-backports main universe restricted multiverse
|
||||
deb [arch=amd64] http://security.ubuntu.com/ubuntu $(lsb_release -sc)-security main universe restricted multiverse
|
||||
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports $(lsb_release -sc) main universe restricted multiverse
|
||||
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports $(lsb_release -sc)-updates main universe restricted multiverse
|
||||
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports $(lsb_release -sc)-backports main universe restricted multiverse
|
||||
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports $(lsb_release -sc)-security main universe restricted multiverse
|
||||
EOF
|
||||
echo "=== Contents of /etc/apt/sources.list ==="
|
||||
cat /etc/apt/sources.list
|
||||
echo "=== Contents of /etc/apt/sources.list.d/ ==="
|
||||
ls -la /etc/apt/sources.list.d/ || true
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu libdrm-dev:arm64 libdrm-amdgpu1:arm64
|
||||
|
||||
- name: Install cross-compilation tools (ARMhf)
|
||||
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-arm-linux-gnueabihf
|
||||
|
||||
- name: Install cross-compilation tools (RISC-V)
|
||||
if: matrix.target == 'riscv64gc-unknown-linux-gnu'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-riscv64-linux-gnu
|
||||
|
||||
- name: Install GPU libraries (x86_64)
|
||||
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libdrm-dev libdrm-amdgpu1
|
||||
|
||||
- name: Configure cross-compilation (ARM64)
|
||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
run: |
|
||||
mkdir -p .cargo
|
||||
cat >> .cargo/config.toml << EOF
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
EOF
|
||||
|
||||
- name: Configure cross-compilation (ARMhf)
|
||||
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
||||
run: |
|
||||
mkdir -p .cargo
|
||||
cat >> .cargo/config.toml << EOF
|
||||
[target.armv7-unknown-linux-gnueabihf]
|
||||
linker = "arm-linux-gnueabihf-gcc"
|
||||
EOF
|
||||
|
||||
- name: Configure cross-compilation (RISC-V)
|
||||
if: matrix.target == 'riscv64gc-unknown-linux-gnu'
|
||||
run: |
|
||||
mkdir -p .cargo
|
||||
cat >> .cargo/config.toml << EOF
|
||||
[target.riscv64gc-unknown-linux-gnu]
|
||||
linker = "riscv64-linux-gnu-gcc"
|
||||
EOF
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache target directory
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-target-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Build socktop .deb package
|
||||
run: |
|
||||
cargo deb --package socktop --target ${{ matrix.target }} --no-strip
|
||||
|
||||
- name: Build socktop_agent .deb package (with GPU support)
|
||||
if: matrix.target == 'x86_64-unknown-linux-gnu' || matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
run: |
|
||||
cargo deb --package socktop_agent --target ${{ matrix.target }} --no-strip
|
||||
|
||||
- name: Build socktop_agent .deb package (without GPU support)
|
||||
if: matrix.target == 'armv7-unknown-linux-gnueabihf' || matrix.target == 'riscv64gc-unknown-linux-gnu'
|
||||
run: |
|
||||
cargo deb --package socktop_agent --target ${{ matrix.target }} --no-strip --no-default-features
|
||||
|
||||
- name: Copy packages to debs directory
|
||||
run: |
|
||||
mkdir -p debs
|
||||
cp target/${{ matrix.target }}/debian/*.deb debs/
|
||||
|
||||
- name: List generated packages
|
||||
run: ls -lh debs/
|
||||
|
||||
- name: Upload .deb packages as artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: debian-packages-${{ matrix.arch }}
|
||||
path: debs/*.deb
|
||||
if-no-files-found: error
|
||||
retention-days: 90
|
||||
|
||||
# Combine all artifacts into a single downloadable archive
|
||||
combine-artifacts:
|
||||
name: Combine all .deb packages
|
||||
needs: build-deb
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download AMD64 packages
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: debian-packages-amd64
|
||||
path: all-debs
|
||||
|
||||
- name: Download ARM64 packages
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: debian-packages-arm64
|
||||
path: all-debs
|
||||
|
||||
- name: Download ARMhf packages
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: debian-packages-armhf
|
||||
path: all-debs
|
||||
|
||||
- name: Download RISC-V packages
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: debian-packages-riscv64
|
||||
path: all-debs
|
||||
|
||||
- name: List all packages
|
||||
run: |
|
||||
echo "All generated .deb packages:"
|
||||
ls -lh all-debs/
|
||||
|
||||
- name: Upload combined artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: all-debian-packages
|
||||
path: all-debs/*.deb
|
||||
if-no-files-found: error
|
||||
retention-days: 90
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
cd all-debs
|
||||
sha256sum *.deb > SHA256SUMS
|
||||
cat SHA256SUMS
|
||||
|
||||
- name: Upload checksums
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: checksums
|
||||
path: all-debs/SHA256SUMS
|
||||
retention-days: 90
|
||||
|
||||
# Publish packages to gh-pages APT repository
|
||||
publish-apt-repo:
|
||||
name: Publish to APT Repository
|
||||
needs: combine-artifacts
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download all packages
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: all-debian-packages
|
||||
path: debs
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dpkg-dev gpg
|
||||
|
||||
- name: Checkout gh-pages branch
|
||||
run: |
|
||||
git fetch origin gh-pages:gh-pages || echo "gh-pages branch doesn't exist yet"
|
||||
if git show-ref --verify --quiet refs/heads/gh-pages; then
|
||||
git checkout gh-pages
|
||||
else
|
||||
git checkout --orphan gh-pages
|
||||
git rm -rf . 2>/dev/null || true
|
||||
# Create basic structure
|
||||
mkdir -p dists/stable/main/{binary-amd64,binary-arm64,binary-armhf,binary-riscv64}
|
||||
mkdir -p pool/main
|
||||
fi
|
||||
|
||||
- name: Copy packages to pool
|
||||
run: |
|
||||
mkdir -p pool/main
|
||||
cp debs/*.deb pool/main/
|
||||
ls -lh pool/main/
|
||||
|
||||
- name: Generate Packages files
|
||||
run: |
|
||||
for arch in amd64 arm64 armhf riscv64; do
|
||||
mkdir -p dists/stable/main/binary-$arch
|
||||
dpkg-scanpackages --arch $arch pool/main /dev/null > dists/stable/main/binary-$arch/Packages 2>/dev/null || true
|
||||
if [ -s dists/stable/main/binary-$arch/Packages ]; then
|
||||
gzip -9 -k -f dists/stable/main/binary-$arch/Packages
|
||||
echo "Generated Packages file for $arch"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Generate Release file
|
||||
run: |
|
||||
cat > dists/stable/Release << EOF
|
||||
Origin: socktop
|
||||
Label: socktop
|
||||
Suite: stable
|
||||
Codename: stable
|
||||
Architectures: amd64 arm64 armhf riscv64
|
||||
Components: main
|
||||
Description: socktop APT repository
|
||||
Date: $(date -Ru)
|
||||
EOF
|
||||
|
||||
# Add MD5Sum
|
||||
echo "MD5Sum:" >> dists/stable/Release
|
||||
for arch in amd64 arm64 armhf riscv64; do
|
||||
for file in dists/stable/main/binary-$arch/Packages*; do
|
||||
if [ -f "$file" ]; then
|
||||
md5sum "$file" | awk '{print " " $1, "'$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null)'", "'"${file#dists/stable/}"'"}' >> dists/stable/Release
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# Add SHA256
|
||||
echo "SHA256:" >> dists/stable/Release
|
||||
for arch in amd64 arm64 armhf riscv64; do
|
||||
for file in dists/stable/main/binary-$arch/Packages*; do
|
||||
if [ -f "$file" ]; then
|
||||
sha256sum "$file" | awk '{print " " $1, "'$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null)'", "'"${file#dists/stable/}"'"}' >> dists/stable/Release
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
- name: Set GPG available flag
|
||||
id: check_gpg
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
run: |
|
||||
if [ -n "$GPG_PRIVATE_KEY" ]; then
|
||||
echo "available=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "available=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Import GPG key
|
||||
if: steps.check_gpg.outputs.available == 'true'
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
run: |
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
gpg --list-secret-keys
|
||||
|
||||
- name: Sign repository
|
||||
if: steps.check_gpg.outputs.available == 'true'
|
||||
env:
|
||||
GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
run: |
|
||||
if [ -n "$GPG_PASSPHRASE" ]; then
|
||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --no-tty --pinentry-mode loopback --passphrase-fd 0 \
|
||||
--default-key "$GPG_KEY_ID" \
|
||||
-abs -o dists/stable/Release.gpg dists/stable/Release
|
||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --no-tty --pinentry-mode loopback --passphrase-fd 0 \
|
||||
--default-key "$GPG_KEY_ID" \
|
||||
--clearsign -o dists/stable/InRelease dists/stable/Release
|
||||
else
|
||||
gpg --batch --yes --no-tty --pinentry-mode loopback \
|
||||
--default-key "$GPG_KEY_ID" \
|
||||
-abs -o dists/stable/Release.gpg dists/stable/Release
|
||||
gpg --batch --yes --no-tty --pinentry-mode loopback \
|
||||
--default-key "$GPG_KEY_ID" \
|
||||
--clearsign -o dists/stable/InRelease dists/stable/Release
|
||||
fi
|
||||
gpg --armor --export "$GPG_KEY_ID" > KEY.gpg
|
||||
echo "✓ Repository signed"
|
||||
|
||||
- name: Create unsigned repository notice
|
||||
if: steps.check_gpg.outputs.available == 'false'
|
||||
run: |
|
||||
echo "⚠️ Warning: GPG_PRIVATE_KEY not set. Repository will be UNSIGNED."
|
||||
echo "⚠️ Add GPG secrets to sign the repository automatically."
|
||||
echo "To add secrets: Settings → Secrets and variables → Actions → Repository secrets"
|
||||
|
||||
- name: Copy index.html if exists
|
||||
run: |
|
||||
git checkout ${{ github.ref_name }} -- index.html 2>/dev/null || echo "No index.html in source branch"
|
||||
|
||||
- name: Commit and push to gh-pages
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "actions@github.com"
|
||||
git add .
|
||||
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
COMMIT_MSG="Update APT repository"
|
||||
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||
COMMIT_MSG="$COMMIT_MSG - Release ${{ github.ref_name }}"
|
||||
else
|
||||
COMMIT_MSG="$COMMIT_MSG - $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
|
||||
fi
|
||||
git commit -m "$COMMIT_MSG"
|
||||
git push origin gh-pages
|
||||
echo "✓ Published to gh-pages"
|
||||
fi
|
||||
|
||||
# Optional: Create a release with the .deb files if this is a tag
|
||||
create-release:
|
||||
name: Create GitHub Release
|
||||
needs: combine-artifacts
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download all packages
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: all-debian-packages
|
||||
path: release-debs
|
||||
|
||||
- name: Download checksums
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: checksums
|
||||
path: release-debs
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: release-debs/*
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,7 +1,16 @@
|
||||
/target
|
||||
.vscode/
|
||||
/socktop-wasm-test/target
|
||||
/.cargo/
|
||||
|
||||
# Documentation files from development sessions (context-specific, not for public repo)
|
||||
/OPTIMIZATION_PROCESS_DETAILS.md
|
||||
/THREAD_SUPPORT.md
|
||||
|
||||
# APT Repository - Safety: Never commit private keys!
|
||||
*.asc
|
||||
*-private.key
|
||||
*-secret.key
|
||||
gpg-private-backup.key
|
||||
secring.gpg
|
||||
# Note: Release.gpg, InRelease, and KEY.gpg (public) ARE safe to commit
|
||||
|
||||
Generated
+1
-1
@@ -2060,7 +2060,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socktop_agent"
|
||||
version = "1.50.1"
|
||||
version = "1.50.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
# Debian Packaging Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
Successfully implemented Debian packaging for socktop using `cargo-deb`, with GitHub Actions automation for building packages for both AMD64 and ARM64 architectures.
|
||||
|
||||
## Branches Created
|
||||
|
||||
1. **`feature/debian-packaging`** - Main branch with debian packaging implementation
|
||||
2. **`feature/man-pages`** - Separate branch for man pages work (to be researched further)
|
||||
|
||||
## What Was Added
|
||||
|
||||
### 1. Cargo.toml Updates
|
||||
|
||||
Both `socktop/Cargo.toml` and `socktop_agent/Cargo.toml` were updated with:
|
||||
- `[package.metadata.deb]` sections
|
||||
- Package metadata (maintainer, description, dependencies)
|
||||
- Asset definitions (binaries, documentation)
|
||||
- Systemd service configuration (agent only)
|
||||
|
||||
### 2. Systemd Service
|
||||
|
||||
**File**: `socktop_agent/socktop-agent.service`
|
||||
- Runs as `socktop` user/group
|
||||
- Listens on port 3000 by default
|
||||
- Security hardening enabled
|
||||
- Disabled by default (user must explicitly enable)
|
||||
|
||||
### 3. Maintainer Scripts
|
||||
|
||||
**Directory**: `socktop_agent/debian/`
|
||||
|
||||
- **`postinst`**: Creates `socktop` user/group, sets up `/var/lib/socktop` directory
|
||||
- **`postrm`**: Cleanup on package removal/purge
|
||||
|
||||
### 4. GitHub Actions Workflow
|
||||
|
||||
**File**: `.github/workflows/build-deb.yml`
|
||||
|
||||
Features:
|
||||
- Builds for both x86_64 and ARM64
|
||||
- Triggered on:
|
||||
- Push to `master` or `feature/debian-packaging`
|
||||
- Pull requests to `master`
|
||||
- Version tags (v*)
|
||||
- Manual workflow dispatch
|
||||
- Creates artifacts:
|
||||
- `debian-packages-amd64`
|
||||
- `debian-packages-arm64`
|
||||
- `all-debian-packages` (combined)
|
||||
- `checksums` (SHA256SUMS)
|
||||
- Automatic GitHub releases for version tags
|
||||
|
||||
### 5. Documentation
|
||||
|
||||
**File**: `docs/DEBIAN_PACKAGING.md`
|
||||
|
||||
Comprehensive guide covering:
|
||||
- Building packages locally
|
||||
- Cross-compilation for ARM64
|
||||
- Installation and configuration
|
||||
- Using GitHub Actions artifacts
|
||||
- Creating local APT repositories
|
||||
- Troubleshooting
|
||||
|
||||
## Package Details
|
||||
|
||||
### socktop (TUI Client)
|
||||
- **Binary**: `/usr/bin/socktop`
|
||||
- **Size**: ~3.5 MB (x86_64)
|
||||
- **Dependencies**: Auto-detected
|
||||
|
||||
### socktop_agent (Daemon)
|
||||
- **Binary**: `/usr/bin/socktop_agent`
|
||||
- **Service**: `socktop-agent.service`
|
||||
- **User/Group**: `socktop` (created automatically)
|
||||
- **State directory**: `/var/lib/socktop`
|
||||
- **Size**: ~6.7 MB (x86_64)
|
||||
- **Dependencies**: Auto-detected
|
||||
|
||||
## Testing
|
||||
|
||||
Both packages successfully built locally:
|
||||
```
|
||||
✓ socktop_1.50.0-1_amd64.deb
|
||||
✓ socktop-agent_1.50.1-1_amd64.deb
|
||||
```
|
||||
|
||||
Verified:
|
||||
- Package contents (dpkg -c)
|
||||
- Package metadata (dpkg -I)
|
||||
- Systemd service file inclusion
|
||||
- Maintainer scripts inclusion
|
||||
- Documentation inclusion
|
||||
|
||||
## Usage
|
||||
|
||||
### For Users
|
||||
|
||||
Download pre-built packages from GitHub Actions artifacts:
|
||||
1. Go to Actions tab
|
||||
2. Select latest "Build Debian Packages" run
|
||||
3. Download architecture-specific artifact
|
||||
4. Install: `sudo dpkg -i socktop*.deb`
|
||||
|
||||
### For Developers
|
||||
|
||||
Build locally:
|
||||
```bash
|
||||
cargo install cargo-deb
|
||||
cargo deb --package socktop
|
||||
cargo deb --package socktop_agent
|
||||
```
|
||||
|
||||
Cross-compile for ARM64:
|
||||
```bash
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
sudo apt install gcc-aarch64-linux-gnu libc6-dev-arm64-cross
|
||||
cargo deb --package socktop --target aarch64-unknown-linux-gnu
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
To get packages in official APT repositories:
|
||||
|
||||
1. **Short term**: Host packages on GitHub Releases (automated)
|
||||
2. **Medium term**: Create PPA for Ubuntu users
|
||||
3. **Long term**: Submit to Debian/Ubuntu official repositories
|
||||
|
||||
## Files Modified/Created
|
||||
|
||||
```
|
||||
Modified:
|
||||
socktop/Cargo.toml
|
||||
socktop_agent/Cargo.toml
|
||||
|
||||
Created:
|
||||
.github/workflows/build-deb.yml
|
||||
docs/DEBIAN_PACKAGING.md
|
||||
socktop_agent/socktop-agent.service
|
||||
socktop_agent/debian/postinst
|
||||
socktop_agent/debian/postrm
|
||||
```
|
||||
|
||||
## Commit
|
||||
|
||||
```
|
||||
532ed16 Add Debian packaging support with cargo-deb
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [cargo-deb documentation](https://github.com/kornelski/cargo-deb)
|
||||
- [Debian Policy Manual](https://www.debian.org/doc/debian-policy/)
|
||||
- Full documentation in `docs/DEBIAN_PACKAGING.md`
|
||||
@@ -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
|
||||
@@ -51,15 +51,23 @@ 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)
|
||||
### Raspberry Pi / Ubuntu / PopOS (required for GPU support)
|
||||
|
||||
Install GPU support with apt command below
|
||||
**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)_
|
||||
|
||||
---
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,42 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQGNBGkih7QBDADgX6sYMx2Lp6qcZxeCCizcy4TFsxcRJfp5mfbMplVES0hQToIP
|
||||
EMC11JqPwQdLliXKjUr8Z2kgM2oqvH+dkdgzUGrw6kTK8YHc+qs37iJAOVS9D72X
|
||||
tTld282NrtFwzb74nS2GKPkpWI7aSKBpHtWFPX/1ONsc56qGqFd3wwikEvCz8MeJ
|
||||
HwCD1JZ9F+2DyyXWsTJNgDwPloJSUbtyVuk2gd6PeTg7AQdx92Pk/mggmYbHtP8N
|
||||
wy072ku1g8K/hplmwIOGpSx1JWvAQkDU/Bb/jSqrYg2wSHO7IQnYE8I3x/zglYBl
|
||||
FYNh47TVQr0zPVSYR1MQkHU5YLBTDc5UgDvtcsYUiTtq4D/m8HWmKja0/UKGxvDJ
|
||||
P5sUPcp4dk77RdoCtUe5HImYGS8lo5N3+t0lz8sd9rYmRiIO4f7FJaJqJeHbUJyn
|
||||
iw/GCQh5D5/D571dICrEq/QhL+k5KhJljPGoVMGPFXJIc7q+CxvGp2oOo5fOlbOn
|
||||
3kSrM93AJPwT8FMAEQEAAbRFSmFzb24gV2l0dHkgKHNvY2t0b3AgYXB0IHNpZ25p
|
||||
bmcga2V5KSA8amFzb25wd2l0dHkrc29ja3RvcEBwcm90b24ubWU+iQHOBBMBCgA4
|
||||
FiEEHnVWqAU5uDlLwoINESwaeYRl+/IFAmkih7QCGwMFCwkIBwIGFQoJCAsCBBYC
|
||||
AwECHgECF4AACgkQESwaeYRl+/KV+gwAzfZVZEhO7MQV2EmNeKVK1GycFSm2oUAl
|
||||
ZbwNIEHu6+tOzqXJb8o65BtGlbLSGavsMpgRCK2SL83DdLOkutG1ahQiJr+5GaXC
|
||||
zbQgX+VWqGPZtQ+I6/rVoYZPMTCrqpAmFgvVpqv0xod7w8/wny8/XmhQ37KY2/0l
|
||||
B38oNTvdA7C8jzSrI6kr3XqurvQRW7z+MnC+nCp9Ob9bYtY0kpd4U3NrVdb8m32U
|
||||
d5LVFwD1OGvzLOSqyJ33IKjSJc4KLvW+aEsHXe+fHO9UEzH8Nbo5MmVvX3QIHiyq
|
||||
jD4zN16AGsGYqCK4irtQCiD3wBOdsG/RVkgIcdlmAH3EGEp7Ux8+7v1PXYI+UrSs
|
||||
XE7f1xFTJ2r5TMex6W3he073Em4qhQsrnMF5syTZsM6N+5UqXVOM1RuDVVXr7929
|
||||
hC3G8pK/A2W5Lwpxl2yzock2CxhvUn7M/xm4VbcPlWTCUd/QzU8VtsgaGHcuhi5e
|
||||
xHY1AU07STLB9RinjBVf2bmk4oDQcmB6uQGNBGkih7QBDACrjE+xSWP92n931/5t
|
||||
+tXcujwFlIpSZdbSQFr0B0YyjPRUP4FSzEGu8vuM5ChUfWKhmN1dDr5C4qFo9NgQ
|
||||
6oCN2HubajSGyXNwnOMlMb5ck79Ubmy9yDV9/ZLqpJJiozGap2/EnNoDhaANlmUg
|
||||
rfqUHpIB8XC2IZ0Itt05tp/u78dJiB+R6ReZn/bVUafNV4jIqYZfLRzI3FTJ4xvK
|
||||
FGs/ER+JajAdJQ8LPfazmDQSGw0huguxhopZwKQ/qWZMn1OHq/ZaPvCqbQt3irLw
|
||||
dLPDC4pEaYGRyADYeyuarG0DVyUQ9XRc/NufKDvOAn33LpBPBpcvNQAsVhWTCYl7
|
||||
ogQ+suVYVN8Tu7v4bUSHKwzXKvLN/ojJX/Fh7eTW4TPsgLHNHAEDUkSQozIe9vO6
|
||||
o+vydDqRxuXJgdkR7lqP6PQDYrhRYZGJf57eKf6VtTKYFaMbiMWPU+vcHeB0/iDe
|
||||
Pv81qro2LD2PG5WCzDpNETBceCTjykb9r0VHx4/JsiojKmsAEQEAAYkBtgQYAQoA
|
||||
IBYhBB51VqgFObg5S8KCDREsGnmEZfvyBQJpIoe0AhsMAAoJEBEsGnmEZfvyNp8M
|
||||
AIH+6+hGB3qADdnhNgb+3fN0511eK9Uk82lxgGARLcD8GN1UP0HlvEqkxCHy3PUe
|
||||
tHcsuYVz7i8pmpEGdFx9zv7MelenUsJniUQ++OZKx6iUG/MYqz//NxY+5lyRmcu2
|
||||
aYvUxhkgf9zgxXTkTyV2VV32mX//cHcwc+c/089QAPzCMaSrHdNK+ED9+k8uquJ1
|
||||
lSL9Bm15z/EV42v9Q/4KTM5OBLHpNw0Rvn9C0iuZVwHXBrrA/HSGXpA54AqNUMpZ
|
||||
kRPgLQcy5yVE2y1aXLXt2XdTn6YPzrAjNoazYYuCWHYIZU7dGkIswpsDirDLKHdD
|
||||
onb3VShmSpemYjsuFiqhfi6qwCkeHsz/CpQAp70SZ+z9oB8H80PJVKPbPIP3zEf3
|
||||
i7bcsqHA7stF+8sJclXgxBUBeDJ3O2jN/scBOcvNA6xoRp7+oJbnjDRuxBmh+fVg
|
||||
TIuw2++vTF2Ml0EMv7ePTpr7b1DofuJRNYGkuAIMVXHjLTqMiTJUce3OUy003zMg
|
||||
Dg==
|
||||
=AaPQ
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -0,0 +1,38 @@
|
||||
# socktop APT Repository
|
||||
|
||||
This repository contains Debian packages for socktop and socktop-agent.
|
||||
|
||||
## Adding this repository
|
||||
|
||||
Add the repository to your system:
|
||||
|
||||
```bash
|
||||
# Add the GPG key
|
||||
curl -fsSL https://jasonwitty.github.io/socktop/KEY.gpg | sudo gpg --dearmor -o /usr/share/keyrings/socktop-archive-keyring.gpg
|
||||
|
||||
# Add the repository
|
||||
echo "deb [signed-by=/usr/share/keyrings/socktop-archive-keyring.gpg] https://jasonwitty.github.io/socktop stable main" | sudo tee /etc/apt/sources.list.d/socktop.list
|
||||
|
||||
# Update and install
|
||||
sudo apt update
|
||||
sudo apt install socktop socktop-agent
|
||||
```
|
||||
|
||||
## Manual Installation
|
||||
|
||||
You can also download and install packages manually from the `pool/main/` directory.
|
||||
|
||||
```bash
|
||||
wget https://jasonwitty.github.io/socktop/pool/main/socktop_VERSION_ARCH.deb
|
||||
sudo dpkg -i socktop_VERSION_ARCH.deb
|
||||
```
|
||||
|
||||
## Supported Architectures
|
||||
|
||||
- amd64 (x86_64)
|
||||
- arm64 (aarch64)
|
||||
- armhf (32-bit ARM)
|
||||
|
||||
## Building from Source
|
||||
|
||||
See the main repository at https://github.com/jasonwitty/socktop
|
||||
@@ -0,0 +1,32 @@
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
Origin: socktop
|
||||
Label: socktop
|
||||
Suite: stable
|
||||
Codename: stable
|
||||
Architectures: amd64 arm64 armhf
|
||||
Components: main
|
||||
Description: socktop APT repository
|
||||
Date: Sun, 23 Nov 2025 04:05:21 +0000
|
||||
MD5Sum:
|
||||
0bddefb2f13cb7c86cd05fe1ce20310f 1549 main/binary-amd64/Packages
|
||||
674f0e552cbb7dc65380651a2a8d279e 799 main/binary-amd64/Packages.gz
|
||||
SHA256:
|
||||
babfbb4839e7fdfbc83742c16996791b0402a1315889b530330b338380398263 1549 main/binary-amd64/Packages
|
||||
f8c48d0f7bf53eb02c6dbf5f1cdd046fe71b87273cf763c5bb2e95d9757a7a82 799 main/binary-amd64/Packages.gz
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQGzBAEBCgAdFiEEHnVWqAU5uDlLwoINESwaeYRl+/IFAmkiiAYACgkQESwaeYRl
|
||||
+/KBsAv/eYhnK/XrNtPhLyw/zX2cGfUtBsBZrypFhV/n+TvudAIwQaqxDEvLlBUn
|
||||
HBAhMKDQXGs7V45+nOgDX4rKWUqJh4SPbJgNbVte2PX7U+hsMpZBsYp3vkjApgTO
|
||||
pq2CCkViyBXgTY+6vUigtvfJ9afTTWI6Qm4dLXZ7hxErBxgHQyowOoO/sF92cNOu
|
||||
AosBMpE+qSy7sVqJU5g/JXJh0kddKFotXHSGA1kFMzJafJC/n5nLrusDzFJRQqyH
|
||||
Io+6inYWjlb5o79z0tJzAvG1mgplLRppMBjoVJ/RJ+gT+QE70kokR6wvsgDqsKNd
|
||||
mvB0TNj0zY0g6Is6V3XMyf0u+6BtLTbua913HPiqBfErgeV58vzsst+y0It42TXi
|
||||
aw+UF2Kw/YhPq1rZFxgnAVcMja3qlXWpH57gmgIPovBCsPsiywWiHLsSHRzAI22b
|
||||
zeTsUST/4toR/ruZVbUZvWoWAR4tzsSuwXJFx/hhinTQQTNHErXASOX986UaL9L7
|
||||
o2/pTKLe
|
||||
=IeBY
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -0,0 +1,14 @@
|
||||
Origin: socktop
|
||||
Label: socktop
|
||||
Suite: stable
|
||||
Codename: stable
|
||||
Architectures: amd64 arm64 armhf
|
||||
Components: main
|
||||
Description: socktop APT repository
|
||||
Date: Sun, 23 Nov 2025 04:05:21 +0000
|
||||
MD5Sum:
|
||||
0bddefb2f13cb7c86cd05fe1ce20310f 1549 main/binary-amd64/Packages
|
||||
674f0e552cbb7dc65380651a2a8d279e 799 main/binary-amd64/Packages.gz
|
||||
SHA256:
|
||||
babfbb4839e7fdfbc83742c16996791b0402a1315889b530330b338380398263 1549 main/binary-amd64/Packages
|
||||
f8c48d0f7bf53eb02c6dbf5f1cdd046fe71b87273cf763c5bb2e95d9757a7a82 799 main/binary-amd64/Packages.gz
|
||||
@@ -0,0 +1,14 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQGzBAABCgAdFiEEHnVWqAU5uDlLwoINESwaeYRl+/IFAmkiiAEACgkQESwaeYRl
|
||||
+/KzeAv+OUIbxud5FboerwpAJULV+rS3+VX4kvwg/daVZ3yX3tJNrsyNCHgmWLVu
|
||||
fLeEFFc2Ax9GvFW4jrbxRAGD+3TXQEEFkb5lGzYyDjlgVzR6wLiVTTrmzWoK+cbB
|
||||
4DMozqeLiZFfQjq4UFn3+mwiYFX9Dj7PVF0M60XAUJSObbJFmaEPZIfx6wcZfkiL
|
||||
lLLk1eeU5MPiyudPOhVGgaD76KrUCw+8DBNKoCKIEcCY0LvuKtUK8mWYXRSPSved
|
||||
4Znd3QZz063Z6R+Lj1XlGLoTPResna28T/Nca+2JgLhbrihsLMcHoFxmrvFP9FpT
|
||||
MChKngj7NnGt0yqHH5J16hdwMra/vvhmF0yoQ0loIcy+q06tYEqOcau8tvAjfbId
|
||||
k3rgQgnxxVE8WUmV9Bugp7jhNMO+ImKWMwzEr6wGd9ZHqpknUlAaWeO73VP+qtAN
|
||||
6mEqWhkqvXGg+srH6qp3Sg0W28dYG29X3Kx8jOp7HeyvA/gLZRN7L+bq/XaA7WFA
|
||||
1hba6LIY
|
||||
=QoLf
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -0,0 +1,38 @@
|
||||
Package: socktop
|
||||
Version: 1.50.0-1
|
||||
Architecture: amd64
|
||||
Maintainer: Jason Witty <jasonpwitty+socktop@proton.me>
|
||||
Installed-Size: 3459
|
||||
Filename: pool/main/socktop_1.50.0-1_amd64.deb
|
||||
Size: 1278940
|
||||
MD5sum: 0215e178e306d9379669065e8c78582b
|
||||
SHA1: 04e0416389f5cecd584fd1f6b3568711f2645eee
|
||||
SHA256: 69eb04b1de48541c95950a97b16357fcd9c51ffaceb143f63de4a9d758fad297
|
||||
Section: admin
|
||||
Priority: optional
|
||||
Homepage: https://github.com/jasonwitty/socktop
|
||||
Description: Remote system monitor over WebSocket, TUI like top
|
||||
socktop is a remote system monitor with a rich terminal user interface (TUI)
|
||||
that connects to remote hosts running the socktop_agent over WebSocket. It
|
||||
provides real-time monitoring of CPU, memory, processes, and more with an
|
||||
interface similar to the traditional 'top' command.
|
||||
|
||||
Package: socktop-agent
|
||||
Version: 1.50.2-1
|
||||
Architecture: amd64
|
||||
Maintainer: Jason Witty <jasonpwitty+socktop@proton.me>
|
||||
Installed-Size: 6793
|
||||
Filename: pool/main/socktop-agent_1.50.2-1_amd64.deb
|
||||
Size: 1896272
|
||||
MD5sum: 22e78d03e83dcf84d6ec4a009b285902
|
||||
SHA1: 26a9f4fedfdba06a047044027223f2944cf72ba6
|
||||
SHA256: 11922af475146f60347a9c52cff4bbce1ce524bdb4293b2c436f3c71876e17d5
|
||||
Section: admin
|
||||
Priority: optional
|
||||
Homepage: https://github.com/jasonwitty/socktop
|
||||
Description: Socktop agent daemon. Serves host metrics over WebSocket.
|
||||
socktop_agent is the daemon component that runs on remote hosts to collect and
|
||||
serve system metrics over WebSocket. It gathers CPU, memory, disk, network,
|
||||
GPU, and process information that can be monitored remotely by the socktop TUI
|
||||
client.
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
Archive: stable
|
||||
Component: main
|
||||
Origin: socktop
|
||||
Label: socktop
|
||||
Architecture: amd64
|
||||
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>socktop APT Repository</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
code {
|
||||
background: #f4f4f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
pre {
|
||||
background: #f4f4f4;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
h1 { color: #333; }
|
||||
h2 { color: #555; margin-top: 30px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>socktop APT Repository</h1>
|
||||
<p>System monitor with remote agent support for Linux systems.</p>
|
||||
|
||||
<h2>Adding this repository</h2>
|
||||
<pre><code># Add the GPG key
|
||||
curl -fsSL https://jasonwitty.github.io/socktop/KEY.gpg | sudo gpg --dearmor -o /usr/share/keyrings/socktop-archive-keyring.gpg
|
||||
|
||||
# Add the repository
|
||||
echo "deb [signed-by=/usr/share/keyrings/socktop-archive-keyring.gpg] https://jasonwitty.github.io/socktop stable main" | sudo tee /etc/apt/sources.list.d/socktop.list
|
||||
|
||||
# Update and install
|
||||
sudo apt update
|
||||
sudo apt install socktop socktop-agent</code></pre>
|
||||
|
||||
<h2>Manual Installation</h2>
|
||||
<p>Download packages from <a href="pool/main/">pool/main/</a></p>
|
||||
|
||||
<h2>Supported Architectures</h2>
|
||||
<ul>
|
||||
<li>amd64 (x86_64)</li>
|
||||
<li>arm64 (aarch64)</li>
|
||||
<li>armhf (32-bit ARM)</li>
|
||||
</ul>
|
||||
|
||||
<h2>Source Code</h2>
|
||||
<p>Visit the <a href="https://github.com/jasonwitty/socktop">GitHub repository</a></p>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||
@@ -0,0 +1,274 @@
|
||||
# Debian Packaging for socktop
|
||||
|
||||
This document describes how to build and use Debian packages for socktop and socktop_agent.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Install `cargo-deb`:
|
||||
|
||||
```bash
|
||||
cargo install cargo-deb
|
||||
```
|
||||
|
||||
## Building Packages Locally
|
||||
|
||||
### Build for your current architecture (x86_64)
|
||||
|
||||
```bash
|
||||
# Build socktop TUI client
|
||||
cargo deb --package socktop
|
||||
|
||||
# Build socktop_agent daemon
|
||||
cargo deb --package socktop_agent
|
||||
```
|
||||
|
||||
The `.deb` files will be created in `target/debian/`.
|
||||
|
||||
### Cross-compile for ARM64 (Raspberry Pi, etc.)
|
||||
|
||||
First, install cross-compilation tools:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install gcc-aarch64-linux-gnu libc6-dev-arm64-cross
|
||||
```
|
||||
|
||||
Add the ARM64 target:
|
||||
|
||||
```bash
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
```
|
||||
|
||||
Configure the linker by creating `.cargo/config.toml`:
|
||||
|
||||
```toml
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
```
|
||||
|
||||
Build the packages:
|
||||
|
||||
```bash
|
||||
# Build for ARM64
|
||||
cargo deb --package socktop --target aarch64-unknown-linux-gnu
|
||||
cargo deb --package socktop_agent --target aarch64-unknown-linux-gnu
|
||||
```
|
||||
|
||||
## Installing Packages
|
||||
|
||||
### Install socktop TUI client
|
||||
|
||||
```bash
|
||||
sudo dpkg -i socktop_*.deb
|
||||
```
|
||||
|
||||
### Install socktop_agent daemon
|
||||
|
||||
```bash
|
||||
sudo dpkg -i socktop_agent_*.deb
|
||||
```
|
||||
|
||||
The agent package will:
|
||||
- Create a `socktop` system user and group
|
||||
- Install the binary to `/usr/bin/socktop_agent`
|
||||
- Install a systemd service file (disabled by default)
|
||||
- Create `/var/lib/socktop` for state files
|
||||
|
||||
### Enable and start the agent service
|
||||
|
||||
```bash
|
||||
# Enable to start on boot
|
||||
sudo systemctl enable socktop-agent
|
||||
|
||||
# Start the service
|
||||
sudo systemctl start socktop-agent
|
||||
|
||||
# Check status
|
||||
sudo systemctl status socktop-agent
|
||||
```
|
||||
|
||||
### Configure the agent
|
||||
|
||||
Edit the systemd service to customize settings:
|
||||
|
||||
```bash
|
||||
sudo systemctl edit socktop-agent
|
||||
```
|
||||
|
||||
Add configuration in the override section:
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
Environment=SOCKTOP_PORT=8080
|
||||
Environment=SOCKTOP_TOKEN=your-secret-token
|
||||
Environment=RUST_LOG=info
|
||||
```
|
||||
|
||||
Then restart:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart socktop-agent
|
||||
```
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
The project includes a GitHub Actions workflow (`.github/workflows/build-deb.yml`) that automatically builds `.deb` packages for both x86_64 and ARM64 architectures on every push to master or when tags are created.
|
||||
|
||||
### Downloading pre-built packages
|
||||
|
||||
1. Go to the [Actions tab](https://github.com/jasonwitty/socktop/actions)
|
||||
2. Click on the latest "Build Debian Packages" workflow run
|
||||
3. Download the artifacts:
|
||||
- `debian-packages-amd64` - x86_64 packages
|
||||
- `debian-packages-arm64` - ARM64 packages
|
||||
- `all-debian-packages` - All packages combined
|
||||
- `checksums` - SHA256 checksums
|
||||
|
||||
### Release packages
|
||||
|
||||
When you create a git tag starting with `v` (e.g., `v1.50.0`), the workflow will automatically create a GitHub Release with all `.deb` packages attached.
|
||||
|
||||
```bash
|
||||
git tag v1.50.0
|
||||
git push origin v1.50.0
|
||||
```
|
||||
|
||||
## Package Details
|
||||
|
||||
### socktop package
|
||||
|
||||
- **Binary**: `/usr/bin/socktop`
|
||||
- **Documentation**: `/usr/share/doc/socktop/`
|
||||
- **Size**: ~5-8 MB (depends on architecture)
|
||||
|
||||
### socktop_agent package
|
||||
|
||||
- **Binary**: `/usr/bin/socktop_agent`
|
||||
- **Service**: `socktop-agent.service`
|
||||
- **User/Group**: `socktop`
|
||||
- **State directory**: `/var/lib/socktop`
|
||||
- **Config directory**: `/etc/socktop` (created but empty by default)
|
||||
- **Documentation**: `/usr/share/doc/socktop_agent/`
|
||||
- **Size**: ~5-8 MB (depends on architecture)
|
||||
|
||||
## Uninstalling
|
||||
|
||||
```bash
|
||||
# Remove packages but keep configuration
|
||||
sudo apt remove socktop socktop_agent
|
||||
|
||||
# Remove packages and all configuration (purge)
|
||||
sudo apt purge socktop socktop_agent
|
||||
```
|
||||
|
||||
When purging `socktop_agent`, the following are removed:
|
||||
- The `socktop` user and group
|
||||
- `/var/lib/socktop` directory
|
||||
- Empty `/etc/socktop` directory (if empty)
|
||||
|
||||
## Verifying Packages
|
||||
|
||||
Check package contents:
|
||||
|
||||
```bash
|
||||
dpkg -c socktop_*.deb
|
||||
dpkg -c socktop_agent_*.deb
|
||||
```
|
||||
|
||||
Check package information:
|
||||
|
||||
```bash
|
||||
dpkg -I socktop_*.deb
|
||||
dpkg -I socktop_agent_*.deb
|
||||
```
|
||||
|
||||
After installation, verify files:
|
||||
|
||||
```bash
|
||||
dpkg -L socktop
|
||||
dpkg -L socktop-agent
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Service fails to start
|
||||
|
||||
Check logs:
|
||||
|
||||
```bash
|
||||
sudo journalctl -u socktop-agent -f
|
||||
```
|
||||
|
||||
Verify the socktop user exists:
|
||||
|
||||
```bash
|
||||
id socktop
|
||||
```
|
||||
|
||||
### Permission issues
|
||||
|
||||
Ensure the state directory has correct permissions:
|
||||
|
||||
```bash
|
||||
sudo chown -R socktop:socktop /var/lib/socktop
|
||||
sudo chmod 755 /var/lib/socktop
|
||||
```
|
||||
|
||||
### Missing dependencies
|
||||
|
||||
If installation fails due to missing dependencies:
|
||||
|
||||
```bash
|
||||
sudo apt --fix-broken install
|
||||
```
|
||||
|
||||
## Creating a Local APT Repository (Advanced)
|
||||
|
||||
To create your own APT repository for easy installation:
|
||||
|
||||
1. Install required tools:
|
||||
```bash
|
||||
sudo apt install dpkg-dev
|
||||
```
|
||||
|
||||
2. Create repository structure:
|
||||
```bash
|
||||
mkdir -p ~/socktop-repo/pool/main
|
||||
cp *.deb ~/socktop-repo/pool/main/
|
||||
```
|
||||
|
||||
3. Generate package index:
|
||||
```bash
|
||||
cd ~/socktop-repo
|
||||
dpkg-scanpackages pool/main /dev/null | gzip -9c > pool/main/Packages.gz
|
||||
```
|
||||
|
||||
4. Serve via HTTP (for testing):
|
||||
```bash
|
||||
cd ~/socktop-repo
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
|
||||
5. Add to sources on client machines:
|
||||
```bash
|
||||
echo "deb [trusted=yes] http://your-server:8000 pool/main/" | \
|
||||
sudo tee /etc/apt/sources.list.d/socktop.list
|
||||
sudo apt update
|
||||
sudo apt install socktop socktop-agent
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new features that affect packaging:
|
||||
|
||||
1. Update `Cargo.toml` metadata in the `[package.metadata.deb]` section
|
||||
2. Add new assets to the `assets` array if needed
|
||||
3. Update maintainer scripts in `socktop_agent/debian/` if needed
|
||||
4. Test package building locally before committing
|
||||
5. Update this documentation
|
||||
|
||||
## References
|
||||
|
||||
- [cargo-deb documentation](https://github.com/kornelski/cargo-deb)
|
||||
- [Debian Policy Manual](https://www.debian.org/doc/debian-policy/)
|
||||
- [systemd service files](https://www.freedesktop.org/software/systemd/man/systemd.service.html)
|
||||
+13
-10
@@ -2,6 +2,8 @@
|
||||
|
||||
This guide explains how to cross-compile the socktop_agent on various host systems and deploy it to a Raspberry Pi. Cross-compiling is particularly useful for older or resource-constrained Pi models where native compilation might be slow.
|
||||
|
||||
**Note:** GPU monitoring support is not available on ARMv7 (32-bit) and RISC-V architectures due to library limitations. When building for these platforms, the `--no-default-features` flag must be used to disable GPU support.
|
||||
|
||||
## Cross-Compilation Host Setup
|
||||
|
||||
Choose your host operating system:
|
||||
@@ -23,8 +25,9 @@ sudo apt update
|
||||
sudo apt install gcc-aarch64-linux-gnu libc6-dev-arm64-cross libdrm-dev:arm64
|
||||
|
||||
# For 32-bit Raspberry Pi (armv7)
|
||||
# Note: GPU support not available on armv7
|
||||
sudo apt update
|
||||
sudo apt install gcc-arm-linux-gnueabihf libc6-dev-armhf-cross libdrm-dev:armhf
|
||||
sudo apt install gcc-arm-linux-gnueabihf libc6-dev-armhf-cross
|
||||
```
|
||||
|
||||
### Setup Rust Cross-Compilation Targets
|
||||
@@ -65,9 +68,8 @@ sudo pacman -S aarch64-linux-gnu-gcc
|
||||
yay -S aarch64-linux-gnu-libdrm
|
||||
|
||||
# For 32-bit Raspberry Pi (armv7)
|
||||
# Note: GPU support not available on armv7
|
||||
sudo pacman -S arm-linux-gnueabihf-gcc
|
||||
# Install libdrm for armv7 using an AUR helper
|
||||
yay -S arm-linux-gnueabihf-libdrm
|
||||
```
|
||||
|
||||
### Setup Rust Cross-Compilation Targets
|
||||
@@ -114,8 +116,8 @@ cd path/to/socktop
|
||||
# For 64-bit Raspberry Pi
|
||||
docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:aarch64-musl cargo build --release --target aarch64-unknown-linux-musl -p socktop_agent
|
||||
|
||||
# For 32-bit Raspberry Pi
|
||||
docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:armv7-musleabihf cargo build --release --target armv7-unknown-linux-musleabihf -p socktop_agent
|
||||
# For 32-bit Raspberry Pi (without GPU support)
|
||||
docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:armv7-musleabihf cargo build --release --target armv7-unknown-linux-musleabihf -p socktop_agent --no-default-features
|
||||
```
|
||||
|
||||
The compiled binaries will be available in your local target directory.
|
||||
@@ -133,11 +135,11 @@ The recommended approach for Windows is to use Windows Subsystem for Linux (WSL2
|
||||
After setting up your environment, build the socktop_agent for your target Raspberry Pi:
|
||||
|
||||
```bash
|
||||
# For 64-bit Raspberry Pi
|
||||
# For 64-bit Raspberry Pi (with GPU support)
|
||||
cargo build --release --target aarch64-unknown-linux-gnu -p socktop_agent
|
||||
|
||||
# For 32-bit Raspberry Pi
|
||||
cargo build --release --target armv7-unknown-linux-gnueabihf -p socktop_agent
|
||||
# For 32-bit Raspberry Pi (without GPU support)
|
||||
cargo build --release --target armv7-unknown-linux-gnueabihf -p socktop_agent --no-default-features
|
||||
```
|
||||
|
||||
## Transfer the Binary to Your Raspberry Pi
|
||||
@@ -161,11 +163,12 @@ SSH into your Raspberry Pi and install the required dependencies:
|
||||
```bash
|
||||
ssh pi@raspberry-pi-ip
|
||||
|
||||
# For Raspberry Pi OS (Debian-based)
|
||||
# For Raspberry Pi OS (Debian-based) - 64-bit only
|
||||
# (32-bit armv7 builds don't require these)
|
||||
sudo apt update
|
||||
sudo apt install libdrm-dev libdrm-amdgpu1
|
||||
|
||||
# For Arch Linux ARM
|
||||
# For Arch Linux ARM - 64-bit only
|
||||
sudo pacman -Syu
|
||||
sudo pacman -S libdrm
|
||||
```
|
||||
|
||||
@@ -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
|
||||
+19
-7
@@ -6,11 +6,10 @@ description = "Remote system monitor over WebSocket, TUI like top"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/jasonwitty/socktop"
|
||||
repository = "https://github.com/jasonwitty/socktop"
|
||||
|
||||
[dependencies]
|
||||
# CLI parsing and man page generation
|
||||
clap = { version = "4.5", features = ["derive", "cargo", "wrap_help"] }
|
||||
|
||||
# socktop connector for agent communication
|
||||
socktop_connector = "1.50.0"
|
||||
|
||||
@@ -25,10 +24,23 @@ anyhow = { workspace = true }
|
||||
dirs-next = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
clap = { version = "4.5", features = ["derive", "cargo"] }
|
||||
clap_mangen = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0"
|
||||
tempfile = "3"
|
||||
|
||||
[package.metadata.deb]
|
||||
maintainer = "Jason Witty <jasonpwitty+socktop@proton.me>"
|
||||
copyright = "2024, Jason Witty <jasonpwitty+socktop@proton.me>"
|
||||
license-file = ["../LICENSE", "4"]
|
||||
extended-description = """\
|
||||
socktop is a remote system monitor with a rich terminal user interface (TUI) \
|
||||
that connects to remote hosts running the socktop_agent over WebSocket. \
|
||||
It provides real-time monitoring of CPU, memory, processes, and more with \
|
||||
an interface similar to the traditional 'top' command."""
|
||||
depends = "$auto"
|
||||
section = "admin"
|
||||
priority = "optional"
|
||||
assets = [
|
||||
["target/release/socktop", "usr/bin/", "755"],
|
||||
["../README.md", "usr/share/doc/socktop/", "644"],
|
||||
]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+122
-4
@@ -1,21 +1,139 @@
|
||||
//! Entry point for the socktop TUI. Parses args and runs the App.
|
||||
|
||||
mod app;
|
||||
mod cli;
|
||||
mod history;
|
||||
mod profiles;
|
||||
mod retry;
|
||||
mod types;
|
||||
mod ui;
|
||||
mod ui; // pure retry timing logic
|
||||
|
||||
use app::App;
|
||||
use cli::Cli;
|
||||
use profiles::{ProfileEntry, ProfileRequest, ResolveProfile, load_profiles, save_profiles};
|
||||
use std::env;
|
||||
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]
|
||||
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")) {
|
||||
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
[package]
|
||||
name = "socktop_agent"
|
||||
version = "1.50.1"
|
||||
version = "1.50.2"
|
||||
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
|
||||
description = "Socktop agent daemon. Serves host metrics over WebSocket."
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/jasonwitty/socktop"
|
||||
repository = "https://github.com/jasonwitty/socktop"
|
||||
|
||||
[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
|
||||
# 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)
|
||||
@@ -24,10 +23,10 @@ flate2 = { version = "1", default-features = false, features = ["rust_backend"]
|
||||
futures-util = "0.3.31"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true }
|
||||
gfxinfo = "0.1.2"
|
||||
gfxinfo = { version = "0.1.2", optional = true }
|
||||
once_cell = "1.19"
|
||||
axum-server = { version = "0.7", features = ["tls-rustls"] }
|
||||
rustls = "0.23"
|
||||
rustls = { version = "0.23", features = ["aws-lc-rs"] }
|
||||
rustls-pemfile = "2.1"
|
||||
rcgen = "0.13"
|
||||
anyhow = "1"
|
||||
@@ -36,12 +35,11 @@ prost = { workspace = true }
|
||||
time = { version = "0.3", default-features = false, features = ["formatting", "macros", "parsing" ] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["gpu"]
|
||||
gpu = ["gfxinfo"]
|
||||
logging = ["tracing", "tracing-subscriber"]
|
||||
|
||||
[build-dependencies]
|
||||
clap = { version = "4.5", features = ["derive", "cargo", "env"] }
|
||||
clap_mangen = "0.2"
|
||||
prost-build = "0.13"
|
||||
tonic-build = { version = "0.12", default-features = false, optional = true }
|
||||
protoc-bin-vendored = "3"
|
||||
@@ -50,3 +48,21 @@ protoc-bin-vendored = "3"
|
||||
assert_cmd = "2.0"
|
||||
tempfile = "3.10"
|
||||
tokio-tungstenite = "0.21"
|
||||
|
||||
[package.metadata.deb]
|
||||
maintainer = "Jason Witty <jasonpwitty+socktop@proton.me>"
|
||||
copyright = "2024, Jason Witty <jasonpwitty+socktop@proton.me>"
|
||||
license-file = ["../LICENSE", "4"]
|
||||
extended-description = """\
|
||||
socktop_agent is the daemon component that runs on remote hosts to collect \
|
||||
and serve system metrics over WebSocket. It gathers CPU, memory, disk, network, \
|
||||
GPU, and process information that can be monitored remotely by the socktop TUI client."""
|
||||
depends = "$auto"
|
||||
section = "admin"
|
||||
priority = "optional"
|
||||
assets = [
|
||||
["target/release/socktop_agent", "usr/bin/", "755"],
|
||||
["../README.md", "usr/share/doc/socktop_agent/", "644"],
|
||||
]
|
||||
maintainer-scripts = "debian/"
|
||||
systemd-units = { unit-name = "socktop-agent", unit-scripts = ".", enable = false }
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
use clap::CommandFactory;
|
||||
use clap_mangen::Man;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
include!("src/cli.rs");
|
||||
|
||||
fn main() {
|
||||
// Vendored protoc for reproducible builds
|
||||
let protoc = protoc_bin_vendored::protoc_bin_path().expect("protoc");
|
||||
|
||||
println!("cargo:rerun-if-changed=proto/processes.proto");
|
||||
println!("cargo:rerun-if-changed=src/cli.rs");
|
||||
|
||||
// Compile protobuf definitions for processes
|
||||
let mut cfg = prost_build::Config::new();
|
||||
@@ -19,28 +11,4 @@ fn main() {
|
||||
// Use local path (ensures file is inside published crate tarball)
|
||||
cfg.compile_protos(&["proto/processes.proto"], &["proto"]) // relative to CARGO_MANIFEST_DIR
|
||||
.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(())
|
||||
}
|
||||
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Create socktop user and group if they don't exist
|
||||
if ! getent group socktop >/dev/null; then
|
||||
addgroup --system socktop
|
||||
fi
|
||||
|
||||
if ! getent passwd socktop >/dev/null; then
|
||||
adduser --system --ingroup socktop --home /var/lib/socktop \
|
||||
--no-create-home --disabled-password --disabled-login \
|
||||
--gecos "Socktop Agent" socktop
|
||||
fi
|
||||
|
||||
# Create state directory
|
||||
mkdir -p /var/lib/socktop
|
||||
chown socktop:socktop /var/lib/socktop
|
||||
chmod 755 /var/lib/socktop
|
||||
|
||||
# Create config directory if it doesn't exist
|
||||
mkdir -p /etc/socktop
|
||||
chmod 755 /etc/socktop
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
# Print helpful message to the user
|
||||
cat <<EOF
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ socktop-agent has been installed successfully! │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ The systemd service has been installed but is NOT enabled by │
|
||||
│ default. To enable and start the service: │
|
||||
│ │
|
||||
│ sudo systemctl enable --now socktop-agent │
|
||||
│ │
|
||||
│ To start without enabling on boot: │
|
||||
│ │
|
||||
│ sudo systemctl start socktop-agent │
|
||||
│ │
|
||||
│ To check service status: │
|
||||
│ │
|
||||
│ sudo systemctl status socktop-agent │
|
||||
│ │
|
||||
│ Default settings: │
|
||||
│ - Port: 3000 (use -p or --port to change) │
|
||||
│ - SSL/TLS: disabled (use --enableSSL to enable) │
|
||||
│ │
|
||||
│ For more information, see: │
|
||||
│ /usr/share/doc/socktop_agent/README.md │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
EOF
|
||||
|
||||
exit 0
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
case "$1" in
|
||||
purge)
|
||||
# Remove user and group on purge
|
||||
if getent passwd socktop >/dev/null; then
|
||||
deluser --quiet socktop || true
|
||||
fi
|
||||
|
||||
if getent group socktop >/dev/null; then
|
||||
delgroup --quiet socktop || true
|
||||
fi
|
||||
|
||||
# Remove state directory on purge
|
||||
rm -rf /var/lib/socktop
|
||||
|
||||
# Remove config directory if empty
|
||||
rmdir --ignore-fail-on-non-empty /etc/socktop 2>/dev/null || true
|
||||
;;
|
||||
|
||||
remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
|
||||
# Do nothing on remove/upgrade
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "postrm called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,27 @@
|
||||
[Unit]
|
||||
Description=Socktop Agent - Remote System Monitor
|
||||
Documentation=https://github.com/jasonwitty/socktop
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/socktop_agent --port 3000
|
||||
Environment=RUST_LOG=info
|
||||
# Optional authentication token:
|
||||
# Environment=SOCKTOP_TOKEN=changeme
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
User=socktop
|
||||
Group=socktop
|
||||
NoNewPrivileges=true
|
||||
|
||||
# Security hardening
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/lib/socktop
|
||||
StateDirectory=socktop
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -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,4 +1,5 @@
|
||||
// gpu.rs
|
||||
#[cfg(feature = "gpu")]
|
||||
use gfxinfo::active_gpu;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
@@ -9,6 +10,7 @@ pub struct GpuMetrics {
|
||||
pub mem_total_bytes: u64,
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu")]
|
||||
pub fn collect_all_gpus() -> Result<Vec<GpuMetrics>, Box<dyn std::error::Error>> {
|
||||
let gpu = active_gpu()?; // Use ? to unwrap Result
|
||||
let info = gpu.info();
|
||||
@@ -22,3 +24,9 @@ pub fn collect_all_gpus() -> Result<Vec<GpuMetrics>, Box<dyn std::error::Error>>
|
||||
|
||||
Ok(vec![metrics])
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "gpu"))]
|
||||
pub fn collect_all_gpus() -> Result<Vec<GpuMetrics>, Box<dyn std::error::Error>> {
|
||||
// GPU support not available on this platform
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! socktop agent entrypoint: sets up sysinfo handles and serves a WebSocket endpoint at /ws.
|
||||
|
||||
mod cli;
|
||||
mod gpu;
|
||||
mod metrics;
|
||||
mod proto;
|
||||
@@ -15,10 +14,28 @@ use std::str::FromStr;
|
||||
|
||||
mod tls;
|
||||
|
||||
use cli::Cli;
|
||||
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<()> {
|
||||
// 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")]
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
@@ -59,8 +76,11 @@ fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
async fn async_main() -> anyhow::Result<()> {
|
||||
// Parse CLI arguments
|
||||
let cli = Cli::parse_args();
|
||||
// Version flag (print and exit). Keep before heavy initialization.
|
||||
if arg_flag("--version") || arg_flag("-V") {
|
||||
println!("socktop_agent {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let state = AppState::new();
|
||||
|
||||
@@ -76,8 +96,15 @@ async fn async_main() -> anyhow::Result<()> {
|
||||
.route("/healthz", get(healthz))
|
||||
.with_state(state.clone());
|
||||
|
||||
if cli.enable_ssl {
|
||||
let port = cli.get_port();
|
||||
let enable_ssl =
|
||||
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 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
|
||||
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));
|
||||
println!("socktop_agent: Listening on ws://{addr}/ws");
|
||||
axum_server::bind(addr)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use assert_cmd::prelude::*;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
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();
|
||||
|
||||
// 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
|
||||
cmd.env("XDG_CONFIG_HOME", &xdg)
|
||||
.arg("--enableSSL")
|
||||
|
||||
Reference in New Issue
Block a user