1 // Taken from https://github.com/cklutz/LockCheck, MIT license. 2 // Copyright (C) Christian Klutz 3 4 #if !RUNTIME_TYPE_NETCORE && !MONO 5 6 using System; 7 using System.Collections.Generic; 8 using System.ComponentModel; 9 using System.Linq; 10 using System.Runtime.InteropServices; 11 using System.Text; 12 13 namespace Microsoft.Build.Tasks 14 { 15 internal class LockCheck 16 { 17 [Flags] 18 internal enum ApplicationStatus 19 { 20 // Members must have the same values as in NativeMethods.RM_APP_STATUS 21 Unknown = 0x0, 22 Running = 0x1, 23 Stopped = 0x2, 24 StoppedOther = 0x4, 25 Restarted = 0x8, 26 ErrorOnStop = 0x10, 27 ErrorOnRestart = 0x20, 28 ShutdownMasked = 0x40, 29 RestartMasked = 0x80 30 } 31 32 internal enum ApplicationType 33 { 34 // Members must have the same values as in NativeMethods.RM_APP_TYPE 35 36 Unknown = 0, 37 MainWindow = 1, 38 OtherWindow = 2, 39 Service = 3, 40 Explorer = 4, 41 Console = 5, 42 Critical = 1000 43 } 44 45 const string RestartManagerDll = "rstrtmgr.dll"; 46 47 [DllImport(RestartManagerDll, CharSet = CharSet.Unicode)] RmRegisterResources(uint pSessionHandle, uint nFiles, string[] rgsFilenames, uint nApplications, [In] RM_UNIQUE_PROCESS[] rgApplications, uint nServices, string[] rgsServiceNames)48 static extern int RmRegisterResources(uint pSessionHandle, 49 uint nFiles, 50 string[] rgsFilenames, 51 uint nApplications, 52 [In] RM_UNIQUE_PROCESS[] rgApplications, 53 uint nServices, 54 string[] rgsServiceNames); 55 56 [DllImport(RestartManagerDll, CharSet = CharSet.Unicode)] RmStartSession(out uint pSessionHandle, int dwSessionFlags, StringBuilder strSessionKey)57 static extern int RmStartSession(out uint pSessionHandle, 58 int dwSessionFlags, StringBuilder strSessionKey); 59 60 [DllImport(RestartManagerDll)] RmEndSession(uint pSessionHandle)61 static extern int RmEndSession(uint pSessionHandle); 62 63 [DllImport(RestartManagerDll, CharSet = CharSet.Unicode)] RmGetList(uint dwSessionHandle, out uint pnProcInfoNeeded, ref uint pnProcInfo, [In, Out] RM_PROCESS_INFO[] rgAffectedApps, ref uint lpdwRebootReasons)64 static extern int RmGetList(uint dwSessionHandle, 65 out uint pnProcInfoNeeded, 66 ref uint pnProcInfo, 67 [In, Out] RM_PROCESS_INFO[] rgAffectedApps, 68 ref uint lpdwRebootReasons); 69 70 [StructLayout(LayoutKind.Sequential)] 71 internal struct FILETIME 72 { 73 public uint dwLowDateTime; 74 public uint dwHighDateTime; 75 } 76 77 [StructLayout(LayoutKind.Sequential)] 78 internal struct RM_UNIQUE_PROCESS 79 { 80 public uint dwProcessId; 81 public FILETIME ProcessStartTime; 82 } 83 84 const int RM_INVALID_SESSION = -1; 85 const int RM_INVALID_PROCESS = -1; 86 const int CCH_RM_MAX_APP_NAME = 255; 87 const int CCH_RM_MAX_SVC_NAME = 63; 88 const int ERROR_SEM_TIMEOUT = 121; 89 const int ERROR_BAD_ARGUMENTS = 160; 90 const int ERROR_MAX_SESSIONS_REACHED = 353; 91 const int ERROR_WRITE_FAULT = 29; 92 const int ERROR_OUTOFMEMORY = 14; 93 const int ERROR_MORE_DATA = 234; 94 const int ERROR_ACCESS_DENIED = 5; 95 const int ERROR_INVALID_HANDLE = 6; 96 const int ERROR_CANCELLED = 1223; 97 98 static readonly int RM_SESSION_KEY_LEN = Guid.Empty.ToByteArray().Length; // 16-byte 99 static readonly int CCH_RM_SESSION_KEY = RM_SESSION_KEY_LEN * 2; 100 101 internal enum RM_APP_TYPE 102 { 103 RmUnknownApp = 0, 104 RmMainWindow = 1, 105 RmOtherWindow = 2, 106 RmService = 3, 107 RmExplorer = 4, 108 RmConsole = 5, 109 RmCritical = 1000 110 } 111 112 enum RM_APP_STATUS 113 { 114 RmStatusUnknown = 0x0, 115 RmStatusRunning = 0x1, 116 RmStatusStopped = 0x2, 117 RmStatusStoppedOther = 0x4, 118 RmStatusRestarted = 0x8, 119 RmStatusErrorOnStop = 0x10, 120 RmStatusErrorOnRestart = 0x20, 121 RmStatusShutdownMasked = 0x40, 122 RmStatusRestartMasked = 0x80 123 } 124 125 enum RM_REBOOT_REASON 126 { 127 RmRebootReasonNone = 0x0, 128 RmRebootReasonPermissionDenied = 0x1, 129 RmRebootReasonSessionMismatch = 0x2, 130 RmRebootReasonCriticalProcess = 0x4, 131 RmRebootReasonCriticalService = 0x8, 132 RmRebootReasonDetectedSelf = 0x10 133 } 134 135 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 136 internal struct RM_PROCESS_INFO 137 { 138 internal RM_UNIQUE_PROCESS Process; 139 [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)] 140 public string strAppName; 141 [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)] 142 public string strServiceShortName; 143 internal RM_APP_TYPE ApplicationType; 144 public uint AppStatus; 145 public uint TSSessionId; 146 [MarshalAs(UnmanagedType.Bool)] 147 public bool bRestartable; 148 } 149 150 internal class ProcessInfo 151 { ProcessInfo(RM_PROCESS_INFO processInfo)152 internal ProcessInfo(RM_PROCESS_INFO processInfo) 153 { 154 ProcessId = (int)processInfo.Process.dwProcessId; 155 // ProcessStartTime is returned as local time, not UTC. 156 StartTime = DateTime.FromFileTime((((long)processInfo.Process.ProcessStartTime.dwHighDateTime) << 32) | 157 processInfo.Process.ProcessStartTime.dwLowDateTime); 158 ApplicationName = processInfo.strAppName; 159 ServiceShortName = processInfo.strServiceShortName; 160 ApplicationType = (ApplicationType)processInfo.ApplicationType; 161 ApplicationStatus = (ApplicationStatus)processInfo.AppStatus; 162 Restartable = processInfo.bRestartable; 163 TerminalServicesSessionId = (int)processInfo.TSSessionId; 164 } 165 166 public int ProcessId { get; private set; } 167 public DateTime StartTime { get; private set; } 168 public string ApplicationName { get; private set; } 169 public string ServiceShortName { get; private set; } 170 public ApplicationType ApplicationType { get; private set; } 171 public ApplicationStatus ApplicationStatus { get; private set; } 172 public int TerminalServicesSessionId { get; private set; } 173 public bool Restartable { get; private set; } 174 GetHashCode()175 public override int GetHashCode() 176 { 177 var h1 = ProcessId.GetHashCode(); 178 var h2 = StartTime.GetHashCode(); 179 return ((h1 << 5) + h1) ^ h2; 180 } 181 Equals(object obj)182 public override bool Equals(object obj) 183 { 184 var other = obj as ProcessInfo; 185 if (other != null) 186 { 187 return other.ProcessId == ProcessId && other.StartTime == StartTime; 188 } 189 return false; 190 } 191 ToString()192 public override string ToString() 193 { 194 return ProcessId + "@" + StartTime.ToString("s"); 195 } 196 } 197 GetProcessesLockingFile(string filePath)198 internal static string GetProcessesLockingFile(string filePath) 199 { 200 return string.Join(", ", GetLockingProcessInfos(filePath).Select(p => $"{p.ApplicationName} ({p.ProcessId})")); 201 } 202 GetLockingProcessInfos(params string[] paths)203 internal static IEnumerable<ProcessInfo> GetLockingProcessInfos(params string[] paths) 204 { 205 if (paths == null) 206 throw new ArgumentNullException("paths"); 207 208 const int maxRetries = 6; 209 210 // See http://blogs.msdn.com/b/oldnewthing/archive/2012/02/17/10268840.aspx. 211 var key = new StringBuilder(new string('\0', CCH_RM_SESSION_KEY + 1)); 212 213 uint handle; 214 int res = RmStartSession(out handle, 0, key); 215 if (res != 0) 216 throw GetException(res, "RmStartSession", "Failed to begin restart manager session."); 217 218 try 219 { 220 string[] resources = paths; 221 res = RmRegisterResources(handle, (uint)resources.Length, resources, 0, null, 0, null); 222 if (res != 0) 223 throw GetException(res, "RmRegisterResources", "Could not register resources."); 224 225 // 226 // Obtain the list of affected applications/services. 227 // 228 // NOTE: Restart Manager returns the results into the buffer allocated by the caller. The first call to 229 // RmGetList() will return the size of the buffer (i.e. nProcInfoNeeded) the caller needs to allocate. 230 // The caller then needs to allocate the buffer (i.e. rgAffectedApps) and make another RmGetList() 231 // call to ask Restart Manager to write the results into the buffer. However, since Restart Manager 232 // refreshes the list every time RmGetList()is called, it is possible that the size returned by the first 233 // RmGetList()call is not sufficient to hold the results discovered by the second RmGetList() call. Therefore, 234 // it is recommended that the caller follows the following practice to handle this race condition: 235 // 236 // Use a loop to call RmGetList() in case the buffer allocated according to the size returned in previous 237 // call is not enough. 238 // 239 uint pnProcInfo = 0; 240 RM_PROCESS_INFO[] rgAffectedApps = null; 241 int retry = 0; 242 do 243 { 244 uint lpdwRebootReasons = (uint)RM_REBOOT_REASON.RmRebootReasonNone; 245 uint pnProcInfoNeeded; 246 res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, rgAffectedApps, ref lpdwRebootReasons); 247 if (res == 0) 248 { 249 // If pnProcInfo == 0, then there is simply no locking process (found), in this case rgAffectedApps is "null". 250 if (pnProcInfo == 0) 251 return Enumerable.Empty<ProcessInfo>(); 252 253 var lockInfos = new List<ProcessInfo>((int)pnProcInfo); 254 for (int i = 0; i < pnProcInfo; i++) 255 { 256 lockInfos.Add(new ProcessInfo(rgAffectedApps[i])); 257 } 258 return lockInfos; 259 } 260 261 if (res != ERROR_MORE_DATA) 262 throw GetException(res, "RmGetList", string.Format("Failed to get entries (retry {0}).", retry)); 263 264 pnProcInfo = pnProcInfoNeeded; 265 rgAffectedApps = new RM_PROCESS_INFO[pnProcInfo]; 266 } while ((res == ERROR_MORE_DATA) && (retry++ < maxRetries)); 267 } 268 finally 269 { 270 res = RmEndSession(handle); 271 if (res != 0) 272 throw GetException(res, "RmEndSession", "Failed to end the restart manager session."); 273 } 274 275 return Enumerable.Empty<ProcessInfo>(); 276 } 277 GetException(int res, string apiName, string message)278 private static Exception GetException(int res, string apiName, string message) 279 { 280 string reason; 281 switch (res) 282 { 283 case ERROR_ACCESS_DENIED: 284 reason = "Access is denied."; 285 break; 286 case ERROR_SEM_TIMEOUT: 287 reason = "A Restart Manager function could not obtain a Registry write mutex in the allotted time. " + 288 "A system restart is recommended because further use of the Restart Manager is likely to fail."; 289 break; 290 case ERROR_BAD_ARGUMENTS: 291 reason = "One or more arguments are not correct. This error value is returned by the Restart Manager " + 292 "function if a NULL pointer or 0 is passed in a parameter that requires a non-null and non-zero value."; 293 break; 294 case ERROR_MAX_SESSIONS_REACHED: 295 reason = "The maximum number of sessions has been reached."; 296 break; 297 case ERROR_WRITE_FAULT: 298 reason = "An operation was unable to read or write to the registry."; 299 break; 300 case ERROR_OUTOFMEMORY: 301 reason = "A Restart Manager operation could not complete because not enough memory was available."; 302 break; 303 case ERROR_CANCELLED: 304 reason = "The current operation is canceled by user."; 305 break; 306 case ERROR_MORE_DATA: 307 reason = "More data is available."; 308 break; 309 case ERROR_INVALID_HANDLE: 310 reason = "No Restart Manager session exists for the handle supplied."; 311 break; 312 default: 313 reason = string.Format("0x{0:x8}", res); 314 break; 315 } 316 317 throw new Win32Exception(res, string.Format("{0} ({1}() error {2}: {3})", message, apiName, res, reason)); 318 } 319 } 320 } 321 322 #endif