You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Service.cs owns two long-lived SemaphoreSlim instances but never disposes them. Each SemaphoreSlim lazily allocates a kernel AvailableWaitHandle; leaving them undisposed leaks a kernel handle for every service teardown cycle.
Usage sites that prove they are in active use: 660, 677, 692, 708, 742, 803, 1548, 1584, 1724, 1759, 1861, 1904.
Teardown paths that dispose everything else but these two:
Cleanup() at lines 2179–…:
// disposes _childProcess output writers, stops _healthCheckTimer, etc._healthCheckTimer?.Stop();_healthCheckTimer?.Dispose();_healthCheckTimer=null;// (no dispose for _fileSemaphore or _healthCheckSemaphore anywhere)
ExecuteTeardown() finally block at lines 2156–2169:
Grepping the whole file for _fileSemaphore.Dispose or _healthCheckSemaphore.Dispose returns zero hits.
Explanation
SemaphoreSlim only allocates its underlying kernel event the first time AvailableWaitHandle is accessed or when contention forces it, but WaitAsync on a contended semaphore will pay that cost. Once allocated, the handle lives until Dispose is called — the finalizer does not clean it up. For a service that restarts repeatedly (recovery cycles, Restart-Service loops), this is a slow but real kernel-handle leak: every teardown keeps two Semaphore handles alive until the hosting process exits.
Ordering note: dispose the semaphores after_healthCheckTimer has been stopped and any in-flight WaitAsync has either completed or been cancelled via _cancellationSource — otherwise pending waiters will throw ObjectDisposedException (already partially handled at line 1585, but cleaner to avoid triggering it).
Severity: Warning
Summary
Service.csowns two long-livedSemaphoreSliminstances but never disposes them. EachSemaphoreSlimlazily allocates a kernelAvailableWaitHandle; leaving them undisposed leaks a kernel handle for every service teardown cycle.File and lines
src/Servy.Service/Service.csFields (lines 52, 59):
Usage sites that prove they are in active use: 660, 677, 692, 708, 742, 803, 1548, 1584, 1724, 1759, 1861, 1904.
Teardown paths that dispose everything else but these two:
Cleanup()at lines 2179–…:ExecuteTeardown()finally block at lines 2156–2169:Grepping the whole file for
_fileSemaphore.Disposeor_healthCheckSemaphore.Disposereturns zero hits.Explanation
SemaphoreSlimonly allocates its underlying kernel event the first timeAvailableWaitHandleis accessed or when contention forces it, butWaitAsyncon a contended semaphore will pay that cost. Once allocated, the handle lives untilDisposeis called — the finalizer does not clean it up. For a service that restarts repeatedly (recovery cycles,Restart-Serviceloops), this is a slow but real kernel-handle leak: every teardown keeps twoSemaphorehandles alive until the hosting process exits.This is distinct from:
_healthCheckTimer.Elapsedhandler is never unsubscribed (delegate leak, different resource)._serviceLockssemaphores inServy.ManagerServiceCommands.cs, a completely different class.Suggested fix
Add disposals in the
ExecuteTeardownfinally block alongside the other owned resources:Ordering note: dispose the semaphores after
_healthCheckTimerhas been stopped and any in-flightWaitAsynchas either completed or been cancelled via_cancellationSource— otherwise pending waiters will throwObjectDisposedException(already partially handled at line 1585, but cleaner to avoid triggering it).