1- using System . Threading . Channels ;
2- using OtterGui . Services ;
1+ using OtterGui . Services ;
32using Penumbra . Mods . Manager ;
43
54namespace Penumbra . Services ;
65
76public 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}
0 commit comments