FileChangeTracker.cs 7.3 KB
Newer Older
1 2 3
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

using System;
4
using System.IO;
5 6 7
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
8
using Microsoft.CodeAnalysis.ErrorReporting;
9 10 11 12 13 14 15
using Microsoft.VisualStudio.Shell.Interop;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem
{
    internal sealed class FileChangeTracker : IVsFileChangeEvents, IDisposable
    {
16 17
        private const uint FileChangeFlags = (uint)(_VSFILECHANGEFLAGS.VSFILECHG_Time | _VSFILECHANGEFLAGS.VSFILECHG_Add | _VSFILECHANGEFLAGS.VSFILECHG_Del | _VSFILECHANGEFLAGS.VSFILECHG_Size);

18
        private static readonly Lazy<uint?> s_none = new Lazy<uint?>(() => null, LazyThreadSafetyMode.ExecutionAndPublication);
19 20 21 22 23 24 25

        private readonly IVsFileChangeEx _fileChangeService;
        private readonly string _filePath;
        private bool _disposed;

        /// <summary>
        /// The cookie received from the IVsFileChangeEx interface that is watching for changes to
26 27
        /// this file. This field may never be null, but might be a Lazy that has a value of null if
        /// we either failed to subscribe over never have tried to subscribe.
28
        /// </summary>
29
        private Lazy<uint?> _fileChangeCookie;
30 31 32

        public event EventHandler UpdatedOnDisk;

33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
        /// <summary>
        /// Operations on <see cref="IVsFileChangeEx"/> synchronize on a single lock within that service, so there's no point
        /// in us trying to have multiple threads all trying to use it at the same time. When we queue a new background thread operation
        /// we'll just do a continuation after the previous one. Any callers of <see cref="EnsureSubscription"/> will bypass that queue
        /// and ensure it happens quickly.
        /// </summary>
        private static Task s_lastBackgroundTask = Task.CompletedTask;

        /// <summary>
        /// The object to use as a monitor guarding <see cref="s_lastBackgroundTask"/>. This lock is not strictly necessary, since we don't need
        /// to ensure the background tasks happen entirely sequentially -- if we just removed the lock, and two subscriptions happened, we end up with
        /// a 'branching' set of continuations, but that's fine since we're generally not running things in parallel. But it's easy to write,
        /// and easy to delete if this lock has contention itself. Given we tend to call <see cref="StartFileChangeListeningAsync"/> on the UI
        /// thread, I don't expect to see contention.
        /// </summary>
        private static readonly object s_lastBackgroundTaskGate = new object();

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
        public FileChangeTracker(IVsFileChangeEx fileChangeService, string filePath)
        {
            _fileChangeService = fileChangeService;
            _filePath = filePath;
            _fileChangeCookie = s_none;
        }

        ~FileChangeTracker()
        {
            if (!Environment.HasShutdownStarted)
            {
                this.AssertUnsubscription();
            }
        }

        public string FilePath
        {
            get { return _filePath; }
        }

70 71 72 73 74 75 76 77 78 79 80 81
        /// <summary>
        /// Returns true if a previous call to <see cref="StartFileChangeListeningAsync"/> has completed.
        /// </summary>
        public bool PreviousCallToStartFileChangeHasAsynchronouslyCompleted
        {
            get
            {
                var cookie = _fileChangeCookie;
                return cookie != s_none && cookie.IsValueCreated;
            }
        }

82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
        public void AssertUnsubscription()
        {
            // We must have been disposed properly.
            Contract.ThrowIfTrue(_fileChangeCookie != s_none);
        }

        public void EnsureSubscription()
        {
            // make sure we have file notification subscribed
            var unused = _fileChangeCookie.Value;
        }

        public void StartFileChangeListeningAsync()
        {
            if (_disposed)
            {
98
                throw new ObjectDisposedException(nameof(FileChangeTracker));
99 100 101 102
            }

            Contract.ThrowIfTrue(_fileChangeCookie != s_none);

103
            _fileChangeCookie = new Lazy<uint?>(() =>
104
            {
105 106 107 108 109 110
                try
                {
                    Marshal.ThrowExceptionForHR(
                        _fileChangeService.AdviseFileChange(_filePath, FileChangeFlags, this, out var newCookie));
                    return newCookie;
                }
111
                catch (Exception e) when (ReportException(e))
112 113 114
                {
                    return null;
                }
115 116
            }, LazyThreadSafetyMode.ExecutionAndPublication);

117 118 119 120
            lock (s_lastBackgroundTaskGate)
            {
                s_lastBackgroundTask = s_lastBackgroundTask.ContinueWith(_ => _fileChangeCookie.Value, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
            }
121 122
        }

123
        private static bool ReportException(Exception e)
124
        {
125 126
            // If we got a PathTooLongException there's really nothing we can do about it; we will fail to read the file later which is fine
            if (!(e is PathTooLongException))
127 128 129
            {
                return FatalError.ReportWithoutCrash(e);
            }
130 131 132 133

            // We'll always capture all exceptions regardless. If we don't, then the exception is captured by our lazy and will be potentially rethrown from
            // StopFileChangeListening or Dispose which causes all sorts of downstream problems.
            return true;
134 135
        }

136 137 138 139
        public void StopFileChangeListening()
        {
            if (_disposed)
            {
140
                throw new ObjectDisposedException(nameof(FileChangeTracker));
141 142 143 144 145
            }

            // there is a slight chance that we haven't subscribed to the service yet so we subscribe and unsubscribe
            // both here unnecessarily. but I believe that probably is a theoretical problem and never happen in real life.
            // and even if that happens, it will be just a perf hit
146
            if (_fileChangeCookie == s_none)
147
            {
148 149 150 151 152
                return;
            }

            var fileChangeCookie = _fileChangeCookie.Value;
            _fileChangeCookie = s_none;
153

154 155 156 157 158 159 160 161
            // We may have tried to subscribe but failed, so have to check a second time
            if (fileChangeCookie.HasValue)
            {
                try
                {
                    Marshal.ThrowExceptionForHR(
                        _fileChangeService.UnadviseFileChange(fileChangeCookie.Value));
                }
162
                catch (Exception e) when (ReportException(e))
163 164
                {
                }
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
            }
        }

        public void Dispose()
        {
            this.StopFileChangeListening();

            _disposed = true;

            GC.SuppressFinalize(this);
        }

        int IVsFileChangeEvents.DirectoryChanged(string directory)
        {
            throw new Exception("We only watch files; we should never be seeing directory changes!");
        }

        int IVsFileChangeEvents.FilesChanged(uint changeCount, string[] files, uint[] changes)
        {
C
Cyrus Najmabadi 已提交
184
            UpdatedOnDisk?.Invoke(this, EventArgs.Empty);
185 186 187 188 189

            return VSConstants.S_OK;
        }
    }
}