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