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;
}
}
}