|   |  | 1 |  | using System; | 
|   |  | 2 |  | using System.Collections.Generic; | 
|   |  | 3 |  | using System.ComponentModel; | 
|   |  | 4 |  | using System.Diagnostics; | 
|   |  | 5 |  | using System.Diagnostics.CodeAnalysis; | 
|   |  | 6 |  | using System.IO; | 
|   |  | 7 |  | using System.Linq; | 
|   |  | 8 |  | using System.Runtime.CompilerServices; | 
|   |  | 9 |  | using System.Runtime.InteropServices; | 
|   |  | 10 |  | using System.Security.Principal; | 
|   |  | 11 |  | using System.Threading; | 
|   |  | 12 |  |  | 
|   |  | 13 |  | using static LockCheck.Windows.NativeMethods; | 
|   |  | 14 |  |  | 
|   |  | 15 |  | namespace LockCheck.Windows; | 
|   |  | 16 |  |  | 
|   |  | 17 |  | internal static class NtDll | 
|   |  | 18 |  | { | 
|   |  | 19 |  |     internal class PseudoPeb | 
|   |  | 20 |  |     { | 
|   | 2 | 21 |  |         public PseudoPeb(SYSTEM_PROCESS_INFORMATION pi, string? executable, string? systemAccount, string? processName = | 
|   |  | 22 |  |         { | 
|   | 2 | 23 |  |             ProcessId = pi.UniqueProcessId.ToInt32(); | 
|   | 2 | 24 |  |             ParentProcessId = pi.InheritedFromUniqueProcessId.ToInt32(); | 
|   | 2 | 25 |  |             ExecutableFullPath = executable; | 
|   | 2 | 26 |  |             ProcessName = pi.NamePtr != IntPtr.Zero ? Marshal.PtrToStringUni(pi.NamePtr) : processName; | 
|   | 2 | 27 |  |             Owner = systemAccount; | 
|   | 2 | 28 |  |             ProcessSequenceNumber = processSequenceNumber; | 
|   | 2 | 29 |  |             StartTime = DateTime.FromFileTime(pi.CreateTime); | 
|   | 2 | 30 |  |         } | 
|   |  | 31 |  |  | 
|   | 2 | 32 |  |         public int ProcessId { get; private set; } | 
|   | 2 | 33 |  |         public int? ParentProcessId { get; private set; } | 
|   | 2 | 34 |  |         public string? ProcessName { get; private set; } | 
|   | 2 | 35 |  |         public string? ExecutableFullPath { get; private set; } | 
|   | 2 | 36 |  |         public string? Owner { get; private set; } | 
|   | 0 | 37 |  |         public ulong? ProcessSequenceNumber { get; } | 
|   | 2 | 38 |  |         public DateTime StartTime { get; private set; } | 
|   | 2 | 39 |  |         public int SessionId => 0; | 
|   | 2 | 40 |  |         public bool? IsCritical => true; | 
|   | 2 | 41 |  |         public bool IsPseudoProcess => true; | 
|   |  | 42 |  |     } | 
|   |  | 43 |  |  | 
|   |  | 44 |  |     // Pseudo processes never change during w/o rebooting. So we can cache them up front. | 
|   | 2 | 45 |  |     private static readonly Lazy<Dictionary<int, PseudoPeb>> s_systemPseudoProcesses = new(() => | 
|   | 2 | 46 |  |     { | 
|   | 2 | 47 |  |         string? systemAccount = GetSystemAccountName(); | 
|   | 2 | 48 |  |         var result = new Dictionary<int, PseudoPeb>(); | 
|   | 2 | 49 |  |  | 
|   | 2 | 50 |  |         EnumerateSystemProcesses(null, result, (res, _, pi, processSequenceNumber) => | 
|   | 2 | 51 |  |         { | 
|   | 2 | 52 |  |             if ((int)pi.UniqueProcessId == 0) | 
|   | 2 | 53 |  |             { | 
|   | 2 | 54 |  |                 // "System Idle Process" always PID 0, does not have a name, even in SYSTEM_PROCESS_INFORMATION.NamePtr | 
|   | 2 | 55 |  |                 res![0] = new PseudoPeb(pi, null, systemAccount, "System Idle Process", processSequenceNumber); | 
|   | 2 | 56 |  |             } | 
|   | 2 | 57 |  |             else if (pi.NamePtr != IntPtr.Zero) | 
|   | 2 | 58 |  |             { | 
|   | 2 | 59 |  |                 string? name = Marshal.PtrToStringUni(pi.NamePtr); | 
|   | 2 | 60 |  |                 if (name != null && IsSystemPseudoProcessByName(name, out var executable)) | 
|   | 2 | 61 |  |                 { | 
|   | 2 | 62 |  |                     var pseudoPeb = new PseudoPeb(pi, executable, systemAccount, processSequenceNumber: processSequenceN | 
|   | 2 | 63 |  |                     res![pseudoPeb.ProcessId] = pseudoPeb; | 
|   | 2 | 64 |  |                 } | 
|   | 2 | 65 |  |             } | 
|   | 2 | 66 |  |             return 0; | 
|   | 2 | 67 |  |         }); | 
|   | 2 | 68 |  |  | 
|   | 2 | 69 |  |         return result; | 
|   | 2 | 70 |  |     }, LazyThreadSafetyMode.ExecutionAndPublication); | 
|   |  | 71 |  |  | 
|   |  | 72 |  |     private static bool IsSystemPseudoProcessByName(string? processName, out string? executablePath) | 
|   |  | 73 |  |     { | 
|   | 2 | 74 |  |         executablePath = null; | 
|   | 2 | 75 |  |         if (processName != null) | 
|   |  | 76 |  |         { | 
|   |  | 77 |  |             switch (processName) | 
|   |  | 78 |  |             { | 
|   |  | 79 |  |                 case "System Idle Process": | 
|   |  | 80 |  |                     // Process Explorer and others also return no executable name here, | 
|   |  | 81 |  |                     // even though this *is* a pseudo process. Technically, we should | 
|   |  | 82 |  |                     // never get here, because this process (PID 0) doesn't have a name | 
|   |  | 83 |  |                     // set in SYSTEM_PROCESS_INFORMATION.NamePtr, but we're playing it | 
|   |  | 84 |  |                     // safe. | 
|   | 0 | 85 |  |                     return true; | 
|   |  | 86 |  |                 case "System": | 
|   |  | 87 |  |                 case "Secure System": | 
|   |  | 88 |  |                 case "Registry": | 
|   |  | 89 |  |                 case "Memory Compression": | 
|   |  | 90 |  |                     // Regardless of whether the current process is WOW64, 64 bit or 32 bit, always return | 
|   |  | 91 |  |                     // the "actual" system directory here. This is compatible to what NativeMethods.GetProcessImagePath( | 
|   |  | 92 |  |                     // does. | 
|   | 2 | 93 |  |                     executablePath = Environment.ExpandEnvironmentVariables("%windir%\\System32\\ntoskrnl.exe"); | 
|   | 2 | 94 |  |                     return true; | 
|   |  | 95 |  |             } | 
|   |  | 96 |  |         } | 
|   |  | 97 |  |  | 
|   | 2 | 98 |  |         return false; | 
|   |  | 99 |  |     } | 
|   |  | 100 |  |  | 
|   |  | 101 |  |     private static string? GetSystemAccountName() | 
|   |  | 102 |  |     { | 
|   |  | 103 |  |         try | 
|   |  | 104 |  |         { | 
|   | 2 | 105 |  |             var sid = new SecurityIdentifier("S-1-5-18"); | 
|   | 2 | 106 |  |             return sid.Translate(typeof(NTAccount)).Value; | 
|   |  | 107 |  |         } | 
|   | 0 | 108 |  |         catch | 
|   |  | 109 |  |         { | 
|   | 0 | 110 |  |         } | 
|   |  | 111 |  |  | 
|   | 0 | 112 |  |         return null; | 
|   | 2 | 113 |  |     } | 
|   |  | 114 |  |  | 
|   |  | 115 |  |     internal static bool TryGetSystemPseudoProcess(int processId, [NotNullWhen(true)] out PseudoPeb? info) | 
|   | 2 | 116 |  |         => s_systemPseudoProcesses.Value.TryGetValue(processId, out info); | 
|   |  | 117 |  |  | 
|   |  | 118 |  |     public static HashSet<IWin32ProcessDetails> GetAllProcesses() | 
|   |  | 119 |  |     { | 
|   | 0 | 120 |  |         return EnumerateSystemProcesses(null, (object?)null, | 
|   | 0 | 121 |  |             static (_, idx, pi, seq) => (IWin32ProcessDetails)new Peb(pi, seq)) | 
|   | 0 | 122 |  |             .Select(v => v.Value) | 
|   | 0 | 123 |  |             .ToHashSet(); | 
|   |  | 124 |  |     } | 
|   |  | 125 |  |  | 
|   |  | 126 |  |     public static HashSet<ProcessInfo> GetLockingProcessInfos(string[] paths, [NotNullIfNotNull(nameof(directories))] re | 
|   |  | 127 |  |     { | 
|   | 2 | 128 |  |         if (paths == null) | 
|   |  | 129 |  |         { | 
|   | 2 | 130 |  |             throw new ArgumentNullException(nameof(paths)); | 
|   |  | 131 |  |         } | 
|   |  | 132 |  |  | 
|   | 2 | 133 |  |         var result = new HashSet<ProcessInfo>(); | 
|   |  | 134 |  |  | 
|   | 2 | 135 |  |         foreach (string path in paths) | 
|   |  | 136 |  |         { | 
|   | 2 | 137 |  |             if (Directory.Exists(path)) | 
|   |  | 138 |  |             { | 
|   | 2 | 139 |  |                 directories?.Add(path); | 
|   | 2 | 140 |  |                 continue; | 
|   |  | 141 |  |             } | 
|   |  | 142 |  |  | 
|   | 2 | 143 |  |             GetLockingProcessInfo(path, result); | 
|   |  | 144 |  |         } | 
|   |  | 145 |  |  | 
|   | 2 | 146 |  |         return result; | 
|   |  | 147 |  |     } | 
|   |  | 148 |  |  | 
|   |  | 149 |  |     private static void GetLockingProcessInfo(string path, HashSet<ProcessInfo> result) | 
|   |  | 150 |  |     { | 
|   | 2 | 151 |  |         if (path == null) | 
|   |  | 152 |  |         { | 
|   | 2 | 153 |  |             throw new ArgumentNullException(nameof(path)); | 
|   |  | 154 |  |         } | 
|   |  | 155 |  |  | 
|   | 2 | 156 |  |         if (result == null) | 
|   |  | 157 |  |         { | 
|   | 0 | 158 |  |             throw new ArgumentNullException(nameof(result)); | 
|   |  | 159 |  |         } | 
|   |  | 160 |  |  | 
|   | 2 | 161 |  |         var statusBlock = new IO_STATUS_BLOCK(); | 
|   |  | 162 |  |  | 
|   | 2 | 163 |  |         using (var handle = GetFileHandle(path)) | 
|   |  | 164 |  |         { | 
|   | 2 | 165 |  |             if (handle.IsInvalid) | 
|   |  | 166 |  |             { | 
|   |  | 167 |  |                 // The file does not exist or is gone already. Could be a race condition. There is nothing we can contri | 
|   |  | 168 |  |                 // Doing this, exhibits the same behavior as the RestartManager implementation. | 
|   | 0 | 169 |  |                 return; | 
|   |  | 170 |  |             } | 
|   |  | 171 |  |  | 
|   |  | 172 |  |             // The resulting FILE_PROCESS_IDS_USING_FILE_INFORMATION structure is rather small (on 64 bit Windows, | 
|   |  | 173 |  |             // 12 byte + padding). Additionally, we assume that there aren't a whole lot of processes locking the | 
|   |  | 174 |  |             // same path. Thus choosing our initial buffer size rather conservative for about 8 processes. | 
|   | 2 | 175 |  |             int bufferSize = (IntPtr.Size + sizeof(int)) * 8; | 
|   |  | 176 |  | #if NET | 
|   | 2 | 177 |  |             using (var mem = new ScopedNativeMemory(stackalloc byte[bufferSize])) | 
|   |  | 178 |  | #else | 
|   |  | 179 |  |             using (var mem = new ScopedNativeMemory(bufferSize)) | 
|   |  | 180 |  | #endif | 
|   |  | 181 |  |             { | 
|   |  | 182 |  |                 uint status; | 
|   | 2 | 183 |  |                 while ((status = NtQueryInformationFile(handle, ref statusBlock, (IntPtr)mem, (uint)mem.Size, | 
|   | 2 | 184 |  |                     FILE_INFORMATION_CLASS.FileProcessIdsUsingFileInformation)) == STATUS_INFO_LENGTH_MISMATCH) | 
|   |  | 185 |  |                 { | 
|   | 0 | 186 |  |                     mem.Resize(mem.Size * 2); | 
|   |  | 187 |  |                 } | 
|   |  | 188 |  |  | 
|   | 2 | 189 |  |                 if (status != STATUS_SUCCESS) | 
|   |  | 190 |  |                 { | 
|   | 0 | 191 |  |                     throw GetException(status); | 
|   |  | 192 |  |                 } | 
|   |  | 193 |  |  | 
|   |  | 194 |  |                 // Buffer contains: | 
|   |  | 195 |  |                 //    struct FILE_PROCESS_IDS_USING_FILE_INFORMATION | 
|   |  | 196 |  |                 //    { | 
|   |  | 197 |  |                 //        ULONG NumberOfProcessIdsInList; | 
|   |  | 198 |  |                 //        ULONG_PTR ProcessIdList[1]; | 
|   |  | 199 |  |                 //    } | 
|   |  | 200 |  |  | 
|   | 2 | 201 |  |                 IntPtr readBuffer = (IntPtr)mem; | 
|   | 2 | 202 |  |                 int numEntries = Marshal.ReadInt32(readBuffer); // NumberOfProcessIdsInList | 
|   | 2 | 203 |  |                 readBuffer += IntPtr.Size; | 
|   |  | 204 |  |  | 
|   | 2 | 205 |  |                 for (int i = 0; i < numEntries; i++) | 
|   |  | 206 |  |                 { | 
|   | 2 | 207 |  |                     int processId = Marshal.ReadIntPtr(readBuffer).ToInt32(); // A single ProcessIdList[] element | 
|   | 2 | 208 |  |                     var entry = ProcessInfoWindows.Create(processId); | 
|   | 2 | 209 |  |                     if (entry != null) | 
|   |  | 210 |  |                     { | 
|   | 2 | 211 |  |                         result.Add(entry); | 
|   |  | 212 |  |                     } | 
|   | 2 | 213 |  |                     readBuffer += IntPtr.Size; | 
|   |  | 214 |  |                 } | 
|   | 2 | 215 |  |             } | 
|   |  | 216 |  |         } | 
|   | 2 | 217 |  |     } | 
|   |  | 218 |  |  | 
|   |  | 219 |  |     internal static Win32Exception GetException(uint status) | 
|   |  | 220 |  |     { | 
|   | 0 | 221 |  |         int res = RtlNtStatusToDosError(status); | 
|   | 0 | 222 |  |         return new Win32Exception(res, GetMessage(res)); | 
|   |  | 223 |  |     } | 
|   |  | 224 |  |  | 
|   |  | 225 |  |     internal static Dictionary<(int, DateTime), ProcessInfo> GetProcessesByWorkingDirectory(List<string> directories) | 
|   |  | 226 |  |     { | 
|   | 2 | 227 |  |         if (directories == null) | 
|   |  | 228 |  |         { | 
|   | 0 | 229 |  |             throw new ArgumentNullException(nameof(directories)); | 
|   |  | 230 |  |         } | 
|   |  | 231 |  |  | 
|   | 2 | 232 |  |         return EnumerateSystemProcesses(null, directories, | 
|   | 2 | 233 |  |             static (dirs, idx, pi, seq) => | 
|   | 2 | 234 |  |             { | 
|   | 2 | 235 |  |                 var peb = new Peb(pi, seq); | 
|   | 2 | 236 |  |  | 
|   | 2 | 237 |  |                 if (!peb.HasError && !string.IsNullOrEmpty(peb.CurrentDirectory)) | 
|   | 2 | 238 |  |                 { | 
|   | 2 | 239 |  |                     // If the process' current directory is the search path itself, or it is somewhere nested below it, | 
|   | 2 | 240 |  |                     // we have to take it into account. This will also account for differences in the two when the | 
|   | 2 | 241 |  |                     // search path does not end with a '\', but the PEB's current directory does (which is always the | 
|   | 2 | 242 |  |                     // case). | 
|   | 2 | 243 |  |                     if (dirs!.FindIndex(d => peb.CurrentDirectory.StartsWith(d, StringComparison.OrdinalIgnoreCase)) !=  | 
|   | 2 | 244 |  |                     { | 
|   | 2 | 245 |  |                         return (ProcessInfo)new ProcessInfoWindows(peb); | 
|   | 2 | 246 |  |                     } | 
|   | 2 | 247 |  |                 } | 
|   | 2 | 248 |  |  | 
|   | 2 | 249 |  |                 return null; | 
|   | 2 | 250 |  |             }); | 
|   |  | 251 |  |     } | 
|   |  | 252 |  |  | 
|   |  | 253 |  |     // Use a smaller buffer size on debug to ensure we hit the retry path. | 
|   | 2 | 254 |  |     private static uint GetDefaultCachedBufferSize() => 1024 * | 
|   | 2 | 255 |  | #if DEBUG | 
|   | 2 | 256 |  |         8; | 
|   | 2 | 257 |  | #else | 
|   | 2 | 258 |  |         1024; | 
|   |  | 259 |  | #endif | 
|   |  | 260 |  |  | 
|   |  | 261 |  | #if NET | 
|   |  | 262 |  |     // | 
|   |  | 263 |  |     // This implementation with based on dotnet/runtime ProcessManager.Win32.cs does. | 
|   |  | 264 |  |     // Basically, it doesn't hold on to a "cached buffer" and uses more modern constructs | 
|   |  | 265 |  |     // which results in "simpler" code. Especially, it does not use GCHandle and also | 
|   |  | 266 |  |     // doesn't have workarounds for "older" versions of Windows anymore. | 
|   |  | 267 |  |     // | 
|   |  | 268 |  |  | 
|   | 2 | 269 |  |     private static uint s_mostRecentSize = GetDefaultCachedBufferSize(); | 
|   |  | 270 |  |  | 
|   |  | 271 |  |     internal static unsafe Dictionary<(int, DateTime), T> EnumerateSystemProcesses<T, TData>( | 
|   |  | 272 |  |         HashSet<int>? processIds, | 
|   |  | 273 |  |         TData? data, | 
|   |  | 274 |  |         Func<TData?, int, SYSTEM_PROCESS_INFORMATION, ulong?, T?> newEntry) | 
|   |  | 275 |  |     { | 
|   |  | 276 |  |         // Start with the default buffer size. | 
|   | 2 | 277 |  |         uint bufferSize = s_mostRecentSize; | 
|   | 2 | 278 |  |         var infoClass = SYSTEM_INFORMATION_CLASS.SystemProcessInformation; | 
|   |  | 279 |  |  | 
|   |  | 280 |  |         while (true) | 
|   |  | 281 |  |         { | 
|   |  | 282 |  |             // Some platforms require the buffer to be 64-bit aligned. ScopedNativeMemory guarantees sufficient alignmen | 
|   | 2 | 283 |  |             using var mem = new ScopedNativeMemory((int)bufferSize); | 
|   |  | 284 |  |  | 
|   | 2 | 285 |  |             uint actualSize = 0; | 
|   | 2 | 286 |  |             uint status = NtQuerySystemInformation(infoClass, (void*)mem, bufferSize, &actualSize); | 
|   |  | 287 |  |  | 
|   | 2 | 288 |  |             if (status != STATUS_INFO_LENGTH_MISMATCH) | 
|   |  | 289 |  |             { | 
|   |  | 290 |  |                 // see definition of NT_SUCCESS(Status) in SDK | 
|   | 2 | 291 |  |                 if ((int)status < 0) | 
|   |  | 292 |  |                 { | 
|   | 0 | 293 |  |                     throw GetException(status); | 
|   |  | 294 |  |                 } | 
|   |  | 295 |  |  | 
|   |  | 296 |  |                 // Remember last buffer size for next attempt. Note that this may also result in smaller | 
|   |  | 297 |  |                 // buffer sizes for further attempts, as the live processes can also decrease in comparison | 
|   |  | 298 |  |                 // to a previous call. | 
|   |  | 299 |  |                 Debug.Assert(actualSize > 0 && actualSize <= bufferSize, $"actualSize={actualSize} bufferSize={bufferSiz | 
|   | 2 | 300 |  |                 s_mostRecentSize = GetEstimatedBufferSize(actualSize); | 
|   |  | 301 |  |  | 
|   | 2 | 302 |  |                 return HandleProcesses(new ReadOnlySpan<byte>((void*)mem, (int)actualSize), processIds, data, newEntry); | 
|   |  | 303 |  |             } | 
|   |  | 304 |  |             else | 
|   |  | 305 |  |             { | 
|   |  | 306 |  |                 // Buffer was too small; retry with a larger buffer. | 
|   |  | 307 |  |                 Debug.Assert(actualSize > bufferSize, $"actualSize={actualSize} bufferSize={bufferSize} (0x{status:x8}). | 
|   | 0 | 308 |  |                 bufferSize = GetEstimatedBufferSize(actualSize); | 
|   |  | 309 |  |             } | 
|   |  | 310 |  |         } | 
|   |  | 311 |  |  | 
|   |  | 312 |  |         // allocating a few more kilo bytes just in case there are some new processes since the last call | 
|   | 2 | 313 |  |         static uint GetEstimatedBufferSize(uint actualSize) => actualSize + 1024 * 10; | 
|   | 2 | 314 |  |     } | 
|   |  | 315 |  |  | 
|   |  | 316 |  |     private static unsafe Dictionary<(int, DateTime), T> HandleProcesses<T, TData>( | 
|   |  | 317 |  |         ReadOnlySpan<byte> current, | 
|   |  | 318 |  |         HashSet<int>? processIds, | 
|   |  | 319 |  |         TData? data, | 
|   |  | 320 |  |         Func<TData?, int, SYSTEM_PROCESS_INFORMATION, ulong?, T?> newEntry) | 
|   |  | 321 |  |     { | 
|   | 2 | 322 |  |         var processInfos = new Dictionary<(int, DateTime), T>(); | 
|   | 2 | 323 |  |         int processInformationOffset = 0; | 
|   | 2 | 324 |  |         int count = 0; | 
|   |  | 325 |  |  | 
|   | 2 | 326 |  |         while (true) | 
|   |  | 327 |  |         { | 
|   | 2 | 328 |  |             ref readonly var pi = ref MemoryMarshal.AsRef<SYSTEM_PROCESS_INFORMATION>(current.Slice(processInformationOf | 
|   | 2 | 329 |  |             int pid = pi.UniqueProcessId.ToInt32(); | 
|   |  | 330 |  |  | 
|   | 2 | 331 |  |             ulong? seq = null; | 
|   | 2 | 332 |  |             if (SupportsProcessSequenceNumber) | 
|   |  | 333 |  |             { | 
|   | 2 | 334 |  |                 ref readonly var pix = ref MemoryMarshal.AsRef<SYSTEM_PROCESS_INFORMATION_EXTENSION>(current.Slice(proce | 
|   | 2 | 335 |  |                 seq = pix.ProcessSequenceNumber; | 
|   |  | 336 |  |             } | 
|   |  | 337 |  |  | 
|   | 2 | 338 |  |             if (processIds == null || processIds.Contains(pid)) | 
|   |  | 339 |  |             { | 
|   | 2 | 340 |  |                 var entry = newEntry(data, count, pi, seq); | 
|   | 2 | 341 |  |                 if (entry != null) | 
|   |  | 342 |  |                 { | 
|   | 2 | 343 |  |                     processInfos.Add((pid, DateTime.FromFileTime(pi.CreateTime)), entry); | 
|   |  | 344 |  |                 } | 
|   |  | 345 |  |             } | 
|   |  | 346 |  |  | 
|   | 2 | 347 |  |             if (pi.NextEntryOffset == 0) | 
|   |  | 348 |  |             { | 
|   |  | 349 |  |                 break; | 
|   |  | 350 |  |             } | 
|   | 2 | 351 |  |             processInformationOffset += (int)pi.NextEntryOffset; | 
|   | 2 | 352 |  |             count++; | 
|   |  | 353 |  |         } | 
|   |  | 354 |  |  | 
|   | 2 | 355 |  |         return processInfos; | 
|   |  | 356 |  |     } | 
|   |  | 357 |  |  | 
|   |  | 358 |  | #else | 
|   |  | 359 |  |     // | 
|   |  | 360 |  |     // This implementation with based on .NET Frameworks Process class. It does not provide all the features as the | 
|   |  | 361 |  |     // .NET version. For example, it does not access SYSTEM_PROCESS_INFORMATION_EXTENSION to get the ProcessSequenceNumb | 
|   |  | 362 |  |     // | 
|   |  | 363 |  |  | 
|   |  | 364 |  |     private static long[]? s_cachedBuffer; | 
|   |  | 365 |  |  | 
|   |  | 366 |  |     internal static Dictionary<(int, DateTime), T> EnumerateSystemProcesses<T, TData>( | 
|   |  | 367 |  |         HashSet<int>? processIds, | 
|   |  | 368 |  |         TData? data, | 
|   |  | 369 |  |         Func<TData?, int, SYSTEM_PROCESS_INFORMATION, ulong?, T?> newEntry) | 
|   |  | 370 |  |     { | 
|   |  | 371 |  |         var processInfos = new Dictionary<(int, DateTime), T>(); | 
|   |  | 372 |  |         var bufferHandle = new GCHandle(); | 
|   |  | 373 |  |  | 
|   |  | 374 |  |         // Start with the default buffer size (smaller in DEBUG to make sure retry path is hit) | 
|   |  | 375 |  |         int bufferSize = (int)GetDefaultCachedBufferSize(); | 
|   |  | 376 |  |  | 
|   |  | 377 |  |         // Get the cached buffer. | 
|   |  | 378 |  |         long[]? buffer = Interlocked.Exchange(ref s_cachedBuffer, null); | 
|   |  | 379 |  |  | 
|   |  | 380 |  |         try | 
|   |  | 381 |  |         { | 
|   |  | 382 |  |             // Retry until we get all the data | 
|   |  | 383 |  |             int status; | 
|   |  | 384 |  |             do | 
|   |  | 385 |  |             { | 
|   |  | 386 |  |                 if (buffer == null) | 
|   |  | 387 |  |                 { | 
|   |  | 388 |  |                     // Allocate buffer of longs since some platforms require the buffer to be 64-bit aligned. | 
|   |  | 389 |  |                     buffer = new long[(bufferSize + 7) / 8]; | 
|   |  | 390 |  |                 } | 
|   |  | 391 |  |                 else | 
|   |  | 392 |  |                 { | 
|   |  | 393 |  |                     // If we have cached buffer, set the size properly. | 
|   |  | 394 |  |                     bufferSize = buffer.Length * sizeof(long); | 
|   |  | 395 |  |                 } | 
|   |  | 396 |  |                 bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); | 
|   |  | 397 |  |  | 
|   |  | 398 |  |                 status = NtQuerySystemInformation( | 
|   |  | 399 |  |                     SYSTEM_INFORMATION_CLASS.SystemProcessInformation, | 
|   |  | 400 |  |                     bufferHandle.AddrOfPinnedObject(), | 
|   |  | 401 |  |                     bufferSize, | 
|   |  | 402 |  |                     out int requiredSize); | 
|   |  | 403 |  |  | 
|   |  | 404 |  |                 if ((uint)status == STATUS_INFO_LENGTH_MISMATCH) | 
|   |  | 405 |  |                 { | 
|   |  | 406 |  |                     if (bufferHandle.IsAllocated) | 
|   |  | 407 |  |                     { | 
|   |  | 408 |  |                         bufferHandle.Free(); | 
|   |  | 409 |  |                     } | 
|   |  | 410 |  |  | 
|   |  | 411 |  |                     buffer = null; | 
|   |  | 412 |  |                     bufferSize = GetNewBufferSize(bufferSize, requiredSize); | 
|   |  | 413 |  |                 } | 
|   |  | 414 |  |             } while ((uint)status == STATUS_INFO_LENGTH_MISMATCH); | 
|   |  | 415 |  |  | 
|   |  | 416 |  |             if (status < 0) | 
|   |  | 417 |  |             { | 
|   |  | 418 |  |                 throw GetException((uint)status); | 
|   |  | 419 |  |             } | 
|   |  | 420 |  |  | 
|   |  | 421 |  |             // Parse the data block to get process information | 
|   |  | 422 |  |             IntPtr dataPtr = bufferHandle.AddrOfPinnedObject(); | 
|   |  | 423 |  |  | 
|   |  | 424 |  |             long totalOffset = 0; | 
|   |  | 425 |  |             int count = 0; | 
|   |  | 426 |  |  | 
|   |  | 427 |  |             while (true) | 
|   |  | 428 |  |             { | 
|   |  | 429 |  |                 nint currentPtr = checked((IntPtr)(dataPtr.ToInt64() + totalOffset)); | 
|   |  | 430 |  |                 var pi = Marshal.PtrToStructure<SYSTEM_PROCESS_INFORMATION>(currentPtr); | 
|   |  | 431 |  |  | 
|   |  | 432 |  |                 int pid = pi.UniqueProcessId.ToInt32(); | 
|   |  | 433 |  |                 if (processIds == null || processIds.Contains(pid)) | 
|   |  | 434 |  |                 { | 
|   |  | 435 |  |                     var startTime = DateTime.FromFileTime(pi.CreateTime); | 
|   |  | 436 |  |                     var entry = newEntry(data, count, pi, null); | 
|   |  | 437 |  |                     if (entry != null) | 
|   |  | 438 |  |                     { | 
|   |  | 439 |  |                         processInfos.Add((pid, startTime), entry); | 
|   |  | 440 |  |                     } | 
|   |  | 441 |  |                 } | 
|   |  | 442 |  |  | 
|   |  | 443 |  |                 if (pi.NextEntryOffset == 0) | 
|   |  | 444 |  |                 { | 
|   |  | 445 |  |                     break; | 
|   |  | 446 |  |                 } | 
|   |  | 447 |  |                 totalOffset += pi.NextEntryOffset; | 
|   |  | 448 |  |                 count++; | 
|   |  | 449 |  |             } | 
|   |  | 450 |  |         } | 
|   |  | 451 |  |         finally | 
|   |  | 452 |  |         { | 
|   |  | 453 |  |             // Cache the final buffer for use on the next call. | 
|   |  | 454 |  |             Interlocked.Exchange(ref s_cachedBuffer, buffer); | 
|   |  | 455 |  |  | 
|   |  | 456 |  |             if (bufferHandle.IsAllocated) | 
|   |  | 457 |  |             { | 
|   |  | 458 |  |                 bufferHandle.Free(); | 
|   |  | 459 |  |             } | 
|   |  | 460 |  |         } | 
|   |  | 461 |  |  | 
|   |  | 462 |  |         return processInfos; | 
|   |  | 463 |  |  | 
|   |  | 464 |  |         static int GetNewBufferSize(int existingBufferSize, int requiredSize) | 
|   |  | 465 |  |         { | 
|   |  | 466 |  |             if (requiredSize == 0) | 
|   |  | 467 |  |             { | 
|   |  | 468 |  |                 // | 
|   |  | 469 |  |                 // On some old OS like win2000, requiredSize will not be set if the buffer | 
|   |  | 470 |  |                 // passed to NtQuerySystemInformation is not enough. | 
|   |  | 471 |  |                 // | 
|   |  | 472 |  |                 int newSize = existingBufferSize * 2; | 
|   |  | 473 |  |                 if (newSize < existingBufferSize) | 
|   |  | 474 |  |                 { | 
|   |  | 475 |  |                     throw new OutOfMemoryException($"Existing buffer size {existingBufferSize:N0} bytes, attempting to a | 
|   |  | 476 |  |                 } | 
|   |  | 477 |  |                 return newSize; | 
|   |  | 478 |  |             } | 
|   |  | 479 |  |             else | 
|   |  | 480 |  |             { | 
|   |  | 481 |  |                 // allocating a few more kilo bytes just in case there are some new process | 
|   |  | 482 |  |                 // kicked in since new call to NtQuerySystemInformation | 
|   |  | 483 |  |                 int newSize = requiredSize + (1024 * 10); | 
|   |  | 484 |  |                 if (newSize < requiredSize) | 
|   |  | 485 |  |                 { | 
|   |  | 486 |  |                     throw new OutOfMemoryException($"Required buffer size {requiredSize:N0} bytes, attempting to allocat | 
|   |  | 487 |  |                 } | 
|   |  | 488 |  |                 return newSize; | 
|   |  | 489 |  |             } | 
|   |  | 490 |  |         } | 
|   |  | 491 |  |     } | 
|   |  | 492 |  | #endif | 
|   |  | 493 |  | } |