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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to this project are documented in this file.
## [Unreleased]

### Fixed
- Task deletion not persisting when `LiteTaskService` is running: the service process keeps its own in-memory `_tasks` dictionary and re-writes the full set to `settings.xml` at the end of every execution (via `SaveTasks`). Because the GUI mutated XML directly without telling the service, the service's stale copy resurrected just-deleted tasks (and missed adds/edits) until the next service restart. The GUI now sends `LiteTaskService.CMD_RELOAD_TASKS` (custom service control 128) via `ServiceController.ExecuteCommand` after every task mutation (delete, create, edit, toggle-enable, import), which routes through `LiteTaskService.OnCustomCommand` → `CustomScheduler.ReloadTasks`. `ReloadTasks` evicts entries no longer present in XML (skipping anything currently executing) and refreshes the rest. The notification is best-effort and logs a warning when the GUI user lacks `SERVICE_USER_DEFINED_CONTROL` on the service.
- Task deletion not persisting: `XMLManager.DeleteTask` only searched the canonical `LiteTaskSettings/Tasks/Task` path while `GetAllTaskNames`/`LoadTask` accept three layouts, so non-canonical entries survived deletion. `CustomScheduler.SaveTasks` then re-merged the surviving XML entry back into `_tasks` via `GetAllTasks`, resurrecting the task. `DeleteTask` now removes every matching node across supported layouts (under the existing write lock, with atomic save) and `SaveTasks` treats the in-memory `_tasks` dictionary as the source of truth.

### Changed
Expand Down
57 changes: 57 additions & 0 deletions Class/CustomScheduler.vb
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,63 @@ Namespace LiteTask
End Try
End Sub

''' <summary>
''' Hot-reload entry point invoked by LiteTaskService.OnCustomCommand
''' after the GUI mutates settings.xml. Evicts in-memory entries for
''' tasks that no longer exist on disk (skipping anything currently
''' executing) and refreshes the rest from XML, so the service stops
''' resurrecting deleted tasks via SaveTasks at the next execution.
''' </summary>
Public Sub ReloadTasks()
Try
_logger.LogInfo("Reloading tasks from XML (hot reload)")

Dim newTaskNames = _xmlManager.GetAllTaskNames()
Dim newNameSet = New HashSet(Of String)(newTaskNames, StringComparer.Ordinal)

Dim removed = 0
Dim deferred = 0
For Each existingName In _tasks.Keys.ToList()
If newNameSet.Contains(existingName) Then Continue For

Dim isRunning As Boolean = False
_taskRunning.TryGetValue(existingName, isRunning)
If isRunning Then
_logger.LogWarning($"Hot reload: task '{existingName}' is running; eviction deferred to next reload")
deferred += 1
Continue For
End If

_tasks.TryRemove(existingName, Nothing)
_taskStates.TryRemove(existingName, Nothing)
_staleTaskAlerts.TryRemove(existingName, Nothing)
_activeMutexes.TryRemove(existingName, Nothing)

Dim removedLock As Object = Nothing
If _taskLocks.TryRemove(existingName, removedLock) Then
If TypeOf removedLock Is SemaphoreSlim Then
DirectCast(removedLock, SemaphoreSlim).Dispose()
End If
End If
removed += 1
Next

Dim refreshed = 0
For Each taskName In newTaskNames
Dim task = _xmlManager.LoadTask(taskName)
If task IsNot Nothing Then
_tasks(taskName) = task
refreshed += 1
End If
Next

_logger.LogInfo($"Hot reload complete: {refreshed} task(s) in memory, {removed} evicted, {deferred} deferred")
Catch ex As Exception
_logger.LogError($"Error during hot reload: {ex.Message}")
_logger.LogError($"StackTrace: {ex.StackTrace}")
End Try
End Sub

Private Sub HandleTaskError(task As ScheduledTask, ex As Exception)
_logger.LogError($"Error executing task {task.Name}: {ex.Message}")
_logger.LogError($"Stack trace: {ex.StackTrace}")
Expand Down
20 changes: 20 additions & 0 deletions Class/LiteTaskService.vb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ Imports System.Timers
Namespace LiteTask
Public Class LiteTaskService
Inherits ServiceBase

' Custom service control code (Windows reserves 128-255 for user-defined
' commands) that the GUI sends after mutating settings.xml so the service
' rereads its in-memory task list without a stop/start cycle.
Public Const CMD_RELOAD_TASKS As Integer = 128

Private _customScheduler As CustomScheduler
Private _credentialManager As CredentialManager
Private _xmlManager As XMLManager
Expand Down Expand Up @@ -266,6 +272,20 @@ Namespace LiteTask
End Try
End Sub

Protected Overrides Sub OnCustomCommand(command As Integer)
Try
Select Case command
Case CMD_RELOAD_TASKS
_logger.LogInfo("Received CMD_RELOAD_TASKS from GUI; reloading tasks from settings.xml")
_customScheduler.ReloadTasks()
Case Else
MyBase.OnCustomCommand(command)
End Select
Catch ex As Exception
_logger.LogError($"Error handling custom service command {command}: {ex.Message}")
End Try
End Sub

Private Function IsServiceInstalled() As Boolean
Try
Using sc = New ServiceController("LiteTaskService")
Expand Down
34 changes: 34 additions & 0 deletions Forms/MainForm.vb
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ Namespace LiteTask
ApplicationContainer.GetService(Of Logger)())

If taskForm.ShowDialog() = DialogResult.OK Then
NotifyServiceTasksChanged()
RefreshTaskList()
_logger.LogInfo("Task created successfully")
End If
Expand Down Expand Up @@ -356,6 +357,8 @@ Namespace LiteTask
"Save Error", MessageBoxButtons.OK, MessageBoxIcon.Warning)
End Try

NotifyServiceTasksChanged()

Await RefreshUIAsync()

If errorCount = 0 AndAlso tasksToDelete.Count > 0 Then
Expand Down Expand Up @@ -438,6 +441,7 @@ Namespace LiteTask

Using taskForm As New TaskForm(_credentialManager, _customScheduler, _logger, task)
If taskForm.ShowDialog() = DialogResult.OK Then
NotifyServiceTasksChanged()
RefreshTaskList()
_logger.LogInfo($"Task {taskName} edited successfully")
End If
Expand Down Expand Up @@ -642,6 +646,7 @@ Namespace LiteTask
End If
Next

NotifyServiceTasksChanged()
RefreshTaskList()
Catch ex As Exception
_logger.LogError($"Error toggling task enabled state: {ex.Message}")
Expand Down Expand Up @@ -770,6 +775,8 @@ Namespace LiteTask
_logger.LogInfo($"Imported task: {taskName}")
Next

NotifyServiceTasksChanged()

MessageBox.Show($"Successfully imported {importedTaskNames.Count} tasks.", "Import Successful", MessageBoxButtons.OK, MessageBoxIcon.Information)
_logger.LogInfo($"Imported {importedTaskNames.Count} tasks successfully")
Catch ex As Exception
Expand Down Expand Up @@ -2099,6 +2106,33 @@ Namespace LiteTask
Return _isServiceRunning
End Function

''' <summary>
''' Signals the running LiteTaskService (separate process) to reload its
''' in-memory task list from settings.xml. Without this, the service keeps
''' a stale _tasks dictionary and resurrects deleted tasks via SaveTasks
''' at the end of the next execution. Best-effort: a non-admin GUI user
''' may lack SERVICE_USER_DEFINED_CONTROL on the service, in which case
''' the call is logged as a warning and the service stays stale until it
''' is restarted.
''' </summary>
Private Sub NotifyServiceTasksChanged()
Try
Using sc = New ServiceController(SERVICE_NAME)
If sc.Status <> ServiceControllerStatus.Running Then Return
sc.ExecuteCommand(LiteTaskService.CMD_RELOAD_TASKS)
End Using
_logger?.LogInfo("Sent CMD_RELOAD_TASKS to LiteTaskService")
Catch ex As InvalidOperationException
' ServiceController throws this when the service is not installed.
' Normal in GUI-only setups; nothing to do.
Catch ex As Exception
_logger?.LogWarning(
$"Could not notify LiteTaskService of task change: {ex.Message}. " &
"The service may keep stale task data until it is restarted. " &
"Grant SERVICE_USER_DEFINED_CONTROL to the GUI user or run the GUI elevated.")
End Try
End Sub

Private Sub UpdateTaskStatus(task As ScheduledTask)
'TODO: Implement task status update
Try
Expand Down
Loading