Permalink
Browse files

Codeplex WorkItem 6118: Throttle file system change notifications to …

…prevent rapid file add/deletes from breaking source control bindings
  • Loading branch information...
1 parent ece5b9e commit 674aa76aa7d90b64214003f50e1b01c4c6d02188 @borland borland committed May 16, 2011
Showing with 119 additions and 52 deletions.
  1. +119 −52 Tools/IronStudio/IronStudio/IronStudio/Project/DirectoryBasedProjectNode.cs
@@ -21,11 +21,17 @@
using Microsoft.VisualStudio.Project;
using Microsoft.VisualStudio.Shell.Interop;
using System.Windows.Forms;
+using System.Collections.Generic;
namespace Microsoft.IronStudio.Project {
public abstract class DirectoryBasedProjectNode : CommonProjectNode {
+ private Dispatcher _dispatcher;
private FileSystemWatcher _projectWatcher;
+ private static readonly TimeSpan _fileChangeThrottleInterval = TimeSpan.FromSeconds(0.2);
+ private DispatcherTimer _timer = null;
+ private readonly Dictionary<string, Action> _pendingFileUpdates = new Dictionary<string, Action>();
+
public DirectoryBasedProjectNode(CommonProjectPackage package, ImageList imageList)
: base(package, imageList) {
}
@@ -101,9 +107,13 @@ public DirectoryBasedProjectNode(CommonProjectPackage package, ImageList imageLi
_projectWatcher.Deleted -= new FileSystemEventHandler(FileDeleted);
}
+ if (_dispatcher == null) {
+ _dispatcher = Dispatcher.CurrentDispatcher;
+ }
+
_projectWatcher = new FileSystemWatcher(ProjectDir);
_projectWatcher.IncludeSubdirectories = true;
- _projectWatcher.SynchronizingObject = new SynchronizingInvoke(Dispatcher.CurrentDispatcher);
+ _projectWatcher.SynchronizingObject = new SynchronizingInvoke(_dispatcher);
_projectWatcher.Created += FileCreated;
_projectWatcher.Deleted += FileDeleted;
@@ -112,46 +122,100 @@ public DirectoryBasedProjectNode(CommonProjectPackage package, ImageList imageLi
_projectWatcher.EnableRaisingEvents = true;
}
- private void FileDeleted(object sender, FileSystemEventArgs e) {
- Debug.Assert(e.ChangeType == WatcherChangeTypes.Deleted);
+ private Action CreateSafeCallback(FileSystemEventArgs notification, Action callback) {
+ Predicate<string> exists = (s) => File.Exists(s) || Directory.Exists(s);
+
+ switch (notification.ChangeType) {
+ case WatcherChangeTypes.Created: // ignore rapid add/delete
+ return () => {
+ if (exists(notification.FullPath))
+ callback();
+ };
+ case WatcherChangeTypes.Deleted: // ignore rapid delete/re-add
+ case WatcherChangeTypes.Renamed: // ignore rapid rename/revert
+ return () => {
+ if (!exists(notification.FullPath))
+ callback();
+ };
+ default:
+ throw new NotSupportedException(String.Format("CreateSafeCallback is not supported for change type {0}", notification.ChangeType));
+ };
+ }
+
+ // Problem: Many applications (including visual studio itself) will save a file by deleting and re-creating it.
+ // If we issue a Delete/Recreate, then source control providers will pend a delete on the file.
+ // This is particularly nasty when merging changes to files just before a checkin, and can lead to accidentally pending
+ // deletes for files you're trying to edit.
+ // Solution: Throttle file system notifications, and only delete the file if it is actually gone some interval (say 0.2 seconds) later
+ private void ThrottleFileUpdate(FileSystemEventArgs notification, Action callback) {
+ _dispatcher.VerifyAccess();
+
+ // we only worry about keeping the single last operation for any given file
+ _pendingFileUpdates[notification.FullPath] = CreateSafeCallback(notification, callback);
- HierarchyNode child = FindChild(e.FullPath);
- if (child != null) {
- // TODO: We shouldn't be closing any documents, we probably need to pass a flag in here.
- // Unfortunately it's not really simple because when we remove the child from the parent
- // the file is no longer savable if it's already open. So for now the file just simply
- // disappears - deleting it from the file system means you better want it gone from
- // the editor as well.
- child.Remove(false);
+ if(_timer != null) {
+ _timer.Stop();
}
+
+ _timer = new DispatcherTimer(_fileChangeThrottleInterval, DispatcherPriority.Normal, (s, e) => {
+ _dispatcher.VerifyAccess();
+ _timer.Stop(); // don't repeat
+
+ // flush all the pending updates.
+ var copy = new List<Action>(_pendingFileUpdates.Values);
+ _pendingFileUpdates.Clear();
+
+ foreach (var t in copy) {
+ callback();
+ }
+ }, _dispatcher);
+ _timer.Start();
+ }
+
+ private void FileDeleted(object sender, FileSystemEventArgs e) {
+ Debug.Assert(e.ChangeType == WatcherChangeTypes.Deleted);
+ ThrottleFileUpdate(e, () => {
+ HierarchyNode child = FindChild(e.FullPath);
+ if (child != null) {
+ // TODO: We shouldn't be closing any documents, we probably need to pass a flag in here.
+ // Unfortunately it's not really simple because when we remove the child from the parent
+ // the file is no longer savable if it's already open. So for now the file just simply
+ // disappears - deleting it from the file system means you better want it gone from
+ // the editor as well.
+ child.Remove(false);
+ }
+ });
}
private void FileRenamed(object sender, RenamedEventArgs e) {
Debug.Assert(e.ChangeType == WatcherChangeTypes.Renamed);
- var child = FindChild(e.OldFullPath);
- if (child != null) {
- FileNode fileNode = child as FileNode;
- if (fileNode != null) {
- // file nodes could be open so we'll need to update any
- // state such as the file caption
- try {
- fileNode.RenameDocument(e.OldFullPath, e.FullPath);
- } catch (Exception) {
+ ThrottleFileUpdate(e, () => {
+
+ var child = FindChild(e.OldFullPath);
+ if (child != null) {
+ FileNode fileNode = child as FileNode;
+ if (fileNode != null) {
+ // file nodes could be open so we'll need to update any
+ // state such as the file caption
+ try {
+ fileNode.RenameDocument(e.OldFullPath, e.FullPath);
+ } catch (Exception) {
+ }
}
- }
- FolderNode folderNode = child as FolderNode;
- if (folderNode != null) {
- try {
- folderNode.RenameFolder(e.FullPath);
- } catch (Exception) {
+ FolderNode folderNode = child as FolderNode;
+ if (folderNode != null) {
+ try {
+ folderNode.RenameFolder(e.FullPath);
+ } catch (Exception) {
+ }
}
- }
- child.ItemNode.Rename(e.FullPath);
- child.OnInvalidateItems(child.Parent);
- }
+ child.ItemNode.Rename(e.FullPath);
+ child.OnInvalidateItems(child.Parent);
+ }
+ });
}
private void FileCreated(object sender, FileSystemEventArgs e) {
@@ -162,31 +226,34 @@ public DirectoryBasedProjectNode(CommonProjectPackage package, ImageList imageLi
Debug.Assert(e.ChangeType == WatcherChangeTypes.Created);
- // find the parent where the new node will be inserted
- HierarchyNode parent;
- string path = e.FullPath;
- for (; ; ) {
- string dir = Path.GetDirectoryName(path);
- if (NativeMethods.IsSamePath(dir, ProjectDir)) {
- parent = this;
- break;
- }
+ ThrottleFileUpdate(e, () => {
+
+ // find the parent where the new node will be inserted
+ HierarchyNode parent;
+ string path = e.FullPath;
+ for (; ; ) {
+ string dir = Path.GetDirectoryName(path);
+ if (NativeMethods.IsSamePath(dir, ProjectDir)) {
+ parent = this;
+ break;
+ }
- parent = FindChild(dir);
- if (parent == null) {
- path = dir;
- } else {
- break;
+ parent = FindChild(dir);
+ if (parent == null) {
+ path = dir;
+ } else {
+ break;
+ }
}
- }
- // and then insert either a file or directory node
- FileInfo fi = new FileInfo(e.FullPath);
- if ((fi.Attributes & FileAttributes.Directory) != 0) {
- AddDirectory(parent, false, path);
- } else if (ShouldIncludeFileInProject(GetProjectFileExtension(), e.FullPath)) {
- AddFile(parent, false, e.FullPath);
- }
+ // and then insert either a file or directory node
+ FileInfo fi = new FileInfo(e.FullPath);
+ if ((fi.Attributes & FileAttributes.Directory) != 0) {
+ AddDirectory(parent, false, path);
+ } else if (ShouldIncludeFileInProject(GetProjectFileExtension(), e.FullPath)) {
+ AddFile(parent, false, e.FullPath);
+ }
+ });
}
/// <summary>

0 comments on commit 674aa76

Please sign in to comment.