mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Integrate FileWatcher
HEAVY WIP
This commit is contained in:
parent
6348c4a639
commit
c3b00ff426
4 changed files with 186 additions and 1 deletions
|
|
@ -53,6 +53,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
|||
|
||||
public string ModDirectory { get; set; } = string.Empty;
|
||||
public string ExportDirectory { get; set; } = string.Empty;
|
||||
public string WatchDirectory { get; set; } = string.Empty;
|
||||
|
||||
public bool? UseCrashHandler { get; set; } = null;
|
||||
public bool OpenWindowAtStart { get; set; } = false;
|
||||
|
|
@ -76,6 +77,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
|||
public bool HideRedrawBar { get; set; } = false;
|
||||
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
|
||||
public bool DefaultTemporaryMode { get; set; } = false;
|
||||
public bool EnableDirectoryWatch { get; set; } = false;
|
||||
public bool EnableCustomShapes { get; set; } = true;
|
||||
public PcpSettings PcpSettings = new();
|
||||
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ public class Penumbra : IDalamudPlugin
|
|||
private readonly TempModManager _tempMods;
|
||||
private readonly TempCollectionManager _tempCollections;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly FileWatcher _fileWatcher;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly Configuration _config;
|
||||
private readonly CharacterUtility _characterUtility;
|
||||
|
|
@ -81,6 +82,7 @@ public class Penumbra : IDalamudPlugin
|
|||
_residentResources = _services.GetService<ResidentResourceManager>();
|
||||
_services.GetService<ResourceManagerService>(); // Initialize because not required anywhere else.
|
||||
_modManager = _services.GetService<ModManager>();
|
||||
_fileWatcher = _services.GetService<FileWatcher>();
|
||||
_collectionManager = _services.GetService<CollectionManager>();
|
||||
_tempCollections = _services.GetService<TempCollectionManager>();
|
||||
_redrawService = _services.GetService<RedrawService>();
|
||||
|
|
|
|||
136
Penumbra/Services/FileWatcher.cs
Normal file
136
Penumbra/Services/FileWatcher.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ public class SettingsTab : ITab, IUiService
|
|||
private readonly Penumbra _penumbra;
|
||||
private readonly FileDialogService _fileDialog;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly FileWatcher _fileWatcher;
|
||||
private readonly ModExportManager _modExportManager;
|
||||
private readonly ModFileSystemSelector _selector;
|
||||
private readonly CharacterUtility _characterUtility;
|
||||
|
|
@ -65,7 +66,7 @@ public class SettingsTab : ITab, IUiService
|
|||
|
||||
public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial,
|
||||
Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector,
|
||||
CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi,
|
||||
CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, FileWatcher fileWatcher, HttpApi httpApi,
|
||||
DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig,
|
||||
IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService,
|
||||
MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService,
|
||||
|
|
@ -82,6 +83,7 @@ public class SettingsTab : ITab, IUiService
|
|||
_characterUtility = characterUtility;
|
||||
_residentResources = residentResources;
|
||||
_modExportManager = modExportManager;
|
||||
_fileWatcher = fileWatcher;
|
||||
_httpApi = httpApi;
|
||||
_dalamudSubstitutionProvider = dalamudSubstitutionProvider;
|
||||
_compactor = compactor;
|
||||
|
|
@ -647,6 +649,10 @@ public class SettingsTab : ITab, IUiService
|
|||
DrawDefaultModImportFolder();
|
||||
DrawPcpFolder();
|
||||
DrawDefaultModExportPath();
|
||||
Checkbox("Enable Automatic Import of Mods from Directory",
|
||||
"Enables a File Watcher that automatically listens for Mod files that enter, causing Penumbra to automatically import these mods.",
|
||||
_config.EnableDirectoryWatch, v => _config.EnableDirectoryWatch = v);
|
||||
DrawFileWatcherPath();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -726,6 +732,45 @@ public class SettingsTab : ITab, IUiService
|
|||
+ "Keep this empty to use the root directory.");
|
||||
}
|
||||
|
||||
private string _tempWatchDirectory = string.Empty;
|
||||
/// <summary> Draw input for the Automatic Mod import path. </summary>
|
||||
private void DrawFileWatcherPath()
|
||||
{
|
||||
var tmp = _config.WatchDirectory;
|
||||
var spacing = new Vector2(UiHelpers.ScaleX3);
|
||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
|
||||
ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3);
|
||||
if (ImGui.InputText("##fileWatchPath", ref tmp, 256))
|
||||
_tempWatchDirectory = tmp;
|
||||
|
||||
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||
_fileWatcher.UpdateDirectory(_tempWatchDirectory);
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##fileWatch", UiHelpers.IconButtonSize,
|
||||
"Select a directory via dialog.", false, true))
|
||||
{
|
||||
var startDir = _config.WatchDirectory.Length > 0 && Directory.Exists(_config.WatchDirectory)
|
||||
? _config.WatchDirectory
|
||||
: Directory.Exists(_config.ModDirectory)
|
||||
? _config.ModDirectory
|
||||
: null;
|
||||
_fileDialog.OpenFolderPicker("Choose Automatic Import Directory", (b, s) =>
|
||||
{
|
||||
if (b)
|
||||
{
|
||||
_fileWatcher.UpdateDirectory(s);
|
||||
_config.WatchDirectory = s;
|
||||
_config.Save();
|
||||
}
|
||||
}, startDir, false);
|
||||
}
|
||||
|
||||
style.Pop();
|
||||
ImGuiUtil.LabeledHelpMarker("Automatic Import Director",
|
||||
"Choose the Directory the File Watcher listens to.");
|
||||
}
|
||||
|
||||
/// <summary> Draw input for the default name to input as author into newly generated mods. </summary>
|
||||
private void DrawDefaultModAuthor()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue