File: ProjectSystem\RuleSets\VisualStudioRuleSetManager.RuleSetFile.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ProjectSystem;
using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
 
internal sealed partial class VisualStudioRuleSetManager
{
    private sealed class RuleSetFile : IRuleSetFile, IDisposable
    {
        private readonly VisualStudioRuleSetManager _ruleSetManager;
        private readonly object _gate = new();
        private readonly CancellationTokenSource _disposalCancellationSource;
        private readonly CancellationToken _disposalToken;
 
        private IFileChangeContext _fileChangeContext;
 
        private ReportDiagnostic _generalDiagnosticOption;
        private ImmutableDictionary<string, ReportDiagnostic> _specificDiagnosticOptions;
        private bool _subscribed = false;
        private bool _optionsRead = false;
        private bool _removedFromRuleSetManager = false;
 
        private Exception _exception;
 
        public RuleSetFile(string filePath, VisualStudioRuleSetManager ruleSetManager)
        {
            FilePath = filePath;
            _ruleSetManager = ruleSetManager;
 
            _disposalCancellationSource = new();
            _disposalToken = _disposalCancellationSource.Token;
        }
 
        public void InitializeFileTracking(IFileChangeWatcher fileChangeWatcher)
        {
            lock (_gate)
            {
                if (_fileChangeContext == null)
                {
                    ImmutableArray<string> includes;
 
                    try
                    {
                        includes = RuleSet.GetEffectiveIncludesFromFile(FilePath);
                    }
                    catch (Exception e)
                    {
                        // We couldn't read the rule set for whatever reason. Capture the exception
                        // so we can surface the error later, and subscribe to file change notifications
                        // so that we'll automatically reload the file if the user can fix the issue.
                        _optionsRead = true;
                        _specificDiagnosticOptions = ImmutableDictionary<string, ReportDiagnostic>.Empty;
                        _exception = e;
 
                        includes = [FilePath];
                    }
 
                    _fileChangeContext = fileChangeWatcher.CreateContext([]);
                    _fileChangeContext.FileChanged += IncludeUpdated;
 
                    foreach (var include in includes)
                    {
                        _fileChangeContext.EnqueueWatchingFile(include);
                    }
                }
            }
        }
 
        public event EventHandler UpdatedOnDisk;
 
        public string FilePath { get; }
 
        public Exception GetException()
        {
            EnsureSubscriptions();
            EnsureDiagnosticOptionsRead();
 
            return _exception;
        }
 
        public ReportDiagnostic GetGeneralDiagnosticOption()
        {
            EnsureSubscriptions();
            EnsureDiagnosticOptionsRead();
 
            return _generalDiagnosticOption;
        }
 
        public ImmutableDictionary<string, ReportDiagnostic> GetSpecificDiagnosticOptions()
        {
            EnsureSubscriptions();
            EnsureDiagnosticOptionsRead();
 
            return _specificDiagnosticOptions;
        }
 
        private void EnsureSubscriptions()
        {
            lock (_gate)
            {
                if (!_subscribed)
                {
                    // TODO: ensure subscriptions now
                    _subscribed = true;
                }
            }
        }
 
        private void EnsureDiagnosticOptionsRead()
        {
            lock (_gate)
            {
                if (!_optionsRead)
                {
                    _optionsRead = true;
                    var specificDiagnosticOptions = new Dictionary<string, ReportDiagnostic>();
 
                    try
                    {
                        var effectiveRuleset = RuleSet.LoadEffectiveRuleSetFromFile(FilePath);
                        _generalDiagnosticOption = effectiveRuleset.GeneralDiagnosticOption;
                        foreach (var rule in effectiveRuleset.SpecificDiagnosticOptions)
                        {
                            specificDiagnosticOptions.Add(rule.Key, rule.Value);
                        }
 
                        _specificDiagnosticOptions = specificDiagnosticOptions.ToImmutableDictionary();
                    }
                    catch (Exception e)
                    {
                        _exception = e;
                    }
                }
            }
        }
 
        public void Dispose()
        {
            RemoveFromRuleSetManagerAndDisconnectFileTrackers();
            _disposalCancellationSource.Cancel();
            _disposalCancellationSource.Dispose();
        }
 
        private void RemoveFromRuleSetManagerAndDisconnectFileTrackers()
        {
            lock (_gate)
            {
                _fileChangeContext.Dispose();
 
                if (_removedFromRuleSetManager)
                {
                    return;
                }
 
                _removedFromRuleSetManager = true;
            }
 
            // Call outside of lock to avoid general surprises; we skip this with the return above inside the lock.
            _ruleSetManager.StopTrackingRuleSetFile(this);
        }
 
        private void IncludeUpdated(object sender, string fileChanged)
        {
            // The file change service is going to notify us of updates on the foreground thread.
            // This is going to cause us to drop our existing subscriptions and create new ones.
            // However, the FileChangeTracker signs up for subscriptions in a Task on a background thread.
            // We can easily end up with the foreground thread waiting on the Task, which is blocked
            // waiting for the foreground thread to release its lock on the file change service.
            // To avoid this, just queue up a Task to do the work on the foreground thread later, after
            // the lock on the file change service has been released.
            _ruleSetManager._threadingContext.JoinableTaskFactory.RunAsync(async () =>
            {
                using var _ = _ruleSetManager._listener.BeginAsyncOperation("IncludeUpdated");
                await _ruleSetManager._threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _disposalToken);
                IncludeUpdateCore();
            });
        }
 
        private void IncludeUpdateCore()
        {
            // It's critical that RemoveFromRuleSetManagerAndDisconnectFileTrackers() is called first prior to raising the event
            // -- this way any callers who call the RuleSetManager asking for the new file are guaranteed to get the new snapshot first.
            // idempotent.
            RemoveFromRuleSetManagerAndDisconnectFileTrackers();
            UpdatedOnDisk?.Invoke(this, EventArgs.Empty);
        }
    }
}