using System.Diagnostics; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; namespace RackPeek.Domain.Discovery; /// Host reads that the BCL already does the same way on every platform. internal static class SystemProbeCommon { public static string Hostname() { try { return Dns.GetHostName(); } catch { return Environment.MachineName; } } public static int Cores() => Environment.ProcessorCount; public static long FallbackMemoryBytes() => GC.GetGCMemoryInfo().TotalAvailableMemoryBytes; public static IReadOnlyList Nics() { try { return NetworkInterface.GetAllNetworkInterfaces() .Select(ToFact) .Where(n => n.Ipv4 != null) .ToList(); } catch { return []; } } private static NicFact ToFact(NetworkInterface nic) { IPInterfaceProperties properties = nic.GetIPProperties(); var ipv4 = properties.UnicastAddresses .FirstOrDefault(a => a.Address.AddressFamily == AddressFamily.InterNetwork) ?.Address.ToString(); var hasGateway = properties.GatewayAddresses .Any(g => g.Address.AddressFamily == AddressFamily.InterNetwork && !g.Address.Equals(IPAddress.Any)); return new NicFact( nic.Name, nic.OperationalStatus == OperationalStatus.Up, nic.NetworkInterfaceType == NetworkInterfaceType.Loopback, hasGateway, ipv4, FormatMac(nic)); } /// Lowercase colon-separated, matching what ARP tables report — or null. private static string? FormatMac(NetworkInterface nic) { try { var bytes = nic.GetPhysicalAddress().GetAddressBytes(); return bytes.Length == 6 ? string.Join(':', bytes.Select(b => b.ToString("x2"))) : null; } catch { return null; } } /// Reads a file, returning null for anything unreadable rather than throwing. public static async Task TryReadFileAsync(string path, CancellationToken cancellationToken) { try { return File.Exists(path) ? await File.ReadAllTextAsync(path, cancellationToken) : null; } catch { return null; } } /// /// Runs a command and returns stdout, or null if it cannot be run, fails, or /// takes longer than a few seconds — a hung probe must not hang a timer-driven /// rpk discover forever. Stderr is drained concurrently so a chatty child /// cannot deadlock on a full pipe. /// public static async Task TryRunAsync( string fileName, string arguments, CancellationToken cancellationToken) { try { using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeout.CancelAfter(TimeSpan.FromSeconds(10)); using var process = new Process { StartInfo = new ProcessStartInfo { FileName = fileName, Arguments = arguments, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true } }; if (!process.Start()) return null; try { Task stdout = process.StandardOutput.ReadToEndAsync(timeout.Token); Task stderr = process.StandardError.ReadToEndAsync(timeout.Token); await process.WaitForExitAsync(timeout.Token); await stderr; return process.ExitCode == 0 ? (await stdout).Trim() : null; } catch (OperationCanceledException) { try { process.Kill(true); } catch { // It may have exited in the meantime; nothing left to do. } return null; } } catch { return null; } } }