| File: InternalUtilities\FileLockCheck.cs | Web Access |
| Project: src\roslyn\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. // Based on https://github.com/dotnet/msbuild/blob/6cd445d84e59a36c7fbb6f50b7a5a62767a6da51/src/Utilities/LockCheck.cs using System; using System.Collections.Immutable; using System.Diagnostics; using System.Runtime.InteropServices; using Microsoft.CodeAnalysis.PooledObjects; namespace Roslyn.Utilities; /// <summary> /// This class implements checking what processes are locking a file on Windows. /// It uses the Restart Manager API to do this. Other platforms are skipped. /// </summary> internal static class FileLockCheck { [StructLayout(LayoutKind.Sequential)] private struct FILETIME { public uint dwLowDateTime; public uint dwHighDateTime; } [StructLayout(LayoutKind.Sequential)] private struct RM_UNIQUE_PROCESS { public uint dwProcessId; public FILETIME ProcessStartTime; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct RM_PROCESS_INFO { private const int CCH_RM_MAX_APP_NAME = 255; private const int CCH_RM_MAX_SVC_NAME = 63; internal RM_UNIQUE_PROCESS Process; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)] public string strAppName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)] public string strServiceShortName; internal int ApplicationType; public uint AppStatus; public uint TSSessionId; [MarshalAs(UnmanagedType.Bool)] public bool bRestartable; } private const string RestartManagerDll = "rstrtmgr.dll"; [DllImport(RestartManagerDll, CharSet = CharSet.Unicode)] private static extern int RmRegisterResources(uint pSessionHandle, uint nFiles, string[] rgsFilenames, uint nApplications, [In] RM_UNIQUE_PROCESS[]? rgApplications, uint nServices, string[]? rgsServiceNames); /// <summary> /// Starts a new Restart Manager session. /// A maximum of 64 Restart Manager sessions per user session /// can be open on the system at the same time. When this /// function starts a session, it returns a session handle /// and session key that can be used in subsequent calls to /// the Restart Manager API. /// </summary> /// <param name="pSessionHandle"> /// A pointer to the handle of a Restart Manager session. /// The session handle can be passed in subsequent calls /// to the Restart Manager API. /// </param> /// <param name="dwSessionFlags"> /// Reserved. This parameter should be 0. /// </param> /// <param name="strSessionKey"> /// A null-terminated string that contains the session key /// to the new session. The string must be allocated before /// calling the RmStartSession function. /// </param> /// <returns>System error codes that are defined in Winerror.h.</returns> /// <remarks> /// The RmStartSession function doesn’t properly null-terminate /// the session key, even though the function is documented as /// returning a null-terminated string. To work around this bug, /// we pre-fill the buffer with null characters so that whatever /// ends gets written will have a null terminator (namely, one of /// the null characters we placed ahead of time). /// <para> /// see <see href="http://blogs.msdn.com/b/oldnewthing/archive/2012/02/17/10268840.aspx"/>. /// </para> /// </remarks> [DllImport(RestartManagerDll, CharSet = CharSet.Unicode)] private static extern unsafe int RmStartSession( out uint pSessionHandle, int dwSessionFlags, char* strSessionKey); /// <summary> /// Ends the Restart Manager session. /// This function should be called by the primary installer that /// has previously started the session by calling the <see cref="RmStartSession"/> /// function. The RmEndSession function can be called by a secondary installer /// that is joined to the session once no more resources need to be registered /// by the secondary installer. /// </summary> /// <param name="pSessionHandle">A handle to an existing Restart Manager session.</param> /// <returns> /// The function can return one of the system error codes that are defined in Winerror.h. /// </returns> [DllImport(RestartManagerDll)] private static extern int RmEndSession(uint pSessionHandle); [DllImport(RestartManagerDll, CharSet = CharSet.Unicode)] private static extern int RmGetList(uint dwSessionHandle, out uint pnProcInfoNeeded, ref uint pnProcInfo, [In, Out] RM_PROCESS_INFO[]? rgAffectedApps, ref uint lpdwRebootReasons); public static ImmutableArray<(int processId, string applicationName)> TryGetLockingProcessInfos(string path) { if (!PlatformInformation.IsWindows) { return []; } try { return GetLockingProcessInfosImpl([path]); } catch { return []; } } private static ImmutableArray<(int processId, string applicationName)> GetLockingProcessInfosImpl(string[] paths) { const int MaxRetries = 6; const int ERROR_MORE_DATA = 234; const uint RM_REBOOT_REASON_NONE = 0; uint handle; int res; unsafe { char* key = stackalloc char[sizeof(Guid) * 2 + 1]; res = RmStartSession(out handle, 0, key); } if (res != 0) { return []; } try { res = RmRegisterResources(handle, (uint)paths.Length, paths, 0, null, 0, null); if (res != 0) { return []; } // // Obtain the list of affected applications/services. // // NOTE: Restart Manager returns the results into the buffer allocated by the caller. The first call to // RmGetList() will return the size of the buffer (i.e. nProcInfoNeeded) the caller needs to allocate. // The caller then needs to allocate the buffer (i.e. rgAffectedApps) and make another RmGetList() // call to ask Restart Manager to write the results into the buffer. However, since Restart Manager // refreshes the list every time RmGetList()is called, it is possible that the size returned by the first // RmGetList()call is not sufficient to hold the results discovered by the second RmGetList() call. Therefore, // it is recommended that the caller follows the following practice to handle this race condition: // // Use a loop to call RmGetList() in case the buffer allocated according to the size returned in previous // call is not enough. // uint pnProcInfo = 0; RM_PROCESS_INFO[]? rgAffectedApps = null; int retry = 0; do { uint lpdwRebootReasons = RM_REBOOT_REASON_NONE; res = RmGetList(handle, out uint pnProcInfoNeeded, ref pnProcInfo, rgAffectedApps, ref lpdwRebootReasons); if (res == 0) { // If pnProcInfo == 0, then there is simply no locking process (found), in this case rgAffectedApps is "null". if (pnProcInfo == 0) { return []; } Debug.Assert(rgAffectedApps != null); var lockInfos = ArrayBuilder<(int, string)>.GetInstance((int)pnProcInfo); for (int i = 0; i < pnProcInfo; i++) { lockInfos.Add(((int)rgAffectedApps[i].Process.dwProcessId, rgAffectedApps[i].strAppName)); } return lockInfos.ToImmutableAndFree(); } if (res != ERROR_MORE_DATA) { return []; } pnProcInfo = pnProcInfoNeeded; rgAffectedApps = new RM_PROCESS_INFO[pnProcInfo]; } while (res == ERROR_MORE_DATA && retry++ < MaxRetries); } finally { _ = RmEndSession(handle); } return []; } }