8
8
using Flow . Launcher . Plugin . BrowserBookmark . Services ;
9
9
using System . ComponentModel ;
10
10
using System . Linq ;
11
- using Flow . Launcher . Plugin . SharedCommands ;
12
11
using System . Collections . Specialized ;
13
- using Flow . Launcher . Plugin . SharedModels ;
14
12
using System . IO ;
15
- using System . Collections . Concurrent ;
16
13
17
14
namespace Flow . Launcher . Plugin . BrowserBookmark ;
18
15
@@ -29,6 +26,7 @@ public class Main : ISettingProvider, IPlugin, IAsyncReloadable, IPluginI18n, IC
29
26
private readonly CancellationTokenSource _cancellationTokenSource = new ( ) ;
30
27
private PeriodicTimer ? _firefoxBookmarkTimer ;
31
28
private static readonly TimeSpan FirefoxPollingInterval = TimeSpan . FromHours ( 3 ) ;
29
+ private readonly SemaphoreSlim _reloadGate = new ( 1 , 1 ) ;
32
30
33
31
public void Init ( PluginInitContext context )
34
32
{
@@ -70,7 +68,7 @@ private string SetupTempDirectory()
70
68
public List < Result > Query ( Query query )
71
69
{
72
70
var search = query . Search . Trim ( ) ;
73
- var bookmarks = _bookmarks ; // use a local copy
71
+ var bookmarks = Volatile . Read ( ref _bookmarks ) ; // use a local copy with proper memory barrier
74
72
75
73
if ( ! string . IsNullOrEmpty ( search ) )
76
74
{
@@ -109,15 +107,23 @@ public List<Result> Query(Query query)
109
107
110
108
public async Task ReloadDataAsync ( )
111
109
{
112
- var ( bookmarks , discoveredFiles ) = await _bookmarkLoader . LoadBookmarksAsync ( _cancellationTokenSource . Token ) ;
110
+ await _reloadGate . WaitAsync ( _cancellationTokenSource . Token ) ;
111
+ try
112
+ {
113
+ var ( bookmarks , discoveredFiles ) = await _bookmarkLoader . LoadBookmarksAsync ( _cancellationTokenSource . Token ) ;
113
114
114
- // Atomically swap the list. This prevents the Query method from seeing a partially loaded list.
115
- Volatile . Write ( ref _bookmarks , bookmarks ) ;
115
+ // Atomically swap the list. This prevents the Query method from seeing a partially loaded list.
116
+ Volatile . Write ( ref _bookmarks , bookmarks ) ;
116
117
117
- _bookmarkWatcher . UpdateWatchers ( discoveredFiles ) ;
118
+ _bookmarkWatcher . UpdateWatchers ( discoveredFiles ) ;
118
119
119
- // Fire and forget favicon processing to not block the UI
120
- _ = _faviconService . ProcessBookmarkFavicons ( _bookmarks , _cancellationTokenSource . Token ) ;
120
+ // Fire and forget favicon processing to not block the UI
121
+ _ = _faviconService . ProcessBookmarkFavicons ( _bookmarks , _cancellationTokenSource . Token ) ;
122
+ }
123
+ finally
124
+ {
125
+ _reloadGate . Release ( ) ;
126
+ }
121
127
}
122
128
123
129
private void OnSettingsPropertyChanged ( object ? sender , PropertyChangedEventArgs e )
@@ -149,71 +155,86 @@ private void StartFirefoxBookmarkTimer()
149
155
150
156
_firefoxBookmarkTimer = new PeriodicTimer ( FirefoxPollingInterval ) ;
151
157
158
+ var timer = _firefoxBookmarkTimer ! ;
152
159
_ = Task . Run ( async ( ) =>
153
160
{
154
- while ( await _firefoxBookmarkTimer . WaitForNextTickAsync ( _cancellationTokenSource . Token ) )
161
+ try
155
162
{
156
- await ReloadFirefoxBookmarksAsync ( ) ;
163
+ while ( await timer . WaitForNextTickAsync ( _cancellationTokenSource . Token ) )
164
+ {
165
+ await ReloadFirefoxBookmarksAsync ( ) ;
166
+ }
157
167
}
168
+ catch ( OperationCanceledException ) { }
169
+ catch ( ObjectDisposedException ) { }
158
170
} , _cancellationTokenSource . Token ) ;
159
171
}
160
172
161
173
private async Task ReloadFirefoxBookmarksAsync ( )
162
174
{
163
- Context . API . LogInfo ( nameof ( Main ) , "Starting periodic reload of Firefox bookmarks." ) ;
164
-
165
- var firefoxLoaders = _bookmarkLoader . GetFirefoxBookmarkLoaders ( ) . ToList ( ) ;
166
- if ( ! firefoxLoaders . Any ( ) )
175
+ // Share the same gate to avoid conflicting with full reloads
176
+ await _reloadGate . WaitAsync ( _cancellationTokenSource . Token ) ;
177
+ try
167
178
{
168
- Context . API . LogInfo ( nameof ( Main ) , "No Firefox bookmark loaders enabled, skipping reload." ) ;
169
- return ;
170
- }
179
+ Context . API . LogInfo ( nameof ( Main ) , "Starting periodic reload of Firefox bookmarks." ) ;
171
180
172
- var tasks = firefoxLoaders . Select ( async loader =>
173
- {
174
- var loadedBookmarks = new List < Bookmark > ( ) ;
175
- try
176
- {
177
- await foreach ( var bookmark in loader . GetBookmarksAsync ( _cancellationTokenSource . Token ) )
178
- {
179
- loadedBookmarks . Add ( bookmark ) ;
180
- }
181
- return ( Loader : loader , Bookmarks : loadedBookmarks , Success : true ) ;
182
- }
183
- catch ( OperationCanceledException )
181
+ var firefoxLoaders = _bookmarkLoader . GetFirefoxBookmarkLoaders ( ) . ToList ( ) ;
182
+ if ( ! firefoxLoaders . Any ( ) )
184
183
{
185
- return ( Loader : loader , Bookmarks : new List < Bookmark > ( ) , Success : false ) ;
184
+ Context . API . LogInfo ( nameof ( Main ) , "No Firefox bookmark loaders enabled, skipping reload." ) ;
185
+ return ;
186
186
}
187
- catch ( Exception e )
187
+
188
+ var tasks = firefoxLoaders . Select ( async loader =>
188
189
{
189
- Context . API . LogException ( nameof ( Main ) , $ "Failed to load bookmarks from { loader . Name } .", e ) ;
190
- return ( Loader : loader , Bookmarks : new List < Bookmark > ( ) , Success : false ) ;
191
- }
192
- } ) ;
190
+ var loadedBookmarks = new List < Bookmark > ( ) ;
191
+ try
192
+ {
193
+ await foreach ( var bookmark in loader . GetBookmarksAsync ( _cancellationTokenSource . Token ) )
194
+ {
195
+ loadedBookmarks . Add ( bookmark ) ;
196
+ }
197
+ return ( Loader : loader , Bookmarks : loadedBookmarks , Success : true ) ;
198
+ }
199
+ catch ( OperationCanceledException )
200
+ {
201
+ return ( Loader : loader , Bookmarks : new List < Bookmark > ( ) , Success : false ) ;
202
+ }
203
+ catch ( Exception e )
204
+ {
205
+ Context . API . LogException ( nameof ( Main ) , $ "Failed to load bookmarks from { loader . Name } .", e ) ;
206
+ return ( Loader : loader , Bookmarks : new List < Bookmark > ( ) , Success : false ) ;
207
+ }
208
+ } ) ;
193
209
194
- var results = await Task . WhenAll ( tasks ) ;
195
- var successfulResults = results . Where ( r => r . Success ) . ToList ( ) ;
210
+ var results = await Task . WhenAll ( tasks ) ;
211
+ var successfulResults = results . Where ( r => r . Success ) . ToList ( ) ;
196
212
197
- if ( ! successfulResults . Any ( ) )
198
- {
199
- Context . API . LogInfo ( nameof ( Main ) , "No Firefox bookmarks successfully reloaded." ) ;
200
- return ;
201
- }
213
+ if ( ! successfulResults . Any ( ) )
214
+ {
215
+ Context . API . LogInfo ( nameof ( Main ) , "No Firefox bookmarks successfully reloaded." ) ;
216
+ return ;
217
+ }
202
218
203
- var newFirefoxBookmarks = successfulResults . SelectMany ( r => r . Bookmarks ) . ToList ( ) ;
204
- var successfulLoaderNames = successfulResults . Select ( r => r . Loader . Name ) . ToHashSet ( ) ;
219
+ var newFirefoxBookmarks = successfulResults . SelectMany ( r => r . Bookmarks ) . ToList ( ) ;
220
+ var successfulLoaderNames = successfulResults . Select ( r => r . Loader . Name ) . ToHashSet ( ) ;
205
221
206
- var currentBookmarks = Volatile . Read ( ref _bookmarks ) ;
222
+ var currentBookmarks = Volatile . Read ( ref _bookmarks ) ;
207
223
208
- var otherBookmarks = currentBookmarks . Where ( b => ! successfulLoaderNames . Any ( name => b . Source . StartsWith ( name , StringComparison . OrdinalIgnoreCase ) ) ) ;
224
+ var otherBookmarks = currentBookmarks . Where ( b => ! successfulLoaderNames . Any ( name => b . Source . StartsWith ( name , StringComparison . OrdinalIgnoreCase ) ) ) ;
209
225
210
- var newBookmarkList = otherBookmarks . Concat ( newFirefoxBookmarks ) . Distinct ( ) . ToList ( ) ;
226
+ var newBookmarkList = otherBookmarks . Concat ( newFirefoxBookmarks ) . Distinct ( ) . ToList ( ) ;
211
227
212
- Volatile . Write ( ref _bookmarks , newBookmarkList ) ;
228
+ Volatile . Write ( ref _bookmarks , newBookmarkList ) ;
213
229
214
- Context . API . LogInfo ( nameof ( Main ) , $ "Periodic reload complete. Loaded { newFirefoxBookmarks . Count } Firefox bookmarks from { successfulLoaderNames . Count } sources.") ;
230
+ Context . API . LogInfo ( nameof ( Main ) , $ "Periodic reload complete. Loaded { newFirefoxBookmarks . Count } Firefox bookmarks from { successfulLoaderNames . Count } sources.") ;
215
231
216
- _ = _faviconService . ProcessBookmarkFavicons ( newFirefoxBookmarks , _cancellationTokenSource . Token ) ;
232
+ _ = _faviconService . ProcessBookmarkFavicons ( newFirefoxBookmarks , _cancellationTokenSource . Token ) ;
233
+ }
234
+ finally
235
+ {
236
+ _reloadGate . Release ( ) ;
237
+ }
217
238
}
218
239
219
240
public Control CreateSettingPanel ( )
@@ -251,7 +272,7 @@ public List<Result> LoadContextMenus(Result selectedResult)
251
272
Context . API . CopyToClipboard ( url ) ;
252
273
return true ;
253
274
}
254
- catch ( Exception ex )
275
+ catch ( Exception ex )
255
276
{
256
277
Context . API . LogException ( nameof ( Main ) , "Failed to copy URL to clipboard" , ex ) ;
257
278
Context . API . ShowMsgError ( Localize . flowlauncher_plugin_browserbookmark_copy_failed ( ) ) ;
0 commit comments