From 081f6337e5837d8160a527f42a847d06adddc328 Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Sun, 19 Jul 2026 03:23:38 -0400 Subject: [PATCH 01/14] Preserve unavailable watched folder state --- CompactGUI.Watcher/WatchedFolder.vb | 113 +++++++++++++++++++++++----- 1 file changed, 95 insertions(+), 18 deletions(-) diff --git a/CompactGUI.Watcher/WatchedFolder.vb b/CompactGUI.Watcher/WatchedFolder.vb index 23d55821..6a583f05 100644 --- a/CompactGUI.Watcher/WatchedFolder.vb +++ b/CompactGUI.Watcher/WatchedFolder.vb @@ -27,6 +27,9 @@ Public Class WatchedFolder Private _IsEditing As Boolean = False + + Private _IsAvailable As Boolean = True + ' --- Monitoring State --- Private _HasTargetChanged As Boolean = False @@ -52,40 +55,93 @@ Public Class WatchedFolder Public Sub New() End Sub - Public Sub InitializeMonitoring() - If FSWatcher Is Nothing Then - FSWatcher = New FileSystemWatcher(Folder) With { - .NotifyFilter = NotifyFilters.Size Or NotifyFilters.CreationTime Or NotifyFilters.LastWrite Or NotifyFilters.FileName, - .IncludeSubdirectories = True, - .Filter = "", - .EnableRaisingEvents = True - } - debounceTimer = New Timer(AddressOf DebounceTimerCallback, Nothing, Timeout.Infinite, Timeout.Infinite) + Public Function InitializeMonitoring() As Boolean + If disposedValue Then Return False + + If String.IsNullOrWhiteSpace(Folder) OrElse Not Directory.Exists(Folder) Then + MarkUnavailable() + Return False End If - End Sub + + Try + If FSWatcher Is Nothing Then + FSWatcher = New FileSystemWatcher(Folder) With { + .NotifyFilter = NotifyFilters.Size Or NotifyFilters.CreationTime Or NotifyFilters.LastWrite Or NotifyFilters.FileName, + .IncludeSubdirectories = True, + .Filter = "", + .EnableRaisingEvents = True + } + debounceTimer = New Timer(AddressOf DebounceTimerCallback, Nothing, Timeout.Infinite, Timeout.Infinite) + Else + FSWatcher.EnableRaisingEvents = True + End If + + IsAvailable = True + Return True + Catch ex As Exception When TypeOf ex Is IOException OrElse TypeOf ex Is UnauthorizedAccessException OrElse TypeOf ex Is ArgumentException + Debug.WriteLine($"Unable to monitor folder {Folder}: {ex.Message}") + MarkUnavailable() + Return False + End Try + End Function + + Public Function RefreshAvailability() As Boolean + Dim wasAvailable = IsAvailable + + If Not InitializeMonitoring() Then Return False + + If Not wasAvailable Then + HasTargetChanged = True + LastChangedDate = DateTime.Now + End If + + Return True + End Function Public Sub PauseMonitoring() - If FSWatcher IsNot Nothing Then - FSWatcher.EnableRaisingEvents = False + If Not IsAvailable OrElse FSWatcher Is Nothing Then Return + + If Not Directory.Exists(Folder) Then + MarkUnavailable() + Return End If + + Try + FSWatcher.EnableRaisingEvents = False + Catch ex As Exception When TypeOf ex Is IOException OrElse TypeOf ex Is ObjectDisposedException + MarkUnavailable() + End Try End Sub Public Sub ResumeMonitoring() - If FSWatcher IsNot Nothing Then + If Not RefreshAvailability() Then Return + + Try FSWatcher.EnableRaisingEvents = True - End If + Catch ex As Exception When TypeOf ex Is IOException OrElse TypeOf ex Is ObjectDisposedException + MarkUnavailable() + End Try End Sub ' --- Monitoring Events --- Private Sub WatcherErrorEvent(sender As Object, e As ErrorEventArgs) Handles FSWatcher.Error Debug.WriteLine(e.GetException.Message) + + If Not Directory.Exists(Folder) Then + MarkUnavailable() + End If End Sub Private Sub WatcherModifiedEvent(sender As Object, e As FileSystemEventArgs) Handles FSWatcher.Created, FSWatcher.Changed, FSWatcher.Renamed, FSWatcher.Deleted - debounceTimer.Change(1000, Timeout.Infinite) + debounceTimer?.Change(1000, Timeout.Infinite) End Sub Private Sub DebounceTimerCallback(state As Object) + If Not Directory.Exists(Folder) Then + MarkUnavailable() + Return + End If + HasTargetChanged = True LastChangedDate = DateTime.Now OnPropertyChanged(NameOf(HasTargetChanged)) @@ -94,6 +150,28 @@ Public Class WatchedFolder Debug.WriteLine("Folder " & Folder & " has changed!") End Sub + Private Sub MarkUnavailable() + IsAvailable = False + IsWorking = False + StopMonitoring() + End Sub + + Private Sub StopMonitoring() + Try + FSWatcher?.Dispose() + Catch ex As ObjectDisposedException + Finally + FSWatcher = Nothing + End Try + + Try + debounceTimer?.Dispose() + Catch ex As ObjectDisposedException + Finally + debounceTimer = Nothing + End Try + End Sub + ' --- Calculated Properties --- Public ReadOnly Property DecayPercentage As Decimal Get @@ -119,8 +197,7 @@ Public Class WatchedFolder Protected Overridable Sub Dispose(disposing As Boolean) If Not disposedValue Then If disposing Then - FSWatcher?.Dispose() - debounceTimer?.Dispose() + StopMonitoring() End If disposedValue = True End If @@ -130,4 +207,4 @@ Public Class WatchedFolder Dispose(disposing:=True) GC.SuppressFinalize(Me) End Sub -End Class \ No newline at end of file +End Class From 214654b861af9a9754bfcdcd6b02d06f30e7a75a Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Sun, 19 Jul 2026 03:25:21 -0400 Subject: [PATCH 02/14] Skip offline watched folders without deleting them --- CompactGUI.Watcher/Watcher.vb | 79 +++++++++++++++++------------------ 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/CompactGUI.Watcher/Watcher.vb b/CompactGUI.Watcher/Watcher.vb index 8986a8ff..cb716c9d 100644 --- a/CompactGUI.Watcher/Watcher.vb +++ b/CompactGUI.Watcher/Watcher.vb @@ -118,7 +118,7 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi _settingsService.AppSettings.ScheduledBackgroundLastRan = DateTime.Now If Not IsWatchingEnabled Then Return False Dim recentThresholdDate As DateTime = DateTime.Now.AddSeconds(-IdleSettings.LastSystemModifiedTimeThresholdSeconds) - If Not runAll AndAlso WatchedFolders.Any(Function(x) x.LastChangedDate > recentThresholdDate) Then Return False + If Not runAll AndAlso WatchedFolders.Any(Function(x) x.IsAvailable AndAlso x.LastChangedDate > recentThresholdDate) Then Return False If _parseWatchersSemaphore.CurrentCount <> 0 Then Await ParseWatchers(runAll, runToken) @@ -234,7 +234,8 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi WatchedFolders.Clear() - For Each folder In initialWatchedFolders.Where(Function(f) IO.Directory.Exists(f.Folder)) + For Each folder In initialWatchedFolders + folder.RefreshAvailability() WatchedFolders.Add(folder) folder.LastChangedDate = folder.LastSystemModifiedDate Next @@ -311,6 +312,7 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi .CompressionLevel = If(newItem.CompressionLevel <> WOFCompressionAlgorithm.NO_COMPRESSION, newItem.CompressionLevel, existingItem.CompressionLevel) End With existingItem.HasTargetChanged = False + existingItem.RefreshAvailability() End Sub Public Async Function RemoveWatched(item As WatchedFolder, Optional writeToFile As Boolean = True) As Task @@ -322,18 +324,11 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi End Function - Public Async Function DeleteWatchersWithNonExistentFolders() As Task - - For i As Integer = WatchedFolders.Count - 1 To 0 Step -1 - If Not IO.Directory.Exists(WatchedFolders(i).Folder) Then - WatcherLog.RemovingNonexistentFolders(_logger, 1) - Await RemoveWatched(WatchedFolders(i), False) - End If + Public Sub RefreshWatchedFolderAvailability() + For Each watchedFolder In WatchedFolders + watchedFolder.RefreshAvailability() Next - - Await WriteToFileAsync() - - End Function + End Sub Private Async Function GetWatchedFoldersFromJson() As Task(Of ObservableCollection(Of WatchedFolder)) @@ -362,8 +357,8 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi validatedResult = JsonSerializer.Deserialize(Of (DateTime, ObservableCollection(Of WatchedFolder)))(WatcherJSON, DeserializeOptions) If validatedResult.Item2 IsNot Nothing Then - For Each folder In validatedResult.Item2.Where(Function(f) IO.Directory.Exists(f.Folder)) - folder.InitializeMonitoring() + For Each folder In validatedResult.Item2 + folder.RefreshAvailability() Next End If @@ -397,16 +392,19 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi Try WatcherLog.ParsingWatchers(_logger, ParseAll) - Await DeleteWatchersWithNonExistentFolders() + RefreshWatchedFolderAvailability() + Dim availableFolders = WatchedFolders.Where(Function(w) w.IsAvailable) Dim WatchersQuery = If(ParseAll, - WatchedFolders, - WatchedFolders.Where(Function(w) w.HasTargetChanged) + availableFolders, + availableFolders.Where(Function(w) w.HasTargetChanged) ).OrderBy(Function(f) f.DisplayName) If Not WatchersQuery.Any() Then Return For Each fsWatcher In WatchersQuery + If Not fsWatcher.RefreshAvailability() Then Continue For + WatcherLog.FolderChanged(_logger, fsWatcher.DisplayName) If cToken <> Nothing AndAlso cToken.IsCancellationRequested Then Return Await Analyse(fsWatcher.Folder, ParseAll, cToken) @@ -429,11 +427,7 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi If Not acquired Then Return Try - If watchedFolder Is Nothing Then Return - If Not IO.Directory.Exists(watchedFolder.Folder) Then - Await RemoveWatched(watchedFolder) - Return - End If + If watchedFolder Is Nothing OrElse Not watchedFolder.RefreshAvailability() Then Return Await Analyse(watchedFolder.Folder, False) LastAnalysed = DateTime.Now @@ -454,11 +448,12 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi If BGCompactor.IsCompactorActive Then Return + RefreshWatchedFolderAvailability() Dim recentThresholdDate As DateTime = DateTime.Now.AddSeconds(-IdleSettings.LastSystemModifiedTimeThresholdSeconds) Dim foldersToCompress = WatchedFolders. Where(Function(folder) - Dim eligible = folder.DecayPercentage <> 0 AndAlso folder.CompressionLevel <> WOFCompressionAlgorithm.NO_COMPRESSION + Dim eligible = folder.IsAvailable AndAlso folder.DecayPercentage <> 0 AndAlso folder.CompressionLevel <> WOFCompressionAlgorithm.NO_COMPRESSION Dim recentlyModified = folder.LastSystemModifiedDate > recentThresholdDate AndAlso Not runAll If eligible AndAlso recentlyModified Then WatcherLog.SkippingRecentlyModifiedFolder(_logger, folder.DisplayName) @@ -480,11 +475,13 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi Public Async Function Analyse(folder As String, checkDiskModified As Boolean, Optional cToken As CancellationToken = Nothing) As Task(Of Boolean) + Dim watched = WatchedFolders.FirstOrDefault(Function(f) f.Folder = folder) + If watched Is Nothing OrElse Not watched.RefreshAvailability() Then Return False + + watched.IsWorking = True - Using analyser As New Analyser(folder, NullLogger(Of Analyser).Instance) - Dim watched = WatchedFolders.First(Function(f) f.Folder = folder) - watched.IsWorking = True - Try + Try + Using analyser As New Analyser(folder, NullLogger(Of Analyser).Instance) Dim analysedFiles = Await analyser.GetAnalysedFilesAsync(cToken) If cToken <> Nothing AndAlso cToken.IsCancellationRequested Then Return False @@ -494,8 +491,8 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi watched.LastSystemModifiedDate = watched.LastChangedDate - If analysedFiles.Count <> 0 Then - Dim mainCompressionLVL = analysedFiles?.Select(Function(f) f.CompressionMode).Max + If analysedFiles?.Count <> 0 Then + Dim mainCompressionLVL = analysedFiles.Select(Function(f) f.CompressionMode).Max watched.CompressionLevel = If(mainCompressionLVL <> WOFCompressionAlgorithm.NO_COMPRESSION, mainCompressionLVL, watched.CompressionLevel) If checkDiskModified Then @@ -510,17 +507,18 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi End If watched.HasTargetChanged = False - Catch ex As OperationCanceledException - Return False - Finally - - watched.IsWorking = False - End Try - - Return True - - End Using + End Using + Catch ex As OperationCanceledException + Return False + Catch ex As Exception When TypeOf ex Is IO.IOException OrElse TypeOf ex Is UnauthorizedAccessException + watched.RefreshAvailability() + _logger.LogDebug(ex, "Unable to analyse watched folder {Folder}.", folder) + Return False + Finally + watched.IsWorking = False + End Try + Return True End Function Public Sub Receive(message As PropertyChangedMessage(Of Boolean)) Implements IRecipient(Of PropertyChangedMessage(Of Boolean)).Receive @@ -536,5 +534,4 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi - End Class From bf31772f7b4780e2e1bca2f8fc0662db61292de1 Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Sun, 19 Jul 2026 03:25:42 -0400 Subject: [PATCH 03/14] Keep watcher actions safe for offline folders --- CompactGUI/ViewModels/WatcherViewModel.vb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CompactGUI/ViewModels/WatcherViewModel.vb b/CompactGUI/ViewModels/WatcherViewModel.vb index 596e415a..ff1b2773 100644 --- a/CompactGUI/ViewModels/WatcherViewModel.vb +++ b/CompactGUI/ViewModels/WatcherViewModel.vb @@ -41,7 +41,7 @@ Public NotInheritable Class WatcherViewModel : Inherits ObservableObject Private Async Function RefreshWatched() As Task - Await Watcher.DeleteWatchersWithNonExistentFolders() + Watcher.RefreshWatchedFolderAvailability() Await Task.Run(Function() Watcher.ParseWatchers(True)) End Function @@ -54,6 +54,7 @@ Public NotInheritable Class WatcherViewModel : Inherits ObservableObject Private Sub AddWatchedFolderToQueue(folder As Watcher.WatchedFolder) + If folder Is Nothing OrElse Not folder.RefreshAvailability() Then Return WeakReferenceMessenger.Default.Send(New WatcherAddedFolderToQueueMessage(folder.Folder)) End Sub @@ -91,6 +92,7 @@ Public NotInheritable Class WatcherViewModel : Inherits ObservableObject End Function + Public Async Function AddFolderAsync(folderPath As String) As Task(Of CompressableFolder) If GetInvalidFolders({folderPath}).InvalidFolders.Count > 0 Then @@ -100,7 +102,6 @@ Public NotInheritable Class WatcherViewModel : Inherits ObservableObject End If Return Await CompressableFolderFactory.CreateCompressableFolder(folderPath) - End Function From b196527c4fac94d07d2da7a4011fcab337652cc8 Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Sun, 19 Jul 2026 03:27:29 -0400 Subject: [PATCH 04/14] Show offline warning for unavailable watched folders --- .../Views/Components/FolderWatcherCard.xaml | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/CompactGUI/Views/Components/FolderWatcherCard.xaml b/CompactGUI/Views/Components/FolderWatcherCard.xaml index a408c107..15981b27 100644 --- a/CompactGUI/Views/Components/FolderWatcherCard.xaml +++ b/CompactGUI/Views/Components/FolderWatcherCard.xaml @@ -37,7 +37,6 @@ - - - - - @@ -126,12 +121,10 @@ Visibility="{Binding Watcher.IsRunning, Converter={StaticResource BooleanToInverseVisibilityConverter}}" /> - - - - - -