FileChangeTracker.cs 5.8 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 19 20 21 22 23 24 25 26 27 28 29 30 31
        private static readonly Lazy<uint> s_none = new Lazy<uint>(() => /* value doesn't matter*/ 42424242, LazyThreadSafetyMode.ExecutionAndPublication);

        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
        /// this file.
        /// </summary>
        private Lazy<uint> _fileChangeCookie;

        public event EventHandler UpdatedOnDisk;

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
        /// <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();

49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
        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; }
        }

        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)
            {
85
                throw new ObjectDisposedException(nameof(FileChangeTracker));
86 87 88 89 90 91 92
            }

            Contract.ThrowIfTrue(_fileChangeCookie != s_none);

            _fileChangeCookie = new Lazy<uint>(() =>
            {
                Marshal.ThrowExceptionForHR(
C
CyrusNajmabadi 已提交
93
                    _fileChangeService.AdviseFileChange(_filePath, FileChangeFlags, this, out var newCookie));
94 95 96
                return newCookie;
            }, LazyThreadSafetyMode.ExecutionAndPublication);

97 98 99 100
            lock (s_lastBackgroundTaskGate)
            {
                s_lastBackgroundTask = s_lastBackgroundTask.ContinueWith(_ => _fileChangeCookie.Value, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
            }
101 102 103 104 105 106
        }

        public void StopFileChangeListening()
        {
            if (_disposed)
            {
107
                throw new ObjectDisposedException(nameof(FileChangeTracker));
108 109 110 111 112 113 114
            }

            // 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
            if (_fileChangeCookie != s_none)
            {
115 116 117 118 119 120 121 122 123
                var hr = _fileChangeService.UnadviseFileChange(_fileChangeCookie.Value);

                // Verify if the file still exists before reporting the unadvise failure.
                // This is a workaround for VSO #248774
                if (hr != VSConstants.S_OK && File.Exists(_filePath))
                {
                    Marshal.ThrowExceptionForHR(hr);
                }

124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
                _fileChangeCookie = s_none;
            }
        }

        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 已提交
144
            UpdatedOnDisk?.Invoke(this, EventArgs.Empty);
145 146 147 148 149

            return VSConstants.S_OK;
        }
    }
}