put all files in their expected file

This commit is contained in:
2025-08-08 14:19:12 +02:00
parent 1a5828f905
commit 0cdab2443d
7 changed files with 835 additions and 944 deletions

View File

@@ -16,7 +16,7 @@ sysinfo = "0.36.1"
metrics = "0.24.2"
chrono = "0.4"
nvml-wrapper = "0.10"
anyhow = "1.0.98"
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winuser", "pdh", "ifmib", "iphlpapi", "winerror" ,"wbemcli", "combaseapi"] }

149
WatcherAgent/src/api.rs Normal file
View File

@@ -0,0 +1,149 @@
use std::time::Duration;
use crate::models::{HardwareDto, HeartbeatDto, IdResponse, MetricDto, RegistrationDto};
use anyhow::Result;
use reqwest::{Client, StatusCode};
use std::error::Error;
use tokio::time::{interval, sleep};
pub 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 = HardwareDto::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(&registration).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>> {
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>> {
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>> {
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 = metrics;
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),
}
}
}

View File

@@ -0,0 +1,79 @@
use crate::models::HardwareDto;
use nvml_wrapper::Nvml;
use sysinfo::System;
impl HardwareDto {
pub async fn collect() -> anyhow::Result<Self> {
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 {
// First try NVML (NVIDIA Management Library)
if let Some(name) = Self::try_nvml_gpu_name() {
return name;
}
// Fallback to OS-specific commands
#[cfg(target_os = "linux")]
{
if let Ok(output) = std::process::Command::new("lshw")
.args(&["-C", "display"])
.output()
{
if let Some(name) = String::from_utf8_lossy(&output.stdout)
.lines()
.find(|l| l.contains("product:"))
.map(|l| l.trim().replace("product:", "").trim().to_string())
{
return name;
}
}
}
#[cfg(target_os = "windows")]
{
if let Ok(output) = std::process::Command::new("wmic")
.args(&["path", "win32_VideoController", "get", "name"])
.output()
{
if let Some(name) = String::from_utf8_lossy(&output.stdout)
.lines()
.nth(1)
.map(|s| s.trim().to_string())
{
return name;
}
}
}
// If all else fails
"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())
}
}

View File

@@ -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()`
*/

View File

@@ -1,940 +1,38 @@
/// WatcherAgent - A Rust-based system monitoring agent
/// This agent collects hardware metrics and sends them to a backend server.
/// It supports CPU, GPU, RAM, disk, and network metrics.
//use chrono::Utc;
use nvml_wrapper::Nvml;
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use std::{error::Error, time::Duration};
use sysinfo::{Components, Disks, System};
use tokio::time::{interval, sleep, Instant};
mod api;
mod hardware;
mod metrics;
mod models;
// Windows specific imports
// Data structures matching the C# DTOs
#[derive(Serialize, Debug)]
struct RegistrationDto {
#[serde(rename = "id")]
id: i32,
#[serde(rename = "ipAddress")]
ip_address: String,
#[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(&registration).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
}
use anyhow::Result;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let server_base_url = "http://localhost:5000";
let server_url = "http://localhost:5000";
// Registration phase
println!("Starting registration process...");
let (server_id, ip_address) = register_with_server(server_base_url).await?;
// Registration
let (server_id, ip) = api::register_with_server(server_url).await?;
// Start background tasks
// Start heartbeat in background
let heartbeat_handle = tokio::spawn({
let ip = ip_address.clone();
let ip = ip.clone();
async move {
if let Err(e) = heartbeat_loop(server_base_url, &ip).await {
if let Err(e) = api::heartbeat_loop(server_url, &ip).await {
eprintln!("Heartbeat loop failed: {}", e);
}
}
});
// Start metrics collection
println!("Starting metrics collection...");
let mut metrics_collector = MetricsCollector::new(server_id, ip_address);
metrics_collector
.collect_and_send_loop(server_base_url)
.await?;
heartbeat_handle.await?;
// Main metrics loop
println!("Starting metrics collection...");
let mut collector = metrics::Collector::new(server_id, ip);
collector.run(server_url).await?;
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()
}
}

507
WatcherAgent/src/metrics.rs Normal file
View File

@@ -0,0 +1,507 @@
use std::error::Error;
use std::time::Duration;
use sysinfo::{Components, Disks, System};
use tokio::time::Instant;
use crate::api;
use crate::models::{MetricDto, NetworkState};
use nvml_wrapper::Nvml;
pub struct Collector {
sys: System,
nvml: Option<Nvml>,
server_id: i32,
ip_address: String,
network_state: NetworkState,
}
impl NetworkState {
fn new() -> Self {
Self {
prev_rx: 0,
prev_tx: 0,
last_update: Instant::now(),
}
}
}
impl Collector {
pub 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(),
}
}
pub async fn run(&mut self, base_url: &str) -> anyhow::Result<(), Box<dyn Error>> {
loop {
let metrics = self.collect();
api::send_metrics(base_url, &metrics).await?;
tokio::time::sleep(Duration::from_secs(20)).await;
}
}
pub fn collect(&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,
}
}
}
pub 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_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
}
pub 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
}

View File

@@ -0,0 +1,81 @@
use serde::{Deserialize, Serialize};
use tokio::time::Instant;
// 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_in: f64,
#[serde(rename = "net_Out")]
pub net_out: 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,
}
pub struct NetworkState {
pub prev_rx: u64,
pub prev_tx: u64,
pub last_update: Instant,
}