using System; using System.Collections; using System.Collections; using System.Text; using System.Threading.Tasks; using System.IO; using Beefy; using System.Threading; using System.Diagnostics; namespace IDE { public class FileWatcher { class WatcherEntry : RefCounted { // Either mFileSystemWatcher or mParentDirWatcher is set public FileSystemWatcher mFileSystemWatcher ~ delete _; public WatcherEntry mParentDirWatcher; public String mDirectoryName; public bool mIgnoreWrites; public bool mHasSubDirDependencies; public HashSet mAmbiguousCreatePaths = new .() ~ DeleteContainerAndItems!(_); } class DepInfo { public List mDependentObjects = new List() ~ delete _; public String mContent ~ delete _; } struct QueuedRefreshEntry { public WatcherEntry mWatcherEntry; public int32 mDelay; } class QueuedFileChange { public String mFileName ~ delete _; public String mNewFileName ~ delete _; public WatcherChangeTypes mChangeType; } public class ChangeRecord { public String mPath ~ delete _; public WatcherChangeTypes mChangeType; } // One watcher per directory public static int32 sDbgFileCreateDelay; Dictionary mWatchers = new Dictionary(); Dictionary mWatchedFiles = new Dictionary() ~ DeleteDictionaryAndKeysAndValues!(_); // Including ref count List mChangeList = new .() ~ DeleteContainerAndItems!(_); Dictionary mChangeMap = new .() ~ delete _; HashSet mDependencyChangeSet = new .() ~ delete _; List mDependencyChangeList = new .() ~ delete _; List mQueuedRefreshWatcherEntries = new List() ~ delete _; public Monitor mMonitor = new Monitor() ~ delete _; List mQueuedFileChanges = new List() ~ DeleteContainerAndItems!(_); public Monitor mFileChangeMonitor = new Monitor() ~ delete _; public int mChangeId; public ~this() { for (var watcher in mWatchers) { delete watcher.key; watcher.value.DeleteUnchecked(); // Allow even when refCount isn't zero } delete mWatchers; } void FixFilePath(String filePath) { String oldPath = scope String(filePath); filePath.Clear(); Path.GetFullPath(oldPath, filePath); IDEUtils.FixFilePath(filePath); if (!Environment.IsFileSystemCaseSensitive) filePath.ToUpper(); } void FileChanged(String filePath, String newPath, WatcherChangeTypes changeType) { bool isDirectory = filePath.EndsWith(Path.DirectorySeparatorChar); if (!filePath.EndsWith('*')) { String starPath = Path.GetDirectoryPath(filePath, .. scope .()); starPath.Append(Path.DirectorySeparatorChar); starPath.Append('*'); FileChanged(starPath, null, .Changed); } var newPath; if (isDirectory) { if ((newPath != null) && (newPath.EndsWith(Path.DirectorySeparatorChar))) { newPath = scope:: String(); newPath.Append(@newPath, 0, @newPath.Length - 1); } } if ((changeType == .Renamed) && (!isDirectory)) { // ALWAYS interpret 'file rename' notifications as a delete of filePath and a create of newPath // A manual rename in the IDE will have manually processed the rename before we get here, so // we expect 'filePath' won't actually match anything and the 'newPath' will already be set up // in the mWatchedFile FileChanged(filePath, null, .Deleted); var dirName = scope String(); Path.GetDirectoryPath(filePath, dirName); dirName.Append(Path.DirectorySeparatorChar); FileChanged(dirName, newPath, .FileCreated); return; } if ((changeType == .FileCreated) && (gApp.IsFilteredOut(newPath))) return; String fixedFilePath = scope String(filePath); FixFilePath(fixedFilePath); bool wantLoadContent = false; if (newPath == null) { using (mMonitor.Enter()) { DepInfo depInfo; mWatchedFiles.TryGetValue(fixedFilePath, out depInfo); if (depInfo != null) wantLoadContent = depInfo.mContent != null; } } //Debug.WriteLine("FileChanged {0} {1} {2}", filePath, newPath, changeType); String newContent = scope String(); if (wantLoadContent) { //TODO: Make this better for (int32 i = 0; i < 25; i++) { if (gApp.LoadTextFile(fixedFilePath, newContent) case .Ok) break; Thread.Sleep(10); } } using (mMonitor.Enter()) { DepInfo depInfo; mWatchedFiles.TryGetValue(fixedFilePath, out depInfo); if (depInfo == null) { if (changeType == .Deleted) { fixedFilePath.Append(IDEUtils.cNativeSlash); mWatchedFiles.TryGetValue(fixedFilePath, out depInfo); } if (depInfo == null) return; } if (depInfo.mContent != null) { /*File.WriteAllText(@"c:\\temp\\file1.txt", newContent); File.WriteAllText(@"c:\\temp\\file2.txt", depInfo.mContent);*/ if (newContent == null) // No actual content (file renamed - as part of file.new, file -> file.old, file.new -> file return; if ((depInfo.mContent != null) && (Utils.FileTextEquals(depInfo.mContent, newContent))) return; // We were only saving this file until the content actually changed delete depInfo.mContent; depInfo.mContent = null; } bool added = mChangeMap.TryAdd(fixedFilePath, var keyPtr, var valuePtr); if (added) { ChangeRecord changeRecord = new .(); changeRecord.mPath = new String(fixedFilePath); changeRecord.mChangeType = changeType; *keyPtr = changeRecord.mPath; *valuePtr = changeRecord; mChangeList.Add(changeRecord); } else { let changeRecord = *valuePtr; changeRecord.mChangeType |= changeType; } ProjectItem projectItem = null; for (var dep in depInfo.mDependentObjects) { if (var tryProjectItem = dep as ProjectItem) projectItem = tryProjectItem; if (dep != null) { var depPtr = Internal.UnsafeCastToPtr(dep); if (mDependencyChangeSet.Add(depPtr)) mDependencyChangeList.Add(depPtr); } } if (projectItem != null) gApp.OnWatchedFileChanged(projectItem, changeType, newPath); } } public void OmitFileChange(String filePath, String content = null) { String fixedFilePath = scope String(filePath); FixFilePath(fixedFilePath); using (mMonitor.Enter()) { DepInfo depInfo; mWatchedFiles.TryGetValue(fixedFilePath, out depInfo); if (depInfo != null) { String.NewOrSet!(depInfo.mContent, content); } } } public void FileChanged(String filePath) { FileChanged(filePath, null, WatcherChangeTypes.Changed); } void TryAddRefreshWatchEntry(WatcherEntry watcherEntry, int32 delay = 0) { Debug.Assert(watcherEntry != null); Debug.Assert(watcherEntry.mParentDirWatcher == null); bool found = false; for (int32 i = 0; i < mQueuedRefreshWatcherEntries.Count; i++) { var refreshEntry = ref mQueuedRefreshWatcherEntries[i]; if (refreshEntry.mWatcherEntry == watcherEntry) { refreshEntry.mDelay = Math.Min(refreshEntry.mDelay, delay); found = true; } } if (!found) { QueuedRefreshEntry refreshEntry; watcherEntry.AddRef(); refreshEntry.mWatcherEntry = watcherEntry; refreshEntry.mDelay = delay; mQueuedRefreshWatcherEntries.Add(refreshEntry); } } void QueueFileChanged(FileSystemWatcher fileSystemWatcher, String fileName, String newName, WatcherChangeTypes changeType) { //Debug.WriteLine("QueueFileChanged {0} {1} {2} {3}", fileSystemWatcher.Directory, fileName, newName, changeType); using (mFileChangeMonitor.Enter()) { String fullPath = scope String(); fullPath.Append(fileSystemWatcher.Directory); fullPath.Append(Path.DirectorySeparatorChar); if (fileName != null) { fullPath.Append(fileName); } var queuedFileChange = new QueuedFileChange(); if ((changeType == .FileCreated) || (changeType == .DirectoryCreated)) { queuedFileChange.mFileName = new String(); Path.GetDirectoryPath(fullPath, queuedFileChange.mFileName); queuedFileChange.mFileName.Append(Path.DirectorySeparatorChar); queuedFileChange.mNewFileName = new String(fullPath); } else { queuedFileChange.mFileName = new String(fullPath); if (newName != null) { var newFullPath = new String(); newFullPath.Append(fileSystemWatcher.Directory); newFullPath.Append(Path.DirectorySeparatorChar); newFullPath.Append(newName); queuedFileChange.mNewFileName = newFullPath; } } queuedFileChange.mChangeType = changeType; mQueuedFileChanges.Add(queuedFileChange); mChangeId++; } } void StartDirectoryWatcher(WatcherEntry watcherEntry) { FileSystemWatcher fileSystemWatcher = null; //Debug.WriteLine("StartDirectoryWatcher {0}", watcherEntry.mDirectoryName); //Console.WriteLine("StartDirectoryWatcher"); fileSystemWatcher = new FileSystemWatcher(watcherEntry.mDirectoryName); fileSystemWatcher.IncludeSubdirectories = watcherEntry.mHasSubDirDependencies; delete watcherEntry.mFileSystemWatcher; watcherEntry.mFileSystemWatcher = null; void GetPath(String fileName, String outPath) { outPath.Append(watcherEntry.mDirectoryName); outPath.Append(Path.DirectorySeparatorChar); outPath.Append(fileName); } void CheckFileCreated(String fileName) { if (sDbgFileCreateDelay > 0) Thread.Sleep(sDbgFileCreateDelay); var filePath = scope String(); GetPath(fileName, filePath); if (File.Exists(filePath)) { QueueFileChanged(fileSystemWatcher, fileName, null, .FileCreated); } else if (Directory.Exists(filePath)) QueueFileChanged(fileSystemWatcher, fileName, null, .DirectoryCreated); else { using (mFileChangeMonitor.Enter()) { if (watcherEntry.mAmbiguousCreatePaths.TryAdd(fileName, var entryPtr)) *entryPtr = new String(fileName); } } } bool HasAmbiguousFileName(String fileName) { switch (watcherEntry.mAmbiguousCreatePaths.GetAndRemove(fileName)) { case .Ok(let str): delete str; return true; case .Err: return false; } } if (!watcherEntry.mIgnoreWrites) fileSystemWatcher.OnChanged.Add(new (fileName) => { using (mFileChangeMonitor.Enter()) { if (HasAmbiguousFileName(fileName)) { CheckFileCreated(fileName); } QueueFileChanged(fileSystemWatcher, fileName, null, .Changed); } }); fileSystemWatcher.OnCreated.Add(new (fileName) => { CheckFileCreated(fileName); }); fileSystemWatcher.OnDeleted.Add(new (fileName) => { using (mFileChangeMonitor.Enter()) { if (HasAmbiguousFileName(fileName)) { // We didn't process the CREATE so just ignore the DELETE } else QueueFileChanged(fileSystemWatcher, fileName, null, .Deleted); } }); fileSystemWatcher.OnRenamed.Add(new (oldName, newName) => { using (mFileChangeMonitor.Enter()) { if (HasAmbiguousFileName(oldName)) { // We didn't process the CREATE so treat this as a new CREATE CheckFileCreated(newName); } else { var newFilePath = scope String(); GetPath(newName, newFilePath); if (Directory.Exists(newFilePath)) { let dirOldName = scope String()..Concat(oldName, Path.DirectorySeparatorChar); let dirNewName = scope String()..Concat(newName, Path.DirectorySeparatorChar); QueueFileChanged(fileSystemWatcher, dirOldName, dirNewName, .Renamed); } else QueueFileChanged(fileSystemWatcher, oldName, newName, .Renamed); } } }); fileSystemWatcher.OnError.Add(new () => QueueFileChanged(fileSystemWatcher, null, null, .Failed)); if (fileSystemWatcher.StartRaisingEvents() case .Err) { delete fileSystemWatcher; //TryAddRefreshWatchEntry(watcherEntry, 30); // Try again later? } else { fileSystemWatcher.OnError.Add(new () => { if (watcherEntry.mFileSystemWatcher == fileSystemWatcher) { TryAddRefreshWatchEntry(watcherEntry); } }); watcherEntry.mFileSystemWatcher = fileSystemWatcher; } } // This corrects an error where we attempted to watch a directory that wasn't valid but now it is public void FileIsValid(String filePath) { String fixedFilePath = scope String(filePath); FixFilePath(fixedFilePath); using (mMonitor.Enter()) { String directoryName = scope String(); Path.GetDirectoryPath(fixedFilePath, directoryName); WatcherEntry watcherEntry; mWatchers.TryGetValue(directoryName, out watcherEntry); if (watcherEntry != null) { if ((watcherEntry.mFileSystemWatcher == null) && (watcherEntry.mParentDirWatcher == null)) { TryAddRefreshWatchEntry(watcherEntry); } } } } public void WatchFile(String filePath, Object dependentObject = null, bool ignoreWrites = false) { //Debug.WriteLine("WatchFile {0}", filePath); #if !CLI String fixedFilePath = scope String(filePath); FixFilePath(fixedFilePath); using (mMonitor.Enter()) { DepInfo depInfo; mWatchedFiles.TryGetValue(fixedFilePath, out depInfo); if (depInfo != null) { depInfo.mDependentObjects.Add(dependentObject); return; } depInfo = new DepInfo(); depInfo.mDependentObjects.Add(dependentObject); mWatchedFiles[new String(fixedFilePath)] = depInfo; String directoryName = scope String(); Path.GetDirectoryPath(fixedFilePath, directoryName); mWatchers.TryGetValue(directoryName, var watcherEntry); if (watcherEntry != null) { watcherEntry.AddRef(); } else { directoryName = new String(directoryName); watcherEntry = new WatcherEntry(); watcherEntry.mDirectoryName = directoryName; watcherEntry.mIgnoreWrites = ignoreWrites; String checkDir = scope String()..Append(directoryName); while (true) { String parentDirName = scope String(); if (Path.GetDirectoryPath(checkDir, parentDirName) case .Err) break; if (mWatchers.TryGetValue(parentDirName, var parentWatcherEntry)) { while (parentWatcherEntry.mParentDirWatcher != null) parentWatcherEntry = parentWatcherEntry.mParentDirWatcher; parentWatcherEntry.AddRef(); watcherEntry.mParentDirWatcher = parentWatcherEntry; if (!parentWatcherEntry.mHasSubDirDependencies) { // Restart the watcher with the IncludeSubdirectories flag parentWatcherEntry.mHasSubDirDependencies = true; StartDirectoryWatcher(parentWatcherEntry); } break; } checkDir.Set(parentDirName); } if (watcherEntry.mParentDirWatcher == null) StartDirectoryWatcher(watcherEntry); mWatchers[directoryName] = watcherEntry; } } #endif } public void DerefWatcherEntry(WatcherEntry watchEntry) { if (watchEntry.ReleaseRefNoDelete() == 0) { if (mWatchers.GetAndRemove(watchEntry.mDirectoryName) case .Ok((var key, var value))) { if (watchEntry.mParentDirWatcher != null) DerefWatcherEntry(watchEntry.mParentDirWatcher); Debug.Assert(watchEntry == value); delete key; delete watchEntry; } } } public void RemoveWatch(String filePath, Object dependentObject = null) { #if !CLI String fixedFilePath = scope String(filePath); FixFilePath(fixedFilePath); using (mMonitor.Enter()) { DepInfo depInfo; String outKey; if (!mWatchedFiles.TryGet(fixedFilePath, out outKey, out depInfo)) return; if (dependentObject != null) depInfo.mDependentObjects.Remove(dependentObject); if (depInfo.mDependentObjects.Count == 0) { mWatchedFiles.Remove(fixedFilePath); String directoryName = scope String(); Path.GetDirectoryPath(fixedFilePath, directoryName); WatcherEntry watcherEntry = null; String key; mWatchers.TryGet(directoryName, out key, out watcherEntry); DerefWatcherEntry(watcherEntry); delete outKey; delete depInfo; } if (dependentObject != null) mDependencyChangeSet.Remove(Internal.UnsafeCastToPtr(dependentObject)); } #endif } public void Update(delegate void(String, String, WatcherChangeTypes) fileChangeHandler = null) { while (true) { QueuedFileChange queuedFileChange; using (mFileChangeMonitor.Enter()) { if (mQueuedFileChanges.Count == 0) break; queuedFileChange = mQueuedFileChanges.PopFront(); } if (fileChangeHandler != null) fileChangeHandler(queuedFileChange.mFileName, queuedFileChange.mNewFileName, queuedFileChange.mChangeType); FileChanged(queuedFileChange.mFileName, queuedFileChange.mNewFileName, queuedFileChange.mChangeType); delete queuedFileChange; } using (mMonitor.Enter()) { if (mQueuedRefreshWatcherEntries.Count == 0) return; ref QueuedRefreshEntry refreshEntry = ref mQueuedRefreshWatcherEntries[0]; Debug.Assert(refreshEntry.mWatcherEntry != null); if (refreshEntry.mDelay > 0) { refreshEntry.mDelay--; } else { StartDirectoryWatcher(refreshEntry.mWatcherEntry); DerefWatcherEntry(refreshEntry.mWatcherEntry); mQueuedRefreshWatcherEntries.RemoveAt(0); } } } public ChangeRecord PopChangedFile() { using (mMonitor.Enter()) { if (mChangeList.Count == 0) return null; let changeRecord = mChangeList[0]; bool removed = mChangeMap.Remove(changeRecord.mPath); Debug.Assert(removed); mChangeList.RemoveAt(0); return changeRecord; } } public void AddChangedFile(ChangeRecord changeRecord) { using (mMonitor.Enter()) { bool added = mChangeMap.TryAdd(changeRecord.mPath, var keyPtr, var valuePtr); if (added) { *keyPtr = changeRecord.mPath; *valuePtr = changeRecord; mChangeList.Add(changeRecord); } else { delete changeRecord; } } } public void RemoveChangedFile(String str) { using (mMonitor.Enter()) { if (mChangeMap.GetAndRemove(str) case .Ok(let kv)) { mChangeList.Remove(kv.value); delete kv.value; } } } public Object PopChangedDependency() { using (mMonitor.Enter()) { while (true) { if (mDependencyChangeList.IsEmpty) return null; var dep = mDependencyChangeList.PopFront(); if (mDependencyChangeSet.Remove(dep)) return Internal.UnsafeCastToObject(dep); } } } public void AddChangedDependency(Object obj) { using (mMonitor.Enter()) { var depPtr = Internal.UnsafeCastToPtr(obj); if (mDependencyChangeSet.Add(depPtr)) mDependencyChangeList.Add(depPtr); } } } }