Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions Class/CustomScheduler.vb
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,13 @@ Namespace LiteTask
Private ReadOnly _tasks As New ConcurrentDictionary(Of String, ScheduledTask)
Private ReadOnly _errorNotifier As ErrorNotifier
Private ReadOnly _taskRunner As TaskRunner
Private disposedValue As Boolean
Private ReadOnly _taskRunning As New ConcurrentDictionary(Of String, Boolean)
Private ReadOnly _dependencyManager As TaskDependencyManager
Private ReadOnly _taskLocks As New ConcurrentDictionary(Of String, Object)
Private ReadOnly _mutexBasePath As String = "Global\LiteTask_Task_"
Private ReadOnly _powerShellPathManager As PowerShellPathManager
Private ReadOnly _taskStates As New ConcurrentDictionary(Of String, TaskState)
Private ReadOnly _schedulerLock As New SemaphoreSlim(1, 1)
Private Const STALE_TIMEOUT_MINUTES As Integer = 15
Private ReadOnly _processingLock As New SemaphoreSlim(1, 1)
Private _isProcessing As Boolean = False
Private ReadOnly _staleTaskAlerts As New ConcurrentDictionary(Of String, DateTime)
Private _lastStaleCleanupTime As DateTime = DateTime.MinValue
Private Const STALE_CLEANUP_INTERVAL_SECONDS As Integer = 300
Expand Down Expand Up @@ -504,9 +500,12 @@ Namespace LiteTask
.WindowStyle = ProcessWindowStyle.Hidden
}

Dim restartProcess = Process.Start(psi)
' Do NOT wait for or dispose the process — it must outlive this service instance
_logger.LogWarning($"[ServiceRestart] Restart helper process launched (PID: {restartProcess?.Id}). Service will restart shortly.")
' Start the helper process, log its PID, then dispose the Process object
' to release the managed handle. The OS process itself continues running
' independently — Dispose only releases the .NET wrapper, not the process.
Using restartProcess = Process.Start(psi)
_logger.LogWarning($"[ServiceRestart] Restart helper process launched (PID: {restartProcess?.Id}). Service will restart shortly.")
End Using

Catch ex As Exception
_logger.LogError($"[ServiceRestart] Failed to initiate service restart: {ex.Message}")
Expand Down Expand Up @@ -558,6 +557,16 @@ Namespace LiteTask

_logger.LogWarning($"[DailyRestart] Initiating scheduled daily restart at {now:HH:mm:ss}")

' Re-read notification setting from XML to pick up any changes
' made via the GUI (which may be a separate process from the service)
Try
_xmlManager.InvalidateCache()
Dim currentSettings = _xmlManager.GetMemoryMonitorSettings()
_dailyRestartNotificationEnabled = Boolean.Parse(If(currentSettings.ContainsKey("DailyRestartNotificationEnabled"), currentSettings("DailyRestartNotificationEnabled"), "True"))
Catch settingsEx As Exception
_logger.LogWarning($"[DailyRestart] Could not re-read notification setting, using cached value: {settingsEx.Message}")
End Try

' Send notification (only if enabled)
If _dailyRestartNotificationEnabled Then
Try
Expand Down Expand Up @@ -712,9 +721,6 @@ Namespace LiteTask
_taskRunning.Clear()
_staleTaskAlerts.Clear()

' Release the processing semaphores
_schedulerLock.Dispose()
_processingLock.Dispose()
End If

' Clean up unmanaged resources
Expand Down
113 changes: 11 additions & 102 deletions Class/NotificationManager.vb
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,15 @@ Namespace LiteTask
Private _processingTask As Task
Private _cancellationTokenSource As New CancellationTokenSource()
Private _isProcessingEmails As Boolean = False
Private disposedValue As Boolean
Private ReadOnly _activeBatches As New ConcurrentDictionary(Of String, NotificationBatch)

Public Property Messages As New List(Of EmailMessage)
Public Property StartTime As DateTime
Public Property Subject As String
Public Property HighestPriority As NotificationPriority = NotificationPriority.Normal
Public Property BatchId As String

Public Sub New(logger As Logger, xmlManager As XMLManager)
_logger = logger
_xmlManager = xmlManager
InitializeEmailSettings()
StartEmailProcessor()
End Sub

Public Sub AddMessage(message As EmailMessage)
Messages.Add(message)
If message.Priority > HighestPriority Then
HighestPriority = message.Priority
End If
End Sub

Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
Expand Down Expand Up @@ -98,39 +84,6 @@ Namespace LiteTask
MyBase.Finalize()
End Sub

Public Function GetCombinedBody() As String
Dim body As New StringBuilder()

' Add execution summary
body.AppendLine($"Execution Summary:")
body.AppendLine($"Start Time: {StartTime:yyyy-MM-dd HH:mm:ss}")
body.AppendLine($"End Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}")
body.AppendLine($"Total Messages: {Messages.Count}")
body.AppendLine()

' Group messages by type
Dim outputs = Messages.Where(Function(m) Not m.Body.StartsWith("Error:")).ToList()
Dim errors = Messages.Where(Function(m) m.Body.StartsWith("Error:")).ToList()

If outputs.Count > 0 Then
body.AppendLine("Execution Output:")
For Each msg In outputs
body.AppendLine(msg.Body)
Next
body.AppendLine()
End If

If errors.Count > 0 Then
body.AppendLine("Errors:")
For Each msg In errors
body.AppendLine(msg.Body)
Next
body.AppendLine()
End If

Return body.ToString()
End Function

Private Sub InitializeEmailSettings()
Try
_emailSettings = _xmlManager.GetEmailSettings()
Expand All @@ -149,31 +102,6 @@ Namespace LiteTask
End Try
End Sub

Private Sub ProcessEmailMessage(message As EmailMessage)
Try
Using mail As New MailMessage()
mail.From = New MailAddress(_emailSettings("EmailFrom"))
For Each recipient In _emailSettings("EmailTo").Split(";"c)
mail.To.Add(recipient.Trim())
Next
mail.Subject = message.Subject
mail.Body = message.Body
mail.Priority = If(message.Priority = NotificationPriority.High,
MailPriority.High, MailPriority.Normal)

_smtpClient.Send(mail)
_logger.LogInfo($"Email sent successfully: {message.Subject}")
End Using
Catch ex As Exception
_logger.LogError($"Failed to send email: {ex.Message}")
If message.RetryCount < 3 Then
message.RetryCount += 1
_messageQueue.Enqueue(message)
Thread.Sleep(1000 * message.RetryCount)
End If
End Try
End Sub

Public Sub QueueNotification(subject As String, body As String, priority As NotificationPriority)
ThrowIfDisposed()

Expand Down Expand Up @@ -213,20 +141,25 @@ Namespace LiteTask
Dim newBatch = New NotificationBatch(message)
_activeBatches.TryAdd(newBatch.BatchId, newBatch)

' Schedule batch processing (respect cancellation, observe exceptions)
' Schedule batch processing — always remove the batch from _activeBatches
' to prevent accumulation, but only enqueue the email if not cancelled.
Task.Delay(5000, _cancellationTokenSource.Token).ContinueWith(Sub(t)
If t.IsCanceled Then Return
Dim batch As NotificationBatch = Nothing
If _activeBatches.TryRemove(newBatch.BatchId, batch) Then
Dim batchedMessage = New EmailMessage With {
.Subject = $"{batch.Subject} - {batch.Messages.Count} messages",
If Not t.IsCanceled Then
Dim batchedMessage = New EmailMessage With {
.Subject = If(batch.Messages.Count > 1,
$"{batch.Subject} - {batch.Messages.Count} messages",
batch.Subject),
.Body = batch.GetCombinedBody(),
.Priority = batch.HighestPriority,
.Timestamp = batch.StartTime
}
_messageQueue.Enqueue(batchedMessage)
_messageQueue.Enqueue(batchedMessage)
End If
batch.Messages.Clear()
End If
End Sub, TaskContinuationOptions.OnlyOnRanToCompletion)
End Sub)
End If

_logger.LogInfo($"Email notification queued: {subject}")
Expand All @@ -235,30 +168,6 @@ Namespace LiteTask
End Try
End Sub

Public Async Function SendEmailAsync(message As EmailMessage) As Task
Try
Using mail As New MailMessage()
mail.From = New MailAddress(_emailSettings("EmailFrom"))
For Each recipient In _emailSettings("EmailTo").Split(";"c)
mail.To.Add(recipient.Trim())
Next
mail.Subject = message.Subject
mail.Body = $"{message.Body}{Environment.NewLine}{Environment.NewLine}Timestamp: {message.Timestamp}"
mail.Priority = If(message.Priority = NotificationPriority.High,
MailPriority.High, MailPriority.Normal)

Await _smtpClient.SendMailAsync(mail)
_logger.LogInfo($"Email sent successfully: {message.Subject}")
End Using
Catch ex As Exception
_logger.LogError($"Failed to send email: {ex.Message}")
If message.RetryCount < 3 Then
message.RetryCount += 1
_messageQueue.Enqueue(message)
End If
End Try
End Function

Private Sub ThrowIfDisposed()
If _disposed Then
Throw New ObjectDisposedException(GetType(NotificationManager).FullName)
Expand Down
52 changes: 35 additions & 17 deletions Class/XMLManager.vb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Namespace LiteTask
Private ReadOnly _tempPath As String

' Simple cache for frequently accessed config values
Private ReadOnly _cacheLock As New Object()
Private _configCache As New Dictionary(Of String, String)
Private _cacheExpiry As DateTime = DateTime.MinValue
Private ReadOnly _cacheTimeout As TimeSpan = TimeSpan.FromMinutes(5)
Expand Down Expand Up @@ -553,27 +554,44 @@ Namespace LiteTask
Return task
End Function

''' <summary>
''' Invalidates the in-memory config cache so the next ReadValue call
''' re-reads from the XML file. Useful when another process (e.g. the
''' GUI) may have updated the file.
''' </summary>
Public Sub InvalidateCache()
SyncLock _cacheLock
_configCache.Clear()
_cacheExpiry = DateTime.MinValue
End SyncLock
End Sub

Public Function ReadValue(section As String, key As String, defaultValue As String) As String
Try
Dim cacheKey = $"{section}/{key}"

' Check cache first
If DateTime.Now < _cacheExpiry AndAlso _configCache.ContainsKey(cacheKey) Then
Return _configCache(cacheKey)
End If


' Check cache first (under lock to avoid races with InvalidateCache)
SyncLock _cacheLock
If DateTime.Now < _cacheExpiry AndAlso _configCache.ContainsKey(cacheKey) Then
Return _configCache(cacheKey)
End If
End SyncLock

Dim xmlDoc As New XmlDocument()
xmlDoc.Load(_filePath)

Dim node As XmlNode = xmlDoc.SelectSingleNode($"LiteTaskSettings/{section}/{key}")
Dim value = If(node?.InnerText, defaultValue)

' Update cache
_configCache(cacheKey) = value
If _configCache.Count = 1 Then ' First item, set expiry
_cacheExpiry = DateTime.Now.Add(_cacheTimeout)
End If


' Update cache – clear expired entries first to prevent unbounded growth
SyncLock _cacheLock
If DateTime.Now >= _cacheExpiry Then
_configCache.Clear()
_cacheExpiry = DateTime.Now.Add(_cacheTimeout)
End If
_configCache(cacheKey) = value
End SyncLock

Return value
Catch ex As Exception
_logger.LogError($"Error reading value for {section}/{key}: {ex.Message}")
Expand Down Expand Up @@ -872,11 +890,11 @@ Namespace LiteTask
keyNode.InnerText = value
xmlDoc.Save(_filePath)

' Clear cache for this key and related values
Dim cacheKey = $"{section}/{key}"
If _configCache.ContainsKey(cacheKey) Then
' Clear cache for this key
SyncLock _cacheLock
Dim cacheKey = $"{section}/{key}"
_configCache.Remove(cacheKey)
End If
End SyncLock
Catch ex As Exception
_logger.LogError($"Error writing value for {section}/{key}: {ex.Message}")
End Try
Expand Down
Loading