using System.ComponentModel; using System.Runtime.InteropServices; namespace MoonlightServers.Daemon.Helpers; public partial class SystemMetrics { public record DiskInfo( string MountPoint, string Device, string FileSystem, long TotalBytes, long UsedBytes, long FreeBytes, double UsedPercent, long InodesTotal, long InodesUsed, long InodesFree, double InodesUsedPercent ); private static readonly HashSet IgnoredFileSystems = new(StringComparer.OrdinalIgnoreCase) { "overlay", "aufs", // Container image layers "tmpfs", "devtmpfs", "ramfs", // RAM-backed virtual filesystems "sysfs", "proc", "devpts", // Kernel virtual filesystems "cgroup", "cgroup2", "pstore", "securityfs", "debugfs", "tracefs", "mqueue", "hugetlbfs", "fusectl", "configfs", "binfmt_misc", "nsfs", "rpc_pipefs", "squashfs", // Snap package loop mounts }; private static readonly string[] IgnoredMountPrefixes = { "/sys", "/proc", "/dev", "/run/docker", "/var/lib/docker", "/var/lib/containers", "/boot", "/boot/efi" }; private static async Task> ReadDisksAsync() { var lines = await File.ReadAllLinesAsync("/proc/mounts"); var results = new List(); foreach (var line in lines) { var parts = line.Split(' '); if (parts.Length < 4) continue; var device = parts[0]; var mountPoint = UnescapeOctal(parts[1]); var fsType = parts[2]; if (IgnoredFileSystems.Contains(fsType)) continue; if (IgnoredMountPrefixes.Any(p => mountPoint.StartsWith(p, StringComparison.Ordinal))) continue; if (device == "none" || device.StartsWith("//", StringComparison.Ordinal)) continue; var disk = ReadSingleDisk(device, mountPoint, fsType); if (disk is not null) results.Add(disk); } return results .GroupBy(d => d.Device) .Select(g => g.OrderBy(d => d.MountPoint.Length).First()) .OrderBy(d => d.MountPoint) .ToList(); } private static DiskInfo? ReadSingleDisk(string device, string mountPoint, string fsType) { if (NativeMethods.StatVfs(mountPoint, out var st) != 0) { var errno = Marshal.GetLastPInvokeError(); Console.WriteLine($"statvfs({mountPoint}) failed: errno {errno} ({new Win32Exception(errno).Message})"); return null; } var blockSize = (long)st.frsize; var totalBytes = (long)st.blocks * blockSize; var freeBytes = (long)st.bavail * blockSize; if (totalBytes <= 0) return null; var usedBytes = totalBytes - ((long)st.bfree * blockSize); var usedPct = Math.Round((double)usedBytes / totalBytes * 100.0, 2); long inodesTotal, inodesUsed, inodesFree; double inodePct; if (st.files == 0) { // Filesystem doesn't expose inode counts (FAT, exFAT, NTFS via ntfs-3g, etc.) inodesTotal = inodesUsed = inodesFree = -1; inodePct = -1.0; } else { inodesTotal = (long)st.files; inodesFree = (long)st.ffree; inodesUsed = inodesTotal - inodesFree; inodePct = Math.Round((double)inodesUsed / inodesTotal * 100.0, 2); } return new DiskInfo( MountPoint: mountPoint, Device: device, FileSystem: fsType, TotalBytes: totalBytes, UsedBytes: usedBytes, FreeBytes: freeBytes, UsedPercent: usedPct, InodesTotal: inodesTotal, InodesUsed: inodesUsed, InodesFree: inodesFree, InodesUsedPercent: inodePct ); } private static string UnescapeOctal(string s) { if (!s.Contains('\\')) return s; var sb = new System.Text.StringBuilder(s.Length); for (var i = 0; i < s.Length; i++) { if (s[i] == '\\' && i + 3 < s.Length && s[i + 1] is >= '0' and <= '7' && s[i + 2] is >= '0' and <= '7' && s[i + 3] is >= '0' and <= '7') { sb.Append((char)((s[i + 1] - '0') * 64 + (s[i + 2] - '0') * 8 + (s[i + 3] - '0'))); i += 3; } else { sb.Append(s[i]); } } return sb.ToString(); } }