Merge pull request 'enhancement/code-cleanup' (#9) from enhancement/code-cleanup into development
Reviewed-on: donpat1to/WatcherAgent#9
This commit is contained in:
60
.github/workflows/build.yml
vendored
60
.github/workflows/build.yml
vendored
@@ -3,7 +3,7 @@ name: Rust Cross-Platform Build
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [ "development", "main", "feature/*", bug/* ]
|
branches: [ "development", "main", "feature/*", bug/*, enhancement/* ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "development", "main" ]
|
branches: [ "development", "main" ]
|
||||||
|
|
||||||
@@ -11,8 +11,8 @@ env:
|
|||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
REGISTRY: git.triggermeelmo.com
|
REGISTRY: git.triggermeelmo.com
|
||||||
IMAGE_NAME: donpat1to/watcher-agent
|
IMAGE_NAME: donpat1to/watcher-agent
|
||||||
TAG: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || github.ref_name }}
|
TAG: development
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
detect-project:
|
detect-project:
|
||||||
name: Detect Rust Project
|
name: Detect Rust Project
|
||||||
@@ -122,8 +122,8 @@ jobs:
|
|||||||
path: ${{ needs.detect-project.outputs.project-dir }}/target/x86_64-pc-windows-gnu/release/${{ needs.detect-project.outputs.project-name }}.exe
|
path: ${{ needs.detect-project.outputs.project-dir }}/target/x86_64-pc-windows-gnu/release/${{ needs.detect-project.outputs.project-name }}.exe
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
name: Build Docker Images
|
name: Build Linux Docker Image
|
||||||
needs: [native-build, windows-cross]
|
needs: [native-build, windows-cross, detect-project]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -135,53 +135,37 @@ jobs:
|
|||||||
name: linux-binary
|
name: linux-binary
|
||||||
path: linux-bin
|
path: linux-bin
|
||||||
|
|
||||||
- name: Download Windows artifact
|
|
||||||
id: download-windows
|
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: windows-binary
|
|
||||||
path: windows-bin
|
|
||||||
|
|
||||||
- name: Verify artifacts
|
- name: Verify artifacts
|
||||||
run: |
|
run: |
|
||||||
echo "Linux binary:"
|
echo "Linux binary:"
|
||||||
ls -la linux-bin/
|
ls -la linux-bin/
|
||||||
echo "Windows binary:"
|
|
||||||
ls -la windows-bin/
|
|
||||||
|
|
||||||
- name: Build using Docker Buildx
|
- name: Login to Docker Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Linux build using Docker Buildx
|
||||||
run: |
|
run: |
|
||||||
docker buildx create --use
|
docker buildx create --use
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--platform linux/amd64 \
|
--platform linux/amd64 \
|
||||||
-f Dockerfile.linux \
|
-f Dockerfile.linux \
|
||||||
--build-arg BINARY_NAME=${{ needs.detect-project.outputs.project-name }} \
|
--build-arg BINARY_NAME=${{ needs.detect-project.outputs.project-name }} \
|
||||||
|
--load \
|
||||||
-t ${{ env.IMAGE_NAME }}:linux-${{ env.TAG }} .
|
-t ${{ env.IMAGE_NAME }}:linux-${{ env.TAG }} .
|
||||||
|
|
||||||
- name: Login to Docker registry
|
- name: Save Docker image as artifact
|
||||||
if: ${{ success() }}
|
|
||||||
run: |
|
run: |
|
||||||
echo "Logging in to Docker registry: ${{ env.REGISTRY }}"
|
docker save ${{ env.IMAGE_NAME }}:linux-${{ env.TAG }} -o linux-image.tar
|
||||||
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login git.triggermeelmo.com -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
|
|
||||||
|
|
||||||
- name: Tag and Push Linux Docker image
|
- name: Upload Docker image artifact
|
||||||
if: ${{ success() }}
|
uses: actions/upload-artifact@v3
|
||||||
run: |
|
with:
|
||||||
echo "Tagging Linux Docker image"
|
name: linux-docker-image
|
||||||
docker tag ${{ env.IMAGE_NAME }}:linux-${{ env.TAG }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:linux-${{ env.TAG }}
|
path: linux-image.tar
|
||||||
echo "Pushing Linux Docker image to registry: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:linux-${{ env.TAG }}"
|
|
||||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:linux-${{ env.TAG }}
|
|
||||||
|
|
||||||
- name: Build Windows Docker image
|
|
||||||
if: ${{ success() }}
|
|
||||||
run: |
|
|
||||||
echo "Building Windows Docker-Image with env-Tag: ${{env.TAG }}"
|
|
||||||
docker build -f Dockerfile.windows -t ${{ env.IMAGE_NAME }}:windows-${{ env.TAG }} .
|
|
||||||
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login git.triggermeelmo.com -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
|
|
||||||
echo "Tagging Windows Docker image"
|
|
||||||
docker tag ${{ env.IMAGE_NAME }}:windows-${{ env.TAG }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:windows-${{ env.TAG }}
|
|
||||||
echo "Pushing Windows Docker image to registry: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:windows-${{ env.TAG }}"
|
|
||||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:windows-${{ env.TAG }}
|
|
||||||
|
|
||||||
cleanup:
|
cleanup:
|
||||||
name: Cleanup
|
name: Cleanup
|
||||||
|
@@ -15,8 +15,11 @@ copy ./WatcherAgent/ .
|
|||||||
# Build for Linux
|
# Build for Linux
|
||||||
run cargo build --release --target x86_64-unknown-linux-gnu
|
run cargo build --release --target x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
|
# Setze Berechtigungen für das Binary
|
||||||
|
run chmod +x /app/target/x86_64-unknown-linux-gnu/release/$BINARY_NAME
|
||||||
|
|
||||||
# Final Linux image
|
# Final Linux image
|
||||||
from debian:bullseye-slim
|
from debian:bookworm-slim
|
||||||
|
|
||||||
run apt-get update && \
|
run apt-get update && \
|
||||||
apt-get install -y apt-transport-https && \
|
apt-get install -y apt-transport-https && \
|
||||||
@@ -25,14 +28,12 @@ run apt-get update && \
|
|||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Make binary name configurable
|
# Make binary name configurable
|
||||||
arg BINARY_NAME=WatcherAgent
|
arg BINARY_NAME
|
||||||
copy --from=linux-builder /app/target/x86_64-unknown-linux-gnu/release/$BINARY_NAME /usr/local/bin/
|
env BINARY_NAME=${BINARY_NAME:-WatcherAgent}
|
||||||
|
copy --from=linux-builder /app/target/x86_64-unknown-linux-gnu/release/${BINARY_NAME} /usr/local/bin/
|
||||||
# Verify binary works
|
|
||||||
run /usr/local/bin/$BINARY_NAME --version
|
|
||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
healthcheck --interval=30s --timeout=3s \
|
healthcheck --interval=30s --timeout=3s \
|
||||||
cmd /usr/local/bin/$BINARY_NAME healthcheck || exit 1
|
cmd /usr/local/bin/${BINARY_NAME} healthcheck || exit 1
|
||||||
|
|
||||||
entrypoint ["/usr/local/bin/$BINARY_NAME"]
|
ENTRYPOINT ["/bin/sh", "-c", "/usr/local/bin/${BINARY_NAME}"]
|
@@ -1,11 +0,0 @@
|
|||||||
# Using Windows Server Core as base
|
|
||||||
FROM mcr.microsoft.com/windows/servercore:ltsc2022
|
|
||||||
|
|
||||||
# Create app directory
|
|
||||||
WORKDIR C:/app
|
|
||||||
|
|
||||||
# Copy the Windows binary
|
|
||||||
ARG BINARY_PATH
|
|
||||||
COPY ${BINARY_PATH} watcher_agent.exe
|
|
||||||
|
|
||||||
ENTRYPOINT ["C:/app/watcher_agent.exe"]
|
|
@@ -15,8 +15,9 @@ reqwest = { version = "0.11", default-features = false, features = ["json", "blo
|
|||||||
sysinfo = "0.36.1"
|
sysinfo = "0.36.1"
|
||||||
metrics = "0.24.2"
|
metrics = "0.24.2"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
nvml-wrapper = "0.10"
|
nvml-wrapper = "0.11"
|
||||||
|
nvml-wrapper-sys = "0.9.0"
|
||||||
|
anyhow = "1.0.98"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winapi = { version = "0.3", features = ["winuser", "pdh", "ifmib", "iphlpapi", "winerror" ,"wbemcli", "combaseapi"] }
|
winapi = { version = "0.3", features = ["winuser", "pdh", "ifmib", "iphlpapi", "winerror" ,"wbemcli", "combaseapi"] }
|
||||||
@@ -25,4 +26,8 @@ com = "0.2"
|
|||||||
widestring = "0.5"
|
widestring = "0.5"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
glob = "0.3"
|
glob = "0.3"
|
||||||
|
libloading = "0.8"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
pkg-config = { version = "0.3", optional = true } # Für Library-Detektion
|
154
WatcherAgent/src/api.rs
Normal file
154
WatcherAgent/src/api.rs
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::hardware::HardwareInfo;
|
||||||
|
use crate::models::{HeartbeatDto, IdResponse, MetricDto, RegistrationDto};
|
||||||
|
use anyhow::Result;
|
||||||
|
use reqwest::{Client, StatusCode};
|
||||||
|
use std::error::Error;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
pub async fn register_with_server(
|
||||||
|
base_url: &str,
|
||||||
|
) -> Result<(i32, String), Box<dyn Error + Send + Sync>> {
|
||||||
|
// First get local IP
|
||||||
|
let ip = local_ip_address::local_ip()?.to_string();
|
||||||
|
println!("Local IP address detected: {}", ip);
|
||||||
|
|
||||||
|
// Get server ID from backend (this will retry until successful)
|
||||||
|
let (server_id, registered_ip) = get_server_id_by_ip(base_url, &ip).await?;
|
||||||
|
|
||||||
|
// Create HTTP client for registration
|
||||||
|
let client = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(true)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
// Collect hardware info
|
||||||
|
let hardware = HardwareInfo::collect().await?;
|
||||||
|
|
||||||
|
// Prepare registration data
|
||||||
|
let registration = RegistrationDto {
|
||||||
|
id: server_id,
|
||||||
|
ip_address: registered_ip.clone(),
|
||||||
|
cpu_type: hardware.cpu.name.clone().unwrap_or_default(),
|
||||||
|
cpu_cores: (hardware.cpu.cores).unwrap_or_default(),
|
||||||
|
gpu_type: hardware.gpu.name.clone().unwrap_or_default(),
|
||||||
|
ram_size: hardware.memory.total.unwrap_or_default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to register (will retry on failure)
|
||||||
|
loop {
|
||||||
|
println!("Attempting to register with server...");
|
||||||
|
let url = format!("{}/monitoring/register-agent-by-id", base_url);
|
||||||
|
match client.post(&url).json(®istration).send().await {
|
||||||
|
Ok(resp) if resp.status().is_success() => {
|
||||||
|
println!("✅ Successfully registered with server.");
|
||||||
|
return Ok((server_id, registered_ip));
|
||||||
|
}
|
||||||
|
Ok(resp) => {
|
||||||
|
let status = resp.status();
|
||||||
|
let text = resp.text().await.unwrap_or_default();
|
||||||
|
println!(
|
||||||
|
"⚠️ Registration failed ({}): {} (will retry in 10 seconds)",
|
||||||
|
status, text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("⚠️ Registration error: {} (will retry in 10 seconds)", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep(Duration::from_secs(10)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_server_id_by_ip(
|
||||||
|
base_url: &str,
|
||||||
|
ip: &str,
|
||||||
|
) -> Result<(i32, String), Box<dyn Error + Send + Sync>> {
|
||||||
|
let client = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(true)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let url = format!("{}/monitoring/server-id-by-ip?ipAddress={}", base_url, ip);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
println!("Attempting to fetch server ID for IP {}...", ip);
|
||||||
|
match client.get(&url).send().await {
|
||||||
|
Ok(resp) if resp.status().is_success() => {
|
||||||
|
let text = resp.text().await?;
|
||||||
|
println!("Raw response: {}", text); // Debug output
|
||||||
|
|
||||||
|
let id_resp: IdResponse = serde_json::from_str(&text).map_err(|e| {
|
||||||
|
println!("Failed to parse response: {}", e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"✅ Received ID {} for IP {}",
|
||||||
|
id_resp.id, id_resp.ip_address
|
||||||
|
);
|
||||||
|
return Ok((id_resp.id, id_resp.ip_address));
|
||||||
|
}
|
||||||
|
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
|
||||||
|
println!(
|
||||||
|
"❌ Server with IP {} not found in database (will retry in 10 seconds)",
|
||||||
|
ip
|
||||||
|
);
|
||||||
|
sleep(Duration::from_secs(10)).await;
|
||||||
|
}
|
||||||
|
Ok(resp) => {
|
||||||
|
println!(
|
||||||
|
"⚠️ Server responded with status: {} - {}",
|
||||||
|
resp.status(),
|
||||||
|
resp.text().await?
|
||||||
|
);
|
||||||
|
sleep(Duration::from_secs(10)).await;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("⚠️ Request failed: {} (will retry in 10 seconds)", err);
|
||||||
|
sleep(Duration::from_secs(10)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn heartbeat_loop(base_url: &str, ip: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
let client = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(true)
|
||||||
|
.build()?;
|
||||||
|
let url = format!("{}/heartbeat/receive", base_url);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let payload = HeartbeatDto {
|
||||||
|
ip_address: ip.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match client.post(&url).json(&payload).send().await {
|
||||||
|
Ok(res) if res.status().is_success() => {
|
||||||
|
println!("✅ Heartbeat sent successfully.");
|
||||||
|
}
|
||||||
|
Ok(res) => eprintln!("Server responded with status: {}", res.status()),
|
||||||
|
Err(e) => eprintln!("Heartbeat error: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_secs(20)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_metrics(
|
||||||
|
base_url: &str,
|
||||||
|
metrics: &MetricDto,
|
||||||
|
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
let client = Client::new();
|
||||||
|
let url = format!("{}/monitoring/metric", base_url);
|
||||||
|
|
||||||
|
match client.post(&url).json(&metrics).send().await {
|
||||||
|
Ok(res) => println!(
|
||||||
|
"✅ Sent metrics for server {} | Status: {}",
|
||||||
|
metrics.server_id,
|
||||||
|
res.status()
|
||||||
|
),
|
||||||
|
Err(err) => eprintln!("❌ Failed to send metrics: {}", err),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
254
WatcherAgent/src/hardware/cpu.rs
Normal file
254
WatcherAgent/src/hardware/cpu.rs
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::error::Error;
|
||||||
|
//use std::result::Result;
|
||||||
|
use sysinfo::System;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CpuInfo {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub cores: Option<i32>,
|
||||||
|
pub current_load: Option<f64>,
|
||||||
|
pub current_temp: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_cpu_info() -> Result<CpuInfo, Box<dyn Error + Send + Sync>> {
|
||||||
|
let mut sys = System::new();
|
||||||
|
sys.refresh_cpu_all();
|
||||||
|
|
||||||
|
let cpus = sys.cpus();
|
||||||
|
Ok(CpuInfo {
|
||||||
|
name: Some(
|
||||||
|
cpus.first()
|
||||||
|
.map(|c| c.brand().to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
),
|
||||||
|
cores: Some(cpus.len() as i32),
|
||||||
|
current_load: get_cpu_load(&mut sys).await.ok(),
|
||||||
|
current_temp: get_cpu_temp().await.ok(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_cpu_load(sys: &mut System) -> Result<f64, Box<dyn Error + Send + Sync>> {
|
||||||
|
sys.refresh_cpu_all();
|
||||||
|
tokio::task::yield_now().await; // Allow other tasks to run
|
||||||
|
Ok(sys.global_cpu_usage() as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_cpu_temp() -> Result<f64, Box<dyn Error + Send + Sync>> {
|
||||||
|
println!("Attempting to get CPU temperature...");
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
use std::fs;
|
||||||
|
use std::process::Command;
|
||||||
|
println!("");
|
||||||
|
if let Ok(output) = Command::new("sensors").output() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
for line in stdout.lines() {
|
||||||
|
if line.contains("Package id") || line.contains("Tdie") || line.contains("CPU Temp")
|
||||||
|
{
|
||||||
|
if let Some(temp_str) = line
|
||||||
|
.split('+')
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split_whitespace().next())
|
||||||
|
{
|
||||||
|
if let Ok(temp) = temp_str.replace("°C", "").parse::<f32>() {
|
||||||
|
return Ok(temp.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Sysfs (Intel/AMD)
|
||||||
|
if let Ok(content) = fs::read_to_string("/sys/class/thermal/thermal_zone0/temp") {
|
||||||
|
if let Ok(temp) = content.trim().parse::<f32>() {
|
||||||
|
return Ok((temp / 1000.0).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Alternative Sysfs-Pfade
|
||||||
|
let paths = [
|
||||||
|
"/sys/class/hwmon/hwmontemp1_input",
|
||||||
|
"/sys/class/hwmon/hwmondevice/temp1_input",
|
||||||
|
];
|
||||||
|
|
||||||
|
for path_pattern in &paths {
|
||||||
|
if let Ok(paths) = glob::glob(path_pattern) {
|
||||||
|
for path in paths.flatten() {
|
||||||
|
if let Ok(content) = fs::read_to_string(&path) {
|
||||||
|
if let Ok(temp) = content.trim().parse::<f32>() {
|
||||||
|
return Ok((temp / 1000.0).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow::anyhow!("Could not find CPU temperature using sensors or sysfs").into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn failed(hr: winapi::shared::winerror::HRESULT) -> bool {
|
||||||
|
hr < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use com::runtime::init_runtime;
|
||||||
|
use com::sys::CLSCTX_INPROC_SERVER;
|
||||||
|
use widestring::U16CString;
|
||||||
|
use winapi::shared::rpcdce::*;
|
||||||
|
use winapi::shared::wtypes::VT_I4;
|
||||||
|
use winapi::um::oaidl::VARIANT;
|
||||||
|
use winapi::um::objidlbase::EOAC_NONE;
|
||||||
|
use winapi::um::{combaseapi, wbemcli};
|
||||||
|
|
||||||
|
init_runtime().ok();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
use anyhow::Ok;
|
||||||
|
|
||||||
|
let mut locator: *mut wbemcli::IWbemLocator = std::ptr::null_mut();
|
||||||
|
let hr = combaseapi::CoCreateInstance(
|
||||||
|
&wbemcli::CLSID_WbemLocator,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
CLSCTX_INPROC_SERVER,
|
||||||
|
&wbemcli::IID_IWbemLocator,
|
||||||
|
&mut locator as *mut _ as *mut _,
|
||||||
|
);
|
||||||
|
|
||||||
|
if hr != 0 {
|
||||||
|
eprintln!("Failed to create WbemLocator (HRESULT: {})", hr);
|
||||||
|
return Err(("Failed to create WbemLocator").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut services: *mut wbemcli::IWbemServices = std::ptr::null_mut();
|
||||||
|
let namespace = U16CString::from_str("root\\cimv2").unwrap(); // Changed to more common namespace
|
||||||
|
let hr = (*locator).ConnectServer(
|
||||||
|
namespace.as_ptr().cast_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
&mut services,
|
||||||
|
);
|
||||||
|
|
||||||
|
if hr != 0 {
|
||||||
|
eprintln!("Failed to connect to WMI (HRESULT: {})", hr);
|
||||||
|
(*locator).Release();
|
||||||
|
return Err(("Failed to connect to WMI").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set security levels
|
||||||
|
let hr = combaseapi::CoSetProxyBlanket(
|
||||||
|
services as *mut _,
|
||||||
|
RPC_C_AUTHN_WINNT,
|
||||||
|
RPC_C_AUTHZ_NONE,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
RPC_C_AUTHN_LEVEL_CALL,
|
||||||
|
RPC_C_IMP_LEVEL_IMPERSONATE,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
EOAC_NONE,
|
||||||
|
);
|
||||||
|
|
||||||
|
if hr != 0 {
|
||||||
|
eprintln!("Failed to set proxy blanket (HRESULT: {})", hr);
|
||||||
|
(*services).Release();
|
||||||
|
(*locator).Release();
|
||||||
|
return Err(("Failed to set proxy blanket").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try different temperature queries - some systems might have different WMI classes
|
||||||
|
let queries = [
|
||||||
|
"SELECT * FROM Win32_PerfFormattedData_Counters_ThermalZoneInformation",
|
||||||
|
"SELECT * FROM MSAcpi_ThermalZoneTemperature",
|
||||||
|
"SELECT * FROM Win32_TemperatureProbe",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut result = None;
|
||||||
|
|
||||||
|
for query_str in queries.iter() {
|
||||||
|
let query = U16CString::from_str(query_str).unwrap();
|
||||||
|
let mut enumerator: *mut wbemcli::IEnumWbemClassObject = std::ptr::null_mut();
|
||||||
|
let hr = (*services).ExecQuery(
|
||||||
|
U16CString::from_str("WQL").unwrap().as_ptr().cast_mut(),
|
||||||
|
query.as_ptr().cast_mut(),
|
||||||
|
wbemcli::WBEM_FLAG_FORWARD_ONLY as i32,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
&mut enumerator,
|
||||||
|
);
|
||||||
|
|
||||||
|
if hr != 0 {
|
||||||
|
continue; // Try next query if this one fails
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut obj: *mut wbemcli::IWbemClassObject = std::ptr::null_mut();
|
||||||
|
let mut returned = 0;
|
||||||
|
let hr = (*enumerator).Next(
|
||||||
|
wbemcli::WBEM_INFINITE as i32, // Fixed: cast directly to i32
|
||||||
|
1,
|
||||||
|
&mut obj,
|
||||||
|
&mut returned,
|
||||||
|
);
|
||||||
|
|
||||||
|
if failed(hr) {
|
||||||
|
eprintln!("Failed to enumerate WMI objects (HRESULT: {})", hr);
|
||||||
|
(*enumerator).Release();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if returned == 0 {
|
||||||
|
// No more items
|
||||||
|
(*enumerator).Release();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if hr == 0 && returned > 0 {
|
||||||
|
let mut variant = std::mem::zeroed::<VARIANT>();
|
||||||
|
// Try different possible property names
|
||||||
|
let property_names = ["CurrentTemperature", "Temperature", "CurrentReading"];
|
||||||
|
|
||||||
|
for prop in property_names.iter() {
|
||||||
|
let hr = (*obj).Get(
|
||||||
|
U16CString::from_str(prop).unwrap().as_ptr(),
|
||||||
|
0,
|
||||||
|
&mut variant,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if hr == 0 && variant.n1.n2().vt as u32 == VT_I4 {
|
||||||
|
let temp_kelvin = *variant.n1.n2().n3.intVal() as f32 / 10.0;
|
||||||
|
result = Some(temp_kelvin - 273.15); // Convert to Celsius
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(*obj).Release();
|
||||||
|
(*enumerator).Release();
|
||||||
|
if result.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !enumerator.is_null() {
|
||||||
|
(*enumerator).Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(*services).Release();
|
||||||
|
(*locator).Release();
|
||||||
|
|
||||||
|
Ok(result.unwrap() as f64).map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||||
|
{
|
||||||
|
println!("CPU temperature retrieval not supported on this OS.");
|
||||||
|
Err(anyhow::anyhow!("CPU temperature retrieval not supported on this OS").into())
|
||||||
|
}
|
||||||
|
}
|
153
WatcherAgent/src/hardware/disk.rs
Normal file
153
WatcherAgent/src/hardware/disk.rs
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use sysinfo::DiskUsage;
|
||||||
|
use sysinfo::{Component, Components, Disk, Disks, System};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DiskInfo {
|
||||||
|
pub total: Option<f64>,
|
||||||
|
pub used: Option<f64>,
|
||||||
|
pub free: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_disk_info() -> Result<DiskInfo> {
|
||||||
|
let disks = Disks::new_with_refreshed_list();
|
||||||
|
let _disk_types = [
|
||||||
|
sysinfo::DiskKind::HDD,
|
||||||
|
sysinfo::DiskKind::SSD,
|
||||||
|
sysinfo::DiskKind::Unknown(0),
|
||||||
|
];
|
||||||
|
|
||||||
|
let (_, _, _, _) = get_disk_utitlization().unwrap();
|
||||||
|
|
||||||
|
let mut total = 0;
|
||||||
|
let mut used = 0;
|
||||||
|
|
||||||
|
for disk in disks.list() {
|
||||||
|
if disk.total_space() > 100 * 1024 * 1024 {
|
||||||
|
// > 100MB
|
||||||
|
total += disk.total_space();
|
||||||
|
used += disk.total_space() - disk.available_space();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DiskInfo {
|
||||||
|
total: Some(total as f64),
|
||||||
|
used: Some(used as f64),
|
||||||
|
free: Some((total - used) as f64),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_disk_utitlization() -> Result<(f64, f64, f64, f64), Box<dyn Error>> {
|
||||||
|
let mut sys = System::new();
|
||||||
|
sys.refresh_all();
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
let mut total_size = 0u64;
|
||||||
|
let mut total_used = 0u64;
|
||||||
|
let mut total_available = 0u64;
|
||||||
|
|
||||||
|
let disks = Disks::new_with_refreshed_list();
|
||||||
|
for disk in disks.list() {
|
||||||
|
// Ignoriere kleine Systempartitionen
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Disk_Name: {:?}, Disk_Kind: {}, Total: {}, Available: {}",
|
||||||
|
disk.name(),
|
||||||
|
disk.kind(),
|
||||||
|
disk.total_space(),
|
||||||
|
disk.available_space(),
|
||||||
|
);
|
||||||
|
println!("[{:?}] {:?}", disk.name(), disk.mount_point());
|
||||||
|
if disk.total_space() > 100 * 1024 * 1024 {
|
||||||
|
// > 100MB
|
||||||
|
total_size += disk.total_space();
|
||||||
|
total_available += disk.available_space();
|
||||||
|
total_used += disk.total_space() - disk.available_space();
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let components = Components::new_with_refreshed_list();
|
||||||
|
for component in &components {
|
||||||
|
if let Some(temperature) = component.temperature() {
|
||||||
|
println!(
|
||||||
|
"Component_Label: {}, Temperature: {}°C",
|
||||||
|
component.label(),
|
||||||
|
temperature
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Berechnungen
|
||||||
|
let total_size = if count > 0 {
|
||||||
|
total_size as f64 // in Bytes
|
||||||
|
} else {
|
||||||
|
// Fallback: Versuche df unter Linux
|
||||||
|
println!("Fallback: Using 'df' command to get disk info.");
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
use std::process::Command;
|
||||||
|
if let Ok(output) = Command::new("df")
|
||||||
|
.arg("-B1")
|
||||||
|
.arg("--output=size,used")
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
for line in stdout.lines().skip(1) {
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if parts.len() == 2 {
|
||||||
|
if let (Ok(size), Ok(used)) =
|
||||||
|
(parts[0].parse::<u64>(), parts[1].parse::<u64>())
|
||||||
|
{
|
||||||
|
total_size += size;
|
||||||
|
total_used += used;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
total_size as f64 // in Bytes
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let usage = if total_size > 0.0 {
|
||||||
|
(total_used as f64 / total_size as f64) * 100.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
total_size,
|
||||||
|
total_used as f64,
|
||||||
|
total_available as f64,
|
||||||
|
usage as f64,
|
||||||
|
)) // Disk-Temp bleibt 0.0 ohne spezielle Hardware
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _get_disk_temp_for_component(component: &Component) -> Option<f64> {
|
||||||
|
component.temperature().map(|temp| temp as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _get_disk_load_for_disk(disk: &Disk) -> Result<(f64, f64, f64, f64), Box<dyn Error>> {
|
||||||
|
let usage: DiskUsage = disk.usage();
|
||||||
|
|
||||||
|
// Assuming DiskUsage has these methods:
|
||||||
|
let total_written_bytes = usage.total_written_bytes as f64;
|
||||||
|
let written_bytes = usage.written_bytes as f64;
|
||||||
|
let total_read_bytes = usage.total_read_bytes as f64;
|
||||||
|
let read_bytes = usage.read_bytes as f64;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
total_written_bytes,
|
||||||
|
written_bytes,
|
||||||
|
total_read_bytes,
|
||||||
|
read_bytes,
|
||||||
|
))
|
||||||
|
}
|
105
WatcherAgent/src/hardware/gpu.rs
Normal file
105
WatcherAgent/src/hardware/gpu.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use nvml_wrapper::Nvml;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GpuInfo {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub current_load: Option<f64>,
|
||||||
|
pub current_temp: Option<f64>,
|
||||||
|
pub vram_total: Option<f64>,
|
||||||
|
pub vram_used: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_gpu_info() -> Result<GpuInfo, Box<dyn Error + Send + Sync>> {
|
||||||
|
match get_gpu_metrics() {
|
||||||
|
Ok((gpu_temp, gpu_load, vram_used, vram_total)) => {
|
||||||
|
let gpu_name = detect_gpu_name();
|
||||||
|
Ok(GpuInfo {
|
||||||
|
name: Some(gpu_name),
|
||||||
|
current_load: Some(gpu_load),
|
||||||
|
current_temp: Some(gpu_temp),
|
||||||
|
vram_total: Some(vram_total),
|
||||||
|
vram_used: Some(vram_used),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Graceful fallback: log error, return empty/None values
|
||||||
|
eprintln!("GPU info unavailable: {e}");
|
||||||
|
Ok(GpuInfo {
|
||||||
|
name: Some(detect_gpu_name()),
|
||||||
|
current_load: None,
|
||||||
|
current_temp: None,
|
||||||
|
vram_total: None,
|
||||||
|
vram_used: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_gpu_metrics() -> Result<(f64, f64, f64, f64), Box<dyn Error + Send + Sync>> {
|
||||||
|
let nvml = Nvml::init();
|
||||||
|
if let Ok(nvml) = nvml {
|
||||||
|
if let Ok(device) = nvml.device_by_index(0) {
|
||||||
|
let temp = device
|
||||||
|
.temperature(nvml_wrapper::enum_wrappers::device::TemperatureSensor::Gpu)
|
||||||
|
.unwrap_or(0) as f64;
|
||||||
|
let load = device
|
||||||
|
.utilization_rates()
|
||||||
|
.map(|u| u.gpu as f64)
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let mem = device.memory_info().ok();
|
||||||
|
let used = mem.clone().map(|m| m.used as f64).unwrap_or(0.0);
|
||||||
|
let total = mem.map(|m| m.total as f64).unwrap_or(0.0);
|
||||||
|
Ok((temp, load, used, total))
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("No NVIDIA GPU found").into())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("Failed to initialize NVML").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_gpu_name() -> String {
|
||||||
|
try_nvml_gpu_name()
|
||||||
|
.or_else(fallback_gpu_name)
|
||||||
|
.unwrap_or_else(|| "Unknown GPU".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_nvml_gpu_name() -> Option<String> {
|
||||||
|
let nvml = Nvml::init().ok()?;
|
||||||
|
let device = nvml.device_by_index(0).ok()?;
|
||||||
|
device.name().ok().map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_gpu_name() -> Option<String> {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let output = std::process::Command::new("lshw")
|
||||||
|
.args(&["-C", "display"])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
String::from_utf8_lossy(&output.stdout)
|
||||||
|
.lines()
|
||||||
|
.find(|l| l.contains("product:"))
|
||||||
|
.map(|l| l.trim().replace("product:", "").trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let output = std::process::Command::new("wmic")
|
||||||
|
.args(["path", "win32_VideoController", "get", "name"])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
String::from_utf8_lossy(&output.stdout)
|
||||||
|
.lines()
|
||||||
|
.skip(1) // Skip header
|
||||||
|
.find(|s| !s.trim().is_empty())
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
27
WatcherAgent/src/hardware/memory.rs
Normal file
27
WatcherAgent/src/hardware/memory.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use sysinfo::System;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MemoryInfo {
|
||||||
|
pub total: Option<f64>,
|
||||||
|
pub used: Option<f64>,
|
||||||
|
pub free: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_memory_info() -> Result<MemoryInfo> {
|
||||||
|
let mut sys = System::new();
|
||||||
|
sys.refresh_memory();
|
||||||
|
|
||||||
|
Ok(MemoryInfo {
|
||||||
|
total: Some(sys.total_memory() as f64),
|
||||||
|
used: Some(sys.used_memory() as f64),
|
||||||
|
free: Some(sys.free_memory() as f64),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _get_memory_usage(sys: &mut System) -> Result<f64, Box<dyn Error + Send + Sync>> {
|
||||||
|
sys.refresh_memory();
|
||||||
|
Ok((sys.used_memory() as f64 / sys.total_memory() as f64) * 100.0)
|
||||||
|
}
|
49
WatcherAgent/src/hardware/mod.rs
Normal file
49
WatcherAgent/src/hardware/mod.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//use anyhow::Result;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
pub mod cpu;
|
||||||
|
pub mod disk;
|
||||||
|
pub mod gpu;
|
||||||
|
pub mod memory;
|
||||||
|
pub mod network;
|
||||||
|
|
||||||
|
pub use cpu::get_cpu_info;
|
||||||
|
pub use disk::get_disk_info;
|
||||||
|
pub use gpu::get_gpu_info;
|
||||||
|
pub use memory::get_memory_info;
|
||||||
|
pub use network::get_network_info;
|
||||||
|
pub use network::NetworkMonitor;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HardwareInfo {
|
||||||
|
pub cpu: cpu::CpuInfo,
|
||||||
|
pub gpu: gpu::GpuInfo,
|
||||||
|
pub memory: memory::MemoryInfo,
|
||||||
|
pub disk: disk::DiskInfo,
|
||||||
|
pub network: network::NetworkInfo,
|
||||||
|
pub network_monitor: network::NetworkMonitor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HardwareInfo {
|
||||||
|
pub async fn collect() -> Result<Self, Box<dyn Error + Send + Sync>> {
|
||||||
|
let mut network_monitor = network::NetworkMonitor::new();
|
||||||
|
Ok(Self {
|
||||||
|
cpu: get_cpu_info().await?,
|
||||||
|
gpu: get_gpu_info().await?,
|
||||||
|
memory: get_memory_info().await?,
|
||||||
|
disk: get_disk_info().await?,
|
||||||
|
network: match get_network_info(&mut network_monitor).await {
|
||||||
|
Ok(info) => info,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error collecting network info: {}", e);
|
||||||
|
network::NetworkInfo {
|
||||||
|
interfaces: None,
|
||||||
|
rx_rate: None,
|
||||||
|
tx_rate: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
network_monitor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
198
WatcherAgent/src/hardware/network.rs
Normal file
198
WatcherAgent/src/hardware/network.rs
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
use std::result::Result;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NetworkInfo {
|
||||||
|
pub interfaces: Option<Vec<String>>,
|
||||||
|
pub rx_rate: Option<f64>,
|
||||||
|
pub tx_rate: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NetworkMonitor {
|
||||||
|
prev_rx: u64,
|
||||||
|
prev_tx: u64,
|
||||||
|
last_update: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NetworkMonitor {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkMonitor {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
prev_rx: 0,
|
||||||
|
prev_tx: 0,
|
||||||
|
last_update: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_usage(&mut self) -> Result<(f64, f64), Box<dyn Error>> {
|
||||||
|
let (current_rx, current_tx) = get_network_bytes()?;
|
||||||
|
let elapsed = self.last_update.elapsed().as_secs_f64();
|
||||||
|
self.last_update = Instant::now();
|
||||||
|
|
||||||
|
let rx_rate = if current_rx >= self.prev_rx {
|
||||||
|
(current_rx - self.prev_rx) as f64 / elapsed
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let tx_rate = if current_tx >= self.prev_tx {
|
||||||
|
(current_tx - self.prev_tx) as f64 / elapsed
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
self.prev_rx = current_rx;
|
||||||
|
self.prev_tx = current_tx;
|
||||||
|
|
||||||
|
Ok((rx_rate, tx_rate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_network_info(monitor: &mut NetworkMonitor) -> Result<NetworkInfo, Box<dyn Error>> {
|
||||||
|
let (rx_rate, tx_rate) = monitor.update_usage()?;
|
||||||
|
Ok(NetworkInfo {
|
||||||
|
interfaces: Some(get_network_interfaces()),
|
||||||
|
rx_rate: Some(rx_rate),
|
||||||
|
tx_rate: Some(tx_rate),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_network_bytes() -> Result<(u64, u64), Box<dyn Error>> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use std::ptr::null_mut;
|
||||||
|
use winapi::shared::ifmib::MIB_IFTABLE;
|
||||||
|
use winapi::um::iphlpapi::GetIfTable;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// Erste Abfrage zur Bestimmung der benötigten Puffergröße
|
||||||
|
let mut buffer_size = 0u32;
|
||||||
|
if GetIfTable(null_mut(), &mut buffer_size, 0)
|
||||||
|
!= winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER
|
||||||
|
{
|
||||||
|
return Err(
|
||||||
|
anyhow::anyhow!("Failed to get buffer size for network interfaces").into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Puffer allozieren
|
||||||
|
let mut buffer = vec![0u8; buffer_size as usize];
|
||||||
|
let if_table = buffer.as_mut_ptr() as *mut MIB_IFTABLE;
|
||||||
|
|
||||||
|
// Tatsächliche Daten abrufen
|
||||||
|
if GetIfTable(if_table, &mut buffer_size, 0) != 0 {
|
||||||
|
return Err(anyhow::anyhow!("Failed to get network interface table").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daten auswerten
|
||||||
|
let mut rx_total = 0u64;
|
||||||
|
let mut tx_total = 0u64;
|
||||||
|
|
||||||
|
for i in 0..(*if_table).dwNumEntries {
|
||||||
|
let row = &*((*if_table).table.as_ptr().offset(i as isize));
|
||||||
|
rx_total += row.dwInOctets as u64;
|
||||||
|
tx_total += row.dwOutOctets as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
if rx_total == 0 && tx_total == 0 {
|
||||||
|
Err(anyhow::anyhow!("No network data available").into())
|
||||||
|
} else {
|
||||||
|
Ok((rx_total, tx_total))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
let mut rx_total = 0u64;
|
||||||
|
let mut tx_total = 0u64;
|
||||||
|
if let Ok(dir) = fs::read_dir("/sys/class/net") {
|
||||||
|
for entry in dir.flatten() {
|
||||||
|
let iface = entry.file_name();
|
||||||
|
let iface_name = iface.to_string_lossy();
|
||||||
|
|
||||||
|
// Ignoriere virtuelle Interfaces
|
||||||
|
if !iface_name.starts_with("lo") && !iface_name.starts_with("virbr") {
|
||||||
|
if let (Ok(rx), Ok(tx)) = (
|
||||||
|
fs::read_to_string(entry.path().join("statistics/rx_bytes")),
|
||||||
|
fs::read_to_string(entry.path().join("statistics/tx_bytes")),
|
||||||
|
) {
|
||||||
|
rx_total += rx.trim().parse::<u64>().unwrap_or(0);
|
||||||
|
tx_total += tx.trim().parse::<u64>().unwrap_or(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rx_total == 0 && tx_total == 0 {
|
||||||
|
return Err(anyhow::anyhow!("No network data available").into());
|
||||||
|
} else {
|
||||||
|
return Ok((rx_total, tx_total));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||||
|
Err("No network data available for this OS".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_network_interfaces() -> Vec<String> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::ptr::null_mut;
|
||||||
|
use winapi::shared::ifmib::MIB_IFTABLE;
|
||||||
|
use winapi::um::iphlpapi::GetIfTable;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let mut buffer_size = 0u32;
|
||||||
|
if GetIfTable(null_mut(), &mut buffer_size, 0)
|
||||||
|
!= winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER
|
||||||
|
{
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = vec![0u8; buffer_size as usize];
|
||||||
|
let if_table = buffer.as_mut_ptr() as *mut MIB_IFTABLE;
|
||||||
|
|
||||||
|
if GetIfTable(if_table, &mut buffer_size, 0) != 0 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
(0..(*if_table).dwNumEntries)
|
||||||
|
.map(|i| {
|
||||||
|
let row = &*((*if_table).table.as_ptr().offset(i as isize));
|
||||||
|
let descr = CStr::from_ptr(row.bDescr.as_ptr() as *const i8)
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned();
|
||||||
|
descr.trim().to_string()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
let mut interfaces = vec![];
|
||||||
|
if let Ok(dir) = fs::read_dir("/sys/class/net") {
|
||||||
|
for entry in dir.flatten() {
|
||||||
|
let iface = entry.file_name();
|
||||||
|
interfaces.push(iface.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||||
|
vec![]
|
||||||
|
}
|
@@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
$ rustc --crate-type=lib rary.rs
|
|
||||||
$ ls lib*
|
|
||||||
library.rlib
|
|
||||||
|
|
||||||
// extern crate rary; // May be required for Rust 2015 edition or earlier
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
rary::public_function();
|
|
||||||
|
|
||||||
// Error! `private_function` is private
|
|
||||||
//rary::private_function();
|
|
||||||
|
|
||||||
rary::indirect_access();
|
|
||||||
}
|
|
||||||
|
|
||||||
# Where library.rlib is the path to the compiled library, assumed that it's
|
|
||||||
# in the same directory here:
|
|
||||||
$ rustc executable.rs --extern rary=library.rlib && ./executable
|
|
||||||
called rary's `public_function()`
|
|
||||||
called rary's `indirect_access()`, that
|
|
||||||
> called rary's `private_function()`
|
|
||||||
*/
|
|
@@ -1,940 +1,71 @@
|
|||||||
/// WatcherAgent - A Rust-based system monitoring agent
|
/// WatcherAgent - A Rust-based system monitoring agent
|
||||||
/// This agent collects hardware metrics and sends them to a backend server.
|
/// This agent collects hardware metrics and sends them to a backend server.
|
||||||
/// It supports CPU, GPU, RAM, disk, and network metrics.
|
/// It supports CPU, GPU, RAM, disk, and network metrics.
|
||||||
//use chrono::Utc;
|
pub mod api;
|
||||||
use nvml_wrapper::Nvml;
|
pub mod hardware;
|
||||||
use reqwest::{Client, StatusCode};
|
pub mod metrics;
|
||||||
use serde::{Deserialize, Serialize};
|
pub mod models;
|
||||||
use std::{error::Error, time::Duration};
|
|
||||||
use sysinfo::{Components, Disks, System};
|
|
||||||
use tokio::time::{interval, sleep, Instant};
|
|
||||||
|
|
||||||
// Windows specific imports
|
pub use crate::hardware::gpu;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::marker::Send;
|
||||||
|
use std::marker::Sync;
|
||||||
|
use std::result::Result;
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
// Data structures matching the C# DTOs
|
async fn flatten<T>(
|
||||||
#[derive(Serialize, Debug)]
|
handle: JoinHandle<Result<T, Box<dyn Error + Send + Sync>>>,
|
||||||
struct RegistrationDto {
|
) -> Result<T, Box<dyn Error + Send + Sync>> {
|
||||||
#[serde(rename = "id")]
|
match handle.await {
|
||||||
id: i32,
|
Ok(Ok(result)) => Ok(result),
|
||||||
#[serde(rename = "ipAddress")]
|
Ok(Err(err)) => Err(err),
|
||||||
ip_address: String,
|
Err(_err) => Err("handling failed".into()),
|
||||||
#[serde(rename = "cpuType")]
|
|
||||||
cpu_type: String,
|
|
||||||
#[serde(rename = "cpuCores")]
|
|
||||||
cpu_cores: i32,
|
|
||||||
#[serde(rename = "gpuType")]
|
|
||||||
gpu_type: String,
|
|
||||||
#[serde(rename = "ramSize")]
|
|
||||||
ram_size: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
|
||||||
struct MetricDto {
|
|
||||||
#[serde(rename = "serverId")]
|
|
||||||
server_id: i32,
|
|
||||||
#[serde(rename = "ipAddress")]
|
|
||||||
ip_address: String,
|
|
||||||
#[serde(rename = "cpu_Load")]
|
|
||||||
cpu_load: f64,
|
|
||||||
#[serde(rename = "cpu_Temp")]
|
|
||||||
cpu_temp: f64,
|
|
||||||
#[serde(rename = "gpu_Load")]
|
|
||||||
gpu_load: f64,
|
|
||||||
#[serde(rename = "gpu_Temp")]
|
|
||||||
gpu_temp: f64,
|
|
||||||
#[serde(rename = "gpu_Vram_Size")]
|
|
||||||
gpu_vram_size: f64,
|
|
||||||
#[serde(rename = "gpu_Vram_Usage")]
|
|
||||||
gpu_vram_usage: f64,
|
|
||||||
#[serde(rename = "ram_Load")]
|
|
||||||
ram_load: f64,
|
|
||||||
#[serde(rename = "ram_Size")]
|
|
||||||
ram_size: f64,
|
|
||||||
#[serde(rename = "disk_Size")]
|
|
||||||
disk_size: f64,
|
|
||||||
#[serde(rename = "disk_Usage")]
|
|
||||||
disk_usage: f64,
|
|
||||||
#[serde(rename = "disk_Temp")]
|
|
||||||
disk_temp: f64,
|
|
||||||
#[serde(rename = "net_In")]
|
|
||||||
net_in: f64,
|
|
||||||
#[serde(rename = "net_Out")]
|
|
||||||
net_out: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct IdResponse {
|
|
||||||
id: i32,
|
|
||||||
#[serde(rename = "ipAddress")]
|
|
||||||
ip_address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct HeartbeatPayload {
|
|
||||||
#[serde(rename = "IpAddress")]
|
|
||||||
ip_address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
|
||||||
struct HardwareInfo {
|
|
||||||
cpu_type: String,
|
|
||||||
cpu_cores: i32,
|
|
||||||
gpu_type: String,
|
|
||||||
ram_size: f64,
|
|
||||||
ip_address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NetworkState {
|
|
||||||
prev_rx: u64,
|
|
||||||
prev_tx: u64,
|
|
||||||
last_update: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetworkState {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
prev_rx: 0,
|
|
||||||
prev_tx: 0,
|
|
||||||
last_update: Instant::now(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HardwareInfo {
|
|
||||||
async fn collect() -> Result<Self, Box<dyn Error>> {
|
|
||||||
let mut sys = System::new_all();
|
|
||||||
sys.refresh_cpu_all();
|
|
||||||
sys.refresh_memory();
|
|
||||||
|
|
||||||
let cpus = sys.cpus();
|
|
||||||
let cpu_type = if !cpus.is_empty() {
|
|
||||||
cpus[0].brand().to_string()
|
|
||||||
} else {
|
|
||||||
"Unknown CPU".to_string()
|
|
||||||
};
|
|
||||||
let cpu_cores = cpus.len() as i32;
|
|
||||||
let ram_bytes = sys.total_memory() as f64;
|
|
||||||
let gpu_type = Self::detect_gpu_name();
|
|
||||||
let ip_address = local_ip_address::local_ip()?.to_string();
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
cpu_type,
|
|
||||||
cpu_cores,
|
|
||||||
gpu_type,
|
|
||||||
ram_size: ram_bytes,
|
|
||||||
ip_address,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn detect_gpu_name() -> String {
|
|
||||||
Self::try_nvml_gpu_name()
|
|
||||||
.or_else(Self::fallback_gpu_name)
|
|
||||||
.unwrap_or_else(|| "Unknown GPU".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_nvml_gpu_name() -> Option<String> {
|
|
||||||
let nvml = Nvml::init().ok()?;
|
|
||||||
let device = nvml.device_by_index(0).ok()?;
|
|
||||||
device.name().ok().map(|s| s.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fallback_gpu_name() -> Option<String> {
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
let output = std::process::Command::new("lshw")
|
|
||||||
.args(&["-C", "display"])
|
|
||||||
.output()
|
|
||||||
.ok()?;
|
|
||||||
Some(
|
|
||||||
String::from_utf8_lossy(&output.stdout)
|
|
||||||
.lines()
|
|
||||||
.find(|l| l.contains("product:"))
|
|
||||||
.map(|l| l.trim().replace("product:", "").trim().to_string())
|
|
||||||
.unwrap_or("Unknown GPU".to_string()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
let output = std::process::Command::new("wmic")
|
|
||||||
.args(&["path", "win32_VideoController", "get", "name"])
|
|
||||||
.output()
|
|
||||||
.ok()?;
|
|
||||||
Some(
|
|
||||||
String::from_utf8_lossy(&output.stdout)
|
|
||||||
.lines()
|
|
||||||
.nth(1)
|
|
||||||
.map(|s| s.trim().to_string())
|
|
||||||
.unwrap_or("Unknown GPU".to_string()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_server_id_by_ip(base_url: &str, ip: &str) -> Result<(i32, String), Box<dyn Error>> {
|
|
||||||
let client = Client::builder()
|
|
||||||
.danger_accept_invalid_certs(true)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let url = format!("{}/monitoring/server-id-by-ip?ipAddress={}", base_url, ip);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
println!("Attempting to fetch server ID for IP {}...", ip);
|
|
||||||
match client.get(&url).send().await {
|
|
||||||
Ok(resp) if resp.status().is_success() => {
|
|
||||||
let text = resp.text().await?;
|
|
||||||
println!("Raw response: {}", text); // Debug output
|
|
||||||
|
|
||||||
let id_resp: IdResponse = serde_json::from_str(&text).map_err(|e| {
|
|
||||||
println!("Failed to parse response: {}", e);
|
|
||||||
e
|
|
||||||
})?;
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"✅ Received ID {} for IP {}",
|
|
||||||
id_resp.id, id_resp.ip_address
|
|
||||||
);
|
|
||||||
return Ok((id_resp.id, id_resp.ip_address));
|
|
||||||
}
|
|
||||||
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
|
|
||||||
println!(
|
|
||||||
"❌ Server with IP {} not found in database (will retry in 10 seconds)",
|
|
||||||
ip
|
|
||||||
);
|
|
||||||
sleep(Duration::from_secs(10)).await;
|
|
||||||
}
|
|
||||||
Ok(resp) => {
|
|
||||||
println!(
|
|
||||||
"⚠️ Server responded with status: {} - {}",
|
|
||||||
resp.status(),
|
|
||||||
resp.text().await?
|
|
||||||
);
|
|
||||||
sleep(Duration::from_secs(10)).await;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!("⚠️ Request failed: {} (will retry in 10 seconds)", err);
|
|
||||||
sleep(Duration::from_secs(10)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn register_with_server(base_url: &str) -> Result<(i32, String), Box<dyn Error>> {
|
|
||||||
// First get local IP
|
|
||||||
let ip = local_ip_address::local_ip()?.to_string();
|
|
||||||
println!("Local IP address detected: {}", ip);
|
|
||||||
|
|
||||||
// Get server ID from backend (this will retry until successful)
|
|
||||||
let (server_id, registered_ip) = get_server_id_by_ip(base_url, &ip).await?;
|
|
||||||
|
|
||||||
// Create HTTP client for registration
|
|
||||||
let client = Client::builder()
|
|
||||||
.danger_accept_invalid_certs(true)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
// Collect hardware info
|
|
||||||
let hardware = HardwareInfo::collect().await?;
|
|
||||||
|
|
||||||
// Prepare registration data
|
|
||||||
let registration = RegistrationDto {
|
|
||||||
id: server_id,
|
|
||||||
ip_address: registered_ip.clone(),
|
|
||||||
cpu_type: hardware.cpu_type,
|
|
||||||
cpu_cores: hardware.cpu_cores,
|
|
||||||
gpu_type: hardware.gpu_type,
|
|
||||||
ram_size: hardware.ram_size,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to register (will retry on failure)
|
|
||||||
loop {
|
|
||||||
println!("Attempting to register with server...");
|
|
||||||
let url = format!("{}/monitoring/register-agent-by-id", base_url);
|
|
||||||
match client.post(&url).json(®istration).send().await {
|
|
||||||
Ok(resp) if resp.status().is_success() => {
|
|
||||||
println!("✅ Successfully registered with server.");
|
|
||||||
return Ok((server_id, registered_ip));
|
|
||||||
}
|
|
||||||
Ok(resp) => {
|
|
||||||
let status = resp.status();
|
|
||||||
let text = resp.text().await.unwrap_or_default();
|
|
||||||
println!(
|
|
||||||
"⚠️ Registration failed ({}): {} (will retry in 10 seconds)",
|
|
||||||
status, text
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!("⚠️ Registration error: {} (will retry in 10 seconds)", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sleep(Duration::from_secs(10)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn heartbeat_loop(base_url: &str, ip: &str) -> Result<(), Box<dyn Error>> {
|
|
||||||
let client = Client::builder()
|
|
||||||
.danger_accept_invalid_certs(true)
|
|
||||||
.build()?;
|
|
||||||
let url = format!("{}/heartbeat/receive", base_url);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let payload = HeartbeatPayload {
|
|
||||||
ip_address: ip.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
match client.post(&url).json(&payload).send().await {
|
|
||||||
Ok(res) if res.status().is_success() => {
|
|
||||||
println!("Heartbeat sent successfully.");
|
|
||||||
}
|
|
||||||
Ok(res) => eprintln!("Server responded with status: {}", res.status()),
|
|
||||||
Err(e) => eprintln!("Heartbeat error: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
sleep(Duration::from_secs(20)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MetricsCollector {
|
|
||||||
sys: System,
|
|
||||||
nvml: Option<Nvml>,
|
|
||||||
server_id: i32,
|
|
||||||
ip_address: String,
|
|
||||||
network_state: NetworkState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MetricsCollector {
|
|
||||||
fn new(server_id: i32, ip_address: String) -> Self {
|
|
||||||
Self {
|
|
||||||
sys: System::new(),
|
|
||||||
nvml: Nvml::init().ok(),
|
|
||||||
server_id,
|
|
||||||
ip_address,
|
|
||||||
network_state: NetworkState::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn collect_and_send_loop(&mut self, base_url: &str) -> Result<(), Box<dyn Error>> {
|
|
||||||
let client = Client::new();
|
|
||||||
let url = format!("{}/monitoring/metric", base_url);
|
|
||||||
let mut interval = interval(Duration::from_secs(20));
|
|
||||||
|
|
||||||
loop {
|
|
||||||
interval.tick().await;
|
|
||||||
let metric = self.collect_metrics();
|
|
||||||
println!("Collected metrics: {:?}", metric);
|
|
||||||
|
|
||||||
match client.post(&url).json(&metric).send().await {
|
|
||||||
Ok(res) => println!(
|
|
||||||
"✅ Sent metrics for server {} | Status: {}",
|
|
||||||
metric.server_id,
|
|
||||||
res.status()
|
|
||||||
),
|
|
||||||
Err(err) => eprintln!("❌ Failed to send metrics: {}", err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_metrics(&mut self) -> MetricDto {
|
|
||||||
self.sys.refresh_all();
|
|
||||||
|
|
||||||
// CPU - updated for sysinfo 0.35
|
|
||||||
let cpu_load = self.sys.global_cpu_usage() as f64;
|
|
||||||
let cpu_temp = get_cpu_temp().unwrap_or(0.0) as f64;
|
|
||||||
|
|
||||||
// RAM - updated for sysinfo 0.35
|
|
||||||
let total_memory = self.sys.total_memory() as f64;
|
|
||||||
let used_memory = self.sys.used_memory() as f64;
|
|
||||||
let ram_load = (used_memory / total_memory) * 100.0;
|
|
||||||
let ram_size = total_memory;
|
|
||||||
|
|
||||||
// Disk - updated for sysinfo 0.35
|
|
||||||
let (disk_size, disk_usage, disk_temp) = get_disk_info();
|
|
||||||
// GPU (NVIDIA)
|
|
||||||
let (gpu_temp, gpu_load, vram_used, vram_total) = if let Some(nvml) = &self.nvml {
|
|
||||||
if let Ok(device) = nvml.device_by_index(0) {
|
|
||||||
let temp = device
|
|
||||||
.temperature(nvml_wrapper::enum_wrappers::device::TemperatureSensor::Gpu)
|
|
||||||
.unwrap_or(0) as f64;
|
|
||||||
let load = device
|
|
||||||
.utilization_rates()
|
|
||||||
.map(|u| u.gpu as f64)
|
|
||||||
.unwrap_or(0.0);
|
|
||||||
let mem = device.memory_info().ok();
|
|
||||||
let used = mem.clone().map(|m| (m.used as f64)).unwrap_or(0.0); // B
|
|
||||||
let total = mem.map(|m| (m.total as f64)).unwrap_or(0.0); // B
|
|
||||||
(temp, load, used, total)
|
|
||||||
} else {
|
|
||||||
(0.0, 0.0, 0.0, 0.0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(0.0, 0.0, 0.0, 0.0)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Network metrics
|
|
||||||
let (current_rx, current_tx) = get_network_traffic().unwrap_or((0, 0));
|
|
||||||
let elapsed_secs = self.network_state.last_update.elapsed().as_secs_f64();
|
|
||||||
self.network_state.last_update = Instant::now();
|
|
||||||
|
|
||||||
// Calculate the difference since the last call
|
|
||||||
let net_in = if current_rx >= self.network_state.prev_rx {
|
|
||||||
((current_rx - self.network_state.prev_rx) as f64 * 8.0) / elapsed_secs
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
let net_out = if current_tx >= self.network_state.prev_tx {
|
|
||||||
((current_tx - self.network_state.prev_tx) as f64 * 8.0) / elapsed_secs
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store the current values for the next call
|
|
||||||
self.network_state.prev_rx = current_rx;
|
|
||||||
self.network_state.prev_tx = current_tx;
|
|
||||||
|
|
||||||
MetricDto {
|
|
||||||
server_id: self.server_id,
|
|
||||||
ip_address: self.ip_address.clone(),
|
|
||||||
cpu_load,
|
|
||||||
cpu_temp,
|
|
||||||
gpu_load,
|
|
||||||
gpu_temp,
|
|
||||||
gpu_vram_size: vram_total,
|
|
||||||
gpu_vram_usage: if vram_total > 0.0 {
|
|
||||||
(vram_used / vram_total) * 100.0
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
},
|
|
||||||
ram_load,
|
|
||||||
ram_size,
|
|
||||||
disk_size,
|
|
||||||
disk_usage: disk_usage,
|
|
||||||
disk_temp: disk_temp, // not supported
|
|
||||||
net_in,
|
|
||||||
net_out,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cpu_temp() -> Option<f32> {
|
|
||||||
println!("Attempting to get CPU temperature...");
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
use std::fs;
|
|
||||||
use std::process::Command;
|
|
||||||
println!("");
|
|
||||||
if let Ok(output) = Command::new("sensors").output() {
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
for line in stdout.lines() {
|
|
||||||
if line.contains("Package id") || line.contains("Tdie") || line.contains("CPU Temp")
|
|
||||||
{
|
|
||||||
if let Some(temp_str) = line
|
|
||||||
.split('+')
|
|
||||||
.nth(1)
|
|
||||||
.and_then(|s| s.split_whitespace().next())
|
|
||||||
{
|
|
||||||
if let Ok(temp) = temp_str.replace("°C", "").parse::<f32>() {
|
|
||||||
return Some(temp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Sysfs (Intel/AMD)
|
|
||||||
if let Ok(content) = fs::read_to_string("/sys/class/thermal/thermal_zone0/temp") {
|
|
||||||
if let Ok(temp) = content.trim().parse::<f32>() {
|
|
||||||
return Some(temp / 1000.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Alternative Sysfs-Pfade
|
|
||||||
let paths = [
|
|
||||||
"/sys/class/hwmon/hwmontemp1_input",
|
|
||||||
"/sys/class/hwmon/hwmondevice/temp1_input",
|
|
||||||
];
|
|
||||||
|
|
||||||
for path_pattern in &paths {
|
|
||||||
if let Ok(paths) = glob::glob(path_pattern) {
|
|
||||||
for path in paths.flatten() {
|
|
||||||
if let Ok(content) = fs::read_to_string(&path) {
|
|
||||||
if let Ok(temp) = content.trim().parse::<f32>() {
|
|
||||||
return Some(temp / 1000.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn failed(hr: winapi::shared::winerror::HRESULT) -> bool {
|
|
||||||
hr < 0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
use com::runtime::init_runtime;
|
|
||||||
use com::sys::CLSCTX_INPROC_SERVER;
|
|
||||||
use widestring::U16CString;
|
|
||||||
use winapi::shared::rpcdce::*;
|
|
||||||
use winapi::shared::wtypes::VT_I4;
|
|
||||||
use winapi::um::oaidl::VARIANT;
|
|
||||||
use winapi::um::objidlbase::EOAC_NONE;
|
|
||||||
use winapi::um::{combaseapi, wbemcli};
|
|
||||||
|
|
||||||
init_runtime().ok()?;
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let mut locator: *mut wbemcli::IWbemLocator = std::ptr::null_mut();
|
|
||||||
let hr = combaseapi::CoCreateInstance(
|
|
||||||
&wbemcli::CLSID_WbemLocator,
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
CLSCTX_INPROC_SERVER,
|
|
||||||
&wbemcli::IID_IWbemLocator,
|
|
||||||
&mut locator as *mut _ as *mut _,
|
|
||||||
);
|
|
||||||
|
|
||||||
if hr != 0 {
|
|
||||||
eprintln!("Failed to create WbemLocator (HRESULT: {})", hr);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut services: *mut wbemcli::IWbemServices = std::ptr::null_mut();
|
|
||||||
let namespace = U16CString::from_str("root\\cimv2").unwrap(); // Changed to more common namespace
|
|
||||||
let hr = (*locator).ConnectServer(
|
|
||||||
namespace.as_ptr().cast_mut(),
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
0,
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
&mut services,
|
|
||||||
);
|
|
||||||
|
|
||||||
if hr != 0 {
|
|
||||||
eprintln!("Failed to connect to WMI (HRESULT: {})", hr);
|
|
||||||
(*locator).Release();
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set security levels
|
|
||||||
let hr = combaseapi::CoSetProxyBlanket(
|
|
||||||
services as *mut _,
|
|
||||||
RPC_C_AUTHN_WINNT,
|
|
||||||
RPC_C_AUTHZ_NONE,
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
RPC_C_AUTHN_LEVEL_CALL,
|
|
||||||
RPC_C_IMP_LEVEL_IMPERSONATE,
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
EOAC_NONE,
|
|
||||||
);
|
|
||||||
|
|
||||||
if hr != 0 {
|
|
||||||
eprintln!("Failed to set proxy blanket (HRESULT: {})", hr);
|
|
||||||
(*services).Release();
|
|
||||||
(*locator).Release();
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try different temperature queries - some systems might have different WMI classes
|
|
||||||
let queries = [
|
|
||||||
"SELECT * FROM Win32_PerfFormattedData_Counters_ThermalZoneInformation",
|
|
||||||
"SELECT * FROM MSAcpi_ThermalZoneTemperature",
|
|
||||||
"SELECT * FROM Win32_TemperatureProbe",
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut result = None;
|
|
||||||
|
|
||||||
for query_str in queries.iter() {
|
|
||||||
let query = U16CString::from_str(query_str).unwrap();
|
|
||||||
let mut enumerator: *mut wbemcli::IEnumWbemClassObject = std::ptr::null_mut();
|
|
||||||
let hr = (*services).ExecQuery(
|
|
||||||
U16CString::from_str("WQL").unwrap().as_ptr().cast_mut(),
|
|
||||||
query.as_ptr().cast_mut(),
|
|
||||||
wbemcli::WBEM_FLAG_FORWARD_ONLY as i32,
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
&mut enumerator,
|
|
||||||
);
|
|
||||||
|
|
||||||
if hr != 0 {
|
|
||||||
continue; // Try next query if this one fails
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut obj: *mut wbemcli::IWbemClassObject = std::ptr::null_mut();
|
|
||||||
let mut returned = 0;
|
|
||||||
let hr = (*enumerator).Next(
|
|
||||||
wbemcli::WBEM_INFINITE as i32, // Fixed: cast directly to i32
|
|
||||||
1,
|
|
||||||
&mut obj,
|
|
||||||
&mut returned,
|
|
||||||
);
|
|
||||||
|
|
||||||
if failed(hr) {
|
|
||||||
eprintln!("Failed to enumerate WMI objects (HRESULT: {})", hr);
|
|
||||||
(*enumerator).Release();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if returned == 0 {
|
|
||||||
// No more items
|
|
||||||
(*enumerator).Release();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if hr == 0 && returned > 0 {
|
|
||||||
let mut variant = std::mem::zeroed::<VARIANT>();
|
|
||||||
// Try different possible property names
|
|
||||||
let property_names = ["CurrentTemperature", "Temperature", "CurrentReading"];
|
|
||||||
|
|
||||||
for prop in property_names.iter() {
|
|
||||||
let hr = (*obj).Get(
|
|
||||||
U16CString::from_str(prop).unwrap().as_ptr(),
|
|
||||||
0,
|
|
||||||
&mut variant,
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if hr == 0 && variant.n1.n2().vt as u32 == VT_I4 {
|
|
||||||
let temp_kelvin = *variant.n1.n2().n3.intVal() as f32 / 10.0;
|
|
||||||
result = Some(temp_kelvin - 273.15); // Convert to Celsius
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(*obj).Release();
|
|
||||||
(*enumerator).Release();
|
|
||||||
if result.is_some() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !enumerator.is_null() {
|
|
||||||
(*enumerator).Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(*services).Release();
|
|
||||||
(*locator).Release();
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
|
||||||
{
|
|
||||||
println!("CPU temperature retrieval not supported on this OS.");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_disk_info() -> (f64, f64, f64) {
|
|
||||||
let mut sys = System::new();
|
|
||||||
sys.refresh_all();
|
|
||||||
//sys.refresh_disks_list();
|
|
||||||
|
|
||||||
let mut total_size = 0u64;
|
|
||||||
let mut total_used = 0u64;
|
|
||||||
let mut count = 0;
|
|
||||||
|
|
||||||
let disks = Disks::new_with_refreshed_list();
|
|
||||||
for disk in disks.list() {
|
|
||||||
// Ignoriere CD-ROMs und kleine Systempartitionen
|
|
||||||
println!(
|
|
||||||
"Disk_Name: {:?}, Disk_Kind: {}, Total: {}, Available: {}",
|
|
||||||
disk.name(),
|
|
||||||
disk.kind(),
|
|
||||||
disk.total_space(),
|
|
||||||
disk.available_space()
|
|
||||||
);
|
|
||||||
if disk.total_space() > 100 * 1024 * 1024 {
|
|
||||||
// > 100MB
|
|
||||||
total_size += disk.total_space();
|
|
||||||
total_used += disk.total_space() - disk.available_space();
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let components = Components::new_with_refreshed_list();
|
|
||||||
for component in &components {
|
|
||||||
if let Some(temperature) = component.temperature() {
|
|
||||||
println!(
|
|
||||||
"Component_Label: {}, Temperature: {}°C",
|
|
||||||
component.label(),
|
|
||||||
temperature
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Berechnungen
|
|
||||||
let size_b = if count > 0 {
|
|
||||||
total_size as f64 // in Bytes
|
|
||||||
} else {
|
|
||||||
// Fallback: Versuche df unter Linux
|
|
||||||
println!("Fallback: Using 'df' command to get disk info.");
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
use std::process::Command;
|
|
||||||
if let Ok(output) = Command::new("df")
|
|
||||||
.arg("-B1")
|
|
||||||
.arg("--output=size,used")
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
for line in stdout.lines().skip(1) {
|
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
||||||
if parts.len() == 2 {
|
|
||||||
if let (Ok(size), Ok(used)) =
|
|
||||||
(parts[0].parse::<u64>(), parts[1].parse::<u64>())
|
|
||||||
{
|
|
||||||
total_size += size;
|
|
||||||
total_used += used;
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
total_size as f64 // in Bytes
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
{
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let usage = if total_size > 0 {
|
|
||||||
(total_used as f64 / total_size as f64) * 100.0
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
(size_b, usage, 0.0) // Disk-Temp bleibt 0.0 ohne spezielle Hardware
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_network_traffic() -> Option<(u64, u64)> {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
use std::ptr::null_mut;
|
|
||||||
use winapi::shared::ifmib::MIB_IFTABLE;
|
|
||||||
use winapi::um::iphlpapi::GetIfTable;
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
// Erste Abfrage zur Bestimmung der benötigten Puffergröße
|
|
||||||
let mut buffer_size = 0u32;
|
|
||||||
if GetIfTable(null_mut(), &mut buffer_size, 0)
|
|
||||||
!= winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Puffer allozieren
|
|
||||||
let mut buffer = vec![0u8; buffer_size as usize];
|
|
||||||
let if_table = buffer.as_mut_ptr() as *mut MIB_IFTABLE;
|
|
||||||
|
|
||||||
// Tatsächliche Daten abrufen
|
|
||||||
if GetIfTable(if_table, &mut buffer_size, 0) != 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Daten auswerten
|
|
||||||
let mut rx_total = 0u64;
|
|
||||||
let mut tx_total = 0u64;
|
|
||||||
|
|
||||||
for i in 0..(*if_table).dwNumEntries {
|
|
||||||
let row = &*((*if_table).table.as_ptr().offset(i as isize));
|
|
||||||
rx_total += row.dwInOctets as u64;
|
|
||||||
tx_total += row.dwOutOctets as u64;
|
|
||||||
}
|
|
||||||
|
|
||||||
if rx_total == 0 && tx_total == 0 {
|
|
||||||
return None;
|
|
||||||
} else {
|
|
||||||
return Some((rx_total, tx_total));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
let mut rx_total = 0u64;
|
|
||||||
let mut tx_total = 0u64;
|
|
||||||
if let Ok(dir) = fs::read_dir("/sys/class/net") {
|
|
||||||
for entry in dir.flatten() {
|
|
||||||
let iface = entry.file_name();
|
|
||||||
let iface_name = iface.to_string_lossy();
|
|
||||||
|
|
||||||
// Ignoriere virtuelle Interfaces
|
|
||||||
if !iface_name.starts_with("lo") && !iface_name.starts_with("virbr") {
|
|
||||||
if let (Ok(rx), Ok(tx)) = (
|
|
||||||
fs::read_to_string(entry.path().join("statistics/rx_bytes")),
|
|
||||||
fs::read_to_string(entry.path().join("statistics/tx_bytes")),
|
|
||||||
) {
|
|
||||||
rx_total += rx.trim().parse::<u64>().unwrap_or(0);
|
|
||||||
tx_total += tx.trim().parse::<u64>().unwrap_or(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if rx_total == 0 && tx_total == 0 {
|
|
||||||
return None;
|
|
||||||
} else {
|
|
||||||
return Some((rx_total, tx_total));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
let server_base_url = "http://localhost:5000";
|
let server_url = "http://localhost:5000";
|
||||||
|
|
||||||
// Registration phase
|
// Registration
|
||||||
println!("Starting registration process...");
|
let (server_id, ip) = match api::register_with_server(server_url).await {
|
||||||
let (server_id, ip_address) = register_with_server(server_base_url).await?;
|
Ok((id, ip)) => (id, ip),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Fehler bei der Registrierung am Server: {e}");
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start background tasks
|
||||||
// Start heartbeat in background
|
// Start heartbeat in background
|
||||||
let heartbeat_handle = tokio::spawn({
|
let heartbeat_handle = tokio::spawn({
|
||||||
let ip = ip_address.clone();
|
let ip = ip.clone();
|
||||||
|
let server_url = server_url.to_string();
|
||||||
|
async move { api::heartbeat_loop(&server_url, &ip).await }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main metrics loop
|
||||||
|
println!("Starting metrics collection...");
|
||||||
|
let metrics_handle = tokio::spawn({
|
||||||
|
let ip = ip.clone();
|
||||||
|
let server_url = server_url.to_string();
|
||||||
async move {
|
async move {
|
||||||
if let Err(e) = heartbeat_loop(server_base_url, &ip).await {
|
let mut collector = metrics::Collector::new(server_id, ip);
|
||||||
eprintln!("Heartbeat loop failed: {}", e);
|
collector.run(&server_url).await
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start metrics collection
|
// Warte auf beide Tasks und prüfe explizit auf Fehler
|
||||||
println!("Starting metrics collection...");
|
let (heartbeat_handle, metrics_handle) =
|
||||||
let mut metrics_collector = MetricsCollector::new(server_id, ip_address);
|
tokio::try_join!(flatten(heartbeat_handle), flatten(metrics_handle))?;
|
||||||
metrics_collector
|
|
||||||
.collect_and_send_loop(server_base_url)
|
let (heartbeat, metrics) = (heartbeat_handle, metrics_handle);
|
||||||
.await?;
|
println!(
|
||||||
|
"All tasks completed successfully: {:?}, {:?}.",
|
||||||
|
heartbeat, metrics
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("All tasks completed successfully.");
|
||||||
|
|
||||||
heartbeat_handle.await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
// Test CPU temperature collection
|
|
||||||
#[test]
|
|
||||||
fn test_cpu_temp() {
|
|
||||||
let temp = get_cpu_temp();
|
|
||||||
println!("CPU Temperature: {:?}°C", temp);
|
|
||||||
|
|
||||||
// Basic validation - temp should be between 0-100°C if available
|
|
||||||
if let Some(t) = temp {
|
|
||||||
assert!(
|
|
||||||
t >= 0.0 && t <= 100.0,
|
|
||||||
"CPU temperature out of reasonable range"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test disk information collection
|
|
||||||
#[test]
|
|
||||||
fn test_disk_info() {
|
|
||||||
let (size, usage, _temp) = get_disk_info();
|
|
||||||
println!("Disk Size: {:.2}GB, Usage: {:.2}%", size, usage);
|
|
||||||
|
|
||||||
assert!(size >= 0.0, "Disk size should be non-negative");
|
|
||||||
assert!(
|
|
||||||
usage >= 0.0 && usage <= 100.0,
|
|
||||||
"Disk usage should be 0-100%"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test hardware info collection
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_hardware_info() {
|
|
||||||
let hardware = HardwareInfo::collect().await.unwrap();
|
|
||||||
println!("Hardware Info: {:?}", hardware);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
!hardware.cpu_type.is_empty(),
|
|
||||||
"CPU type should not be empty"
|
|
||||||
);
|
|
||||||
assert!(hardware.cpu_cores > 0, "CPU cores should be positive");
|
|
||||||
assert!(
|
|
||||||
!hardware.gpu_type.is_empty(),
|
|
||||||
"GPU type should not be empty"
|
|
||||||
);
|
|
||||||
assert!(hardware.ram_size > 0.0, "RAM size should be positive");
|
|
||||||
assert!(
|
|
||||||
!hardware.ip_address.is_empty(),
|
|
||||||
"IP address should not be empty"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test metrics collector
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_metrics_collector() {
|
|
||||||
let mut collector = MetricsCollector::new(1, "127.0.0.1".to_string());
|
|
||||||
let metrics = collector.collect_metrics();
|
|
||||||
println!("Collected Metrics: {:?}", metrics);
|
|
||||||
|
|
||||||
// Validate basic metrics ranges
|
|
||||||
assert!(
|
|
||||||
metrics.cpu_load >= 0.0 && metrics.cpu_load <= 100.0,
|
|
||||||
"CPU load should be 0-100%"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
metrics.ram_load >= 0.0 && metrics.ram_load <= 100.0,
|
|
||||||
"RAM load should be 0-100%"
|
|
||||||
);
|
|
||||||
assert!(metrics.ram_size > 0.0, "RAM size should be positive");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test registration flow (mock server needed for full test)
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_registration_flow() {
|
|
||||||
// Note: This would require a mock server for proper testing
|
|
||||||
// Currently just testing the hardware detection part
|
|
||||||
let hardware = HardwareInfo::collect().await.unwrap();
|
|
||||||
let registration = RegistrationDto {
|
|
||||||
id: 1,
|
|
||||||
ip_address: hardware.ip_address.clone(),
|
|
||||||
cpu_type: hardware.cpu_type,
|
|
||||||
cpu_cores: hardware.cpu_cores,
|
|
||||||
gpu_type: hardware.gpu_type,
|
|
||||||
ram_size: hardware.ram_size,
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("Registration DTO: {:?}", registration);
|
|
||||||
assert_eq!(registration.id, 1);
|
|
||||||
assert!(!registration.ip_address.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test error cases
|
|
||||||
#[test]
|
|
||||||
fn test_error_handling() {
|
|
||||||
// Test with invalid paths
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
let temp = get_cpu_temp_with_path("/invalid/path");
|
|
||||||
assert!(temp.is_none(), "Should handle invalid paths gracefully");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function for testing with custom paths
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn get_cpu_temp_with_path(path: &str) -> Option<f32> {
|
|
||||||
fs::read_to_string(path)
|
|
||||||
.ok()?
|
|
||||||
.trim()
|
|
||||||
.parse::<f32>()
|
|
||||||
.map(|t| t / 1000.0)
|
|
||||||
.ok()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
72
WatcherAgent/src/metrics.rs
Normal file
72
WatcherAgent/src/metrics.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::api;
|
||||||
|
use crate::hardware::network::NetworkMonitor;
|
||||||
|
use crate::hardware::HardwareInfo;
|
||||||
|
use crate::models::MetricDto;
|
||||||
|
|
||||||
|
pub struct Collector {
|
||||||
|
network_monitor: NetworkMonitor,
|
||||||
|
server_id: i32,
|
||||||
|
ip_address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collector {
|
||||||
|
pub fn new(server_id: i32, ip_address: String) -> Self {
|
||||||
|
Self {
|
||||||
|
network_monitor: NetworkMonitor::new(),
|
||||||
|
server_id,
|
||||||
|
ip_address,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&mut self, base_url: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
loop {
|
||||||
|
println!(
|
||||||
|
"[{}] Starting metrics collection...",
|
||||||
|
chrono::Local::now().format("%H:%M:%S")
|
||||||
|
);
|
||||||
|
let metrics = match self.collect().await {
|
||||||
|
Ok(metrics) => metrics,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error collecting metrics: {}", e);
|
||||||
|
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
api::send_metrics(base_url, &metrics).await?;
|
||||||
|
tokio::time::sleep(Duration::from_secs(20)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn collect(&mut self) -> Result<MetricDto, Box<dyn Error + Send + Sync>> {
|
||||||
|
let hardware = match HardwareInfo::collect().await {
|
||||||
|
Ok(hw) => hw,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Fehler beim Sammeln der Hardware-Infos: {e}");
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Collect network usage
|
||||||
|
let (_, _) = self.network_monitor.update_usage().unwrap_or((0.0, 0.0));
|
||||||
|
|
||||||
|
Ok(MetricDto {
|
||||||
|
server_id: self.server_id,
|
||||||
|
ip_address: self.ip_address.clone(),
|
||||||
|
cpu_load: hardware.cpu.current_load.unwrap_or_default(),
|
||||||
|
cpu_temp: hardware.cpu.current_temp.unwrap_or_default(),
|
||||||
|
gpu_load: hardware.gpu.current_load.unwrap_or_default(),
|
||||||
|
gpu_temp: hardware.gpu.current_temp.unwrap_or_default(),
|
||||||
|
gpu_vram_size: hardware.gpu.vram_total.unwrap_or_default(),
|
||||||
|
gpu_vram_usage: hardware.gpu.vram_used.unwrap_or_default(),
|
||||||
|
ram_load: hardware.memory.used.unwrap_or_default(),
|
||||||
|
ram_size: hardware.memory.total.unwrap_or_default(),
|
||||||
|
disk_size: hardware.disk.total.unwrap_or_default(),
|
||||||
|
disk_usage: hardware.disk.used.unwrap_or_default(),
|
||||||
|
disk_temp: 0.0, // not supported
|
||||||
|
net_rx: hardware.network.rx_rate.unwrap_or_default(),
|
||||||
|
net_tx: hardware.network.tx_rate.unwrap_or_default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
74
WatcherAgent/src/models.rs
Normal file
74
WatcherAgent/src/models.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// Data structures matching the C# DTOs
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
pub struct RegistrationDto {
|
||||||
|
#[serde(rename = "id")]
|
||||||
|
pub id: i32,
|
||||||
|
#[serde(rename = "ipAddress")]
|
||||||
|
pub ip_address: String,
|
||||||
|
#[serde(rename = "cpuType")]
|
||||||
|
pub cpu_type: String,
|
||||||
|
#[serde(rename = "cpuCores")]
|
||||||
|
pub cpu_cores: i32,
|
||||||
|
#[serde(rename = "gpuType")]
|
||||||
|
pub gpu_type: String,
|
||||||
|
#[serde(rename = "ramSize")]
|
||||||
|
pub ram_size: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
pub struct MetricDto {
|
||||||
|
#[serde(rename = "serverId")]
|
||||||
|
pub server_id: i32,
|
||||||
|
#[serde(rename = "ipAddress")]
|
||||||
|
pub ip_address: String,
|
||||||
|
#[serde(rename = "cpu_Load")]
|
||||||
|
pub cpu_load: f64,
|
||||||
|
#[serde(rename = "cpu_Temp")]
|
||||||
|
pub cpu_temp: f64,
|
||||||
|
#[serde(rename = "gpu_Load")]
|
||||||
|
pub gpu_load: f64,
|
||||||
|
#[serde(rename = "gpu_Temp")]
|
||||||
|
pub gpu_temp: f64,
|
||||||
|
#[serde(rename = "gpu_Vram_Size")]
|
||||||
|
pub gpu_vram_size: f64,
|
||||||
|
#[serde(rename = "gpu_Vram_Usage")]
|
||||||
|
pub gpu_vram_usage: f64,
|
||||||
|
#[serde(rename = "ram_Load")]
|
||||||
|
pub ram_load: f64,
|
||||||
|
#[serde(rename = "ram_Size")]
|
||||||
|
pub ram_size: f64,
|
||||||
|
#[serde(rename = "disk_Size")]
|
||||||
|
pub disk_size: f64,
|
||||||
|
#[serde(rename = "disk_Usage")]
|
||||||
|
pub disk_usage: f64,
|
||||||
|
#[serde(rename = "disk_Temp")]
|
||||||
|
pub disk_temp: f64,
|
||||||
|
#[serde(rename = "net_In")]
|
||||||
|
pub net_rx: f64,
|
||||||
|
#[serde(rename = "net_Out")]
|
||||||
|
pub net_tx: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct IdResponse {
|
||||||
|
pub id: i32,
|
||||||
|
#[serde(rename = "ipAddress")]
|
||||||
|
pub ip_address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct HeartbeatDto {
|
||||||
|
#[serde(rename = "IpAddress")]
|
||||||
|
pub ip_address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
pub struct HardwareDto {
|
||||||
|
pub cpu_type: String,
|
||||||
|
pub cpu_cores: i32,
|
||||||
|
pub gpu_type: String,
|
||||||
|
pub ram_size: f64,
|
||||||
|
pub ip_address: String,
|
||||||
|
}
|
Reference in New Issue
Block a user