using File_Watcher.Models; using System.Collections.ObjectModel; using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; namespace File_Watcher.Helpers; internal static partial class DiskInfoHelper { public record DiskDrive(string? Caption, string? DeviceID, string? FirmwareRevision, uint? Index, string? Model, string? Name, uint? Partitions, DiskPartition[]? PartitionCollection, string? SerialNumber, ulong? Size, string? Status, string? SystemName, ulong? TotalCylinders, ulong? TotalHeads) { internal static DiskDrive Get(DiskDrive diskDrive, DiskPartition[] diskPartitions, LogicalDrive[] logicalDrives) { DiskDrive result; DiskPartition[] collection = DiskPartition.Get((from l in diskPartitions where l.DiskIndex == diskDrive.Index select l).ToArray(), logicalDrives); result = new(Caption: diskDrive.Caption, DeviceID: diskDrive.DeviceID, FirmwareRevision: diskDrive.FirmwareRevision, Index: diskDrive.Index, Model: diskDrive.Model, Name: diskDrive.Name, Partitions: diskDrive.Partitions, PartitionCollection: collection, SerialNumber: diskDrive.SerialNumber, Size: diskDrive.Size, Status: diskDrive.Status, SystemName: diskDrive.SystemName, TotalCylinders: diskDrive.TotalCylinders, TotalHeads: diskDrive.TotalHeads); return result; } } [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(DiskDrive[]))] private partial class DiskDriveArraySourceGenerationContext : JsonSerializerContext { } [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Dictionary))] private partial class DictionaryStringObjectSourceGenerationContext : JsonSerializerContext { } [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(JsonElement))] private partial class JsonElementSourceGenerationContext : JsonSerializerContext { } public record DiskPartition(bool? BootPartition, string? Caption, string? Description, string? DeviceID, uint? DiskIndex, uint? Index, LogicalDrive[]? LogicalDrives, ulong? Size, ulong? StartingOffset, string? SystemName, string? Type) { internal static DiskPartition[] Get(DiskPartition[] diskPartitions, LogicalDrive[] logicalDrives) { List results = []; ulong lowerMatch; ulong upperMatch; LogicalDrive[] collection; DiskPartition diskPartition; foreach (DiskPartition p in diskPartitions) { if (p.Size is null) continue; lowerMatch = p.Size.Value - 10240; upperMatch = p.Size.Value + 10240; collection = (from l in logicalDrives where l.DriveType != 4 && l.Size > lowerMatch && l.Size < upperMatch select l).ToArray(); diskPartition = new(BootPartition: p.BootPartition, Caption: p.Caption, Description: p.Description, DeviceID: p.DeviceID, DiskIndex: p.DiskIndex, Index: p.Index, LogicalDrives: collection, Size: p.Size, StartingOffset: p.StartingOffset, SystemName: p.SystemName, Type: p.Type); results.Add(diskPartition); } return results.ToArray(); } } [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(DiskPartition[]))] private partial class DiskPartitionSourceGenerationContext : JsonSerializerContext { } public record LogicalDrive(string? Caption, string? DeviceID, uint? DriveType, string? FileSystem, ulong? FreeSpace, string? Name, ulong? PercentageFree, ulong? PercentageUsed, string? ProviderName, ulong? Size, string? SystemName, string? VolumeName, string? VolumeSerialNumber); [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(LogicalDrive[]))] private partial class LogicalDriveSourceGenerationContext : JsonSerializerContext { } public record Record(long AvailableFreeSpace, string DriveFormat, string DriveType, bool IsReady, char Name, ulong PercentageFree, ulong PercentageUsed, long TotalFreeSpace, long TotalSize, string VolumeLabel) { public static Record Get(DriveInfo driveInfo) => new(AvailableFreeSpace: driveInfo.AvailableFreeSpace, DriveFormat: driveInfo.DriveFormat, DriveType: driveInfo.DriveType.ToString(), IsReady: driveInfo.IsReady, Name: driveInfo.Name.ToUpper()[0], PercentageUsed: (ulong)(Math.Round(driveInfo.AvailableFreeSpace / (double)driveInfo.TotalSize, 2) * 100), PercentageFree: (ulong)(Math.Round((driveInfo.TotalSize - driveInfo.AvailableFreeSpace) / (double)driveInfo.TotalSize, 2) * 100), TotalFreeSpace: driveInfo.TotalFreeSpace, TotalSize: driveInfo.TotalSize, VolumeLabel: driveInfo.VolumeLabel); } [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Record[]))] private partial class RecordSourceGenerationContext : JsonSerializerContext { } private record Win32(string? DiskDrive, string? Processor, string? LogicalDisk, string? DiskPartition, string? NetworkAdapter); internal static bool WriteDiskInfo(AppSettings appSettings, ILogger logger) { string? json = WriteRecord(); ReadOnlyCollection devices = GetDrives(appSettings, logger); string yaml = GetYetAnotherMarkupLanguage(devices); ReadOnlyCollection normalized = GetNormalized(appSettings, logger, devices); string text = string.Join(Environment.NewLine, normalized); string markdown = string.Concat("# ", Environment.MachineName.ToUpper(), Environment.NewLine, Environment.NewLine, "## Normalized", Environment.NewLine, Environment.NewLine, "```log", Environment.NewLine, text, Environment.NewLine, "```", Environment.NewLine, Environment.NewLine, "## json", Environment.NewLine, Environment.NewLine, "```json", Environment.NewLine, json, Environment.NewLine, "```", Environment.NewLine, Environment.NewLine, "## yaml", Environment.NewLine, Environment.NewLine, "```yaml", Environment.NewLine, yaml, Environment.NewLine, "```", Environment.NewLine); WriteAllText(Path.Combine(appSettings.DiskInfoConfiguration.Destination, $"{Environment.MachineName.ToUpper()}.md"), markdown); return true; } private static string WriteRecord() { string result; Record record; List records = []; DriveInfo[] drives = DriveInfo.GetDrives(); foreach (DriveInfo driveInfo in drives) { record = Record.Get(driveInfo); records.Add(record); } result = JsonSerializer.Serialize(records.ToArray(), RecordSourceGenerationContext.Default.RecordArray); return result; } private static ReadOnlyCollection GetDrives(AppSettings appSettings, ILogger logger) { #pragma warning disable CA1416 List results = []; Win32 win32 = GetWin32(appSettings, logger); if (win32.DiskDrive is not null && win32.DiskPartition is not null && win32.LogicalDisk is not null) { DiskDrive diskDrive; string diskDriveJson = win32.DiskDrive[0] == '[' ? win32.DiskDrive : $"[{win32.DiskDrive}]"; string logicalDiskJson = win32.LogicalDisk[0] == '[' ? win32.LogicalDisk : $"[{win32.LogicalDisk}]"; string diskPartitionJson = win32.DiskPartition[0] == '[' ? win32.DiskPartition : $"[{win32.DiskPartition}]"; DiskDrive[] diskDrives = JsonSerializer.Deserialize(diskDriveJson, DiskDriveArraySourceGenerationContext.Default.DiskDriveArray) ?? throw new NullReferenceException(nameof(DiskDrive)); LogicalDrive[] logicalDrives = JsonSerializer.Deserialize(logicalDiskJson, LogicalDriveSourceGenerationContext.Default.LogicalDriveArray) ?? throw new NullReferenceException(nameof(LogicalDrive)); DiskPartition[] diskPartitions = JsonSerializer.Deserialize(diskPartitionJson, DiskPartitionSourceGenerationContext.Default.DiskPartitionArray) ?? throw new NullReferenceException(nameof(DiskPartition)); foreach (DiskDrive d in diskDrives) { diskDrive = DiskDrive.Get(d, diskPartitions, logicalDrives); results.Add(diskDrive); } } return results.AsReadOnly(); #pragma warning restore } private static Win32 GetWin32(AppSettings appSettings, ILogger logger) { Win32 win32; Process? process; string[] segments; string standardError; string standardOutput; string? diskDrive = null; string? processor = null; string? logicalDisk = null; string? diskPartition = null; string? networkAdapter = null; ProcessStartInfo processStartInfo; string fileName = "powershell.exe"; string date = DateTime.Now.ToString("yyyy-MM-dd"); foreach (string className in appSettings.DiskInfoConfiguration.Classes) { segments = className.Split(' '); if (segments[0].Length < 3) continue; processStartInfo = new(fileName, $"-NoProfile -ExecutionPolicy ByPass Get-WmiObject {className} | ConvertTo-JSON") { RedirectStandardError = true, RedirectStandardOutput = true, UseShellExecute = false }; try { process = Process.Start(processStartInfo); if (process is not null) { for (int j = 1; j < 45; j++) { _ = process.WaitForExit(1000); if (process.HasExited) break; } if (!process.HasExited) { logger.LogWarning($"// {segments[0]} Never exited!"); if (segments[0] is "Win32_DiskDrive" or "Win32_DiskPartition" or "Win32_LogicalDisk") break; } else { standardError = process.StandardError.ReadToEnd(); standardOutput = process.StandardOutput.ReadToEnd(); logger.LogInformation("// {className}{line}{error}{line}{line}// {output}", segments[0], Environment.NewLine, standardError, Environment.NewLine, Environment.NewLine, standardOutput); if (string.IsNullOrEmpty(standardError)) { if (segments[0] == "Win32_DiskDrive") diskDrive = standardOutput; else if (segments[0] == "Win32_DiskPartition") diskPartition = standardOutput; else if (segments[0] == "Win32_LogicalDisk") logicalDisk = standardOutput; else if (segments[0] == "Win32_NetworkAdapter") networkAdapter = standardOutput; else if (segments[0] == "Win32_Processor") processor = standardOutput; WriteAllText(Path.Combine(appSettings.DiskInfoConfiguration.Destination, $"{Environment.MachineName.ToUpper()}-{segments[0]}-{date}.json"), standardOutput); } } } process?.Dispose(); } catch (Exception ex) { logger.LogError(ex, "Error:"); } } win32 = new(DiskDrive: diskDrive, Processor: processor, LogicalDisk: logicalDisk, DiskPartition: diskPartition, NetworkAdapter: networkAdapter); return win32; } public static Dictionary DeserializeAndFlatten(string json, char? remove) { Dictionary results = []; JsonElement jsonElement = JsonSerializer.Deserialize(json, JsonElementSourceGenerationContext.Default.JsonElement); FillDictionaryFromJToken(results, jsonElement, string.Empty, remove); return results; } private static void FillDictionaryFromJToken(Dictionary results, JsonElement jsonElement, string prefix, char? remove) { switch (jsonElement.ValueKind) { case JsonValueKind.Object: foreach (JsonProperty jsonProperty in jsonElement.EnumerateObject()) { FillDictionaryFromJToken(results, jsonProperty.Value, Join(prefix, jsonProperty.Name), remove); } break; case JsonValueKind.Array: int index = 0; foreach (JsonElement value in jsonElement.EnumerateArray()) { FillDictionaryFromJToken(results, value, Join(prefix, index.ToString()), remove); index++; } break; case JsonValueKind.String: if (remove is null) { results.Add(prefix, jsonElement.ToString()); } else { results.Add(prefix, jsonElement.ToString().Replace(remove.Value, '_')); } break; case JsonValueKind.True: case JsonValueKind.False: case JsonValueKind.Number: results.Add(prefix, jsonElement.ToString()); break; } } private static string Join(string prefix, string name) => string.IsNullOrEmpty(prefix) ? name : prefix + "." + name; private static string GetYetAnotherMarkupLanguage(ReadOnlyCollection devices) { string result; List results = []; string json = JsonSerializer.Serialize(devices.ToArray(), DiskDriveArraySourceGenerationContext.Default.DiskDriveArray); Dictionary keyValuePairs = DeserializeAndFlatten(json, ':'); string[] lines = JsonSerializer.Serialize(keyValuePairs, DictionaryStringObjectSourceGenerationContext.Default.DictionaryStringObject).Split(Environment.NewLine); for (int i = 1; i < lines.Length - 1; i++) { results.Add(lines[i].Trim() .Trim(',') .Trim('"') .Replace("\": \"", ": ") .Replace("\": ", ": ")); } result = string.Join(Environment.NewLine, results); return result; } private static string GetSizeWithSuffix(ulong value) { string result; int i = 0; double displayValue = value * 1f; string[] SizeSuffixes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; while (Math.Round(displayValue / 1024f) >= 1) { displayValue /= 1024; i++; } result = string.Format("{0:n1} {1}", displayValue, SizeSuffixes[i]); return result; } private static void WriteAllText(string path, string text) { string check = !File.Exists(path) ? string.Empty : File.ReadAllText(path); if (check != text) File.WriteAllText(path, text); } private static ReadOnlyCollection GetNormalized(AppSettings appSettings, ILogger logger, ReadOnlyCollection drives) { List results = []; decimal free; decimal used; string display; int deviceCount; int offsetCount; string deviceSize; int partitionCount; results.Add("Max"); string offsetDisplay; string partitionSize; decimal devicePercent; string logicalDriveUsed; string logicalDriveSize; decimal partitionPercent; int logicalDriveFreeCount; int logicalDriveUsedCount; decimal partitionDecimalSize; string logicalDriveFreeSpace; decimal logicalDriveDecimalFreeSpace; DiskInfoConfiguration diskInfoConfiguration = appSettings.DiskInfoConfiguration; results.Add($"{new string('-', diskInfoConfiguration.Bars)} Value of Max Argument"); foreach (DiskDrive drive in drives.OrderBy(l => l.Index)) { if (drive.Size is null || drive.PartitionCollection is null) continue; devicePercent = drive.Size.Value / diskInfoConfiguration.Max; deviceSize = GetSizeWithSuffix(drive.Size.Value); results.Add($"{drive.DeviceID} - Cylinders: {drive.TotalCylinders} - {drive.Model}"); deviceCount = (int)Math.Round(devicePercent * diskInfoConfiguration.Bars); results.Add($"{new string('-', deviceCount)} {Math.Round(devicePercent, 2) * 100:000}% of Max Argument {deviceSize}"); foreach (DiskPartition partition in drive.PartitionCollection.OrderBy(l => l.Index)) { if (partition.Size is null || partition.StartingOffset is null || partition.LogicalDrives is null) continue; partitionPercent = partition.Size.Value / diskInfoConfiguration.Max; partitionSize = GetSizeWithSuffix(partition.Size.Value); partitionCount = (int)Math.Round(partitionPercent * diskInfoConfiguration.Bars); offsetCount = (int)Math.Round(partition.StartingOffset.Value / diskInfoConfiguration.Max * diskInfoConfiguration.Bars); offsetDisplay = new string('_', offsetCount); results.Add($"{partition.DeviceID} - {string.Join(", ", partition.LogicalDrives.Select(l => l.DeviceID))}"); results.Add($"{offsetDisplay}{new string('-', partitionCount)}{new string('_', diskInfoConfiguration.Bars - offsetCount - partitionCount)} {Math.Round(partitionPercent, 2) * 100:000}% of Disk {partitionSize}"); foreach (LogicalDrive logicalDrive in partition.LogicalDrives) { if (logicalDrive.Size is null || logicalDrive.FreeSpace is null) continue; if ((int)Math.Round(logicalDrive.Size.Value / diskInfoConfiguration.Max * diskInfoConfiguration.Bars) != (int)Math.Round(partition.Size.Value / diskInfoConfiguration.Max * diskInfoConfiguration.Bars)) { logger.LogWarning("logicalDrive.Size !~ partition.Size"); break; } partitionDecimalSize = partition.Size.Value; logicalDriveDecimalFreeSpace = logicalDrive.FreeSpace.Value; logicalDriveSize = GetSizeWithSuffix(logicalDrive.Size.Value); logicalDriveFreeSpace = GetSizeWithSuffix(logicalDrive.FreeSpace.Value); logicalDriveUsed = GetSizeWithSuffix(logicalDrive.Size.Value - logicalDrive.FreeSpace.Value); logicalDriveFreeCount = (int)Math.Round(logicalDriveDecimalFreeSpace / diskInfoConfiguration.Max * diskInfoConfiguration.Bars); display = string.IsNullOrEmpty(logicalDrive.DeviceID) ? "Partition" : logicalDrive.DeviceID; logicalDriveUsedCount = (int)Math.Round((partitionDecimalSize - logicalDriveDecimalFreeSpace) / diskInfoConfiguration.Max * diskInfoConfiguration.Bars); free = (int)(Math.Round(logicalDriveDecimalFreeSpace / partitionDecimalSize, 2) * 100); used = (int)(Math.Round((partitionDecimalSize - logicalDriveDecimalFreeSpace) / partitionDecimalSize, 2) * 100); results.Add($"{offsetDisplay}{new string('-', logicalDriveUsedCount)}{new string('*', logicalDriveFreeCount)}{new string('_', diskInfoConfiguration.Bars - offsetCount - logicalDriveUsedCount - logicalDriveFreeCount)} {used:000}% of [{display}] {logicalDriveUsed} ({free:000}% {logicalDriveFreeSpace} free)"); } } } return results.AsReadOnly(); } }