diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7fec8bd..f60912c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,7 @@ name: Rust Cross-Platform Build on: workflow_dispatch: push: - branches: [ "development", "main", "feature/*", bug/* ] + branches: [ "development", "main", "feature/*", bug/*, enhancement/* ] pull_request: branches: [ "development", "main" ] @@ -11,8 +11,8 @@ env: CARGO_TERM_COLOR: always REGISTRY: git.triggermeelmo.com IMAGE_NAME: donpat1to/watcher-agent - TAG: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || github.ref_name }} - + TAG: development + jobs: detect-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 docker-build: - name: Build Docker Images - needs: [native-build, windows-cross] + name: Build Linux Docker Image + needs: [native-build, windows-cross, detect-project] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -135,53 +135,37 @@ jobs: name: linux-binary 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 run: | echo "Linux binary:" 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: | - docker buildx create --use + docker buildx create --use docker buildx build \ --platform linux/amd64 \ -f Dockerfile.linux \ --build-arg BINARY_NAME=${{ needs.detect-project.outputs.project-name }} \ + --load \ -t ${{ env.IMAGE_NAME }}:linux-${{ env.TAG }} . - - name: Login to Docker registry - if: ${{ success() }} + - name: Save Docker image as artifact run: | - echo "Logging in to Docker registry: ${{ env.REGISTRY }}" - echo "${{ secrets.DOCKER_PASSWORD }}" | docker login git.triggermeelmo.com -u ${{ secrets.DOCKER_USERNAME }} --password-stdin + docker save ${{ env.IMAGE_NAME }}:linux-${{ env.TAG }} -o linux-image.tar - - name: Tag and Push Linux Docker image - if: ${{ success() }} - run: | - echo "Tagging Linux Docker image" - docker tag ${{ env.IMAGE_NAME }}:linux-${{ env.TAG }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:linux-${{ env.TAG }} - 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 }} + - name: Upload Docker image artifact + uses: actions/upload-artifact@v3 + with: + name: linux-docker-image + path: linux-image.tar cleanup: name: Cleanup diff --git a/Dockerfile.linux b/Dockerfile.linux index 6dd2861..f9d8d91 100644 --- a/Dockerfile.linux +++ b/Dockerfile.linux @@ -15,8 +15,11 @@ copy ./WatcherAgent/ . # Build for Linux 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 -from debian:bullseye-slim +from debian:bookworm-slim run apt-get update && \ apt-get install -y apt-transport-https && \ @@ -25,14 +28,12 @@ run apt-get update && \ && rm -rf /var/lib/apt/lists/* # Make binary name configurable -arg 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 +arg BINARY_NAME +env BINARY_NAME=${BINARY_NAME:-WatcherAgent} +copy --from=linux-builder /app/target/x86_64-unknown-linux-gnu/release/${BINARY_NAME} /usr/local/bin/ # Health check 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"] \ No newline at end of file +ENTRYPOINT ["/bin/sh", "-c", "/usr/local/bin/${BINARY_NAME}"] \ No newline at end of file diff --git a/Dockerfile.windows b/Dockerfile.windows deleted file mode 100644 index 6d2b20a..0000000 --- a/Dockerfile.windows +++ /dev/null @@ -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"] \ No newline at end of file diff --git a/WatcherAgent/Cargo.toml b/WatcherAgent/Cargo.toml index 3c79d8d..35c2b05 100644 --- a/WatcherAgent/Cargo.toml +++ b/WatcherAgent/Cargo.toml @@ -15,8 +15,9 @@ reqwest = { version = "0.11", default-features = false, features = ["json", "blo sysinfo = "0.36.1" metrics = "0.24.2" 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] winapi = { version = "0.3", features = ["winuser", "pdh", "ifmib", "iphlpapi", "winerror" ,"wbemcli", "combaseapi"] } @@ -25,4 +26,8 @@ com = "0.2" widestring = "0.5" [target.'cfg(unix)'.dependencies] -glob = "0.3" \ No newline at end of file +glob = "0.3" +libloading = "0.8" + +[build-dependencies] +pkg-config = { version = "0.3", optional = true } # Für Library-Detektion \ No newline at end of file diff --git a/WatcherAgent/src/api.rs b/WatcherAgent/src/api.rs new file mode 100644 index 0000000..4339cfa --- /dev/null +++ b/WatcherAgent/src/api.rs @@ -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> { + // 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> { + 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> { + 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> { + 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(()) +} diff --git a/WatcherAgent/src/hardware/cpu.rs b/WatcherAgent/src/hardware/cpu.rs new file mode 100644 index 0000000..73efa06 --- /dev/null +++ b/WatcherAgent/src/hardware/cpu.rs @@ -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, + pub cores: Option, + pub current_load: Option, + pub current_temp: Option, +} + +pub async fn get_cpu_info() -> Result> { + 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> { + 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> { + 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::() { + 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::() { + 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::() { + 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::(); + // 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()) + } +} diff --git a/WatcherAgent/src/hardware/disk.rs b/WatcherAgent/src/hardware/disk.rs new file mode 100644 index 0000000..0365bff --- /dev/null +++ b/WatcherAgent/src/hardware/disk.rs @@ -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, + pub used: Option, + pub free: Option, +} + +pub async fn get_disk_info() -> Result { + 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> { + 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::(), parts[1].parse::()) + { + 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 { + component.temperature().map(|temp| temp as f64) +} + +pub fn _get_disk_load_for_disk(disk: &Disk) -> Result<(f64, f64, f64, f64), Box> { + 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, + )) +} diff --git a/WatcherAgent/src/hardware/gpu.rs b/WatcherAgent/src/hardware/gpu.rs new file mode 100644 index 0000000..9e667e6 --- /dev/null +++ b/WatcherAgent/src/hardware/gpu.rs @@ -0,0 +1,105 @@ +use anyhow::Result; +use nvml_wrapper::Nvml; +use std::error::Error; + +#[derive(Debug)] +pub struct GpuInfo { + pub name: Option, + pub current_load: Option, + pub current_temp: Option, + pub vram_total: Option, + pub vram_used: Option, +} + +pub async fn get_gpu_info() -> Result> { + 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> { + 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 { + 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 { + #[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 + } +} diff --git a/WatcherAgent/src/hardware/memory.rs b/WatcherAgent/src/hardware/memory.rs new file mode 100644 index 0000000..758c57a --- /dev/null +++ b/WatcherAgent/src/hardware/memory.rs @@ -0,0 +1,27 @@ +use std::error::Error; + +use anyhow::Result; +use sysinfo::System; + +#[derive(Debug)] +pub struct MemoryInfo { + pub total: Option, + pub used: Option, + pub free: Option, +} + +pub async fn get_memory_info() -> Result { + 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> { + sys.refresh_memory(); + Ok((sys.used_memory() as f64 / sys.total_memory() as f64) * 100.0) +} diff --git a/WatcherAgent/src/hardware/mod.rs b/WatcherAgent/src/hardware/mod.rs new file mode 100644 index 0000000..0b94094 --- /dev/null +++ b/WatcherAgent/src/hardware/mod.rs @@ -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> { + 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, + }) + } +} diff --git a/WatcherAgent/src/hardware/network.rs b/WatcherAgent/src/hardware/network.rs new file mode 100644 index 0000000..c5bd603 --- /dev/null +++ b/WatcherAgent/src/hardware/network.rs @@ -0,0 +1,198 @@ +use std::error::Error; +use std::result::Result; +use std::time::Instant; + +#[derive(Debug)] +pub struct NetworkInfo { + pub interfaces: Option>, + pub rx_rate: Option, + pub tx_rate: Option, +} + +#[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> { + 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> { + 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> { + #[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::().unwrap_or(0); + tx_total += tx.trim().parse::().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 { + #[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![] +} diff --git a/WatcherAgent/src/library.rs b/WatcherAgent/src/library.rs deleted file mode 100644 index 5145f0a..0000000 --- a/WatcherAgent/src/library.rs +++ /dev/null @@ -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()` -*/ diff --git a/WatcherAgent/src/main.rs b/WatcherAgent/src/main.rs index 0a33294..fd1fcdc 100644 --- a/WatcherAgent/src/main.rs +++ b/WatcherAgent/src/main.rs @@ -1,940 +1,71 @@ /// 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}; +pub mod api; +pub mod hardware; +pub mod metrics; +pub mod models; -// 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 -#[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(), - } +async fn flatten( + handle: JoinHandle>>, +) -> Result> { + match handle.await { + Ok(Ok(result)) => Ok(result), + Ok(Err(err)) => Err(err), + Err(_err) => Err("handling failed".into()), } } -impl HardwareInfo { - async fn collect() -> Result> { - 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 { - 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 { - #[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> { - 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> { - // 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> { - 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, - 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> { - 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 { - 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::() { - 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::() { - 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::() { - 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::(); - // 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::(), parts[1].parse::()) - { - 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::().unwrap_or(0); - tx_total += tx.trim().parse::().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] -async fn main() -> Result<(), Box> { - let server_base_url = "http://localhost:5000"; +async fn main() -> Result<(), Box> { + 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) = match api::register_with_server(server_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 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 { - if let Err(e) = heartbeat_loop(server_base_url, &ip).await { - eprintln!("Heartbeat loop failed: {}", e); - } + let mut collector = metrics::Collector::new(server_id, ip); + collector.run(&server_url).await } }); - // 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?; + // Warte auf beide Tasks und prüfe explizit auf Fehler + let (heartbeat_handle, metrics_handle) = + tokio::try_join!(flatten(heartbeat_handle), flatten(metrics_handle))?; + + let (heartbeat, metrics) = (heartbeat_handle, metrics_handle); + println!( + "All tasks completed successfully: {:?}, {:?}.", + heartbeat, metrics + ); + + println!("All tasks completed successfully."); - heartbeat_handle.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 { - fs::read_to_string(path) - .ok()? - .trim() - .parse::() - .map(|t| t / 1000.0) - .ok() - } -} diff --git a/WatcherAgent/src/metrics.rs b/WatcherAgent/src/metrics.rs new file mode 100644 index 0000000..381141b --- /dev/null +++ b/WatcherAgent/src/metrics.rs @@ -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> { + 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> { + 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(), + }) + } +} diff --git a/WatcherAgent/src/models.rs b/WatcherAgent/src/models.rs new file mode 100644 index 0000000..32ab76c --- /dev/null +++ b/WatcherAgent/src/models.rs @@ -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, +}