Integrate FileWatcher

HEAVY WIP
This commit is contained in:
Stoia 2025-09-06 14:22:18 +02:00
parent 6348c4a639
commit c3b00ff426
4 changed files with 186 additions and 1 deletions

View file

@ -0,0 +1,136 @@
using System.Threading.Channels;
using OtterGui.Services;
using Penumbra.Mods.Manager;
namespace Penumbra.Services;
public class FileWatcher : IDisposable, IService
{
private readonly FileSystemWatcher _fsw;
private readonly Channel<string> _queue;
private readonly CancellationTokenSource _cts = new();
private readonly Task _consumer;
private readonly ConcurrentDictionary<string, byte> _pending = new(StringComparer.OrdinalIgnoreCase);
private readonly ModImportManager _modImportManager;
private readonly Configuration _config;
private readonly bool _enabled;
public FileWatcher(ModImportManager modImportManager, Configuration config)
{
_config = config;
_modImportManager = modImportManager;
_enabled = config.EnableDirectoryWatch;
if (!_enabled) return;
_queue = Channel.CreateBounded<string>(new BoundedChannelOptions(256)
{
SingleReader = true,
SingleWriter = false,
FullMode = BoundedChannelFullMode.DropOldest
});
_fsw = new FileSystemWatcher(_config.WatchDirectory)
{
IncludeSubdirectories = false,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime,
InternalBufferSize = 32 * 1024
};
// Only wake us for the exact patterns we care about
_fsw.Filters.Add("*.pmp");
_fsw.Filters.Add("*.pcp");
_fsw.Filters.Add("*.ttmp");
_fsw.Filters.Add("*.ttmp2");
_fsw.Created += OnPath;
_fsw.Renamed += OnPath;
_consumer = Task.Factory.StartNew(
() => ConsumerLoopAsync(_cts.Token),
_cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
_fsw.EnableRaisingEvents = true;
}
private void OnPath(object? sender, FileSystemEventArgs e)
{
// Cheap de-dupe: only queue once per filename until processed
if (!_enabled || !_pending.TryAdd(e.FullPath, 0)) return;
_ = _queue.Writer.TryWrite(e.FullPath);
}
private async Task ConsumerLoopAsync(CancellationToken token)
{
if (!_enabled) return;
var reader = _queue.Reader;
while (await reader.WaitToReadAsync(token).ConfigureAwait(false))
{
while (reader.TryRead(out var path))
{
try
{
await ProcessOneAsync(path, token).ConfigureAwait(false);
}
catch (OperationCanceledException) { Penumbra.Log.Debug($"[FileWatcher] Canceled via Token."); }
catch (Exception ex)
{
Penumbra.Log.Debug($"[FileWatcher] Error during Processing: {ex}");
}
finally
{
_pending.TryRemove(path, out _);
}
}
}
}
private async Task ProcessOneAsync(string path, CancellationToken token)
{
// Downloads often finish via rename; file may be locked briefly.
// Wait until it exists and is readable; also require two stable size checks.
const int maxTries = 40;
long lastLen = -1;
for (int i = 0; i < maxTries && !token.IsCancellationRequested; i++)
{
if (!File.Exists(path)) { await Task.Delay(100, token); continue; }
try
{
var fi = new FileInfo(path);
var len = fi.Length;
if (len > 0 && len == lastLen)
{
_modImportManager.AddUnpack(path);
return;
}
lastLen = len;
}
catch (IOException) { Penumbra.Log.Debug($"[FileWatcher] File is still being written to."); }
catch (UnauthorizedAccessException) { Penumbra.Log.Debug($"[FileWatcher] File is locked."); }
await Task.Delay(150, token);
}
}
public void UpdateDirectory(string newPath)
{
if (!_enabled || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) return;
_fsw.EnableRaisingEvents = false;
_fsw.Path = newPath;
_fsw.EnableRaisingEvents = true;
}
public void Dispose()
{
if (!_enabled) return;
_fsw.EnableRaisingEvents = false;
_cts.Cancel();
_fsw.Dispose();
_queue.Writer.TryComplete();
try { _consumer.Wait(TimeSpan.FromSeconds(5)); } catch { /* swallow */ }
_cts.Dispose();
}
}