1
0
Fork 0
mirror of https://github.com/beefytech/Beef.git synced 2025-06-16 23:34:10 +02:00
Beef/IDE/src/FileWatcher.bf

694 lines
21 KiB
Beef

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<String> mAmbiguousCreatePaths = new .() ~ DeleteContainerAndItems!(_);
}
class DepInfo
{
public List<Object> mDependentObjects = new List<Object>() ~ 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<String, WatcherEntry> mWatchers = new Dictionary<String, WatcherEntry>();
Dictionary<String, DepInfo> mWatchedFiles = new Dictionary<String, DepInfo>() ~ DeleteDictionaryAndKeysAndValues!(_); // Including ref count
List<ChangeRecord> mChangeList = new .() ~ DeleteContainerAndItems!(_);
Dictionary<String, ChangeRecord> mChangeMap = new .() ~ delete _;
HashSet<void*> mDependencyChangeSet = new .() ~ delete _;
List<void*> mDependencyChangeList = new .() ~ delete _;
List<QueuedRefreshEntry> mQueuedRefreshWatcherEntries = new List<QueuedRefreshEntry>() ~ delete _;
public Monitor mMonitor = new Monitor() ~ delete _;
List<QueuedFileChange> mQueuedFileChanges = new List<QueuedFileChange>() ~ 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);
}
if ((isDirectory) && (changeType == .Renamed))
{
if (filePath.Equals(newPath, .OrdinalIgnoreCase))
{
// On Windows, renaming a directory with only case changes will result in a remove before a rename
var dirName = scope String();
Path.GetDirectoryPath(newPath.Substring(0, newPath.Length - 1), dirName);
dirName.Append(Path.DirectorySeparatorChar);
FileChanged(dirName, newPath, .DirectoryCreated);
}
}
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);
}
}
}
}