diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3843fd2..2681968 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/Class/CustomScheduler.vb b/Class/CustomScheduler.vb
index 56a17ee..d5979c0 100644
--- a/Class/CustomScheduler.vb
+++ b/Class/CustomScheduler.vb
@@ -1113,6 +1113,63 @@ Namespace LiteTask
End Try
End Sub
+ '''
+ ''' 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.
+ '''
+ 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}")
diff --git a/Class/LiteTaskService.vb b/Class/LiteTaskService.vb
index 110b20c..9f4fda0 100644
--- a/Class/LiteTaskService.vb
+++ b/Class/LiteTaskService.vb
@@ -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
@@ -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")
diff --git a/Forms/MainForm.vb b/Forms/MainForm.vb
index acafc5b..7420a57 100644
--- a/Forms/MainForm.vb
+++ b/Forms/MainForm.vb
@@ -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
@@ -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
@@ -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
@@ -642,6 +646,7 @@ Namespace LiteTask
End If
Next
+ NotifyServiceTasksChanged()
RefreshTaskList()
Catch ex As Exception
_logger.LogError($"Error toggling task enabled state: {ex.Message}")
@@ -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
@@ -2099,6 +2106,33 @@ Namespace LiteTask
Return _isServiceRunning
End Function
+ '''
+ ''' 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.
+ '''
+ 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