Created
October 2, 2026 00:07
-
-
Save sean-m/3bf2b597cec605e49dcf137071eeda14 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Reports drive statistics and SSD endurance for physical disks attached to the system. | |
| .DESCRIPTION | |
| Self-contained: the .NET implementation (identical to DriveStats.cs) is compiled in-process via Add-Type. | |
| Queries \\.\PhysicalDriveN using IOCTL_STORAGE_QUERY_PROPERTY (device descriptor, seek penalty, temperature, | |
| NVMe SMART/Health log), IOCTL_STORAGE_PREDICT_FAILURE and SMART_RCV_DRIVE_DATA (ATA SMART attributes). | |
| Works on Windows PowerShell 5.1 and PowerShell 7+. Run elevated for full SMART / NVMe health data. | |
| .PARAMETER DiskNumber | |
| One or more physical disk numbers to query. Defaults to all disks. | |
| .PARAMETER Attributes | |
| Output the raw ATA SMART attribute table instead of the disk summary objects. | |
| .EXAMPLE | |
| .\DriveStats.ps1 | |
| .EXAMPLE | |
| .\DriveStats.ps1 -DiskNumber 0 | Format-List * | |
| .EXAMPLE | |
| .\DriveStats.ps1 -Attributes | Format-Table | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [int[]]$DiskNumber, | |
| [switch]$Attributes | |
| ) | |
| $source = @' | |
| // Core drive statistics / endurance reader. | |
| // Written in C# 5 syntax so the same source can be embedded in Windows PowerShell 5.1 via Add-Type. | |
| using System; | |
| using System.Collections.Generic; | |
| using System.ComponentModel; | |
| using System.Runtime.InteropServices; | |
| using System.Text; | |
| using Microsoft.Win32.SafeHandles; | |
| namespace DriveStats | |
| { | |
| public sealed class SmartAttribute | |
| { | |
| public byte Id { get; set; } | |
| public string Name { get; set; } | |
| public ushort Flags { get; set; } | |
| public byte Current { get; set; } | |
| public byte Worst { get; set; } | |
| public ulong Raw { get; set; } | |
| public override string ToString() | |
| { | |
| return string.Format("0x{0:X2} {1}: current={2} worst={3} raw={4}", Id, Name, Current, Worst, Raw); | |
| } | |
| } | |
| public sealed class DiskStats | |
| { | |
| public DiskStats() | |
| { | |
| SmartAttributes = new List<SmartAttribute>(); | |
| Notes = new List<string>(); | |
| MediaType = "Unknown"; | |
| } | |
| public int DiskNumber { get; set; } | |
| public string DevicePath { get; set; } | |
| public string Vendor { get; set; } | |
| public string Model { get; set; } | |
| public string SerialNumber { get; set; } | |
| public string Firmware { get; set; } | |
| public string BusType { get; set; } | |
| public bool Removable { get; set; } | |
| public ulong? SizeBytes { get; set; } | |
| public string MediaType { get; set; } | |
| // Endurance / health | |
| public int? RemainingLifePercent { get; set; } | |
| public int? PercentageUsed { get; set; } | |
| public string EnduranceSource { get; set; } | |
| public bool? PredictFailure { get; set; } | |
| public byte? CriticalWarning { get; set; } | |
| public int? AvailableSparePercent { get; set; } | |
| public int? AvailableSpareThresholdPercent { get; set; } | |
| public int? TemperatureC { get; set; } | |
| // Usage counters | |
| public ulong? PowerOnHours { get; set; } | |
| public ulong? PowerCycles { get; set; } | |
| public ulong? UnsafeShutdowns { get; set; } | |
| public ulong? MediaErrors { get; set; } | |
| public ulong? ReallocatedSectors { get; set; } | |
| public ulong? PendingSectors { get; set; } | |
| public ulong? DataReadBytes { get; set; } | |
| public ulong? DataWrittenBytes { get; set; } | |
| public bool FullAccess { get; set; } | |
| public List<SmartAttribute> SmartAttributes { get; private set; } | |
| public List<string> Notes { get; private set; } | |
| } | |
| internal static class NativeMethods | |
| { | |
| public const uint GENERIC_READ = 0x80000000; | |
| public const uint GENERIC_WRITE = 0x40000000; | |
| public const uint FILE_SHARE_READ = 0x00000001; | |
| public const uint FILE_SHARE_WRITE = 0x00000002; | |
| public const uint OPEN_EXISTING = 3; | |
| public const int ERROR_FILE_NOT_FOUND = 2; | |
| public const int ERROR_PATH_NOT_FOUND = 3; | |
| public const int ERROR_ACCESS_DENIED = 5; | |
| public const uint IOCTL_STORAGE_QUERY_PROPERTY = 0x002D1400; | |
| public const uint IOCTL_STORAGE_PREDICT_FAILURE = 0x002D1100; | |
| public const uint IOCTL_DISK_GET_DRIVE_GEOMETRY_EX = 0x000700A0; | |
| public const uint SMART_RCV_DRIVE_DATA = 0x0007C088; | |
| // STORAGE_PROPERTY_ID | |
| public const int StorageDeviceProperty = 0; | |
| public const int StorageDeviceSeekPenaltyProperty = 7; | |
| public const int StorageAdapterProtocolSpecificProperty = 49; | |
| public const int StorageDeviceProtocolSpecificProperty = 50; | |
| public const int StorageDeviceTemperatureProperty = 52; | |
| // STORAGE_PROTOCOL_TYPE / STORAGE_PROTOCOL_NVME_DATA_TYPE | |
| public const int ProtocolTypeNvme = 3; | |
| public const int NVMeDataTypeLogPage = 2; | |
| public const int NVME_LOG_PAGE_HEALTH_INFO = 2; | |
| public const int NVME_LOG_PAGE_SIZE = 512; | |
| // ATA SMART | |
| public const byte SMART_CMD = 0xB0; | |
| public const byte READ_ATTRIBUTES = 0xD0; | |
| public const byte SMART_CYL_LOW = 0x4F; | |
| public const byte SMART_CYL_HI = 0xC2; | |
| [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | |
| public static extern SafeFileHandle CreateFileW( | |
| string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, | |
| uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); | |
| [DllImport("kernel32.dll", SetLastError = true)] | |
| [return: MarshalAs(UnmanagedType.Bool)] | |
| public static extern bool DeviceIoControl( | |
| SafeFileHandle hDevice, uint dwIoControlCode, byte[] lpInBuffer, int nInBufferSize, | |
| byte[] lpOutBuffer, int nOutBufferSize, out int lpBytesReturned, IntPtr lpOverlapped); | |
| public static SafeFileHandle Open(string path, uint access) | |
| { | |
| return CreateFileW(path, access, FILE_SHARE_READ | FILE_SHARE_WRITE, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero); | |
| } | |
| public static bool Ioctl(SafeFileHandle h, uint code, byte[] input, byte[] output, out int returned, out int error) | |
| { | |
| bool ok = DeviceIoControl(h, code, input, input == null ? 0 : input.Length, | |
| output, output == null ? 0 : output.Length, out returned, IntPtr.Zero); | |
| error = ok ? 0 : Marshal.GetLastWin32Error(); | |
| return ok; | |
| } | |
| // STORAGE_PROPERTY_QUERY { PropertyId, QueryType = PropertyStandardQuery, AdditionalParameters[1] } (12 bytes padded) | |
| public static byte[] QueryProperty(SafeFileHandle h, int propertyId, int outSize, out int returned) | |
| { | |
| byte[] input = new byte[12]; | |
| WriteInt32(input, 0, propertyId); | |
| byte[] output = new byte[outSize]; | |
| int error; | |
| if (!Ioctl(h, IOCTL_STORAGE_QUERY_PROPERTY, input, output, out returned, out error)) return null; | |
| return output; | |
| } | |
| public static void WriteInt32(byte[] b, int offset, int value) | |
| { | |
| Buffer.BlockCopy(BitConverter.GetBytes(value), 0, b, offset, 4); | |
| } | |
| public static ulong ReadUInt48(byte[] b, int offset) | |
| { | |
| ulong v = 0; | |
| for (int i = 5; i >= 0; i--) v = (v << 8) | b[offset + i]; | |
| return v; | |
| } | |
| // NVMe counters are 128-bit little-endian; saturate to ulong. | |
| public static ulong ReadUInt128Saturated(byte[] b, int offset) | |
| { | |
| return BitConverter.ToUInt64(b, offset + 8) != 0 ? ulong.MaxValue : BitConverter.ToUInt64(b, offset); | |
| } | |
| public static string ReadAnsiString(byte[] b, int offset) | |
| { | |
| if (offset <= 0 || offset >= b.Length) return null; | |
| int end = offset; | |
| while (end < b.Length && b[end] != 0) end++; | |
| string s = Encoding.ASCII.GetString(b, offset, end - offset).Trim(); | |
| return s.Length == 0 ? null : s; | |
| } | |
| } | |
| public static class DriveStatsReader | |
| { | |
| private static readonly string[] BusTypeNames = | |
| { | |
| "Unknown", "SCSI", "ATAPI", "ATA", "1394", "SSA", "FibreChannel", "USB", "RAID", "iSCSI", | |
| "SAS", "SATA", "SD", "MMC", "Virtual", "FileBackedVirtual", "Spaces", "NVMe", "SCM", "UFS" | |
| }; | |
| private static readonly Dictionary<byte, string> AttributeNames = new Dictionary<byte, string> | |
| { | |
| { 0x01, "Raw Read Error Rate" }, { 0x05, "Reallocated Sectors Count" }, { 0x09, "Power-On Hours" }, | |
| { 0x0C, "Power Cycle Count" }, { 0xA9, "Remaining Life" }, { 0xAA, "Available Reserved Space" }, | |
| { 0xAB, "Program Fail Count" }, { 0xAC, "Erase Fail Count" }, { 0xAD, "Wear Leveling Count" }, | |
| { 0xAE, "Unexpected Power Loss Count" }, { 0xB1, "Wear Range Delta / Wear Leveling" }, | |
| { 0xB3, "Used Reserved Block Count" }, { 0xB4, "Unused Reserved Block Count" }, | |
| { 0xB5, "Program Fail Count" }, { 0xB6, "Erase Fail Count" }, { 0xBB, "Reported Uncorrectable Errors" }, | |
| { 0xBE, "Airflow Temperature" }, { 0xC0, "Power-off Retract / Unsafe Shutdown Count" }, | |
| { 0xC2, "Temperature" }, { 0xC3, "Hardware ECC Recovered" }, { 0xC4, "Reallocation Event Count" }, | |
| { 0xC5, "Current Pending Sector Count" }, { 0xC6, "Offline Uncorrectable" }, { 0xC7, "UDMA CRC Error Count" }, | |
| { 0xCA, "Percent Lifetime Remaining" }, { 0xE7, "SSD Life Left" }, { 0xE8, "Available Reserved Space" }, | |
| { 0xE9, "Media Wearout Indicator" }, { 0xF1, "Total LBAs Written" }, { 0xF2, "Total LBAs Read" } | |
| }; | |
| // Attributes whose normalized value is commonly "% life remaining", in priority order. | |
| private static readonly byte[] EnduranceAttributeIds = { 0xE7, 0xA9, 0xE9, 0xCA, 0xB1, 0xAD }; | |
| public static List<DiskStats> GetAll() | |
| { | |
| return GetAll(64); | |
| } | |
| public static List<DiskStats> GetAll(int maxDiskNumber) | |
| { | |
| List<DiskStats> disks = new List<DiskStats>(); | |
| for (int i = 0; i < maxDiskNumber; i++) | |
| { | |
| DiskStats d = Get(i); | |
| if (d != null) disks.Add(d); | |
| } | |
| return disks; | |
| } | |
| /// <summary>Returns stats for \\.\PhysicalDriveN, or null if that disk does not exist.</summary> | |
| public static DiskStats Get(int diskNumber) | |
| { | |
| string path = @"\\.\PhysicalDrive" + diskNumber; | |
| DiskStats d = new DiskStats(); | |
| d.DiskNumber = diskNumber; | |
| d.DevicePath = path; | |
| // Zero-access handle: enough for descriptor/property queries without elevation. | |
| using (SafeFileHandle query = NativeMethods.Open(path, 0)) | |
| { | |
| if (query.IsInvalid) | |
| { | |
| int err = Marshal.GetLastWin32Error(); | |
| if (err == NativeMethods.ERROR_FILE_NOT_FOUND || err == NativeMethods.ERROR_PATH_NOT_FOUND) return null; | |
| d.Notes.Add("Unable to open device: " + new Win32Exception(err).Message); | |
| return d; | |
| } | |
| // Read/write handle: required for SMART pass-through and usually for NVMe log pages. Needs admin. | |
| SafeFileHandle rw = NativeMethods.Open(path, NativeMethods.GENERIC_READ | NativeMethods.GENERIC_WRITE); | |
| if (rw.IsInvalid) | |
| { | |
| rw.Dispose(); | |
| rw = null; | |
| } | |
| d.FullAccess = rw != null; | |
| try | |
| { | |
| Populate(d, query, rw); | |
| } | |
| finally | |
| { | |
| if (rw != null) rw.Dispose(); | |
| } | |
| } | |
| return d; | |
| } | |
| private static void Populate(DiskStats d, SafeFileHandle query, SafeFileHandle rw) | |
| { | |
| ReadDeviceDescriptor(d, query); | |
| ReadSize(d, query); | |
| ReadSeekPenalty(d, query); | |
| byte[] predictData = ReadPredictFailure(d, query); | |
| if (d.BusType == "NVMe") | |
| { | |
| d.MediaType = "SSD"; | |
| bool ok = (rw != null && ReadNvmeHealth(d, rw)) || ReadNvmeHealth(d, query); | |
| if (!ok) | |
| { | |
| d.Notes.Add(rw == null | |
| ? "NVMe health log unavailable; run elevated (as Administrator)." | |
| : "NVMe health log query not supported by this driver/device."); | |
| } | |
| } | |
| else | |
| { | |
| byte[] smart = rw != null ? ReadSmartAttributes(rw) : null; | |
| if (smart == null && HasAttributes(predictData)) smart = predictData; | |
| if (smart != null) | |
| { | |
| ParseSmartAttributes(d, smart, 0); | |
| ApplyAtaAttributes(d); | |
| } | |
| else | |
| { | |
| d.Notes.Add(rw == null | |
| ? "SMART data unavailable; run elevated (as Administrator)." | |
| : "SMART data not supported by this device/bridge."); | |
| } | |
| } | |
| if (!d.TemperatureC.HasValue) ReadTemperatureProperty(d, query); | |
| } | |
| private static void ReadDeviceDescriptor(DiskStats d, SafeFileHandle h) | |
| { | |
| int returned; | |
| byte[] b = NativeMethods.QueryProperty(h, NativeMethods.StorageDeviceProperty, 1024, out returned); | |
| if (b == null || returned < 36) return; | |
| // STORAGE_DEVICE_DESCRIPTOR | |
| d.Removable = b[10] != 0; | |
| d.Vendor = NativeMethods.ReadAnsiString(b, BitConverter.ToInt32(b, 12)); | |
| d.Model = NativeMethods.ReadAnsiString(b, BitConverter.ToInt32(b, 16)); | |
| d.Firmware = NativeMethods.ReadAnsiString(b, BitConverter.ToInt32(b, 20)); | |
| d.SerialNumber = NativeMethods.ReadAnsiString(b, BitConverter.ToInt32(b, 24)); | |
| int bus = BitConverter.ToInt32(b, 28); | |
| d.BusType = bus >= 0 && bus < BusTypeNames.Length ? BusTypeNames[bus] : "Bus" + bus; | |
| } | |
| private static void ReadSize(DiskStats d, SafeFileHandle h) | |
| { | |
| // DISK_GEOMETRY_EX { DISK_GEOMETRY Geometry (24 bytes); LARGE_INTEGER DiskSize; ... } | |
| byte[] b = new byte[256]; | |
| int returned, error; | |
| if (NativeMethods.Ioctl(h, NativeMethods.IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, null, b, out returned, out error) && returned >= 32) | |
| d.SizeBytes = (ulong)BitConverter.ToInt64(b, 24); | |
| } | |
| private static void ReadSeekPenalty(DiskStats d, SafeFileHandle h) | |
| { | |
| int returned; | |
| byte[] b = NativeMethods.QueryProperty(h, NativeMethods.StorageDeviceSeekPenaltyProperty, 12, out returned); | |
| if (b != null && returned >= 9) d.MediaType = b[8] != 0 ? "HDD" : "SSD"; | |
| } | |
| private static byte[] ReadPredictFailure(DiskStats d, SafeFileHandle h) | |
| { | |
| // STORAGE_PREDICT_FAILURE { ULONG PredictFailure; UCHAR VendorSpecific[512]; } | |
| byte[] b = new byte[516]; | |
| int returned, error; | |
| if (!NativeMethods.Ioctl(h, NativeMethods.IOCTL_STORAGE_PREDICT_FAILURE, null, b, out returned, out error) || returned < 4) | |
| return null; | |
| d.PredictFailure = BitConverter.ToUInt32(b, 0) != 0; | |
| if (returned < 516) return null; | |
| byte[] vendor = new byte[512]; | |
| Buffer.BlockCopy(b, 4, vendor, 0, 512); | |
| return vendor; | |
| } | |
| private static byte[] ReadSmartAttributes(SafeFileHandle h) | |
| { | |
| // SENDCMDINPARAMS without the trailing bBuffer[1] (32 bytes) | |
| byte[] input = new byte[32]; | |
| NativeMethods.WriteInt32(input, 0, 512); // cBufferSize | |
| input[4] = NativeMethods.READ_ATTRIBUTES; // IDEREGS.bFeaturesReg | |
| input[5] = 1; // bSectorCountReg | |
| input[6] = 1; // bSectorNumberReg | |
| input[7] = NativeMethods.SMART_CYL_LOW; // bCylLowReg | |
| input[8] = NativeMethods.SMART_CYL_HI; // bCylHighReg | |
| input[9] = 0xA0; // bDriveHeadReg | |
| input[10] = NativeMethods.SMART_CMD; // bCommandReg | |
| // SENDCMDOUTPARAMS: cBufferSize(4) + DRIVERSTATUS(12) + 512 data bytes | |
| byte[] output = new byte[4 + 12 + 512]; | |
| int returned, error; | |
| if (!NativeMethods.Ioctl(h, NativeMethods.SMART_RCV_DRIVE_DATA, input, output, out returned, out error)) return null; | |
| if (returned < output.Length || output[4] != 0) return null; // bDriverError | |
| byte[] data = new byte[512]; | |
| Buffer.BlockCopy(output, 16, data, 0, 512); | |
| return HasAttributes(data) ? data : null; | |
| } | |
| private static bool HasAttributes(byte[] data) | |
| { | |
| if (data == null || data.Length < 362) return false; | |
| for (int i = 0; i < 30; i++) | |
| if (data[2 + i * 12] != 0) return true; | |
| return false; | |
| } | |
| private static void ParseSmartAttributes(DiskStats d, byte[] data, int offset) | |
| { | |
| // Revision (2 bytes) followed by 30 x 12-byte entries: | |
| // Id(1) Flags(2) Current(1) Worst(1) Raw(6) Reserved(1) | |
| for (int i = 0; i < 30; i++) | |
| { | |
| int p = offset + 2 + i * 12; | |
| byte id = data[p]; | |
| if (id == 0) continue; | |
| SmartAttribute a = new SmartAttribute(); | |
| a.Id = id; | |
| a.Flags = BitConverter.ToUInt16(data, p + 1); | |
| a.Current = data[p + 3]; | |
| a.Worst = data[p + 4]; | |
| a.Raw = NativeMethods.ReadUInt48(data, p + 5); | |
| string name; | |
| a.Name = AttributeNames.TryGetValue(id, out name) ? name : "Vendor Specific"; | |
| d.SmartAttributes.Add(a); | |
| } | |
| } | |
| private static SmartAttribute FindAttribute(DiskStats d, byte id) | |
| { | |
| foreach (SmartAttribute a in d.SmartAttributes) | |
| if (a.Id == id) return a; | |
| return null; | |
| } | |
| private static void ApplyAtaAttributes(DiskStats d) | |
| { | |
| if (d.MediaType != "HDD") | |
| { | |
| foreach (byte id in EnduranceAttributeIds) | |
| { | |
| SmartAttribute a = FindAttribute(d, id); | |
| if (a == null || a.Current > 100) continue; | |
| d.RemainingLifePercent = a.Current; | |
| d.PercentageUsed = 100 - a.Current; | |
| d.EnduranceSource = string.Format("SMART 0x{0:X2} ({1}) normalized value", a.Id, a.Name); | |
| break; | |
| } | |
| if (!d.RemainingLifePercent.HasValue && d.MediaType == "SSD") | |
| d.Notes.Add("No recognized SSD endurance attribute; see SmartAttributes for vendor-specific values."); | |
| } | |
| SmartAttribute t = FindAttribute(d, 0xC2) ?? FindAttribute(d, 0xBE); | |
| if (t != null) | |
| { | |
| int c = (int)(t.Raw & 0xFF); | |
| if (c > 0 && c < 150) d.TemperatureC = c; | |
| } | |
| SmartAttribute x; | |
| if ((x = FindAttribute(d, 0x09)) != null) d.PowerOnHours = x.Raw & 0xFFFFFFFF; | |
| if ((x = FindAttribute(d, 0x0C)) != null) d.PowerCycles = x.Raw; | |
| if ((x = FindAttribute(d, 0xAE) ?? FindAttribute(d, 0xC0)) != null) d.UnsafeShutdowns = x.Raw & 0xFFFFFFFF; | |
| if ((x = FindAttribute(d, 0x05)) != null) d.ReallocatedSectors = x.Raw & 0xFFFFFFFF; | |
| if ((x = FindAttribute(d, 0xC5)) != null) d.PendingSectors = x.Raw & 0xFFFFFFFF; | |
| if ((x = FindAttribute(d, 0xBB) ?? FindAttribute(d, 0xC6)) != null) d.MediaErrors = x.Raw & 0xFFFFFFFF; | |
| } | |
| private static bool ReadNvmeHealth(DiskStats d, SafeFileHandle h) | |
| { | |
| return ReadNvmeHealth(d, h, NativeMethods.StorageDeviceProtocolSpecificProperty) | |
| || ReadNvmeHealth(d, h, NativeMethods.StorageAdapterProtocolSpecificProperty); | |
| } | |
| private static bool ReadNvmeHealth(DiskStats d, SafeFileHandle h, int propertyId) | |
| { | |
| const int queryHeader = 8; // STORAGE_PROPERTY_QUERY up to AdditionalParameters | |
| const int protoSize = 40; // sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA) | |
| byte[] buf = new byte[queryHeader + protoSize + NativeMethods.NVME_LOG_PAGE_SIZE]; | |
| NativeMethods.WriteInt32(buf, 0, propertyId); | |
| NativeMethods.WriteInt32(buf, 4, 0); // PropertyStandardQuery | |
| NativeMethods.WriteInt32(buf, 8, NativeMethods.ProtocolTypeNvme); // ProtocolType | |
| NativeMethods.WriteInt32(buf, 12, NativeMethods.NVMeDataTypeLogPage); // DataType | |
| NativeMethods.WriteInt32(buf, 16, NativeMethods.NVME_LOG_PAGE_HEALTH_INFO);// ProtocolDataRequestValue | |
| NativeMethods.WriteInt32(buf, 20, 0); // ProtocolDataRequestSubValue | |
| NativeMethods.WriteInt32(buf, 24, protoSize); // ProtocolDataOffset | |
| NativeMethods.WriteInt32(buf, 28, NativeMethods.NVME_LOG_PAGE_SIZE); // ProtocolDataLength | |
| int returned, error; | |
| if (!NativeMethods.Ioctl(h, NativeMethods.IOCTL_STORAGE_QUERY_PROPERTY, buf, buf, out returned, out error)) return false; | |
| // Output: STORAGE_PROTOCOL_DATA_DESCRIPTOR { Version; Size; STORAGE_PROTOCOL_SPECIFIC_DATA ProtocolSpecificData; } | |
| int version = BitConverter.ToInt32(buf, 0); | |
| int size = BitConverter.ToInt32(buf, 4); | |
| if (version != 48 || size != 48) return false; | |
| int dataOffset = BitConverter.ToInt32(buf, 8 + 16); | |
| int dataLength = BitConverter.ToInt32(buf, 8 + 20); | |
| int p = 8 + dataOffset; | |
| if (dataLength < NativeMethods.NVME_LOG_PAGE_SIZE || p + NativeMethods.NVME_LOG_PAGE_SIZE > buf.Length) return false; | |
| // NVMe SMART / Health Information log page (Log Identifier 02h) | |
| d.CriticalWarning = buf[p + 0]; | |
| int kelvin = BitConverter.ToUInt16(buf, p + 1); | |
| if (kelvin > 0) d.TemperatureC = kelvin - 273; | |
| d.AvailableSparePercent = buf[p + 3]; | |
| d.AvailableSpareThresholdPercent = buf[p + 4]; | |
| d.PercentageUsed = buf[p + 5]; // may exceed 100 | |
| d.RemainingLifePercent = Math.Max(0, 100 - buf[p + 5]); | |
| d.EnduranceSource = "NVMe SMART/Health log: Percentage Used"; | |
| const ulong dataUnit = 512000; // 1000 x 512-byte units | |
| ulong read = NativeMethods.ReadUInt128Saturated(buf, p + 32); | |
| ulong written = NativeMethods.ReadUInt128Saturated(buf, p + 48); | |
| d.DataReadBytes = read > ulong.MaxValue / dataUnit ? ulong.MaxValue : read * dataUnit; | |
| d.DataWrittenBytes = written > ulong.MaxValue / dataUnit ? ulong.MaxValue : written * dataUnit; | |
| d.PowerCycles = NativeMethods.ReadUInt128Saturated(buf, p + 112); | |
| d.PowerOnHours = NativeMethods.ReadUInt128Saturated(buf, p + 128); | |
| d.UnsafeShutdowns = NativeMethods.ReadUInt128Saturated(buf, p + 144); | |
| d.MediaErrors = NativeMethods.ReadUInt128Saturated(buf, p + 160); | |
| return true; | |
| } | |
| private static void ReadTemperatureProperty(DiskStats d, SafeFileHandle h) | |
| { | |
| // STORAGE_TEMPERATURE_DATA_DESCRIPTOR: 24-byte header (InfoCount at 12) then | |
| // STORAGE_TEMPERATURE_INFO[] (16 bytes each, Temperature SHORT at +2) | |
| int returned; | |
| byte[] b = NativeMethods.QueryProperty(h, NativeMethods.StorageDeviceTemperatureProperty, 24 + 16 * 8, out returned); | |
| if (b == null || returned < 40 || BitConverter.ToUInt16(b, 12) == 0) return; | |
| int c = BitConverter.ToInt16(b, 24 + 2); | |
| if (c > 0 && c < 150) d.TemperatureC = c; | |
| } | |
| } | |
| } | |
| '@ | |
| if (-not ('DriveStats.DriveStatsReader' -as [type])) { | |
| Add-Type -TypeDefinition $source -Language CSharp | |
| } | |
| Update-TypeData -TypeName DriveStats.DiskStats -Force -DefaultDisplayPropertySet ` | |
| DiskNumber, Model, BusType, MediaType, SizeBytes, RemainingLifePercent, TemperatureC, PowerOnHours | |
| $disks = @(if ($PSBoundParameters.ContainsKey('DiskNumber')) { | |
| foreach ($n in $DiskNumber) { | |
| $d = [DriveStats.DriveStatsReader]::Get($n) | |
| if ($d) { $d } else { Write-Error "PhysicalDrive$n not found." } | |
| } | |
| } else { | |
| [DriveStats.DriveStatsReader]::GetAll() | |
| }) | |
| if ($disks | Where-Object { -not $_.FullAccess }) { | |
| Write-Warning 'Some data requires elevation. Re-run from an Administrator prompt for full SMART/NVMe data.' | |
| } | |
| if ($Attributes) { | |
| foreach ($d in $disks) { | |
| foreach ($a in $d.SmartAttributes) { | |
| [pscustomobject]@{ | |
| DiskNumber = $d.DiskNumber | |
| Id = '0x{0:X2}' -f $a.Id | |
| Name = $a.Name | |
| Current = $a.Current | |
| Worst = $a.Worst | |
| Raw = $a.Raw | |
| } | |
| } | |
| } | |
| } else { | |
| $disks | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment