mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
209 lines
5.7 KiB
C#
209 lines
5.7 KiB
C#
using OtterGui.Services;
|
|
using Penumbra.Mods.Manager;
|
|
|
|
namespace Penumbra.Services;
|
|
|
|
public class FileWatcher : IDisposable, IService
|
|
{
|
|
// TODO: use ConcurrentSet when it supports comparers in Luna.
|
|
private readonly ConcurrentDictionary<string, byte> _pending = new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly ModImportManager _modImportManager;
|
|
private readonly MessageService _messageService;
|
|
private readonly Configuration _config;
|
|
|
|
private bool _pausedConsumer;
|
|
private FileSystemWatcher? _fsw;
|
|
private CancellationTokenSource? _cts = new();
|
|
private Task? _consumer;
|
|
|
|
public FileWatcher(ModImportManager modImportManager, MessageService messageService, Configuration config)
|
|
{
|
|
_modImportManager = modImportManager;
|
|
_messageService = messageService;
|
|
_config = config;
|
|
|
|
if (_config.EnableDirectoryWatch)
|
|
{
|
|
SetupFileWatcher(_config.WatchDirectory);
|
|
SetupConsumerTask();
|
|
}
|
|
}
|
|
|
|
public void Toggle(bool value)
|
|
{
|
|
if (_config.EnableDirectoryWatch == value)
|
|
return;
|
|
|
|
_config.EnableDirectoryWatch = value;
|
|
_config.Save();
|
|
if (value)
|
|
{
|
|
SetupFileWatcher(_config.WatchDirectory);
|
|
SetupConsumerTask();
|
|
}
|
|
else
|
|
{
|
|
EndFileWatcher();
|
|
EndConsumerTask();
|
|
}
|
|
}
|
|
|
|
internal void PauseConsumer(bool pause)
|
|
=> _pausedConsumer = pause;
|
|
|
|
private void EndFileWatcher()
|
|
{
|
|
if (_fsw is null)
|
|
return;
|
|
|
|
_fsw.Dispose();
|
|
_fsw = null;
|
|
}
|
|
|
|
private void SetupFileWatcher(string directory)
|
|
{
|
|
EndFileWatcher();
|
|
_fsw = new FileSystemWatcher
|
|
{
|
|
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;
|
|
UpdateDirectory(directory);
|
|
}
|
|
|
|
|
|
private void EndConsumerTask()
|
|
{
|
|
if (_cts is not null)
|
|
{
|
|
_cts.Cancel();
|
|
_cts = null;
|
|
}
|
|
_consumer = null;
|
|
}
|
|
|
|
private void SetupConsumerTask()
|
|
{
|
|
EndConsumerTask();
|
|
_cts = new CancellationTokenSource();
|
|
_consumer = Task.Factory.StartNew(
|
|
() => ConsumerLoopAsync(_cts.Token),
|
|
_cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
|
|
}
|
|
|
|
public void UpdateDirectory(string newPath)
|
|
{
|
|
if (_config.WatchDirectory != newPath)
|
|
{
|
|
_config.WatchDirectory = newPath;
|
|
_config.Save();
|
|
}
|
|
|
|
if (_fsw is null)
|
|
return;
|
|
|
|
_fsw.EnableRaisingEvents = false;
|
|
if (!Directory.Exists(newPath) || newPath.Length is 0)
|
|
{
|
|
_fsw.Path = string.Empty;
|
|
}
|
|
else
|
|
{
|
|
_fsw.Path = newPath;
|
|
_fsw.EnableRaisingEvents = true;
|
|
}
|
|
}
|
|
|
|
private void OnPath(object? sender, FileSystemEventArgs e)
|
|
=> _pending.TryAdd(e.FullPath, 0);
|
|
|
|
private async Task ConsumerLoopAsync(CancellationToken token)
|
|
{
|
|
while (true)
|
|
{
|
|
var (path, _) = _pending.FirstOrDefault();
|
|
if (path is null || _pausedConsumer)
|
|
{
|
|
await Task.Delay(500, token).ConfigureAwait(false);
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
await ProcessOneAsync(path, token).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
Penumbra.Log.Debug("[FileWatcher] Canceled via Token.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Penumbra.Log.Warning($"[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 (var 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)
|
|
{
|
|
if (_config.EnableAutomaticModImport)
|
|
_modImportManager.AddUnpack(path);
|
|
else
|
|
_messageService.AddMessage(new InstallNotification(_modImportManager, path), false);
|
|
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 Dispose()
|
|
{
|
|
EndConsumerTask();
|
|
EndFileWatcher();
|
|
}
|
|
}
|