简体   繁体   中英

How to use Rx to monitor a project file and files for external changes?

I would like to reproduce the behavior of Visual Studio which informs you when a project file is touched externally and proposes to reload it!

Due to the requirements, I believe reactive is a great match to solve that problem.

I am using a modified reactive FileSystemWatcher described in this post: http://www.jaylee.org/post/2012/08/26/An-update-to-matthieumezil-Rx-and-the-FileSystemWatcher.aspx

public class FileWatcher
{
    private static readonly ILog Logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

    public static IObservable<FileChanged> ObserveFolderChanges(string path, string filter, TimeSpan throttle, Predicate<string> isPartOfProject)
    {
        return Observable.Create<FileChanged>(
            observer =>
            {
                var fileSystemWatcher = new FileSystemWatcher(path, filter) { EnableRaisingEvents = true, IncludeSubdirectories = true };

                var sources = new[]
                {
                    Observable.FromEventPattern<FileSystemEventArgs>(fileSystemWatcher, "Created")
                              .Where(IsMaybeAProjectFile)
                              .Select(ev => new FileChanged(ev.EventArgs.FullPath, FileChangeTypes.Added, SourceChangeTypes.FileSystem)),

                    Observable.FromEventPattern<FileSystemEventArgs>(fileSystemWatcher, "Deleted")
                              .Where(IsMaybeAProjectFile)
                              .Select(ev => new FileChanged(ev.EventArgs.FullPath, FileChangeTypes.Deleted, SourceChangeTypes.FileSystem))
                };

                return sources.Merge()
                              .Throttle(throttle)
                              .Do(changed =>
                              {
                                  if (Logger.IsDebugEnabled)
                                  {
                                      Logger.Debug($"FileWatcher event [{changed.FileChangeType}] {changed.FullPath}");
                                  }
                              })
                              .Finally(() => fileSystemWatcher.Dispose())
                              .Subscribe(observer);
            }
        );
    }

    private static bool IsMaybeAProjectFile(EventPattern<FileSystemEventArgs> ev)
    {
        return ev.EventArgs.FullPath.EndsWith(".zip") || ev.EventArgs.FullPath.EndsWith(".skye");
    }
}

public class FileChanged
{
    public string FullPath { get; }

    public FileChangeTypes FileChangeType { get; }

    public SourceChangeTypes SourceChangeType { get; }

    public FileChanged(string fullPath, FileChangeTypes fileChangeType, SourceChangeTypes sourceChangeType)
    {
        FullPath = fullPath;
        FileChangeType = fileChangeType;
        SourceChangeType = sourceChangeType;
    }
}

[Flags]
public enum FileChangeTypes
{
    Added = 1,
    Deleted = 2
}

[Flags]
public enum SourceChangeTypes
{
    FileSystem = 1,
    Project = 2
}

Now in my application I created an event

    private ProjectChangedEventHandler ProjectChanged { get; set; }

    private void OnProjectChanged(FileChanged fileChanged)
    {
        ProjectChanged?.Invoke(this, fileChanged);
    }

    public delegate void ProjectChangedEventHandler(object sender, FileChanged fileChanged);

Which is used like this when I delete or a add a file from the project

        OnProjectChanged(new FileChanged(archive.Filename, FileChangeTypes.Deleted, SourceChangeTypes.Project));

        OnProjectChanged(new FileChanged(archive.Filename, FileChangeTypes.Added, SourceChangeTypes.Project));

Now I can start to leverage those two streams and with a join (which needs fine tuning for the left and right duration selector) I am able to detect which file was modified by my application:

    private void ObserveProjectModifications(string projectFilePath)
    {
        _observeFolderChanges = FileWatcher.ObserveFolderChanges(Path.GetDirectoryName(projectFilePath), "*.*", TimeSpan.FromMilliseconds(500), IsPartOfProject);

        _observeProjectChanges = Observable.FromEventPattern<ProjectChangedEventHandler, FileChanged>(h => ProjectChanged += h, h => ProjectChanged -= h).Select(pattern => pattern.EventArgs);

        _changes = _observeProjectChanges.Join(_observeFolderChanges, _ => Observable.Never<Unit>(),  _ => Observable.Never<Unit>(), ResultSelector).Where(changed => IsPartOfProject(changed.FullPath));
    }

    private FileChanged ResultSelector(FileChanged fileChanged, FileChanged projectChanged)
    {
        if (Logger.IsDebugEnabled)
        {
            Logger.Debug($"ResultSelector File [{fileChanged.FileChangeType}] {fileChanged.FullPath} # Project [{projectChanged.FileChangeType}] {projectChanged.FullPath}");
        }

        if (fileChanged.FullPath == projectChanged.FullPath)
        {
            if (fileChanged.FileChangeType == projectChanged.FileChangeType)
            {
                if (fileChanged.SourceChangeType != projectChanged.SourceChangeType)
                {
                    return projectChanged;
                }

                return fileChanged;
            }

            return fileChanged;
        }

        return fileChanged;
    }

    private bool IsPartOfProject(string fullPath)
    {
        if (_projectFileManager.ProjectFilePath.Equals(fullPath)) return true;

        return _archives.Values.Any(a => a.Filename.Equals(fullPath));
    }

My issue is that I also want to know that a file was modified externally! Any idea would be really helpful! Thanks

Unfortunatelly the FileSystemWatcher doesn't provide information which process has modified the file, so you are bit out of luck there. There are few possibilities that I can think of:

  1. Ignore flag - When your application is doing a change you can set a flag and ignore the events when the flag is set. This is the simplest way, but you might miss some external change if it happens concurrently when the flag is set and also it gets even more complicated due to throttling you have.
  2. Tagging the file - whenever you do a change to the file you generate a guid (or similar) which you will use to tag the file. And then whenever the file change is fired, you check the file property (can be stored either as real filesystem file property - similar for example to jpeg metadata you see in details in file explorer, there are more ways to set such file property) and then if the tag is different from what you have or is missing then you know it is external - there you need to also take care due to throttling and the tag being outdated etc
  3. Minifilter file system driver - This would be the cleanest solution and probably is very close to what Visual studio is using - just a guess though. It is basically a universal windows driver that monitors any I/O change. Microsoft has created reference implementation called minispy , which is small tool to monitor and log any I/O and transaction activity that occurs in the system. You don't have to implement the driver yourself as there is already a 3rd party FileSystemWatcher implemented using this approach on github. That file system watcher provides information which process has modified the file. The only problem here is that the driver itself needs to be installed, before it can be used, so you need admin privileged installer of sort.

At the moment that's all I can think of.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM