| File: BackEnd\Components\FileAccesses\FileAccessManager.cs | Web Access |
| Project: src\msbuild\src\Build\Microsoft.Build.csproj (Microsoft.Build) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #if FEATURE_REPORTFILEACCESSES using System; using System.Collections.Concurrent; using System.Runtime.Versioning; using System.Threading; using Microsoft.Build.BackEnd; using Microsoft.Build.Execution; using Microsoft.Build.Experimental.FileAccess; using Microsoft.Build.Framework; using Microsoft.Build.Shared.FileSystem; namespace Microsoft.Build.FileAccesses { internal sealed class FileAccessManager : IFileAccessManager { private record Handlers(Action<BuildRequest, FileAccessData> FileAccessHander, Action<BuildRequest, ProcessData> ProcessHandler); // In order to synchronize between the node communication and the file access reporting, a special file access // is used to mark when the file accesses should be considered complete. Only after both this special file access is seen // and the build result is reported can plugins be notified about project completion. // NOTE! This is currently Windows-specific and will need to change once this feature is opened up to more scenarios. private static readonly string FileAccessCompletionPrefix = $@"{BuildParameters.StartupDirectory[0]}:\{{MSBuildFileAccessCompletion}}\"; private IScheduler? _scheduler; private IConfigCache? _configCache; private LockType _handlersWriteLock = new LockType(); private Handlers[] _handlers = Array.Empty<Handlers>(); private string? _tempDirectory; // Keyed on global request id private readonly ConcurrentDictionary<int, ManualResetEventSlim> _fileAccessCompletionWaitHandles = new(); public static IBuildComponent CreateComponent(BuildComponentType type) { ArgumentOutOfRangeException.ThrowIfNotEqual(type, BuildComponentType.FileAccessManager); return new FileAccessManager(); } public void InitializeComponent(IBuildComponentHost host) { _scheduler = host.GetComponent(BuildComponentType.Scheduler) as IScheduler; _configCache = host.GetComponent(BuildComponentType.ConfigCache) as IConfigCache; _tempDirectory = FileUtilities.EnsureNoTrailingSlash(FileUtilities.TempFileDirectory); } public void ShutdownComponent() { _scheduler = null; _configCache = null; _tempDirectory = null; _fileAccessCompletionWaitHandles.Clear(); } public void ReportFileAccess(FileAccessData fileAccessData, int nodeId) { string fileAccessPath = fileAccessData.Path; // Intercept and avoid forwarding the file access completion if (fileAccessPath.StartsWith(FileAccessCompletionPrefix, StringComparison.Ordinal)) { // Parse out the global request id. Note, this must match what NotifyFileAccessCompletion does. int globalRequestId = int.Parse(fileAccessPath.Substring(FileAccessCompletionPrefix.Length)); ManualResetEventSlim handle = _fileAccessCompletionWaitHandles.GetOrAdd(globalRequestId, static _ => new ManualResetEventSlim()); handle.Set(); } else if (_tempDirectory != null && fileAccessPath.StartsWith(_tempDirectory, FileUtilities.PathComparison)) { // Ignore MSBuild's temp directory as these are related to internal MSBuild functionality and not always directly related to the execution of the project itself, // so should not be exposed to handlers. Note that this is not %TEMP% but instead a subdir under %TEMP% which is only expected to be used by MSBuild. return; } else { // Forward the file access to handlers. BuildRequest? buildRequest = GetBuildRequest(nodeId); if (buildRequest != null) { Handlers[] localHandlers = _handlers; foreach (Handlers handlers in localHandlers) { handlers.FileAccessHander.Invoke(buildRequest, fileAccessData); } } } } public void ReportProcess(ProcessData processData, int nodeId) { BuildRequest? buildRequest = GetBuildRequest(nodeId); if (buildRequest != null) { Handlers[] localHandlers = _handlers; foreach (Handlers handlers in localHandlers) { handlers.ProcessHandler.Invoke(buildRequest, processData); } } } public HandlerRegistration RegisterHandlers(Action<BuildRequest, FileAccessData> fileAccessHandler, Action<BuildRequest, ProcessData> processHandler) { lock (_handlersWriteLock) { Handlers[] newHandlers = new Handlers[_handlers.Length + 1]; _handlers.CopyTo(newHandlers, 0); Handlers addedHandlers = new(fileAccessHandler, processHandler); newHandlers[_handlers.Length] = addedHandlers; _handlers = newHandlers; return new HandlerRegistration(() => UnregisterHandlers(addedHandlers)); } } private void UnregisterHandlers(Handlers handlersToRemove) { lock (_handlersWriteLock) { Handlers[] newHandlers = new Handlers[_handlers.Length - 1]; int newHandlersIdx = 0; for (int handlersIdx = 0; handlersIdx < _handlers.Length; handlersIdx++) { if (_handlers[handlersIdx] != handlersToRemove) { newHandlers[newHandlersIdx] = _handlers[handlersIdx]; newHandlersIdx++; } } _handlers = newHandlers; } } // The [SupportedOSPlatform] attribute is a safeguard to ensure that the comment on FileAccessCompletionPrefix regarding being Windows-only gets addressed. // [SupportedOSPlatform] doesn't apply to fields, so using it here as a reasonable proxy. [SupportedOSPlatform("windows")] public static void NotifyFileAccessCompletion(int globalRequestId) { // Make a dummy file access to use as a notification that the file accesses should be completed for a project. string filePath = FileAccessCompletionPrefix + globalRequestId.ToString(); _ = FileSystems.Default.FileExists(filePath); } public void WaitForFileAccessReportCompletion(int globalRequestId, CancellationToken cancellationToken) { ManualResetEventSlim handle = _fileAccessCompletionWaitHandles.GetOrAdd(globalRequestId, static _ => new ManualResetEventSlim()); if (!handle.IsSet) { handle.Wait(cancellationToken); } // Try to keep the collection clean. A request should not need to be completed twice. _fileAccessCompletionWaitHandles.TryRemove(globalRequestId, out _); } private BuildRequest? GetBuildRequest(int nodeId) { Assumed.True(_scheduler != null && _configCache != null, "Component has not been initialized"); // Note: If the node isn't executing anything it may be accessing binaries required to run, eg. the MSBuild binaries return _scheduler!.GetExecutingRequestByNode(nodeId); } internal readonly struct HandlerRegistration : IDisposable { private readonly Action _unregisterAction; public HandlerRegistration(Action unregisterAction) { _unregisterAction = unregisterAction; } public void Dispose() { _unregisterAction(); } } } } #endif