From 033e2532592f830f3e46bb09f62558e86626ac67 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 18 Feb 2026 20:13:00 +0300 Subject: [PATCH] initial commit Signed-off-by: NotAShelf Change-Id: Ib131388c1056b6708b730a35011811026a6a6964 --- .envrc | 2 + .gitignore | 1 + Cargo.lock | 1171 +++++++++++++++++++++++++++ Cargo.toml | 41 + contrib/pscand.example.conf | 27 + contrib/systemd/pscand.service | 34 + flake.lock | 27 + flake.nix | 23 + nix/package.nix | 29 + nix/shell.nix | 30 + pscand-cli/Cargo.toml | 31 + pscand-cli/src/main.rs | 228 ++++++ pscand-core/Cargo.toml | 23 + pscand-core/src/config.rs | 126 +++ pscand-core/src/helpers/mod.rs | 11 + pscand-core/src/helpers/power.rs | 157 ++++ pscand-core/src/helpers/process.rs | 143 ++++ pscand-core/src/helpers/resource.rs | 123 +++ pscand-core/src/helpers/sensor.rs | 99 +++ pscand-core/src/helpers/system.rs | 45 + pscand-core/src/lib.rs | 9 + pscand-core/src/logging.rs | 137 ++++ pscand-core/src/scanner.rs | 112 +++ pscand-macros/Cargo.toml | 14 + pscand-macros/src/lib.rs | 62 ++ scanners/scanner-power/Cargo.toml | 15 + scanners/scanner-power/src/lib.rs | 94 +++ scanners/scanner-proc/Cargo.toml | 15 + scanners/scanner-proc/src/lib.rs | 99 +++ scanners/scanner-sensor/Cargo.toml | 15 + scanners/scanner-sensor/src/lib.rs | 80 ++ scanners/scanner-system/Cargo.toml | 15 + scanners/scanner-system/src/lib.rs | 88 ++ 33 files changed, 3126 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 contrib/pscand.example.conf create mode 100644 contrib/systemd/pscand.service create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/package.nix create mode 100644 nix/shell.nix create mode 100644 pscand-cli/Cargo.toml create mode 100644 pscand-cli/src/main.rs create mode 100644 pscand-core/Cargo.toml create mode 100644 pscand-core/src/config.rs create mode 100644 pscand-core/src/helpers/mod.rs create mode 100644 pscand-core/src/helpers/power.rs create mode 100644 pscand-core/src/helpers/process.rs create mode 100644 pscand-core/src/helpers/resource.rs create mode 100644 pscand-core/src/helpers/sensor.rs create mode 100644 pscand-core/src/helpers/system.rs create mode 100644 pscand-core/src/lib.rs create mode 100644 pscand-core/src/logging.rs create mode 100644 pscand-core/src/scanner.rs create mode 100644 pscand-macros/Cargo.toml create mode 100644 pscand-macros/src/lib.rs create mode 100644 scanners/scanner-power/Cargo.toml create mode 100644 scanners/scanner-power/src/lib.rs create mode 100644 scanners/scanner-proc/Cargo.toml create mode 100644 scanners/scanner-proc/src/lib.rs create mode 100644 scanners/scanner-sensor/Cargo.toml create mode 100644 scanners/scanner-sensor/src/lib.rs create mode 100644 scanners/scanner-system/Cargo.toml create mode 100644 scanners/scanner-system/src/lib.rs diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..e3fecb3 --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +use flake + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b60de5b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5897a70 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1171 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c81d250916401487680ed13b8b675660281dcfc3ab0121fe44c94bcab9eae2fb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pscand-cli" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "dirs", + "env_logger", + "libloading", + "log", + "parking_lot", + "pscand-core", + "pscand-macros", + "ringbuf", + "serde", + "serde_json", + "sysinfo", + "thiserror", + "tokio", + "toml", +] + +[[package]] +name = "pscand-core" +version = "0.1.0" +dependencies = [ + "chrono", + "dirs", + "log", + "parking_lot", + "ringbuf", + "serde", + "serde_json", + "sysinfo", + "thiserror", + "tokio", + "toml", +] + +[[package]] +name = "pscand-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "ringbuf" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c" +dependencies = [ + "crossbeam-utils", + "portable-atomic", + "portable-atomic-util", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scanner-power" +version = "0.1.0" +dependencies = [ + "pscand-core", + "toml", +] + +[[package]] +name = "scanner-proc" +version = "0.1.0" +dependencies = [ + "pscand-core", + "toml", +] + +[[package]] +name = "scanner-sensor" +version = "0.1.0" +dependencies = [ + "pscand-core", + "toml", +] + +[[package]] +name = "scanner-system" +version = "0.1.0" +dependencies = [ + "pscand-core", + "toml", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sysinfo" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efc19935b4b66baa6f654ac7924c192f55b175c00a7ab72410fc24284dacda8" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "1.0.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1dfefef6a142e93f346b64c160934eb13b5594b84ab378133ac6815cb2bd57f" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..29ca12b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,41 @@ +[workspace] +resolver = "3" +members = [ + "pscand-cli", + "pscand-core", + "pscand-macros", + "scanners/scanner-system", + "scanners/scanner-sensor", + "scanners/scanner-power", + "scanners/scanner-proc", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" +authors = ["NotAShelf "] + +[workspace.dependencies] +pscand-core = { path = "pscand-core" } +pscand-macros = { path = "pscand-macros" } + +tokio = { version = "1.49.0", features = ["full"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +toml = "1.0.0" +libloading = "0.9.0" +chrono = { version = "0.4.43", features = ["serde"] } +sysinfo = "0.38.2" +log = "0.4.29" +env_logger = "0.11.9" +thiserror = "2.0.18" +parking_lot = "0.12.5" +ringbuf = "0.4.8" +dirs = "6.0.0" +clap = { version = "4.5.59", features = ["derive"] } + +[profile.release] +lto = true +opt-level = "z" +codegen-units = 1 diff --git a/contrib/pscand.example.conf b/contrib/pscand.example.conf new file mode 100644 index 0000000..b032bb8 --- /dev/null +++ b/contrib/pscand.example.conf @@ -0,0 +1,27 @@ +log_dir = "/var/log/pscand" +retention_days = 7 +ring_buffer_size = 60 +journal_enabled = true +file_enabled = true + +[scanner_dirs] +# Directories to load scanner plugins from +dirs = [ + "~/.local/share/pscand/scanners", +] + +[scanners.system] +enabled = true +interval_secs = 1 + +[scanners.sensor] +enabled = true +interval_secs = 2 + +[scanners.power] +enabled = true +interval_secs = 2 + +[scanners.proc] +enabled = true +interval_secs = 5 diff --git a/contrib/systemd/pscand.service b/contrib/systemd/pscand.service new file mode 100644 index 0000000..d18b748 --- /dev/null +++ b/contrib/systemd/pscand.service @@ -0,0 +1,34 @@ +[Unit] +Description=Pluggable System Condition Monitoring Daemon +Documentation=https://github.com/pscand/pscand +After=network.target + +[Service] +Type=simple +ExecStart=/usr/bin/pscand run --config /etc/pscand/pscand.conf +Restart=on-failure +RestartSec=5 +User=root +Group=root + +# Log to journal +StandardOutput=journal +StandardError=journal + +# Capabilities for sensor access +AmbientCapabilities=CAP_SYS_ADMIN CAP_DAC_OVERRIDE + +# Security hardening +ProtectSystem=full +ProtectHome=true +NoNewPrivileges=false + +# Runtime directory +RuntimeDirectory=pscand +RuntimeDirectoryMode=0755 + +# Tempfs for sensitive /proc data +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5dcdbd4 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1771008912, + "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a82ccc39b39b621151d6732718e3e250109076fa", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..0f9bf25 --- /dev/null +++ b/flake.nix @@ -0,0 +1,23 @@ +{ + description = "Rust Project Template"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable"; + + outputs = { + self, + nixpkgs, + }: let + systems = ["x86_64-linux" "aarch64-linux"]; + forEachSystem = nixpkgs.lib.genAttrs systems; + pkgsForEach = nixpkgs.legacyPackages; + in { + packages = forEachSystem (system: { + default = pkgsForEach.${system}.callPackage ./nix/package.nix {}; + }); + + devShells = forEachSystem (system: { + default = pkgsForEach.${system}.callPackage ./nix/shell.nix {}; + }); + + hydraJobs = self.packages; + }; +} diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 0000000..0966fb5 --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,29 @@ +{ + lib, + rustPlatform, +}: +rustPlatform.buildRustPackage (finalAttrs: { + pname = "sample-rust"; + version = "0.1.0"; + + src = let + fs = lib.fileset; + s = ../.; + in + fs.toSource { + root = s; + fileset = fs.unions [ + (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /src)) + (s + /Cargo.lock) + (s + /Cargo.toml) + ]; + }; + + cargoLock.lockFile = "${finalAttrs.src}/Cargo.lock"; + enableParallelBuilding = true; + + meta = { + description = "Sample Rust project"; + maintainers = with lib.maintainers; [NotAShelf]; + }; +}) diff --git a/nix/shell.nix b/nix/shell.nix new file mode 100644 index 0000000..3b1df12 --- /dev/null +++ b/nix/shell.nix @@ -0,0 +1,30 @@ +{ + mkShell, + rustc, + cargo, + rust-analyzer-unwrapped, + rustfmt, + clippy, + taplo, + rustPlatform, +}: +mkShell { + name = "rust"; + + strictDeps = true; + packages = [ + rustc + cargo + + # Tools + rustfmt + clippy + cargo + taplo + + # LSP + rust-analyzer-unwrapped + ]; + + RUST_SRC_PATH = "${rustPlatform.rustLibSrc}"; +} diff --git a/pscand-cli/Cargo.toml b/pscand-cli/Cargo.toml new file mode 100644 index 0000000..ab41096 --- /dev/null +++ b/pscand-cli/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "pscand-cli" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[[bin]] +name = "pscand" +path = "src/main.rs" + +[dependencies] +pscand-core.workspace = true +pscand-macros.workspace = true +tokio.workspace = true +serde.workspace = true +serde_json.workspace = true +toml.workspace = true +libloading.workspace = true +chrono.workspace = true +log.workspace = true +env_logger.workspace = true +thiserror.workspace = true +parking_lot.workspace = true +ringbuf.workspace = true +dirs.workspace = true +sysinfo.workspace = true +clap.workspace = true + +[profile.release] +lto = true diff --git a/pscand-cli/src/main.rs b/pscand-cli/src/main.rs new file mode 100644 index 0000000..0fd514e --- /dev/null +++ b/pscand-cli/src/main.rs @@ -0,0 +1,228 @@ +use clap::Parser; +use libloading::Library; +use pscand_core::logging::{LogEntry, RingBufferLogger}; +use pscand_core::scanner::Scanner; +use pscand_core::Config as CoreConfig; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::RwLock; +use tokio::time::interval; + +type ScannerCreator = unsafe extern "C" fn() -> Box; + +#[derive(Parser, Debug)] +#[command( + name = "pscand", + version = "0.1.0", + about = "Pluggable System Condition Monitoring Daemon" +)] +enum Args { + Run(RunArgs), + List, +} + +#[derive(Parser, Debug)] +struct RunArgs { + #[arg(short, long, default_value = "/etc/pscand/pscand.conf")] + config: PathBuf, + + #[arg(short, long)] + debug: bool, +} + +struct LoadedScanner { + name: String, + scanner: Arc>>, + #[allow(dead_code)] + library: Library, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = Args::parse(); + + match args { + Args::Run(run_args) => { + run_daemon(run_args).await?; + } + Args::List => { + list_scanners().await?; + } + } + + Ok(()) +} + +async fn run_daemon(args: RunArgs) -> Result<(), Box> { + if args.debug { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init(); + } else { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + } + + log::info!("Starting pscand daemon"); + + let config = if args.config.exists() { + CoreConfig::load(&args.config)? + } else { + log::warn!("Config file not found, using defaults"); + CoreConfig::default() + }; + + std::fs::create_dir_all(&config.log_dir)?; + + let log_file = config.log_dir.join("pscand.log"); + let logger = Arc::new(RingBufferLogger::new( + config.ring_buffer_size, + Some(log_file), + config.journal_enabled, + config.file_enabled, + )); + + std::panic::set_hook(Box::new({ + let logger = Arc::clone(&logger); + let log_dir = config.log_dir.clone(); + move |panic_info| { + let entries = logger.get_recent(60); + let crash_log = log_dir.join("crash.log"); + if let Ok(mut file) = std::fs::File::create(&crash_log) { + use std::io::Write; + let _ = writeln!(file, "=== Crash at {} ===", chrono::Utc::now()); + let _ = writeln!(file, "Panic: {}", panic_info); + let _ = writeln!(file, "\n=== Last {} log entries ===", entries.len()); + for entry in entries { + let _ = writeln!(file, "{}", entry.to_json()); + } + } + } + })); + + let scanners = load_scanners(&config).await?; + + if scanners.is_empty() { + log::warn!("No scanners loaded!"); + } else { + log::info!("Loaded {} scanners", scanners.len()); + } + + let mut handles = Vec::new(); + + for loaded in scanners { + let logger = Arc::clone(&logger); + let name = loaded.name.clone(); + let scanner = loaded.scanner.clone(); + + let handle = tokio::spawn(async move { + let mut ticker = interval(Duration::from_secs(1)); + + loop { + ticker.tick().await; + + let scanner_guard = scanner.read().await; + match scanner_guard.collect() { + Ok(metrics) => { + let entry = LogEntry::new(name.as_str(), metrics); + logger.push(entry); + } + Err(e) => { + log::error!("Scanner {} error: {}", name, e); + } + } + } + }); + + handles.push(handle); + } + + tokio::signal::ctrl_c().await?; + log::info!("Shutting down pscand"); + + for handle in handles { + handle.abort(); + } + + Ok(()) +} + +async fn load_scanners( + config: &CoreConfig, +) -> Result, Box> { + let mut loaded = Vec::new(); + + for dir in &config.scanner_dirs { + if !dir.exists() { + continue; + } + + log::info!("Loading scanners from {:?}", dir); + + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + + if path.extension().and_then(|s| s.to_str()) != Some("so") { + continue; + } + + unsafe { + match Library::new(&path) { + Ok(lib) => { + let creator: libloading::Symbol = + match lib.get(b"pscand_scanner") { + Ok(s) => s, + Err(e) => { + log::warn!("Scanner {:?} missing symbol: {}", path, e); + continue; + } + }; + + let scanner = creator(); + let name = scanner.name().to_string(); + + let scanner_enabled = config.is_scanner_enabled(&name); + + if !scanner_enabled { + log::info!("Scanner {} disabled in config", name); + continue; + } + + let mut scanner = scanner; + + if let Some(scanner_config) = config.scanner_config(&name) { + let toml_map: toml::map::Map = + scanner_config.extra.clone().into_iter().collect(); + let toml_val = toml::Value::Table(toml_map); + if let Err(e) = scanner.init(&toml_val) { + log::error!("Failed to init scanner {}: {}", name, e); + continue; + } + } + + loaded.push(LoadedScanner { + name, + scanner: Arc::new(RwLock::new(scanner)), + library: lib, + }); + } + Err(e) => { + log::warn!("Failed to load scanner {:?}: {}", path, e); + } + } + } + } + } + + Ok(loaded) +} + +async fn list_scanners() -> Result<(), Box> { + println!("Available built-in scanners:"); + println!(" - system: CPU, memory, disk, network, load average"); + println!(" - sensor: hwmon temperature, fan, voltage sensors"); + println!(" - power: battery and power supply status"); + println!(" - proc: process count and zombie detection"); + println!("\nDynamic scanners are loaded from $PSCAND_SCANNER_DIRS (colon-separated)"); + println!(" Default fallback: ~/.local/share/pscand/scanners/ or ~/.config/pscand/scanners/"); + Ok(()) +} diff --git a/pscand-core/Cargo.toml b/pscand-core/Cargo.toml new file mode 100644 index 0000000..a711562 --- /dev/null +++ b/pscand-core/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "pscand-core" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +tokio.workspace = true +serde.workspace = true +serde_json.workspace = true +toml.workspace = true +chrono.workspace = true +log.workspace = true +thiserror.workspace = true +parking_lot.workspace = true +dirs.workspace = true +sysinfo.workspace = true +ringbuf.workspace = true + +[lib] +name = "pscand_core" +path = "src/lib.rs" diff --git a/pscand-core/src/config.rs b/pscand-core/src/config.rs new file mode 100644 index 0000000..e1e3d46 --- /dev/null +++ b/pscand-core/src/config.rs @@ -0,0 +1,126 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ConfigError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Parse error: {0}")] + Parse(#[from] toml::de::Error), + #[error("Scanner {0} not configured")] + ScannerNotConfigured(String), + #[error("Invalid scanner name: {0}")] + InvalidScannerName(String), +} + +pub type ConfigResult = std::result::Result; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScannerConfig { + pub enabled: bool, + pub interval_secs: Option, + pub extra: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + #[serde(default = "default_scanner_dirs")] + pub scanner_dirs: Vec, + #[serde(default = "default_log_dir")] + pub log_dir: PathBuf, + #[serde(default = "default_retention_days")] + pub retention_days: u32, + #[serde(default = "default_ring_buffer_size")] + pub ring_buffer_size: usize, + #[serde(default)] + pub scanners: HashMap, + #[serde(default)] + pub journal_enabled: bool, + #[serde(default)] + pub file_enabled: bool, +} + +fn default_scanner_dirs() -> Vec { + let mut dirs = Vec::new(); + + if let Ok(env_var) = std::env::var("PSCAND_SCANNER_DIRS") { + for path in env_var.split(':') { + let path = PathBuf::from(path); + if !path.as_os_str().is_empty() { + dirs.push(path); + } + } + if !dirs.is_empty() { + return dirs; + } + } + + if let Some(lib) = std::env::var_os("LIB_PSCAND") { + dirs.push(PathBuf::from(lib)); + } + + if let Ok(lib_dir) = std::env::var("LIBDIR_PSCAND") { + dirs.push(PathBuf::from(lib_dir)); + } + + if let Some(local) = dirs::data_local_dir() { + dirs.push(local.join("pscand/scanners")); + } + + if let Some(config) = dirs::config_dir() { + dirs.push(config.join("pscand/scanners")); + } + + if dirs.is_empty() { + dirs.push(PathBuf::from(".pscand/scanners")); + } + + dirs +} + +fn default_log_dir() -> PathBuf { + dirs::data_local_dir() + .map(|p| p.join("pscand/logs")) + .unwrap_or_else(|| PathBuf::from(".pscand/logs")) +} + +fn default_retention_days() -> u32 { + 7 +} + +fn default_ring_buffer_size() -> usize { + 60 +} + +impl Default for Config { + fn default() -> Self { + Self { + scanner_dirs: default_scanner_dirs(), + log_dir: default_log_dir(), + retention_days: default_retention_days(), + ring_buffer_size: default_ring_buffer_size(), + scanners: HashMap::new(), + journal_enabled: true, + file_enabled: true, + } + } +} + +impl Config { + pub fn load(path: &PathBuf) -> ConfigResult { + let content = std::fs::read_to_string(path)?; + let mut config: Config = toml::from_str(&content)?; + config.scanner_dirs.retain(|p| p.exists()); + Ok(config) + } + + pub fn scanner_config(&self, name: &str) -> Option<&ScannerConfig> { + self.scanners.get(name) + } + + pub fn is_scanner_enabled(&self, name: &str) -> bool { + self.scanners.get(name).map(|c| c.enabled).unwrap_or(true) + } +} diff --git a/pscand-core/src/helpers/mod.rs b/pscand-core/src/helpers/mod.rs new file mode 100644 index 0000000..fbf826a --- /dev/null +++ b/pscand-core/src/helpers/mod.rs @@ -0,0 +1,11 @@ +pub mod power; +pub mod process; +pub mod resource; +pub mod sensor; +pub mod system; + +pub use power::PowerHelper; +pub use process::ProcessHelper; +pub use resource::ResourceHelper; +pub use sensor::SensorHelper; +pub use system::SystemHelper; diff --git a/pscand-core/src/helpers/power.rs b/pscand-core/src/helpers/power.rs new file mode 100644 index 0000000..0277f07 --- /dev/null +++ b/pscand-core/src/helpers/power.rs @@ -0,0 +1,157 @@ +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; + +pub struct PowerHelper; + +#[derive(Debug, Clone)] +pub struct BatteryInfo { + pub name: String, + pub status: String, + pub capacity: u32, + pub charge_percent: i32, + pub voltage: f64, + pub current_now: i64, + pub power_now: i64, + pub present: bool, +} + +impl PowerHelper { + pub fn battery_info() -> std::io::Result> { + let battery_path = PathBuf::from("/sys/class/power_supply"); + + for entry in fs::read_dir(&battery_path)? { + let entry = entry?; + let path = entry.path(); + let type_path = path.join("type"); + + if type_path.exists() { + let battery_type = fs::read_to_string(&type_path)?.trim().to_string(); + if battery_type == "Battery" { + let present_path = path.join("present"); + let present = fs::read_to_string(&present_path) + .map(|s| s.trim() == "1") + .unwrap_or(false); + + if !present { + continue; + } + + let name = fs::read_to_string(path.join("name")) + .unwrap_or_else(|_| "Unknown".to_string()) + .trim() + .to_string(); + + let status = fs::read_to_string(path.join("status")) + .unwrap_or_else(|_| "Unknown".to_string()) + .trim() + .to_string(); + + let capacity = fs::read_to_string(path.join("capacity")) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); + + let charge_now = fs::read_to_string(path.join("charge_now")) + .or_else(|_| fs::read_to_string(path.join("energy_now"))) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); + + let charge_full = fs::read_to_string(path.join("charge_full")) + .or_else(|_| fs::read_to_string(path.join("energy_full"))) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(1); + + let charge_percent = if charge_full > 0 { + ((charge_now as f64 / charge_full as f64) * 100.0) as i32 + } else { + 0 + }; + + let voltage = fs::read_to_string(path.join("voltage_now")) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0.0) + / 1_000_000.0; + + let current_now = fs::read_to_string(path.join("current_now")) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); + + let power_now = fs::read_to_string(path.join("power_now")) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); + + return Ok(Some(BatteryInfo { + name, + status, + capacity, + charge_percent, + voltage, + current_now, + power_now, + present, + })); + } + } + } + + Ok(None) + } + + pub fn power_supplies() -> std::io::Result>> { + let mut supplies = HashMap::new(); + let power_supply_path = PathBuf::from("/sys/class/power_supply"); + + if !power_supply_path.exists() { + return Ok(supplies); + } + + for entry in fs::read_dir(&power_supply_path)? { + let entry = entry?; + let path = entry.path(); + let name = path + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_default(); + + let mut info = HashMap::new(); + + for attr in [ + "type", + "status", + "capacity", + "voltage_now", + "power_now", + "online", + ] { + let attr_path = path.join(attr); + if attr_path.exists() { + if let Ok(content) = fs::read_to_string(&attr_path) { + info.insert(attr.to_string(), content.trim().to_string()); + } + } + } + + if !info.is_empty() { + supplies.insert(name, info); + } + } + + Ok(supplies) + } + + pub fn suspend_state() -> std::io::Result { + let state_path = PathBuf::from("/sys/power/state"); + fs::read_to_string(state_path).map(|s| s.trim().to_string()) + } + + pub fn mem_sleep_state() -> std::io::Result { + let state_path = PathBuf::from("/sys/power/mem_sleep"); + fs::read_to_string(state_path).map(|s| s.trim().to_string()) + } +} diff --git a/pscand-core/src/helpers/process.rs b/pscand-core/src/helpers/process.rs new file mode 100644 index 0000000..f31cf74 --- /dev/null +++ b/pscand-core/src/helpers/process.rs @@ -0,0 +1,143 @@ +use std::collections::HashMap; +use std::fs; + +pub struct ProcessHelper; + +#[derive(Debug)] +pub struct ProcessInfo { + pub pid: u32, + pub name: String, + pub state: String, + pub ppid: u32, + pub memory_kb: u64, + pub cpu_percent: f32, +} + +impl ProcessHelper { + pub fn list_processes() -> std::io::Result> { + let mut processes = Vec::new(); + let proc_path = fs::read_dir("/proc")?; + + for entry in proc_path.flatten() { + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let pid: u32 = match path.file_name() { + Some(name) => match name.to_str() { + Some(s) => s.parse().ok(), + None => None, + } + .ok_or(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "invalid pid", + ))?, + None => continue, + }; + + if let Ok(info) = Self::process_info(pid) { + processes.push(info); + } + } + + Ok(processes) + } + + pub fn process_info(pid: u32) -> std::io::Result { + let status_path = format!("/proc/{}/status", pid); + let content = fs::read_to_string(status_path)?; + + let mut name = String::new(); + let mut state = String::new(); + let mut ppid: u32 = 0; + let mut memory_kb: u64 = 0; + + for line in content.lines() { + if line.starts_with("Name:") { + name = line + .split_whitespace() + .skip(1) + .collect::>() + .join(" "); + } else if line.starts_with("State:") { + state = line + .split_whitespace() + .skip(1) + .next() + .unwrap_or("") + .to_string(); + } else if line.starts_with("PPid:") { + ppid = line + .split_whitespace() + .skip(1) + .next() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + } else if line.starts_with("VmRSS:") { + memory_kb = line + .split_whitespace() + .skip(1) + .next() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + } + } + + Ok(ProcessInfo { + pid, + name, + state, + ppid, + memory_kb, + cpu_percent: 0.0, + }) + } + + pub fn zombie_processes() -> std::io::Result> { + Ok(Self::list_processes()? + .into_iter() + .filter(|p| p.state.starts_with('Z')) + .collect()) + } + + pub fn process_count() -> std::io::Result> { + let mut counts = HashMap::new(); + counts.insert("total".to_string(), 0); + counts.insert("running".to_string(), 0); + counts.insert("sleeping".to_string(), 0); + counts.insert("zombie".to_string(), 0); + + for proc in Self::list_processes()? { + *counts.get_mut("total").unwrap() += 1; + + let first_char = proc.state.chars().next().unwrap_or(' '); + match first_char { + 'R' => *counts.get_mut("running").unwrap() += 1, + 'S' | 'D' => *counts.get_mut("sleeping").unwrap() += 1, + 'Z' => *counts.get_mut("zombie").unwrap() += 1, + _ => {} + } + } + + Ok(counts) + } + + pub fn top_memory_processes(count: usize) -> std::io::Result> { + let mut processes = Self::list_processes()?; + processes.sort_by(|a, b| b.memory_kb.cmp(&a.memory_kb)); + processes.truncate(count); + Ok(processes) + } + + pub fn top_cpu_processes(count: usize) -> std::io::Result> { + let mut processes = Self::list_processes()?; + processes.sort_by(|a, b| { + b.cpu_percent + .partial_cmp(&a.cpu_percent) + .unwrap_or(std::cmp::Ordering::Equal) + }); + processes.truncate(count); + Ok(processes) + } +} diff --git a/pscand-core/src/helpers/resource.rs b/pscand-core/src/helpers/resource.rs new file mode 100644 index 0000000..8522523 --- /dev/null +++ b/pscand-core/src/helpers/resource.rs @@ -0,0 +1,123 @@ +use std::collections::HashMap; +use std::fs; + +pub struct ResourceHelper; + +impl ResourceHelper { + pub fn cpu_usage() -> std::io::Result> { + let content = fs::read_to_string("/proc/stat")?; + let mut result = HashMap::new(); + + for line in content.lines() { + if line.starts_with("cpu") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 5 { + let cpu = parts[0]; + let values: Vec = parts[1..] + .iter() + .take(7) + .filter_map(|s| s.parse().ok()) + .collect(); + + if values.len() >= 4 { + let user = values[0] as f64; + let nice = values[1] as f64; + let system = values[2] as f64; + let idle = values[3] as f64; + let iowait = values.get(4).copied().unwrap_or(0) as f64; + let irq = values.get(5).copied().unwrap_or(0) as f64; + let softirq = values.get(6).copied().unwrap_or(0) as f64; + + let total = user + nice + system + idle + iowait + irq + softirq; + let active = user + nice + system + irq + softirq; + + if cpu == "cpu" { + result.insert("total_user".to_string(), user); + result.insert("total_nice".to_string(), nice); + result.insert("total_system".to_string(), system); + result.insert("total_idle".to_string(), idle); + result.insert("total_iowait".to_string(), iowait); + result.insert( + "total_usage_percent".to_string(), + (active / total) * 100.0, + ); + } else { + let core = cpu.replace("cpu", "core_"); + result.insert(format!("{}_user", core), user); + result.insert( + format!("{}_usage_percent", core), + (active / total) * 100.0, + ); + } + } + } + } + } + Ok(result) + } + + pub fn memory_info() -> std::io::Result> { + let content = fs::read_to_string("/proc/meminfo")?; + let mut result = HashMap::new(); + + for line in content.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + let key = parts[0].trim_end_matches(':'); + if let Ok(value) = parts[1].parse::() { + result.insert(key.to_string(), value * 1024); + } + } + } + Ok(result) + } + + pub fn disk_stats() -> std::io::Result>> { + let content = fs::read_to_string("/proc/diskstats")?; + let mut result = HashMap::new(); + + for line in content.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 14 { + let device = parts[2].to_string(); + let mut stats = HashMap::new(); + stats.insert("reads_completed".to_string(), parts[3].parse().unwrap_or(0)); + stats.insert("reads_merged".to_string(), parts[4].parse().unwrap_or(0)); + stats.insert("sectors_read".to_string(), parts[5].parse().unwrap_or(0)); + stats.insert("reads_ms".to_string(), parts[6].parse().unwrap_or(0)); + stats.insert( + "writes_completed".to_string(), + parts[7].parse().unwrap_or(0), + ); + stats.insert("writes_merged".to_string(), parts[8].parse().unwrap_or(0)); + stats.insert("sectors_written".to_string(), parts[9].parse().unwrap_or(0)); + stats.insert("writes_ms".to_string(), parts[10].parse().unwrap_or(0)); + result.insert(device, stats); + } + } + Ok(result) + } + + pub fn net_dev() -> std::io::Result>> { + let content = fs::read_to_string("/proc/net/dev")?; + let mut result = HashMap::new(); + + for line in content.lines().skip(2) { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 17 { + let iface = parts[0].trim_end_matches(':'); + let mut stats = HashMap::new(); + stats.insert("rx_bytes".to_string(), parts[1].parse().unwrap_or(0)); + stats.insert("rx_packets".to_string(), parts[2].parse().unwrap_or(0)); + stats.insert("rx_errors".to_string(), parts[3].parse().unwrap_or(0)); + stats.insert("rx_dropped".to_string(), parts[4].parse().unwrap_or(0)); + stats.insert("tx_bytes".to_string(), parts[9].parse().unwrap_or(0)); + stats.insert("tx_packets".to_string(), parts[10].parse().unwrap_or(0)); + stats.insert("tx_errors".to_string(), parts[11].parse().unwrap_or(0)); + stats.insert("tx_dropped".to_string(), parts[12].parse().unwrap_or(0)); + result.insert(iface.to_string(), stats); + } + } + Ok(result) + } +} diff --git a/pscand-core/src/helpers/sensor.rs b/pscand-core/src/helpers/sensor.rs new file mode 100644 index 0000000..183b3d4 --- /dev/null +++ b/pscand-core/src/helpers/sensor.rs @@ -0,0 +1,99 @@ +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; + +pub struct SensorHelper; + +impl SensorHelper { + pub fn discover_hwmon() -> std::io::Result> { + let hwmon_path = PathBuf::from("/sys/class/hwmon"); + let mut hwmons = Vec::new(); + + if hwmon_path.exists() { + for entry in fs::read_dir(&hwmon_path)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + hwmons.push(path); + } + } + } + Ok(hwmons) + } + + pub fn read_hwmon_sensor(hwmon_path: &PathBuf, sensor: &str) -> std::io::Result> { + let sensor_path = hwmon_path.join(sensor); + if sensor_path.exists() { + let content = fs::read_to_string(sensor_path)?; + Ok(content.trim().parse::().ok()) + } else { + Ok(None) + } + } + + pub fn hwmon_info(hwmon_path: &PathBuf) -> std::io::Result> { + let mut info = HashMap::new(); + + let name_path = hwmon_path.join("name"); + if name_path.exists() { + info.insert( + "name".to_string(), + fs::read_to_string(name_path)?.trim().to_string(), + ); + } + + if let Ok(files) = fs::read_dir(hwmon_path) { + for file in files.flatten() { + let filename = file.file_name().to_string_lossy().to_string(); + if filename.starts_with("temp") && filename.ends_with("_input") { + let id = filename + .trim_start_matches("temp") + .trim_end_matches("_input"); + if let Ok(temp) = Self::read_hwmon_sensor(hwmon_path, &filename) { + if let Some(t) = temp { + info.insert(format!("temp_{}_celsius", id), format!("{}", t / 1000.0)); + } + } + } + if filename.starts_with("fan") && filename.ends_with("_input") { + let id = filename + .trim_start_matches("fan") + .trim_end_matches("_input"); + if let Ok(fan) = Self::read_hwmon_sensor(hwmon_path, &filename) { + if let Some(f) = fan { + info.insert(format!("fan_{}_rpm", id), format!("{}", f)); + } + } + } + if filename.starts_with("in") && filename.ends_with("_input") { + let id = filename.trim_start_matches("in").trim_end_matches("_input"); + if let Ok(voltage) = Self::read_hwmon_sensor(hwmon_path, &filename) { + if let Some(v) = voltage { + info.insert(format!("voltage_{}_mv", id), format!("{}", v / 1000.0)); + } + } + } + } + } + + Ok(info) + } + + pub fn all_sensors() -> std::io::Result>> { + let mut all = HashMap::new(); + + for hwmon in Self::discover_hwmon()? { + if let Ok(info) = Self::hwmon_info(&hwmon) { + let name = info.get("name").cloned().unwrap_or_else(|| { + hwmon + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| "unknown".to_string()) + }); + all.insert(name, info); + } + } + + Ok(all) + } +} diff --git a/pscand-core/src/helpers/system.rs b/pscand-core/src/helpers/system.rs new file mode 100644 index 0000000..c274111 --- /dev/null +++ b/pscand-core/src/helpers/system.rs @@ -0,0 +1,45 @@ +use std::fs; +use std::time::Duration; + +pub struct SystemHelper; + +impl SystemHelper { + pub fn uptime() -> std::io::Result { + let uptime_secs = fs::read_to_string("/proc/uptime")? + .split_whitespace() + .next() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0.0); + Ok(Duration::from_secs_f64(uptime_secs)) + } + + pub fn boot_id() -> std::io::Result { + fs::read_to_string("/proc/sys/kernel/random/boot_id").map(|s| s.trim().to_string()) + } + + pub fn load_average() -> std::io::Result<(f64, f64, f64)> { + let content = fs::read_to_string("/proc/loadavg")?; + let mut parts = content.split_whitespace(); + let load1 = parts + .next() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0.0); + let load5 = parts + .next() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0.0); + let load15 = parts + .next() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0.0); + Ok((load1, load5, load15)) + } + + pub fn hostname() -> std::io::Result { + fs::read_to_string("/proc/sys/kernel/hostname").map(|s| s.trim().to_string()) + } + + pub fn kernel_version() -> std::io::Result { + fs::read_to_string("/proc/sys/kernel/osrelease").map(|s| s.trim().to_string()) + } +} diff --git a/pscand-core/src/lib.rs b/pscand-core/src/lib.rs new file mode 100644 index 0000000..eb30000 --- /dev/null +++ b/pscand-core/src/lib.rs @@ -0,0 +1,9 @@ +pub mod config; +pub mod helpers; +pub mod logging; +pub mod scanner; + +pub use config::Config; +pub use logging::{LogEntry, RingBufferLogger}; +pub use scanner::{MetricValue, Scanner, ScannerError}; +pub type Result = std::result::Result; diff --git a/pscand-core/src/logging.rs b/pscand-core/src/logging.rs new file mode 100644 index 0000000..709b0ee --- /dev/null +++ b/pscand-core/src/logging.rs @@ -0,0 +1,137 @@ +use chrono::{DateTime, Utc}; +use parking_lot::Mutex; +use ringbuf::{ + storage::Heap, + traits::*, + wrap::caching::{CachingCons, CachingProd}, + SharedRb, +}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::path::PathBuf; +use std::sync::Arc; + +use crate::scanner::MetricValue; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogEntry { + pub timestamp: DateTime, + pub scanner: String, + pub metrics: HashMap, +} + +impl LogEntry { + pub fn new(scanner: impl Into, metrics: HashMap) -> Self { + Self { + timestamp: Utc::now(), + scanner: scanner.into(), + metrics, + } + } + + pub fn to_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|e| format!("{{\"error\":\"{}\"}}", e)) + } + + pub fn to_journal(&self) -> String { + let metrics_json = serde_json::to_string(&self.metrics).unwrap_or_default(); + format!( + "PSCAND_SCANNER={} PSCAND_METRICS={}", + self.scanner, metrics_json + ) + } +} + +type RbStorage = Heap; +type SharedRbLog = SharedRb; + +struct RingBufferHandles { + prod: CachingProd>, + cons: CachingCons>, +} + +pub struct RingBufferLogger { + buffer: Arc>, + file_path: Option, + journal_enabled: bool, + file_enabled: bool, +} + +impl RingBufferLogger { + pub fn new( + capacity: usize, + file_path: Option, + journal_enabled: bool, + file_enabled: bool, + ) -> Self { + let rb = SharedRb::::new(capacity); + let (prod, cons) = rb.split(); + + let handles = RingBufferHandles { prod, cons }; + + Self { + buffer: Arc::new(Mutex::new(handles)), + file_path, + journal_enabled, + file_enabled, + } + } + + pub fn push(&self, entry: LogEntry) { + { + let mut handles = self.buffer.lock(); + if handles.prod.is_full() { + let _ = handles.cons.try_pop(); + } + let _ = handles.prod.try_push(entry.clone()); + } + + if self.journal_enabled { + self.write_to_journal(&entry); + } + if self.file_enabled { + self.write_to_file(&entry); + } + } + + fn write_to_journal(&self, entry: &LogEntry) { + let msg = entry.to_journal(); + let _ = std::process::Command::new("logger") + .arg("-t") + .arg("pscand") + .arg("-p") + .arg("info") + .arg(msg) + .spawn(); + } + + fn write_to_file(&self, entry: &LogEntry) { + if let Some(ref path) = self.file_path { + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(file, "{}", entry.to_json()); + } + } + } + + pub fn get_recent(&self, count: usize) -> Vec { + let handles = self.buffer.lock(); + handles.cons.iter().rev().take(count).cloned().collect() + } + + pub fn flush_to_file(&self, path: &PathBuf) -> std::io::Result<()> { + let entries = self.get_recent(usize::MAX); + let mut file = fs::File::create(path)?; + for entry in entries { + writeln!(file, "{}", entry.to_json())?; + } + Ok(()) + } +} + +impl Default for RingBufferLogger { + fn default() -> Self { + Self::new(60, None, true, false) + } +} diff --git a/pscand-core/src/scanner.rs b/pscand-core/src/scanner.rs new file mode 100644 index 0000000..dccb505 --- /dev/null +++ b/pscand-core/src/scanner.rs @@ -0,0 +1,112 @@ +use std::collections::HashMap; +use std::sync::Mutex; +use std::time::Duration; + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ScannerError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Parse error: {0}")] + Parse(String), + #[error("Configuration error: {0}")] + Config(String), + #[error("Scanner not initialized")] + NotInitialized, + #[error("Scanner {0} not found")] + NotFound(String), +} + +pub type Result = std::result::Result; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum MetricValue { + Integer(i64), + Float(f64), + Boolean(bool), + String(String), +} + +impl MetricValue { + pub fn from_i64(v: i64) -> Self { + MetricValue::Integer(v) + } + + pub fn from_f64(v: f64) -> Self { + MetricValue::Float(v) + } + + pub fn from_bool(v: bool) -> Self { + MetricValue::Boolean(v) + } + + pub fn from_str(v: impl Into) -> Self { + MetricValue::String(v.into()) + } +} + +pub trait Scanner: Send + Sync { + fn name(&self) -> &'static str; + fn interval(&self) -> Duration; + fn init(&mut self, config: &toml::Value) -> Result<()>; + fn collect(&self) -> Result>; + fn cleanup(&mut self) -> Result<()>; +} + +pub trait ScannerRegistry: Send + Sync { + fn list_scanners(&self) -> Vec<&'static str>; + fn get_scanner(&self, name: &str) -> Option>; +} + +pub struct DynamicScanner { + name: &'static str, + interval: Duration, + collect_fn: Box Result> + Send + Sync>, + init_fn: Mutex Result<()> + Send>>, + cleanup_fn: Mutex Result<()> + Send>>, +} + +impl DynamicScanner { + pub fn new( + name: &'static str, + interval: Duration, + collect_fn: impl Fn() -> Result> + Send + Sync + 'static, + init_fn: impl FnMut(&toml::Value) -> Result<()> + Send + 'static, + cleanup_fn: impl FnMut() -> Result<()> + Send + 'static, + ) -> Self { + Self { + name, + interval, + collect_fn: Box::new(collect_fn), + init_fn: Mutex::new(Box::new(init_fn)), + cleanup_fn: Mutex::new(Box::new(cleanup_fn)), + } + } +} + +impl Scanner for DynamicScanner { + fn name(&self) -> &'static str { + self.name + } + + fn interval(&self) -> Duration { + self.interval + } + + fn init(&mut self, config: &toml::Value) -> Result<()> { + let mut init_fn = self.init_fn.lock().unwrap(); + (init_fn)(config) + } + + fn collect(&self) -> Result> { + (self.collect_fn)() + } + + fn cleanup(&mut self) -> Result<()> { + let mut cleanup_fn = self.cleanup_fn.lock().unwrap(); + (cleanup_fn)() + } +} diff --git a/pscand-macros/Cargo.toml b/pscand-macros/Cargo.toml new file mode 100644 index 0000000..cfcb2a5 --- /dev/null +++ b/pscand-macros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "pscand-macros" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full"] } diff --git a/pscand-macros/src/lib.rs b/pscand-macros/src/lib.rs new file mode 100644 index 0000000..ceb6a8a --- /dev/null +++ b/pscand-macros/src/lib.rs @@ -0,0 +1,62 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, ItemFn}; + +#[proc_macro_attribute] +pub fn scanner(name: TokenStream, input: TokenStream) -> TokenStream { + let name_str = parse_macro_input!(name as syn::LitStr).value(); + let input = parse_macro_input!(input as ItemFn); + + let _fn_name = input.sig.ident.clone(); + let body = &input.block; + + let result = quote! { + #[no_mangle] + pub extern "C" fn pscand_scanner() -> Box { + struct ScannerImpl; + + impl pscand_core::Scanner for ScannerImpl { + fn name(&self) -> &'static str { + #name_str + } + + fn interval(&self) -> std::time::Duration { + std::time::Duration::from_secs(1) + } + + fn init(&mut self, _config: &toml::Value) -> pscand_core::Result<()> { + Ok(()) + } + + fn collect(&self) -> pscand_core::Result> { + #body + } + + fn cleanup(&mut self) -> pscand_core::Result<()> { + Ok(()) + } + } + + Box::new(ScannerImpl) + } + }; + + result.into() +} + +#[proc_macro] +pub fn register_scanner(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemFn); + let fn_name = input.sig.ident.clone(); + + let result = quote! { + #input + + #[no_mangle] + pub extern "C" fn pscand_scanner() -> Box { + Box::new(#fn_name()) + } + }; + + result.into() +} diff --git a/scanners/scanner-power/Cargo.toml b/scanners/scanner-power/Cargo.toml new file mode 100644 index 0000000..111a6d7 --- /dev/null +++ b/scanners/scanner-power/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "scanner-power" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +pscand-core.workspace = true +toml.workspace = true + +[lib] +name = "scanner_power" +path = "src/lib.rs" +crate-type = ["cdylib"] diff --git a/scanners/scanner-power/src/lib.rs b/scanners/scanner-power/src/lib.rs new file mode 100644 index 0000000..3bb53d0 --- /dev/null +++ b/scanners/scanner-power/src/lib.rs @@ -0,0 +1,94 @@ +#![allow(improper_ctypes)] +use pscand_core::helpers::PowerHelper; +use pscand_core::scanner::{MetricValue, Scanner}; +use std::collections::HashMap; +use std::time::Duration; + +struct PowerScanner; + +#[unsafe(no_mangle)] +pub extern "C" fn pscand_scanner() -> Box { + Box::new(PowerScanner) +} + +impl Default for PowerScanner { + fn default() -> Self { + Self + } +} + +impl Scanner for PowerScanner { + fn name(&self) -> &'static str { + "power" + } + + fn interval(&self) -> Duration { + Duration::from_secs(2) + } + + fn init(&mut self, _config: &toml::Value) -> pscand_core::Result<()> { + Ok(()) + } + + fn collect(&self) -> pscand_core::Result> { + let mut metrics = HashMap::new(); + + if let Ok(Some(battery)) = PowerHelper::battery_info() { + metrics.insert( + "battery_present".to_string(), + MetricValue::from_bool(battery.present), + ); + metrics.insert( + "battery_charge_percent".to_string(), + MetricValue::Integer(battery.charge_percent as i64), + ); + metrics.insert( + "battery_voltage_v".to_string(), + MetricValue::from_f64(battery.voltage), + ); + metrics.insert( + "battery_power_now_mw".to_string(), + MetricValue::Integer(battery.power_now), + ); + metrics.insert( + "battery_status".to_string(), + MetricValue::from_str(&battery.status), + ); + } + + if let Ok(supplies) = PowerHelper::power_supplies() { + for (name, info) in supplies { + if let Some(status) = info.get("status") { + metrics.insert( + format!("supply_{}_status", name), + MetricValue::from_str(status), + ); + } + if let Some(online) = info.get("online") { + metrics.insert( + format!("supply_{}_online", name), + MetricValue::from_bool(online == "1"), + ); + } + if let Some(capacity) = info.get("capacity") { + if let Ok(cap) = capacity.parse::() { + metrics.insert( + format!("supply_{}_capacity", name), + MetricValue::Integer(cap as i64), + ); + } + } + } + } + + if let Ok(state) = PowerHelper::suspend_state() { + metrics.insert("suspend_state".to_string(), MetricValue::from_str(&state)); + } + + Ok(metrics) + } + + fn cleanup(&mut self) -> pscand_core::Result<()> { + Ok(()) + } +} diff --git a/scanners/scanner-proc/Cargo.toml b/scanners/scanner-proc/Cargo.toml new file mode 100644 index 0000000..37f6f71 --- /dev/null +++ b/scanners/scanner-proc/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "scanner-proc" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +pscand-core.workspace = true +toml.workspace = true + +[lib] +name = "scanner_proc" +path = "src/lib.rs" +crate-type = ["cdylib"] diff --git a/scanners/scanner-proc/src/lib.rs b/scanners/scanner-proc/src/lib.rs new file mode 100644 index 0000000..3ab0c26 --- /dev/null +++ b/scanners/scanner-proc/src/lib.rs @@ -0,0 +1,99 @@ +use pscand_core::helpers::ProcessHelper; +use pscand_core::scanner::{MetricValue, Scanner}; +use std::collections::HashMap; +use std::time::Duration; + +struct ProcScanner; + +#[unsafe(no_mangle)] +pub extern "C" fn pscand_scanner() -> Box { + Box::new(ProcScanner) +} + +impl Default for ProcScanner { + fn default() -> Self { + Self + } +} + +impl Scanner for ProcScanner { + fn name(&self) -> &'static str { + "proc" + } + + fn interval(&self) -> Duration { + Duration::from_secs(5) + } + + fn init(&mut self, _config: &toml::Value) -> pscand_core::Result<()> { + Ok(()) + } + + fn collect(&self) -> pscand_core::Result> { + let mut metrics = HashMap::new(); + + if let Ok(counts) = ProcessHelper::process_count() { + if let Some(total) = counts.get("total") { + metrics.insert( + "process_total".to_string(), + MetricValue::Integer(*total as i64), + ); + } + if let Some(running) = counts.get("running") { + metrics.insert( + "process_running".to_string(), + MetricValue::Integer(*running as i64), + ); + } + if let Some(sleeping) = counts.get("sleeping") { + metrics.insert( + "process_sleeping".to_string(), + MetricValue::Integer(*sleeping as i64), + ); + } + if let Some(zombie) = counts.get("zombie") { + metrics.insert( + "process_zombie".to_string(), + MetricValue::Integer(*zombie as i64), + ); + } + } + + if let Ok(zombies) = ProcessHelper::zombie_processes() { + metrics.insert( + "zombie_count".to_string(), + MetricValue::Integer(zombies.len() as i64), + ); + + if !zombies.is_empty() { + let mut zombie_info = Vec::new(); + for z in zombies.iter().take(5) { + zombie_info.push(format!("{}({})", z.name, z.pid)); + } + metrics.insert( + "zombie_processes".to_string(), + MetricValue::from_str(zombie_info.join(",")), + ); + } + } + + if let Ok(top_mem) = ProcessHelper::top_memory_processes(3) { + for (i, proc) in top_mem.iter().enumerate() { + metrics.insert( + format!("top_mem_{}_name", i + 1), + MetricValue::from_str(&proc.name), + ); + metrics.insert( + format!("top_mem_{}_mb", i + 1), + MetricValue::Integer((proc.memory_kb / 1024) as i64), + ); + } + } + + Ok(metrics) + } + + fn cleanup(&mut self) -> pscand_core::Result<()> { + Ok(()) + } +} diff --git a/scanners/scanner-sensor/Cargo.toml b/scanners/scanner-sensor/Cargo.toml new file mode 100644 index 0000000..419a293 --- /dev/null +++ b/scanners/scanner-sensor/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "scanner-sensor" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +pscand-core.workspace = true +toml.workspace = true + +[lib] +name = "scanner_sensor" +path = "src/lib.rs" +crate-type = ["cdylib"] diff --git a/scanners/scanner-sensor/src/lib.rs b/scanners/scanner-sensor/src/lib.rs new file mode 100644 index 0000000..988eee5 --- /dev/null +++ b/scanners/scanner-sensor/src/lib.rs @@ -0,0 +1,80 @@ +#![allow(improper_ctypes)] +use pscand_core::helpers::SensorHelper; +use pscand_core::scanner::{MetricValue, Scanner}; +use pscand_core::Result; +use std::collections::HashMap; +use std::time::Duration; + +struct SensorScanner; + +#[unsafe(no_mangle)] +pub extern "C" fn pscand_scanner() -> Box { + Box::new(SensorScanner) +} + +impl Default for SensorScanner { + fn default() -> Self { + Self + } +} + +impl Scanner for SensorScanner { + fn name(&self) -> &'static str { + "sensor" + } + + fn interval(&self) -> Duration { + Duration::from_secs(2) + } + + fn init(&mut self, _config: &toml::Value) -> Result<()> { + Ok(()) + } + + fn collect(&self) -> Result> { + let mut metrics = HashMap::new(); + + if let Ok(sensors) = SensorHelper::all_sensors() { + let mut temp_count = 0; + let mut fan_count = 0; + + for (hwmon, values) in sensors { + for (key, value) in values { + if key.starts_with("temp_") && key.ends_with("_celsius") { + if let Ok(v) = value.parse::() { + metrics.insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v)); + temp_count += 1; + if temp_count <= 3 { + metrics.insert( + format!("temp_{}", temp_count), + MetricValue::from_f64(v), + ); + } + } + } + if key.starts_with("fan_") && key.ends_with("_rpm") { + if let Ok(v) = value.parse::() { + metrics.insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v)); + fan_count += 1; + if fan_count <= 2 { + metrics + .insert(format!("fan_{}", fan_count), MetricValue::from_f64(v)); + } + } + } + if key.starts_with("voltage_") { + if let Ok(v) = value.parse::() { + metrics.insert(format!("{}_{}", hwmon, key), MetricValue::from_f64(v)); + } + } + } + } + } + + Ok(metrics) + } + + fn cleanup(&mut self) -> Result<()> { + Ok(()) + } +} diff --git a/scanners/scanner-system/Cargo.toml b/scanners/scanner-system/Cargo.toml new file mode 100644 index 0000000..548aa5a --- /dev/null +++ b/scanners/scanner-system/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "scanner-system" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +pscand-core.workspace = true +toml.workspace = true + +[lib] +name = "scanner_system" +path = "src/lib.rs" +crate-type = ["cdylib"] diff --git a/scanners/scanner-system/src/lib.rs b/scanners/scanner-system/src/lib.rs new file mode 100644 index 0000000..1319e56 --- /dev/null +++ b/scanners/scanner-system/src/lib.rs @@ -0,0 +1,88 @@ +#![allow(improper_ctypes)] +use pscand_core::helpers::{ResourceHelper, SystemHelper}; +use pscand_core::scanner::{MetricValue, Scanner}; +use pscand_core::Result; +use std::collections::HashMap; +use std::time::Duration; + +struct SystemScanner { + _prev_cpu: Option>, +} + +impl Default for SystemScanner { + fn default() -> Self { + Self { _prev_cpu: None } + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn pscand_scanner() -> Box { + Box::new(SystemScanner::default()) +} + +impl Scanner for SystemScanner { + fn name(&self) -> &'static str { + "system" + } + + fn interval(&self) -> Duration { + Duration::from_secs(1) + } + + fn init(&mut self, _config: &toml::Value) -> Result<()> { + Ok(()) + } + + fn collect(&self) -> Result> { + let mut metrics = HashMap::new(); + + if let Ok(uptime) = SystemHelper::uptime() { + metrics.insert( + "uptime_secs".to_string(), + MetricValue::from_f64(uptime.as_secs_f64()), + ); + } + + if let Ok((load1, load5, load15)) = SystemHelper::load_average() { + metrics.insert("load_1m".to_string(), MetricValue::from_f64(load1)); + metrics.insert("load_5m".to_string(), MetricValue::from_f64(load5)); + metrics.insert("load_15m".to_string(), MetricValue::from_f64(load15)); + } + + if let Ok(cpu) = ResourceHelper::cpu_usage() { + if let Some(total) = cpu.get("total_usage_percent") { + metrics.insert("cpu_percent".to_string(), MetricValue::from_f64(*total)); + } + } + + if let Ok(mem) = ResourceHelper::memory_info() { + if let Some(total) = mem.get("MemTotal") { + metrics.insert( + "mem_total_bytes".to_string(), + MetricValue::Integer(*total as i64), + ); + } + if let Some(available) = mem.get("MemAvailable") { + metrics.insert( + "mem_available_bytes".to_string(), + MetricValue::Integer(*available as i64), + ); + } + if let Some(used) = mem.get("MemAvailable") { + if let Some(total) = mem.get("MemTotal") { + let used_mem = total.saturating_sub(*used); + metrics.insert( + "mem_used_bytes".to_string(), + MetricValue::Integer(used_mem as i64), + ); + } + } + } + + Ok(metrics) + } + + fn cleanup(&mut self) -> Result<()> { + Ok(()) + } +}