Skip to content

Commit 912c183

Browse files
committed
Improve file watcher.
1 parent 5bf901d commit 912c183

File tree

5 files changed

+163
-131
lines changed

5 files changed

+163
-131
lines changed

Penumbra.GameData

Penumbra/Services/FileWatcher.cs

Lines changed: 109 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,69 @@
1-
using System.Threading.Channels;
2-
using OtterGui.Services;
1+
using OtterGui.Services;
32
using Penumbra.Mods.Manager;
43

54
namespace Penumbra.Services;
65

76
public class FileWatcher : IDisposable, IService
87
{
9-
private readonly FileSystemWatcher _fsw;
10-
private readonly Channel<string> _queue;
11-
private readonly CancellationTokenSource _cts = new();
12-
private readonly Task _consumer;
8+
// TODO: use ConcurrentSet when it supports comparers in Luna.
139
private readonly ConcurrentDictionary<string, byte> _pending = new(StringComparer.OrdinalIgnoreCase);
1410
private readonly ModImportManager _modImportManager;
1511
private readonly MessageService _messageService;
1612
private readonly Configuration _config;
1713

14+
private bool _pausedConsumer;
15+
private FileSystemWatcher? _fsw;
16+
private CancellationTokenSource? _cts = new();
17+
private Task? _consumer;
18+
1819
public FileWatcher(ModImportManager modImportManager, MessageService messageService, Configuration config)
1920
{
2021
_modImportManager = modImportManager;
2122
_messageService = messageService;
2223
_config = config;
2324

24-
if (!_config.EnableDirectoryWatch)
25+
if (_config.EnableDirectoryWatch)
26+
{
27+
SetupFileWatcher(_config.WatchDirectory);
28+
SetupConsumerTask();
29+
}
30+
}
31+
32+
public void Toggle(bool value)
33+
{
34+
if (_config.EnableDirectoryWatch == value)
2535
return;
2636

27-
_queue = Channel.CreateBounded<string>(new BoundedChannelOptions(256)
37+
_config.EnableDirectoryWatch = value;
38+
_config.Save();
39+
if (value)
40+
{
41+
SetupFileWatcher(_config.WatchDirectory);
42+
SetupConsumerTask();
43+
}
44+
else
2845
{
29-
SingleReader = true,
30-
SingleWriter = false,
31-
FullMode = BoundedChannelFullMode.DropOldest,
32-
});
46+
EndFileWatcher();
47+
EndConsumerTask();
48+
}
49+
}
3350

34-
_fsw = new FileSystemWatcher(_config.WatchDirectory)
51+
internal void PauseConsumer(bool pause)
52+
=> _pausedConsumer = pause;
53+
54+
private void EndFileWatcher()
55+
{
56+
if (_fsw is null)
57+
return;
58+
59+
_fsw.Dispose();
60+
_fsw = null;
61+
}
62+
63+
private void SetupFileWatcher(string directory)
64+
{
65+
EndFileWatcher();
66+
_fsw = new FileSystemWatcher
3567
{
3668
IncludeSubdirectories = false,
3769
NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime,
@@ -46,49 +78,81 @@ public FileWatcher(ModImportManager modImportManager, MessageService messageServ
4678

4779
_fsw.Created += OnPath;
4880
_fsw.Renamed += OnPath;
81+
UpdateDirectory(directory);
82+
}
4983

84+
85+
private void EndConsumerTask()
86+
{
87+
if (_cts is not null)
88+
{
89+
_cts.Cancel();
90+
_cts = null;
91+
}
92+
_consumer = null;
93+
}
94+
95+
private void SetupConsumerTask()
96+
{
97+
EndConsumerTask();
98+
_cts = new CancellationTokenSource();
5099
_consumer = Task.Factory.StartNew(
51100
() => ConsumerLoopAsync(_cts.Token),
52101
_cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
53-
54-
_fsw.EnableRaisingEvents = true;
55102
}
56103

57-
private void OnPath(object? sender, FileSystemEventArgs e)
104+
public void UpdateDirectory(string newPath)
58105
{
59-
// Cheap de-dupe: only queue once per filename until processed
60-
if (!_config.EnableDirectoryWatch || !_pending.TryAdd(e.FullPath, 0))
106+
if (_config.WatchDirectory != newPath)
107+
{
108+
_config.WatchDirectory = newPath;
109+
_config.Save();
110+
}
111+
112+
if (_fsw is null)
61113
return;
62114

63-
_ = _queue.Writer.TryWrite(e.FullPath);
115+
_fsw.EnableRaisingEvents = false;
116+
if (!Directory.Exists(newPath) || newPath.Length is 0)
117+
{
118+
_fsw.Path = string.Empty;
119+
}
120+
else
121+
{
122+
_fsw.Path = newPath;
123+
_fsw.EnableRaisingEvents = true;
124+
}
64125
}
65126

127+
private void OnPath(object? sender, FileSystemEventArgs e)
128+
=> _pending.TryAdd(e.FullPath, 0);
129+
66130
private async Task ConsumerLoopAsync(CancellationToken token)
67131
{
68-
if (!_config.EnableDirectoryWatch)
69-
return;
70-
71-
var reader = _queue.Reader;
72-
while (await reader.WaitToReadAsync(token).ConfigureAwait(false))
132+
while (true)
73133
{
74-
while (reader.TryRead(out var path))
134+
var (path, _) = _pending.FirstOrDefault();
135+
if (path is null || _pausedConsumer)
75136
{
76-
try
77-
{
78-
await ProcessOneAsync(path, token).ConfigureAwait(false);
79-
}
80-
catch (OperationCanceledException)
81-
{
82-
Penumbra.Log.Debug($"[FileWatcher] Canceled via Token.");
83-
}
84-
catch (Exception ex)
85-
{
86-
Penumbra.Log.Debug($"[FileWatcher] Error during Processing: {ex}");
87-
}
88-
finally
89-
{
90-
_pending.TryRemove(path, out _);
91-
}
137+
await Task.Delay(500, token).ConfigureAwait(false);
138+
continue;
139+
}
140+
141+
try
142+
{
143+
await ProcessOneAsync(path, token).ConfigureAwait(false);
144+
}
145+
catch (OperationCanceledException)
146+
{
147+
Penumbra.Log.Debug("[FileWatcher] Canceled via Token.");
148+
}
149+
catch (Exception ex)
150+
{
151+
Penumbra.Log.Warning($"[FileWatcher] Error during Processing: {ex}");
152+
}
153+
finally
154+
{
155+
_pending.TryRemove(path, out _);
92156
}
93157
}
94158
}
@@ -115,28 +179,10 @@ private async Task ProcessOneAsync(string path, CancellationToken token)
115179
if (len > 0 && len == lastLen)
116180
{
117181
if (_config.EnableAutomaticModImport)
118-
{
119182
_modImportManager.AddUnpack(path);
120-
return;
121-
}
122183
else
123-
{
124-
var invoked = false;
125-
Action<bool> installRequest = args =>
126-
{
127-
if (invoked)
128-
return;
129-
130-
invoked = true;
131-
_modImportManager.AddUnpack(path);
132-
};
133-
134-
_messageService.PrintModFoundInfo(
135-
Path.GetFileNameWithoutExtension(path),
136-
installRequest);
137-
138-
return;
139-
}
184+
_messageService.AddMessage(new InstallNotification(_modImportManager, path), false);
185+
return;
140186
}
141187

142188
lastLen = len;
@@ -154,34 +200,10 @@ private async Task ProcessOneAsync(string path, CancellationToken token)
154200
}
155201
}
156202

157-
public void UpdateDirectory(string newPath)
158-
{
159-
if (!_config.EnableDirectoryWatch || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath))
160-
return;
161-
162-
_fsw.EnableRaisingEvents = false;
163-
_fsw.Path = newPath;
164-
_fsw.EnableRaisingEvents = true;
165-
}
166203

167204
public void Dispose()
168205
{
169-
if (!_config.EnableDirectoryWatch)
170-
return;
171-
172-
_fsw.EnableRaisingEvents = false;
173-
_cts.Cancel();
174-
_fsw.Dispose();
175-
_queue.Writer.TryComplete();
176-
try
177-
{
178-
_consumer.Wait(TimeSpan.FromSeconds(5));
179-
}
180-
catch
181-
{
182-
/* swallow */
183-
}
184-
185-
_cts.Dispose();
206+
EndConsumerTask();
207+
EndFileWatcher();
186208
}
187209
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Dalamud.Bindings.ImGui;
2+
using Dalamud.Interface.ImGuiNotification;
3+
using Dalamud.Interface.ImGuiNotification.EventArgs;
4+
using OtterGui.Text;
5+
using Penumbra.Mods.Manager;
6+
7+
namespace Penumbra.Services;
8+
9+
public class InstallNotification(ModImportManager modImportManager, string filePath) : OtterGui.Classes.MessageService.IMessage
10+
{
11+
public string Message
12+
=> "A new mod has been found!";
13+
14+
public NotificationType NotificationType
15+
=> NotificationType.Info;
16+
17+
public uint NotificationDuration
18+
=> uint.MaxValue;
19+
20+
public string NotificationTitle { get; } = Path.GetFileNameWithoutExtension(filePath);
21+
22+
public string LogMessage
23+
=> $"A new mod has been found: {Path.GetFileName(filePath)}";
24+
25+
public void OnNotificationActions(INotificationDrawArgs args)
26+
{
27+
var region = ImGui.GetContentRegionAvail();
28+
var buttonSize = new Vector2((region.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
29+
if (ImUtf8.ButtonEx("Install"u8, ""u8, buttonSize))
30+
{
31+
modImportManager.AddUnpack(filePath);
32+
args.Notification.DismissNow();
33+
}
34+
35+
ImGui.SameLine();
36+
if (ImUtf8.ButtonEx("Ignore"u8, ""u8, buttonSize))
37+
args.Notification.DismissNow();
38+
}
39+
}

Penumbra/Services/MessageService.cs

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,19 @@
1-
using Dalamud.Bindings.ImGui;
21
using Dalamud.Game.Text;
32
using Dalamud.Game.Text.SeStringHandling;
43
using Dalamud.Game.Text.SeStringHandling.Payloads;
54
using Dalamud.Interface;
65
using Dalamud.Interface.ImGuiNotification;
7-
using Dalamud.Interface.ImGuiNotification.EventArgs;
86
using Dalamud.Plugin.Services;
97
using Lumina.Excel.Sheets;
108
using OtterGui.Log;
119
using OtterGui.Services;
12-
using OtterGui.Text;
1310
using Penumbra.GameData.Data;
1411
using Penumbra.Mods.Manager;
1512
using Penumbra.String.Classes;
16-
using static OtterGui.Classes.MessageService;
1713
using Notification = OtterGui.Classes.Notification;
1814

1915
namespace Penumbra.Services;
2016

21-
public class InstallNotification(string message, Action<bool> installRequest) : IMessage
22-
{
23-
private bool _invoked = false;
24-
25-
public string Message { get; } = message;
26-
27-
public NotificationType NotificationType => NotificationType.Info;
28-
29-
public uint NotificationDuration => 10000;
30-
31-
public void OnNotificationActions(INotificationDrawArgs args)
32-
{
33-
if (ImUtf8.ButtonEx("Install"u8, "Install this mod."u8, disabled: _invoked))
34-
{
35-
installRequest(true);
36-
_invoked = true;
37-
}
38-
}
39-
}
40-
4117
public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INotificationManager notificationManager)
4218
: OtterGui.Classes.MessageService(log, builder, chat, notificationManager), IService
4319
{
@@ -79,11 +55,4 @@ public void PrintFileWarning(ModManager modManager, string fullPath, Utf8GamePat
7955
$"Cowardly refusing to load replacement for {originalGamePath.Filename().ToString().ToLowerInvariant()} by {mod.Name}{(messageComplement.Length > 0 ? ":\n" : ".")}{messageComplement}",
8056
NotificationType.Warning, 10000));
8157
}
82-
83-
public void PrintModFoundInfo(string fileName, Action<bool> installRequest)
84-
{
85-
AddMessage(
86-
new InstallNotification($"A new mod has been found: {fileName}", installRequest)
87-
);
88-
}
8958
}

0 commit comments

Comments
 (0)