Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce59dd9dfe | |||
| 8d48fa4c3b | |||
| 51e702368e | |||
| 85f9a44e46 | |||
| b2468a5936 | |||
| 8de5943f34 | |||
| e624751f56 | |||
| 8bd1af7a27 | |||
| 5c32d15156 | |||
| 471d547b5d | |||
| d3aff590bc | |||
| 47910725a8 | |||
| a8e3f4ef26 | |||
| fab1e5a104 | |||
| d0455611d5 | |||
| 4c45b85c98 | |||
| d9fdc31e8f | |||
| dc9aa4c026 | |||
| c2e91bd20c | |||
| 25229d6b03 | |||
| 290e2a8fb2 | |||
| 30d263c71e | |||
| 9b177f3206 | |||
| 8a6ae3fcd7 | |||
| 5b8ec7efc1 | |||
| 155c420a1a | |||
| d3fa55e572 | |||
| faf2861b29 | |||
| 59432ab1d3 | |||
| d1c8a64418 | |||
| 8def4b2d06 | |||
| a42ca71a9f | |||
| 9f675fa804 | |||
| 3ac03c07ba | |||
| 384953d5d5 |
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "[pre-commit] Running cargo fmt --all" >&2
|
||||||
|
|
||||||
|
if ! command -v cargo >/dev/null 2>&1; then
|
||||||
|
echo "[pre-commit] cargo not found in PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cargo fmt --all
|
||||||
|
|
||||||
|
# Stage any Rust files that were reformatted
|
||||||
|
changed=$(git diff --name-only --diff-filter=M | grep -E '\\.rs$' || true)
|
||||||
|
if [ -n "$changed" ]; then
|
||||||
|
echo "$changed" | xargs git add
|
||||||
|
echo "[pre-commit] Added formatted files" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fail if further diffs remain (shouldn't happen normally)
|
||||||
|
unfmt=$(git diff --name-only --diff-filter=M | grep -E '\\.rs$' || true)
|
||||||
|
if [ -n "$unfmt" ]; then
|
||||||
|
echo "[pre-commit] Some Rust files still differ after formatting:" >&2
|
||||||
|
echo "$unfmt" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
.vscode/
|
||||||
|
|||||||
Vendored
-83
@@ -1,83 +0,0 @@
|
|||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug executable 'socktop'",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"--bin=socktop",
|
|
||||||
"--package=socktop"
|
|
||||||
],
|
|
||||||
"filter": {
|
|
||||||
"name": "socktop",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": ["ws://127.0.0.1:3000/ws"],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug unit tests in executable 'socktop'",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"test",
|
|
||||||
"--no-run",
|
|
||||||
"--bin=socktop",
|
|
||||||
"--package=socktop"
|
|
||||||
],
|
|
||||||
"filter": {
|
|
||||||
"name": "socktop",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug executable 'socktop_agent'",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"--bin=socktop_agent",
|
|
||||||
"--package=socktop_agent"
|
|
||||||
],
|
|
||||||
"filter": {
|
|
||||||
"name": "socktop_agent",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug unit tests in executable 'socktop_agent'",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"test",
|
|
||||||
"--no-run",
|
|
||||||
"--bin=socktop_agent",
|
|
||||||
"--package=socktop_agent"
|
|
||||||
],
|
|
||||||
"filter": {
|
|
||||||
"name": "socktop_agent",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Generated
+94
-139
@@ -357,10 +357,8 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
"wasm-bindgen",
|
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -524,6 +522,15 @@ version = "2.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "difflib"
|
name = "difflib"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -640,21 +647,6 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "foreign-types"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
|
||||||
dependencies = [
|
|
||||||
"foreign-types-shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "foreign-types-shared"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -1394,6 +1386,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -1460,54 +1458,6 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openssl"
|
|
||||||
version = "0.10.73"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"cfg-if",
|
|
||||||
"foreign-types",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
"openssl-macros",
|
|
||||||
"openssl-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openssl-macros"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openssl-src"
|
|
||||||
version = "300.5.2+3.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openssl-sys"
|
|
||||||
version = "0.9.109"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"libc",
|
|
||||||
"openssl-src",
|
|
||||||
"pkg-config",
|
|
||||||
"vcpkg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "overload"
|
name = "overload"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -1543,6 +1493,16 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem"
|
||||||
|
version = "3.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@@ -1591,12 +1551,6 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pkg-config"
|
|
||||||
version = "0.3.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -1606,6 +1560,12 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -1799,18 +1759,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha 0.3.1",
|
"rand_chacha",
|
||||||
"rand_core 0.6.4",
|
"rand_core",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand"
|
|
||||||
version = "0.9.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
|
||||||
dependencies = [
|
|
||||||
"rand_chacha 0.9.0",
|
|
||||||
"rand_core 0.9.3",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1820,17 +1770,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core 0.6.4",
|
"rand_core",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_chacha"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
|
||||||
dependencies = [
|
|
||||||
"ppv-lite86",
|
|
||||||
"rand_core 0.9.3",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1842,15 +1782,6 @@ dependencies = [
|
|||||||
"getrandom 0.2.16",
|
"getrandom 0.2.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_core"
|
|
||||||
version = "0.9.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.3.3",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ratatui"
|
name = "ratatui"
|
||||||
version = "0.28.1"
|
version = "0.28.1"
|
||||||
@@ -1872,6 +1803,19 @@ dependencies = [
|
|||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rcgen"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
|
||||||
|
dependencies = [
|
||||||
|
"pem",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"time",
|
||||||
|
"yasna",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.17"
|
version = "0.5.17"
|
||||||
@@ -2218,16 +2162,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socktop"
|
name = "socktop"
|
||||||
version = "0.1.11"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
"bytes",
|
|
||||||
"chrono",
|
|
||||||
"crossterm 0.27.0",
|
"crossterm 0.27.0",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
"flate2",
|
"flate2",
|
||||||
"futures",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"prost",
|
"prost",
|
||||||
"prost-build",
|
"prost-build",
|
||||||
@@ -2237,6 +2178,7 @@ dependencies = [
|
|||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sysinfo",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
@@ -2245,36 +2187,32 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socktop_agent"
|
name = "socktop_agent"
|
||||||
version = "0.1.11"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-server",
|
"axum-server",
|
||||||
"bytes",
|
|
||||||
"flate2",
|
"flate2",
|
||||||
"futures",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"gfxinfo",
|
"gfxinfo",
|
||||||
"hostname",
|
"hostname",
|
||||||
"nvml-wrapper",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"openssl",
|
|
||||||
"prost",
|
"prost",
|
||||||
"prost-build",
|
"prost-build",
|
||||||
"prost-types",
|
|
||||||
"protoc-bin-vendored",
|
"protoc-bin-vendored",
|
||||||
|
"rcgen",
|
||||||
"rustls 0.23.31",
|
"rustls 0.23.31",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tonic-build",
|
"tonic-build",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tungstenite 0.27.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2433,6 +2371,37 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"itoa",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -2506,7 +2475,7 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls 0.26.2",
|
"tokio-rustls 0.26.2",
|
||||||
"tungstenite 0.24.0",
|
"tungstenite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2651,7 +2620,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"httparse",
|
"httparse",
|
||||||
"log",
|
"log",
|
||||||
"rand 0.8.5",
|
"rand",
|
||||||
"rustls 0.23.31",
|
"rustls 0.23.31",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"sha1",
|
"sha1",
|
||||||
@@ -2659,23 +2628,6 @@ dependencies = [
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tungstenite"
|
|
||||||
version = "0.27.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"data-encoding",
|
|
||||||
"http",
|
|
||||||
"httparse",
|
|
||||||
"log",
|
|
||||||
"rand 0.9.2",
|
|
||||||
"sha1",
|
|
||||||
"thiserror 2.0.12",
|
|
||||||
"utf-8",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.18.0"
|
version = "1.18.0"
|
||||||
@@ -2746,12 +2698,6 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "vcpkg"
|
|
||||||
version = "0.2.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
@@ -3324,6 +3270,15 @@ version = "0.6.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yasna"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||||
|
dependencies = [
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|||||||
+2
-9
@@ -8,37 +8,30 @@ members = [
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# async + streams
|
# async + streams
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
futures = "0.3"
|
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
|
||||||
# websocket
|
# websocket
|
||||||
tokio-tungstenite = { version = "0.24", features = ["__rustls-tls", "connect"] }
|
tokio-tungstenite = { version = "0.24", features = ["__rustls-tls", "connect"] }
|
||||||
tungstenite = "0.24"
|
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
|
|
||||||
# JSON + error handling
|
# JSON + error handling
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
thiserror = "1.0"
|
|
||||||
|
|
||||||
# system stats
|
# system stats (align across crates)
|
||||||
sysinfo = "0.32"
|
sysinfo = "0.37"
|
||||||
|
|
||||||
# CLI UI
|
# CLI UI
|
||||||
ratatui = "0.28"
|
ratatui = "0.28"
|
||||||
crossterm = "0.27"
|
crossterm = "0.27"
|
||||||
|
|
||||||
# date/time
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
|
||||||
|
|
||||||
# web server (remote-agent)
|
# web server (remote-agent)
|
||||||
axum = { version = "0.7", features = ["ws"] }
|
axum = { version = "0.7", features = ["ws"] }
|
||||||
|
|
||||||
# protobuf
|
# protobuf
|
||||||
prost = "0.13"
|
prost = "0.13"
|
||||||
prost-types = "0.13"
|
|
||||||
bytes = "1"
|
|
||||||
dirs-next = "2"
|
dirs-next = "2"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ sudo apt-get install libdrm-dev libdrm-amdgpu1
|
|||||||
|
|
||||||
Two components:
|
Two components:
|
||||||
|
|
||||||
1) Agent (remote): small Rust WS server using sysinfo + /proc. It collects on demand when the client asks (fast metrics ~500 ms, processes ~2 s, disks ~5 s). No background loop when nobody is connected.
|
1) Agent (remote): small Rust WS server using sysinfo + /proc. It collects metrics only when the client requests them over the WebSocket (request-driven). No background sampling loop.
|
||||||
|
|
||||||
2) Client (local): TUI that connects to ws://HOST:PORT/ws (or wss://HOST:PORT/ws when TLS is enabled) and renders updates.
|
2) Client (local): TUI that connects to ws://HOST:PORT/ws (or wss://HOST:PORT/ws when TLS is enabled) and renders updates.
|
||||||
|
|
||||||
@@ -126,7 +126,8 @@ Notes:
|
|||||||
- After installing Rust via rustup, reload your shell (e.g., exec bash) so cargo is on PATH.
|
- After installing Rust via rustup, reload your shell (e.g., exec bash) so cargo is on PATH.
|
||||||
- Windows: you can also grab prebuilt EXEs from GitHub Actions artifacts if rustup scares you. It shouldn’t. Be brave.
|
- Windows: you can also grab prebuilt EXEs from GitHub Actions artifacts if rustup scares you. It shouldn’t. Be brave.
|
||||||
|
|
||||||
Option B: System-wide agent (Linux)
|
System-wide agent (Linux)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# If you installed with cargo, binaries are in ~/.cargo/bin
|
# If you installed with cargo, binaries are in ~/.cargo/bin
|
||||||
sudo install -o root -g root -m 0755 "$HOME/.cargo/bin/socktop_agent" /usr/local/bin/socktop_agent
|
sudo install -o root -g root -m 0755 "$HOME/.cargo/bin/socktop_agent" /usr/local/bin/socktop_agent
|
||||||
@@ -137,6 +138,36 @@ sudo systemctl daemon-reload
|
|||||||
sudo systemctl enable --now socktop-agent
|
sudo systemctl enable --now socktop-agent
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
|
||||||
|
# Enable SSL
|
||||||
|
|
||||||
|
# Stop service
|
||||||
|
sudo systemctl stop socktop-agent
|
||||||
|
|
||||||
|
# Edit service to append SSL option and port
|
||||||
|
sudo micro /etc/systemd/system/socktop-agent.service
|
||||||
|
|
||||||
|
--
|
||||||
|
ExecStart=/usr/local/bin/socktop_agent --enableSSL --port 8443
|
||||||
|
--
|
||||||
|
|
||||||
|
# Reload
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
sudo systemctl start socktop-agent
|
||||||
|
|
||||||
|
# check logs for certificate location
|
||||||
|
sudo journalctl -u socktop-agent -f
|
||||||
|
|
||||||
|
--
|
||||||
|
Aug 22 22:25:26 rpi-master socktop_agent[2913998]: socktop_agent: generated self-signed TLS certificate at /var/lib/socktop/.config/socktop_agent/tls/cert.pem
|
||||||
|
--
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -159,6 +190,8 @@ socktop ws://HOST:3000/ws
|
|||||||
socktop "ws://HOST:3000/ws?token=changeme"
|
socktop "ws://HOST:3000/ws?token=changeme"
|
||||||
# TLS with pinned server certificate (recommended over the internet):
|
# TLS with pinned server certificate (recommended over the internet):
|
||||||
socktop --tls-ca /path/to/cert.pem wss://HOST:8443/ws
|
socktop --tls-ca /path/to/cert.pem wss://HOST:8443/ws
|
||||||
|
# (By default hostname/SAN verification is skipped for ease on home networks. To enforce it add --verify-hostname)
|
||||||
|
socktop --verify-hostname --tls-ca /path/to/cert.pem wss://HOST:8443/ws
|
||||||
# shorthand:
|
# shorthand:
|
||||||
socktop -t /path/to/cert.pem wss://HOST:8443/ws
|
socktop -t /path/to/cert.pem wss://HOST:8443/ws
|
||||||
# Note: providing --tls-ca/-t automatically upgrades ws:// to wss:// if you forget
|
# Note: providing --tls-ca/-t automatically upgrades ws:// to wss:// if you forget
|
||||||
@@ -304,6 +337,18 @@ Tip: If only the binary changed, restart is enough. If the unit file changed, ru
|
|||||||
- Linux (XDG): $XDG_CONFIG_HOME/socktop_agent/tls/{cert.pem,key.pem} (defaults to ~/.config)
|
- Linux (XDG): $XDG_CONFIG_HOME/socktop_agent/tls/{cert.pem,key.pem} (defaults to ~/.config)
|
||||||
- The agent prints these paths on creation.
|
- The agent prints these paths on creation.
|
||||||
- You can set XDG_CONFIG_HOME before first run to control where certs are written.
|
- You can set XDG_CONFIG_HOME before first run to control where certs are written.
|
||||||
|
- Additional SANs: set `SOCKTOP_AGENT_EXTRA_SANS` (comma‑separated) before first TLS start to include extra IPs/DNS names in the cert. Example:
|
||||||
|
```bash
|
||||||
|
SOCKTOP_AGENT_EXTRA_SANS="192.168.1.101,myhost.internal" socktop_agent --enableSSL
|
||||||
|
```
|
||||||
|
This prevents client errors like `NotValidForName` when connecting via an IP not present in the default cert SAN list.
|
||||||
|
- Expiry / rotation: the generated cert is valid for ~397 days from creation. If the agent fails to start with an "ExpiredCertificate" error (or your client reports expiry), simply delete the existing cert and key:
|
||||||
|
```bash
|
||||||
|
rm ~/.config/socktop_agent/tls/cert.pem ~/.config/socktop_agent/tls/key.pem
|
||||||
|
# (adjust path if XDG_CONFIG_HOME is set or different user)
|
||||||
|
systemctl restart socktop-agent # if running under systemd
|
||||||
|
```
|
||||||
|
On next TLS start the agent will generate a fresh pair. Only distribute the new cert.pem to clients (never the key).
|
||||||
- Auth token (optional): SOCKTOP_TOKEN=changeme
|
- Auth token (optional): SOCKTOP_TOKEN=changeme
|
||||||
- Disable GPU metrics: SOCKTOP_AGENT_GPU=0
|
- Disable GPU metrics: SOCKTOP_AGENT_GPU=0
|
||||||
- Disable CPU temperature: SOCKTOP_AGENT_TEMP=0
|
- Disable CPU temperature: SOCKTOP_AGENT_TEMP=0
|
||||||
@@ -385,6 +430,7 @@ socktop --tls-ca /path/to/agent/cert.pem wss://HOST:8443/ws
|
|||||||
Notes:
|
Notes:
|
||||||
- Do not copy the private key off the server; only the cert.pem is needed by clients.
|
- Do not copy the private key off the server; only the cert.pem is needed by clients.
|
||||||
- When --tls-ca/-t is supplied, the client auto‑upgrades ws:// to wss:// to avoid protocol mismatch.
|
- When --tls-ca/-t is supplied, the client auto‑upgrades ws:// to wss:// to avoid protocol mismatch.
|
||||||
|
- Hostname (SAN) verification is DISABLED by default (the cert is still pinned). Use `--verify-hostname` to enable strict SAN checking.
|
||||||
- You can run multiple clients with different cert paths by passing --tls-ca per invocation.
|
- You can run multiple clients with different cert paths by passing --tls-ca per invocation.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -460,6 +506,18 @@ cargo run -p socktop -- ws://127.0.0.1:3000/ws
|
|||||||
cargo run -p socktop_agent -- --enableSSL --port 8443
|
cargo run -p socktop_agent -- --enableSSL --port 8443
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Auto-format on commit
|
||||||
|
|
||||||
|
A sample pre-commit hook that runs `cargo fmt --all` is provided in `.githooks/pre-commit`.
|
||||||
|
Enable it (one-time):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config core.hooksPath .githooks
|
||||||
|
chmod +x .githooks/pre-commit
|
||||||
|
```
|
||||||
|
|
||||||
|
Every commit will then format Rust sources and restage them automatically.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|||||||
+2
-4
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "socktop"
|
name = "socktop"
|
||||||
version = "0.1.11"
|
version = "0.1.3"
|
||||||
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
|
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
|
||||||
description = "Remote system monitor over WebSocket, TUI like top"
|
description = "Remote system monitor over WebSocket, TUI like top"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -9,21 +9,19 @@ license = "MIT"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tokio-tungstenite = { workspace = true }
|
tokio-tungstenite = { workspace = true }
|
||||||
futures = { workspace = true }
|
|
||||||
futures-util = { workspace = true }
|
futures-util = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
ratatui = { workspace = true }
|
ratatui = { workspace = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
chrono = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
||||||
dirs-next = { workspace = true }
|
dirs-next = { workspace = true }
|
||||||
|
sysinfo = { workspace = true }
|
||||||
rustls = "0.23"
|
rustls = "0.23"
|
||||||
rustls-pemfile = "2.1"
|
rustls-pemfile = "2.1"
|
||||||
prost = { workspace = true }
|
prost = { workspace = true }
|
||||||
bytes = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2.0"
|
assert_cmd = "2.0"
|
||||||
|
|||||||
+8
-2
@@ -1,8 +1,14 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
|
// Vendored protoc for reproducible builds (works on crates.io build machines)
|
||||||
let protoc = protoc_bin_vendored::protoc_bin_path().expect("protoc");
|
let protoc = protoc_bin_vendored::protoc_bin_path().expect("protoc");
|
||||||
std::env::set_var("PROTOC", protoc);
|
std::env::set_var("PROTOC", &protoc);
|
||||||
|
|
||||||
|
// Tell Cargo when to re-run
|
||||||
|
println!("cargo:rerun-if-changed=proto/processes.proto");
|
||||||
|
|
||||||
let mut cfg = prost_build::Config::new();
|
let mut cfg = prost_build::Config::new();
|
||||||
cfg.out_dir(std::env::var("OUT_DIR").unwrap());
|
cfg.out_dir(std::env::var("OUT_DIR").unwrap());
|
||||||
cfg.compile_protos(&["../proto/processes.proto"], &["../proto"])
|
// Use in-crate relative path so `cargo package` includes the file
|
||||||
|
cfg.compile_protos(&["proto/processes.proto"], &["proto"]) // paths relative to CARGO_MANIFEST_DIR
|
||||||
.expect("compile protos");
|
.expect("compile protos");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
package socktop;
|
||||||
|
|
||||||
|
// All running processes. Sorting is done client-side.
|
||||||
|
message Processes {
|
||||||
|
uint64 process_count = 1; // total processes in the system
|
||||||
|
repeated Process rows = 2; // all processes
|
||||||
|
}
|
||||||
|
|
||||||
|
message Process {
|
||||||
|
uint32 pid = 1;
|
||||||
|
string name = 2;
|
||||||
|
float cpu_usage = 3; // 0..100
|
||||||
|
uint64 mem_bytes = 4; // RSS bytes
|
||||||
|
}
|
||||||
+33
-5
@@ -21,6 +21,7 @@ pub(crate) struct ParsedArgs {
|
|||||||
dry_run: bool, // hidden test helper: skip connecting
|
dry_run: bool, // hidden test helper: skip connecting
|
||||||
metrics_interval_ms: Option<u64>,
|
metrics_interval_ms: Option<u64>,
|
||||||
processes_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> {
|
pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, String> {
|
||||||
@@ -34,14 +35,21 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
|||||||
let mut dry_run = false;
|
let mut dry_run = false;
|
||||||
let mut metrics_interval_ms: Option<u64> = None;
|
let mut metrics_interval_ms: Option<u64> = None;
|
||||||
let mut processes_interval_ms: Option<u64> = None;
|
let mut processes_interval_ms: Option<u64> = None;
|
||||||
|
let mut verify_hostname = false;
|
||||||
while let Some(arg) = it.next() {
|
while let Some(arg) = it.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"-h" | "--help" => {
|
"-h" | "--help" => {
|
||||||
return Err(format!("Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [--demo] [--metrics-interval-ms N] [--processes-interval-ms N] [ws://HOST:PORT/ws]\n"));
|
return Err(format!("Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--verify-hostname] [--profile NAME|-P NAME] [--save] [--demo] [--metrics-interval-ms N] [--processes-interval-ms N] [ws://HOST:PORT/ws]\n"));
|
||||||
}
|
}
|
||||||
"--tls-ca" | "-t" => {
|
"--tls-ca" | "-t" => {
|
||||||
tls_ca = it.next();
|
tls_ca = it.next();
|
||||||
}
|
}
|
||||||
|
"--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" | "-P" => {
|
||||||
profile = it.next();
|
profile = it.next();
|
||||||
}
|
}
|
||||||
@@ -89,7 +97,7 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
|||||||
if url.is_none() {
|
if url.is_none() {
|
||||||
url = Some(arg);
|
url = Some(arg);
|
||||||
} else {
|
} else {
|
||||||
return Err(format!("Unexpected argument. Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [--demo] [ws://HOST:PORT/ws]"));
|
return Err(format!("Unexpected argument. Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--verify-hostname] [--profile NAME|-P NAME] [--save] [--demo] [ws://HOST:PORT/ws]"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,6 +111,7 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
|||||||
dry_run,
|
dry_run,
|
||||||
metrics_interval_ms,
|
metrics_interval_ms,
|
||||||
processes_interval_ms,
|
processes_interval_ms,
|
||||||
|
verify_hostname,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +127,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if parsed.demo || matches!(parsed.profile.as_deref(), Some("demo")) {
|
if parsed.demo || matches!(parsed.profile.as_deref(), Some("demo")) {
|
||||||
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
||||||
}
|
}
|
||||||
|
if parsed.verify_hostname {
|
||||||
|
// Set env var consumed by ws::connect logic
|
||||||
|
std::env::set_var("SOCKTOP_VERIFY_NAME", "1");
|
||||||
|
}
|
||||||
let profiles_file = load_profiles();
|
let profiles_file = load_profiles();
|
||||||
let req = ProfileRequest {
|
let req = ProfileRequest {
|
||||||
profile_name: parsed.profile.clone(),
|
profile_name: parsed.profile.clone(),
|
||||||
@@ -204,7 +217,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
(u, t, entry.metrics_interval_ms, entry.processes_interval_ms)
|
(u, t, entry.metrics_interval_ms, entry.processes_interval_ms)
|
||||||
}
|
}
|
||||||
ResolveProfile::PromptSelect(mut names) => {
|
ResolveProfile::PromptSelect(mut names) => {
|
||||||
if !names.iter().any(|n| n == "demo") {
|
if !names.iter().any(|n: &String| n == "demo") {
|
||||||
names.push("demo".into());
|
names.push("demo".into());
|
||||||
}
|
}
|
||||||
eprintln!("Select profile:");
|
eprintln!("Select profile:");
|
||||||
@@ -268,10 +281,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
(url.trim().to_string(), ca_opt, mi, pi)
|
(url.trim().to_string(), ca_opt, mi, pi)
|
||||||
}
|
}
|
||||||
ResolveProfile::None => {
|
ResolveProfile::None => {
|
||||||
eprintln!("No URL provided and no profiles to select.");
|
//eprintln!("No URL provided and no profiles to select.");
|
||||||
return Ok(());
|
|
||||||
|
//first run, no args, no profiles: show welcome message and offer demo mode
|
||||||
|
if profiles_mut.profiles.is_empty() && parsed.url.is_none() {
|
||||||
|
eprintln!("Welcome to socktop!");
|
||||||
|
eprintln!("It looks like this is your first time running the application.");
|
||||||
|
eprintln!("You can connect to a socktop_agent instance to monitor system metrics and processes.");
|
||||||
|
eprintln!("If you don't have an agent running, you can try the demo mode.");
|
||||||
|
if prompt_yes_no("Would you like to start the demo mode now? [Y/n]: ") {
|
||||||
|
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
||||||
|
} else {
|
||||||
|
eprintln!("Aborting. You can run 'socktop --help' for usage information.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err("No URL provided and no profiles to select.".into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_tls = url.starts_with("wss://");
|
let is_tls = url.starts_with("wss://");
|
||||||
let has_token = url.contains("token=");
|
let has_token = url.contains("token=");
|
||||||
let mut app = App::new()
|
let mut app = App::new()
|
||||||
|
|||||||
+50
-3
@@ -3,7 +3,10 @@
|
|||||||
use flate2::bufread::GzDecoder;
|
use flate2::bufread::GzDecoder;
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use prost::Message as _;
|
use prost::Message as _;
|
||||||
|
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
||||||
|
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||||
use rustls::{ClientConfig, RootCertStore};
|
use rustls::{ClientConfig, RootCertStore};
|
||||||
|
use rustls::{DigitallySignedStruct, SignatureScheme};
|
||||||
use rustls_pemfile::Item;
|
use rustls_pemfile::Item;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::{fs::File, io::BufReader, sync::Arc};
|
use std::{fs::File, io::BufReader, sync::Arc};
|
||||||
@@ -50,14 +53,58 @@ async fn connect_with_ca(url: &str, ca_path: &str) -> Result<WsStream, Box<dyn s
|
|||||||
}
|
}
|
||||||
root.add_parsable_certificates(der_certs);
|
root.add_parsable_certificates(der_certs);
|
||||||
|
|
||||||
let cfg = ClientConfig::builder()
|
let mut cfg = ClientConfig::builder()
|
||||||
.with_root_certificates(root)
|
.with_root_certificates(root)
|
||||||
.with_no_client_auth();
|
.with_no_client_auth();
|
||||||
let cfg = Arc::new(cfg);
|
|
||||||
|
|
||||||
let req = url.into_client_request()?;
|
let req = url.into_client_request()?;
|
||||||
|
let verify_domain = std::env::var("SOCKTOP_VERIFY_NAME").ok().as_deref() == Some("1");
|
||||||
|
if !verify_domain {
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct NoVerify;
|
||||||
|
impl ServerCertVerifier for NoVerify {
|
||||||
|
fn verify_server_cert(
|
||||||
|
&self,
|
||||||
|
_end_entity: &CertificateDer<'_>,
|
||||||
|
_intermediates: &[CertificateDer<'_>],
|
||||||
|
_server_name: &ServerName,
|
||||||
|
_ocsp_response: &[u8],
|
||||||
|
_now: UnixTime,
|
||||||
|
) -> Result<ServerCertVerified, rustls::Error> {
|
||||||
|
Ok(ServerCertVerified::assertion())
|
||||||
|
}
|
||||||
|
fn verify_tls12_signature(
|
||||||
|
&self,
|
||||||
|
_message: &[u8],
|
||||||
|
_cert: &CertificateDer<'_>,
|
||||||
|
_dss: &DigitallySignedStruct,
|
||||||
|
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||||
|
Ok(HandshakeSignatureValid::assertion())
|
||||||
|
}
|
||||||
|
fn verify_tls13_signature(
|
||||||
|
&self,
|
||||||
|
_message: &[u8],
|
||||||
|
_cert: &CertificateDer<'_>,
|
||||||
|
_dss: &DigitallySignedStruct,
|
||||||
|
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||||
|
Ok(HandshakeSignatureValid::assertion())
|
||||||
|
}
|
||||||
|
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||||
|
// Provide common schemes; not strictly needed for skipping but keeps API happy
|
||||||
|
vec![
|
||||||
|
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||||
|
SignatureScheme::ED25519,
|
||||||
|
SignatureScheme::RSA_PSS_SHA256,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.dangerous().set_certificate_verifier(Arc::new(NoVerify));
|
||||||
|
eprintln!("socktop: hostname verification disabled (default). Use --verify-hostname to enable strict SAN checking.");
|
||||||
|
}
|
||||||
|
let cfg = Arc::new(cfg);
|
||||||
let (ws, _) =
|
let (ws, _) =
|
||||||
connect_async_tls_with_config(req, None, true, Some(Connector::Rustls(cfg))).await?;
|
connect_async_tls_with_config(req, None, verify_domain, Some(Connector::Rustls(cfg)))
|
||||||
|
.await?;
|
||||||
Ok(ws)
|
Ok(ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "socktop_agent"
|
name = "socktop_agent"
|
||||||
version = "0.1.11"
|
version = "0.1.3"
|
||||||
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
|
authors = ["Jason Witty <jasonpwitty+socktop@proton.me>"]
|
||||||
description = "Remote system monitor over WebSocket, TUI like top"
|
description = "Remote system monitor over WebSocket, TUI like top"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -13,26 +13,23 @@ sysinfo = { version = "0.37", features = ["network", "disk", "component"] }
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
||||||
futures = "0.3"
|
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
nvml-wrapper = "0.10"
|
# nvml-wrapper removed (unused; GPU metrics via gfxinfo only now)
|
||||||
gfxinfo = "0.1.2"
|
gfxinfo = "0.1.2"
|
||||||
tungstenite = "0.27.0"
|
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
axum-server = { version = "0.6", features = ["tls-rustls"] }
|
axum-server = { version = "0.6", features = ["tls-rustls"] }
|
||||||
rustls = "0.23"
|
rustls = "0.23"
|
||||||
rustls-pemfile = "2.1"
|
rustls-pemfile = "2.1"
|
||||||
openssl = { version = "0.10", features = ["vendored"] } # for cross‑platform self‑signed generation
|
rcgen = "0.13" # pure-Rust self-signed cert generation (replaces openssl vendored build)
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
hostname = "0.3"
|
hostname = "0.3"
|
||||||
bytes = { workspace = true }
|
|
||||||
prost = { workspace = true }
|
prost = { workspace = true }
|
||||||
|
time = { version = "0.3", default-features = false, features = ["formatting", "macros", "parsing" ] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
prost-build = "0.13"
|
prost-build = "0.13"
|
||||||
prost-types = { workspace = true }
|
|
||||||
tonic-build = { version = "0.12", default-features = false, optional = true }
|
tonic-build = { version = "0.12", default-features = false, optional = true }
|
||||||
protoc-bin-vendored = "3"
|
protoc-bin-vendored = "3"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
// Ensure protoc exists (vendored for reproducible builds)
|
// Vendored protoc for reproducible builds
|
||||||
let protoc = protoc_bin_vendored::protoc_bin_path().expect("protoc");
|
let protoc = protoc_bin_vendored::protoc_bin_path().expect("protoc");
|
||||||
std::env::set_var("PROTOC", protoc);
|
std::env::set_var("PROTOC", &protoc);
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-changed=proto/processes.proto");
|
||||||
|
|
||||||
// Compile protobuf definitions for processes
|
// Compile protobuf definitions for processes
|
||||||
let mut cfg = prost_build::Config::new();
|
let mut cfg = prost_build::Config::new();
|
||||||
cfg.out_dir(std::env::var("OUT_DIR").unwrap());
|
cfg.out_dir(std::env::var("OUT_DIR").unwrap());
|
||||||
cfg.compile_protos(&["../proto/processes.proto"], &["../proto"])
|
// 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");
|
.expect("compile protos");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
package socktop;
|
||||||
|
|
||||||
|
// All running processes. Sorting is done client-side.
|
||||||
|
message Processes {
|
||||||
|
uint64 process_count = 1; // total processes in the system
|
||||||
|
repeated Process rows = 2; // all processes
|
||||||
|
}
|
||||||
|
|
||||||
|
message Process {
|
||||||
|
uint32 pid = 1;
|
||||||
|
string name = 2;
|
||||||
|
float cpu_usage = 3; // 0..100
|
||||||
|
uint64 mem_bytes = 4; // RSS bytes
|
||||||
|
}
|
||||||
+10
-53
@@ -1,10 +1,9 @@
|
|||||||
//! socktop agent entrypoint: sets up sysinfo handles, launches a sampler,
|
//! socktop agent entrypoint: sets up sysinfo handles and serves a WebSocket endpoint at /ws.
|
||||||
//! and serves a WebSocket endpoint at /ws.
|
|
||||||
|
|
||||||
mod gpu;
|
mod gpu;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
mod proto;
|
mod proto;
|
||||||
mod sampler;
|
// sampler module removed (metrics now purely request-driven)
|
||||||
mod state;
|
mod state;
|
||||||
mod types;
|
mod types;
|
||||||
mod ws;
|
mod ws;
|
||||||
@@ -15,7 +14,6 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
mod tls;
|
mod tls;
|
||||||
|
|
||||||
use crate::sampler::{spawn_disks_sampler, spawn_process_sampler, spawn_sampler};
|
|
||||||
use state::AppState;
|
use state::AppState;
|
||||||
|
|
||||||
fn arg_flag(name: &str) -> bool {
|
fn arg_flag(name: &str) -> bool {
|
||||||
@@ -37,15 +35,15 @@ fn arg_value(name: &str) -> Option<String> {
|
|||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
// 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();
|
let state = AppState::new();
|
||||||
|
|
||||||
// Start background sampler (adjust cadence as needed)
|
// No background samplers: metrics collected on-demand per websocket request.
|
||||||
// 500ms fast metrics
|
|
||||||
let _h_fast = spawn_sampler(state.clone(), std::time::Duration::from_millis(500));
|
|
||||||
// 2s processes (top 50)
|
|
||||||
let _h_procs = spawn_process_sampler(state.clone(), std::time::Duration::from_secs(2), 50);
|
|
||||||
// 5s disks
|
|
||||||
let _h_disks = spawn_disks_sampler(state.clone(), std::time::Duration::from_secs(5));
|
|
||||||
|
|
||||||
// Web app: route /ws to the websocket handler
|
// Web app: route /ws to the websocket handler
|
||||||
async fn healthz() -> StatusCode {
|
async fn healthz() -> StatusCode {
|
||||||
@@ -92,45 +90,4 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
// Unit tests for CLI parsing moved to `tests/port_parse.rs`.
|
||||||
mod tests_cli_agent {
|
|
||||||
// Local helper for testing port parsing
|
|
||||||
fn parse_port<I: IntoIterator<Item = String>>(args: I, default_port: u16) -> u16 {
|
|
||||||
let mut it = args.into_iter();
|
|
||||||
let _ = it.next(); // prog
|
|
||||||
let mut long: Option<String> = None;
|
|
||||||
let mut short: Option<String> = None;
|
|
||||||
while let Some(a) = it.next() {
|
|
||||||
match a.as_str() {
|
|
||||||
"--port" => long = it.next(),
|
|
||||||
"-p" => short = it.next(),
|
|
||||||
_ if a.starts_with("--port=") => {
|
|
||||||
if let Some((_, v)) = a.split_once('=') {
|
|
||||||
long = Some(v.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
long.or(short)
|
|
||||||
.and_then(|s| s.parse::<u16>().ok())
|
|
||||||
.unwrap_or(default_port)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn port_long_short_and_assign() {
|
|
||||||
assert_eq!(
|
|
||||||
parse_port(vec!["agent".into(), "--port".into(), "9001".into()], 8443),
|
|
||||||
9001
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
parse_port(vec!["agent".into(), "-p".into(), "9002".into()], 8443),
|
|
||||||
9002
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
parse_port(vec!["agent".into(), "--port=9003".into()], 8443),
|
|
||||||
9003
|
|
||||||
);
|
|
||||||
assert_eq!(parse_port(vec!["agent".into()], 8443), 8443);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+148
-55
@@ -11,8 +11,9 @@ use std::fs;
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
use std::time::Duration as StdDuration;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
|
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
// Runtime toggles (read once)
|
// Runtime toggles (read once)
|
||||||
@@ -97,6 +98,20 @@ fn set_gpus(v: Option<Vec<crate::gpu::GpuMetrics>>) {
|
|||||||
|
|
||||||
// Collect only fast-changing metrics (CPU/mem/net + optional temps/gpus).
|
// Collect only fast-changing metrics (CPU/mem/net + optional temps/gpus).
|
||||||
pub async fn collect_fast_metrics(state: &AppState) -> Metrics {
|
pub async fn collect_fast_metrics(state: &AppState) -> Metrics {
|
||||||
|
// TTL (ms) overridable via env, default 250ms
|
||||||
|
let ttl_ms: u64 = std::env::var("SOCKTOP_AGENT_METRICS_TTL_MS")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(250);
|
||||||
|
let ttl = StdDuration::from_millis(ttl_ms);
|
||||||
|
{
|
||||||
|
let cache = state.cache_metrics.lock().await;
|
||||||
|
if cache.is_fresh(ttl) {
|
||||||
|
if let Some(c) = cache.take_clone() {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut sys = state.sys.lock().await;
|
let mut sys = state.sys.lock().await;
|
||||||
if let Err(e) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
if let Err(e) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
sys.refresh_cpu_usage();
|
sys.refresh_cpu_usage();
|
||||||
@@ -105,7 +120,7 @@ pub async fn collect_fast_metrics(state: &AppState) -> Metrics {
|
|||||||
warn!("sysinfo selective refresh panicked: {e:?}");
|
warn!("sysinfo selective refresh panicked: {e:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let hostname = System::host_name().unwrap_or_else(|| "unknown".to_string());
|
let hostname = state.hostname.clone();
|
||||||
let cpu_total = sys.global_cpu_usage();
|
let cpu_total = sys.global_cpu_usage();
|
||||||
let cpu_per_core: Vec<f32> = sys.cpus().iter().map(|c| c.cpu_usage()).collect();
|
let cpu_per_core: Vec<f32> = sys.cpus().iter().map(|c| c.cpu_usage()).collect();
|
||||||
let mem_total = sys.total_memory();
|
let mem_total = sys.total_memory();
|
||||||
@@ -153,25 +168,46 @@ pub async fn collect_fast_metrics(state: &AppState) -> Metrics {
|
|||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
// GPUs: refresh only when cache is stale
|
// GPUs: if we already determined none exist, short-circuit (no repeated probing)
|
||||||
let gpus = if cached_gpus().is_some() {
|
let gpus = if gpu_enabled() {
|
||||||
cached_gpus()
|
if state.gpu_checked.load(std::sync::atomic::Ordering::Acquire)
|
||||||
} else if gpu_enabled() {
|
&& !state.gpu_present.load(std::sync::atomic::Ordering::Relaxed)
|
||||||
let v = match collect_all_gpus() {
|
{
|
||||||
Ok(v) if !v.is_empty() => Some(v),
|
None
|
||||||
Ok(_) => None,
|
} else if cached_gpus().is_some() {
|
||||||
Err(e) => {
|
cached_gpus()
|
||||||
warn!("gpu collection failed: {e}");
|
} else {
|
||||||
None
|
let v = match collect_all_gpus() {
|
||||||
|
Ok(v) if !v.is_empty() => Some(v),
|
||||||
|
Ok(_) => None,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("gpu collection failed: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// First probe records presence; subsequent calls rely on cache flags.
|
||||||
|
if !state
|
||||||
|
.gpu_checked
|
||||||
|
.swap(true, std::sync::atomic::Ordering::AcqRel)
|
||||||
|
{
|
||||||
|
if v.is_some() {
|
||||||
|
state
|
||||||
|
.gpu_present
|
||||||
|
.store(true, std::sync::atomic::Ordering::Release);
|
||||||
|
} else {
|
||||||
|
state
|
||||||
|
.gpu_present
|
||||||
|
.store(false, std::sync::atomic::Ordering::Release);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
set_gpus(v.clone());
|
||||||
set_gpus(v.clone());
|
v
|
||||||
v
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
Metrics {
|
let metrics = Metrics {
|
||||||
cpu_total,
|
cpu_total,
|
||||||
cpu_per_core,
|
cpu_per_core,
|
||||||
mem_total,
|
mem_total,
|
||||||
@@ -184,21 +220,44 @@ pub async fn collect_fast_metrics(state: &AppState) -> Metrics {
|
|||||||
networks,
|
networks,
|
||||||
top_processes: Vec::new(),
|
top_processes: Vec::new(),
|
||||||
gpus,
|
gpus,
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let mut cache = state.cache_metrics.lock().await;
|
||||||
|
cache.set(metrics.clone());
|
||||||
}
|
}
|
||||||
|
metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cached disks
|
// Cached disks
|
||||||
pub async fn collect_disks(state: &AppState) -> Vec<DiskInfo> {
|
pub async fn collect_disks(state: &AppState) -> Vec<DiskInfo> {
|
||||||
|
let ttl_ms: u64 = std::env::var("SOCKTOP_AGENT_DISKS_TTL_MS")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(1_000);
|
||||||
|
let ttl = StdDuration::from_millis(ttl_ms);
|
||||||
|
{
|
||||||
|
let cache = state.cache_disks.lock().await;
|
||||||
|
if cache.is_fresh(ttl) {
|
||||||
|
if let Some(v) = cache.take_clone() {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut disks_list = state.disks.lock().await;
|
let mut disks_list = state.disks.lock().await;
|
||||||
disks_list.refresh(false); // don't drop missing disks
|
disks_list.refresh(false); // don't drop missing disks
|
||||||
disks_list
|
let disks: Vec<DiskInfo> = disks_list
|
||||||
.iter()
|
.iter()
|
||||||
.map(|d| DiskInfo {
|
.map(|d| DiskInfo {
|
||||||
name: d.name().to_string_lossy().into_owned(),
|
name: d.name().to_string_lossy().into_owned(),
|
||||||
total: d.total_space(),
|
total: d.total_space(),
|
||||||
available: d.available_space(),
|
available: d.available_space(),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect();
|
||||||
|
{
|
||||||
|
let mut cache = state.cache_disks.lock().await;
|
||||||
|
cache.set(disks.clone());
|
||||||
|
}
|
||||||
|
disks
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linux-only helpers and implementation using /proc deltas for accurate CPU%.
|
// Linux-only helpers and implementation using /proc deltas for accurate CPU%.
|
||||||
@@ -239,8 +298,22 @@ fn read_proc_jiffies(pid: u32) -> Option<u64> {
|
|||||||
/// Collect all processes (Linux): compute CPU% via /proc jiffies delta; sorting moved to client.
|
/// Collect all processes (Linux): compute CPU% via /proc jiffies delta; sorting moved to client.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub async fn collect_processes_all(state: &AppState) -> ProcessesPayload {
|
pub async fn collect_processes_all(state: &AppState) -> ProcessesPayload {
|
||||||
// Fresh view to avoid lingering entries and select "no tasks" (no per-thread rows).
|
let ttl_ms: u64 = std::env::var("SOCKTOP_AGENT_PROCESSES_TTL_MS")
|
||||||
let mut sys = System::new();
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(1_000);
|
||||||
|
let ttl = StdDuration::from_millis(ttl_ms);
|
||||||
|
{
|
||||||
|
let cache = state.cache_processes.lock().await;
|
||||||
|
if cache.is_fresh(ttl) {
|
||||||
|
if let Some(v) = cache.take_clone() {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reuse shared System to avoid reallocation; refresh processes fully.
|
||||||
|
let mut sys_guard = state.sys.lock().await;
|
||||||
|
let sys = &mut *sys_guard;
|
||||||
sys.refresh_processes_specifics(
|
sys.refresh_processes_specifics(
|
||||||
ProcessesToUpdate::All,
|
ProcessesToUpdate::All,
|
||||||
false,
|
false,
|
||||||
@@ -315,50 +388,70 @@ pub async fn collect_processes_all(state: &AppState) -> ProcessesPayload {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
ProcessesPayload {
|
let payload = ProcessesPayload {
|
||||||
process_count: total_count,
|
process_count: total_count,
|
||||||
top_processes: procs,
|
top_processes: procs,
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let mut cache = state.cache_processes.lock().await;
|
||||||
|
cache.set(payload.clone());
|
||||||
}
|
}
|
||||||
|
payload
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collect all processes (non-Linux): use sysinfo's internal CPU% by doing a double refresh.
|
/// Collect all processes (non-Linux): use sysinfo's internal CPU% by doing a double refresh.
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
pub async fn collect_processes_all(state: &AppState) -> ProcessesPayload {
|
pub async fn collect_processes_all(state: &AppState) -> ProcessesPayload {
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
let ttl_ms: u64 = std::env::var("SOCKTOP_AGENT_PROCESSES_TTL_MS")
|
||||||
let mut sys = state.sys.lock().await;
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
// First refresh to set baseline
|
.unwrap_or(1_000);
|
||||||
sys.refresh_processes_specifics(
|
let ttl = StdDuration::from_millis(ttl_ms);
|
||||||
ProcessesToUpdate::All,
|
{
|
||||||
false,
|
let cache = state.cache_processes.lock().await;
|
||||||
ProcessRefreshKind::everything().without_tasks(),
|
if cache.is_fresh(ttl) {
|
||||||
);
|
if let Some(v) = cache.take_clone() {
|
||||||
// Small delay so sysinfo can compute CPU deltas on next refresh
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut sys = state.sys.lock().await;
|
||||||
|
sys.refresh_processes_specifics(
|
||||||
|
ProcessesToUpdate::All,
|
||||||
|
false,
|
||||||
|
ProcessRefreshKind::everything().without_tasks(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Release lock during sleep interval
|
||||||
sleep(Duration::from_millis(250)).await;
|
sleep(Duration::from_millis(250)).await;
|
||||||
sys.refresh_processes_specifics(
|
{
|
||||||
ProcessesToUpdate::All,
|
let mut sys = state.sys.lock().await;
|
||||||
false,
|
sys.refresh_processes_specifics(
|
||||||
ProcessRefreshKind::everything().without_tasks(),
|
ProcessesToUpdate::All,
|
||||||
);
|
false,
|
||||||
|
ProcessRefreshKind::everything().without_tasks(),
|
||||||
let total_count = sys.processes().len();
|
);
|
||||||
|
let total_count = sys.processes().len();
|
||||||
let procs: Vec<ProcessInfo> = sys
|
let procs: Vec<ProcessInfo> = sys
|
||||||
.processes()
|
.processes()
|
||||||
.values()
|
.values()
|
||||||
.map(|p| ProcessInfo {
|
.map(|p| ProcessInfo {
|
||||||
pid: p.pid().as_u32(),
|
pid: p.pid().as_u32(),
|
||||||
name: p.name().to_string_lossy().into_owned(),
|
name: p.name().to_string_lossy().into_owned(),
|
||||||
cpu_usage: p.cpu_usage(),
|
cpu_usage: p.cpu_usage(),
|
||||||
mem_bytes: p.memory(),
|
mem_bytes: p.memory(),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
ProcessesPayload {
|
let payload = ProcessesPayload {
|
||||||
process_count: total_count,
|
process_count: total_count,
|
||||||
top_processes: procs,
|
top_processes: procs,
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let mut cache = state.cache_processes.lock().await;
|
||||||
|
cache.set(payload.clone());
|
||||||
|
}
|
||||||
|
payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small helper to select and sort top-k by cpu
|
|
||||||
// Client now handles sorting/pagination.
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
//! Background sampler: periodically collects metrics and updates precompressed caches,
|
|
||||||
//! so WS replies just read and send cached bytes.
|
|
||||||
|
|
||||||
use crate::state::AppState;
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
use tokio::time::{sleep, Duration};
|
|
||||||
|
|
||||||
// 500ms: fast path (cpu/mem/net/temp/gpu)
|
|
||||||
pub fn spawn_sampler(_state: AppState, _period: Duration) -> JoinHandle<()> {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
// no-op background sampler (request-driven collection elsewhere)
|
|
||||||
loop {
|
|
||||||
sleep(Duration::from_secs(3600)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2s: processes top-k
|
|
||||||
pub fn spawn_process_sampler(_state: AppState, _period: Duration, _top_k: usize) -> JoinHandle<()> {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
sleep(Duration::from_secs(3600)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5s: disks
|
|
||||||
pub fn spawn_disks_sampler(_state: AppState, _period: Duration) -> JoinHandle<()> {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
sleep(Duration::from_secs(3600)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::atomic::AtomicUsize;
|
use std::sync::atomic::{AtomicBool, AtomicUsize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
use sysinfo::{Components, Disks, Networks, System};
|
use sysinfo::{Components, Disks, Networks, System};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ pub struct AppState {
|
|||||||
pub components: SharedComponents,
|
pub components: SharedComponents,
|
||||||
pub disks: SharedDisks,
|
pub disks: SharedDisks,
|
||||||
pub networks: SharedNetworks,
|
pub networks: SharedNetworks,
|
||||||
|
pub hostname: String,
|
||||||
|
|
||||||
// For correct per-process CPU% using /proc deltas (Linux only path uses this tracker)
|
// For correct per-process CPU% using /proc deltas (Linux only path uses this tracker)
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -34,6 +36,42 @@ pub struct AppState {
|
|||||||
pub client_count: Arc<AtomicUsize>,
|
pub client_count: Arc<AtomicUsize>,
|
||||||
|
|
||||||
pub auth_token: Option<String>,
|
pub auth_token: Option<String>,
|
||||||
|
// GPU negative cache (probe once). gpu_checked=true after first attempt; gpu_present reflects result.
|
||||||
|
pub gpu_checked: Arc<AtomicBool>,
|
||||||
|
pub gpu_present: Arc<AtomicBool>,
|
||||||
|
|
||||||
|
// Lightweight on-demand caches (TTL based) to cap CPU under bursty polling.
|
||||||
|
pub cache_metrics: Arc<Mutex<CacheEntry<crate::types::Metrics>>>,
|
||||||
|
pub cache_disks: Arc<Mutex<CacheEntry<Vec<crate::types::DiskInfo>>>>,
|
||||||
|
pub cache_processes: Arc<Mutex<CacheEntry<crate::types::ProcessesPayload>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CacheEntry<T> {
|
||||||
|
pub at: Option<Instant>,
|
||||||
|
pub value: Option<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> CacheEntry<T> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
at: None,
|
||||||
|
value: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn is_fresh(&self, ttl: Duration) -> bool {
|
||||||
|
self.at.is_some_and(|t| t.elapsed() < ttl) && self.value.is_some()
|
||||||
|
}
|
||||||
|
pub fn set(&mut self, v: T) {
|
||||||
|
self.value = Some(v);
|
||||||
|
self.at = Some(Instant::now());
|
||||||
|
}
|
||||||
|
pub fn take_clone(&self) -> Option<T>
|
||||||
|
where
|
||||||
|
T: Clone,
|
||||||
|
{
|
||||||
|
self.value.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
@@ -48,12 +86,18 @@ impl AppState {
|
|||||||
components: Arc::new(Mutex::new(components)),
|
components: Arc::new(Mutex::new(components)),
|
||||||
disks: Arc::new(Mutex::new(disks)),
|
disks: Arc::new(Mutex::new(disks)),
|
||||||
networks: Arc::new(Mutex::new(networks)),
|
networks: Arc::new(Mutex::new(networks)),
|
||||||
|
hostname: System::host_name().unwrap_or_else(|| "unknown".into()),
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
proc_cpu: Arc::new(Mutex::new(ProcCpuTracker::default())),
|
proc_cpu: Arc::new(Mutex::new(ProcCpuTracker::default())),
|
||||||
client_count: Arc::new(AtomicUsize::new(0)),
|
client_count: Arc::new(AtomicUsize::new(0)),
|
||||||
auth_token: std::env::var("SOCKTOP_TOKEN")
|
auth_token: std::env::var("SOCKTOP_TOKEN")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|s| !s.is_empty()),
|
.filter(|s| !s.is_empty()),
|
||||||
|
gpu_checked: Arc::new(AtomicBool::new(false)),
|
||||||
|
gpu_present: Arc::new(AtomicBool::new(false)),
|
||||||
|
cache_metrics: Arc::new(Mutex::new(CacheEntry::new())),
|
||||||
|
cache_disks: Arc::new(Mutex::new(CacheEntry::new())),
|
||||||
|
cache_processes: Arc::new(Mutex::new(CacheEntry::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-49
@@ -1,18 +1,11 @@
|
|||||||
use openssl::asn1::Asn1Time;
|
use rcgen::{CertificateParams, DistinguishedName, DnType, IsCa, SanType};
|
||||||
use openssl::hash::MessageDigest;
|
|
||||||
use openssl::nid::Nid;
|
|
||||||
use openssl::pkey::PKey;
|
|
||||||
use openssl::rsa::Rsa;
|
|
||||||
use openssl::x509::extension::{
|
|
||||||
BasicConstraints, ExtendedKeyUsage, KeyUsage, SubjectAlternativeName,
|
|
||||||
};
|
|
||||||
use openssl::x509::{X509NameBuilder, X509};
|
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
io::Write,
|
io::Write,
|
||||||
net::{IpAddr, Ipv4Addr},
|
net::{IpAddr, Ipv4Addr},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
use time::{Duration, OffsetDateTime};
|
||||||
|
|
||||||
fn config_dir() -> PathBuf {
|
fn config_dir() -> PathBuf {
|
||||||
std::env::var_os("XDG_CONFIG_HOME")
|
std::env::var_os("XDG_CONFIG_HOME")
|
||||||
@@ -35,57 +28,59 @@ pub fn ensure_self_signed_cert() -> anyhow::Result<(PathBuf, PathBuf)> {
|
|||||||
}
|
}
|
||||||
fs::create_dir_all(cert_path.parent().unwrap())?;
|
fs::create_dir_all(cert_path.parent().unwrap())?;
|
||||||
|
|
||||||
// Key
|
|
||||||
let rsa = Rsa::generate(4096)?;
|
|
||||||
let pkey = PKey::from_rsa(rsa)?;
|
|
||||||
|
|
||||||
// Subject/issuer
|
|
||||||
let hostname = hostname::get()
|
let hostname = hostname::get()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| s.into_string().ok())
|
.and_then(|s| s.into_string().ok())
|
||||||
.unwrap_or_else(|| "localhost".to_string());
|
.unwrap_or_else(|| "localhost".to_string());
|
||||||
let mut name = X509NameBuilder::new()?;
|
|
||||||
name.append_entry_by_nid(Nid::COMMONNAME, &hostname)?;
|
|
||||||
let name = name.build();
|
|
||||||
|
|
||||||
// Cert builder
|
let mut params = CertificateParams::new(vec![hostname.clone(), "localhost".into()])?;
|
||||||
let mut builder = X509::builder()?;
|
params
|
||||||
builder.set_version(2)?;
|
.subject_alt_names
|
||||||
builder.set_subject_name(&name)?;
|
.push(SanType::IpAddress(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
|
||||||
builder.set_issuer_name(&name)?;
|
params.subject_alt_names.push(SanType::IpAddress(IpAddr::V6(
|
||||||
builder.set_pubkey(&pkey)?;
|
::std::net::Ipv6Addr::LOCALHOST,
|
||||||
|
)));
|
||||||
|
params
|
||||||
|
.subject_alt_names
|
||||||
|
.push(SanType::IpAddress(IpAddr::V4(Ipv4Addr::UNSPECIFIED)));
|
||||||
|
|
||||||
builder.set_not_before(Asn1Time::days_from_now(0)?.as_ref())?;
|
// Allow operator to provide extra SANs (comma-separated), e.g. IPs or DNS names
|
||||||
builder.set_not_after(Asn1Time::days_from_now(397)?.as_ref())?;
|
if let Ok(extra) = std::env::var("SOCKTOP_AGENT_EXTRA_SANS") {
|
||||||
|
for raw in extra.split(',') {
|
||||||
|
let s = raw.trim();
|
||||||
|
if s.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Ok(ip) = s.parse::<IpAddr>() {
|
||||||
|
params.subject_alt_names.push(SanType::IpAddress(ip));
|
||||||
|
} else {
|
||||||
|
match s.to_string().try_into() {
|
||||||
|
Ok(dns) => params.subject_alt_names.push(SanType::DnsName(dns)),
|
||||||
|
Err(_) => eprintln!("socktop_agent: ignoring invalid SAN entry: {s}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SANs: hostname + localhost loopbacks
|
let mut dn = DistinguishedName::new();
|
||||||
let mut san = SubjectAlternativeName::new();
|
dn.push(DnType::CommonName, hostname.clone());
|
||||||
san.dns(&hostname)
|
params.distinguished_name = dn;
|
||||||
.dns("localhost")
|
params.is_ca = IsCa::NoCa;
|
||||||
.ip("127.0.0.1")
|
// Dynamic validity: start slightly in the past to avoid clock skew issues, end ~397 days later
|
||||||
.ip("::1");
|
let now = OffsetDateTime::now_utc();
|
||||||
// Add a generic 0.0.0.0 for convenience; some TLS libs ignore this, but harmless.
|
params.not_before = now - Duration::minutes(5);
|
||||||
let _ = san.ip(&IpAddr::V4(Ipv4Addr::UNSPECIFIED).to_string());
|
params.not_after = now + Duration::days(397);
|
||||||
let san = san.build(&builder.x509v3_context(None, None))?;
|
|
||||||
// End-entity cert: not a CA
|
|
||||||
builder.append_extension(BasicConstraints::new().critical().build()?)?;
|
|
||||||
builder.append_extension(
|
|
||||||
KeyUsage::new()
|
|
||||||
.digital_signature()
|
|
||||||
.key_encipherment()
|
|
||||||
.build()?,
|
|
||||||
)?;
|
|
||||||
// TLS server usage
|
|
||||||
builder.append_extension(ExtendedKeyUsage::new().server_auth().build()?)?;
|
|
||||||
builder.append_extension(san)?;
|
|
||||||
|
|
||||||
builder.sign(&pkey, MessageDigest::sha256())?;
|
// Generate key pair (default is ECDSA P256 SHA256)
|
||||||
let cert: X509 = builder.build();
|
let key_pair = rcgen::KeyPair::generate()?; // defaults to ECDSA P256 SHA256
|
||||||
|
let cert = params.self_signed(&key_pair)?;
|
||||||
|
let cert_pem = cert.pem();
|
||||||
|
let key_pem = key_pair.serialize_pem();
|
||||||
|
|
||||||
let mut f = fs::File::create(&cert_path)?;
|
let mut f = fs::File::create(&cert_path)?;
|
||||||
f.write_all(&cert.to_pem()?)?;
|
f.write_all(cert_pem.as_bytes())?;
|
||||||
let mut k = fs::File::create(&key_path)?;
|
let mut k = fs::File::create(&key_path)?;
|
||||||
k.write_all(&pkey.private_key_to_pem_pkcs8()?)?;
|
k.write_all(key_pem.as_bytes())?;
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"socktop_agent: generated self-signed TLS certificate at {}",
|
"socktop_agent: generated self-signed TLS certificate at {}",
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
//! Unit test for port parsing logic moved out of `main.rs`.
|
||||||
|
|
||||||
|
fn parse_port<I: IntoIterator<Item = String>>(args: I, default_port: u16) -> u16 {
|
||||||
|
let mut it = args.into_iter();
|
||||||
|
let _ = it.next(); // program name
|
||||||
|
let mut long: Option<String> = None;
|
||||||
|
let mut short: Option<String> = None;
|
||||||
|
while let Some(a) = it.next() {
|
||||||
|
match a.as_str() {
|
||||||
|
"--port" => long = it.next(),
|
||||||
|
"-p" => short = it.next(),
|
||||||
|
_ if a.starts_with("--port=") => {
|
||||||
|
if let Some((_, v)) = a.split_once('=') {
|
||||||
|
long = Some(v.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long.or(short)
|
||||||
|
.and_then(|s| s.parse::<u16>().ok())
|
||||||
|
.unwrap_or(default_port)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn port_long_short_and_assign() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_port(vec!["agent".into(), "--port".into(), "9001".into()], 8443),
|
||||||
|
9001
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_port(vec!["agent".into(), "-p".into(), "9002".into()], 8443),
|
||||||
|
9002
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_port(vec!["agent".into(), "--port=9003".into()], 8443),
|
||||||
|
9003
|
||||||
|
);
|
||||||
|
assert_eq!(parse_port(vec!["agent".into()], 8443), 8443);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user