Releases: aelassas/servy
Servy 8.4
Servy 8.4 introduces enhanced recovery orchestration, improved security, significant performance optimizations, and bug fixes across the entire service ecosystem. The full changelog is available below.
Full Changelog
Click to expand release notes!
- feat(core): recovery for when the service process exits cleanly (#1311)
- fix(core): Timing/retry magic numbers scattered across LogTailer, ServiceHelper, DapperExecutor, ProcessHelper, RotatingStreamWriter - consolidate into AppConfig (#818)
- fix(core): ProcessKiller.KillProcessTree - GetParentProcessId called O(N×depth) times during recursion (#826)
- fix(core): Helper.IsRunningInUnitTest - only detects xUnit; NUnit/MSTest assemblies fall through to production code paths (#830)
- fix(core): ServiceManager constructor - missing ArgumentNullException guards (inconsistent with rest of codebase) (#839)
- fix(core): Logger.cs - Log timestamp lacks UTC/local indicator, ambiguous when UseLocalTimeForRotation is enabled (#842)
- fix(core): DefaultRotationSize is duplicated between AppConfig and Logger (DRY) (#845)
- fix(core): DapperExecutor.cs - unreachable 'return default' after for-loops with const retry counts (#850)
- fix(core): NativeMethods.cs - duplicate Win32 status struct (ServiceStatus and SERVICE_STATUS) - name also collides with the public ServiceStatus enum (#865)
- fix(core): ServiceManager.cs - Win32 access-right and service-type constants are re-declared shadowing the same names in NativeMethods.cs (#867)
- fix(core): IServiceManager - async lifecycle methods (Start/Stop/Restart/Install) lack CancellationToken while Uninstall and read methods accept one (#871)
- fix(core): Helper.EscapeArgs and ProcessHelper.EscapeProcessArgument implement the same Win32 algorithm in two different files (#872)
- fix(core): ProcessHelper.ResolvePath - XML doc and inline comment claim 'SERVICE’s environment' but the call expands the CALLER's environment (#878)
- fix(core): ServiceMapper.ToDomain - RecoveryAction default hardcoded as 'RecoveryAction.RestartService' instead of using 'AppConfig.DefaultRecoveryAction' (#892)
- fix(core): Service.cs (Domain) - RecoveryAction property has no default initializer; falls back to enum 0 (None) instead of AppConfig.DefaultRecoveryAction (RestartService) (#893)
- fix(core): Two parallel ServiceDto validators with different rule sets - XML/JSON imports skip upper-bound checks that CLI install enforces (#898)
- fix(core): EnvironmentVariablesValidator and EnvironmentVariableParser - duplicated 'SplitByUnescapedDelimiters' and 'IndexOfUnescapedChar' implementations (DRY) (#901)
- fix(core): ServiceDependenciesValidator - XML doc, inline comment, and error message all say 'letters/digits/hyphens/underscores' but the regex also allows '.' (#902)
- fix(core): Servy.Core Helper.GetBuiltWithFramework - naive 'net' prefix strip mangles 'netstandard*' TFMs into '.NET standard*' (#917)
- fix(core): Helper.EnsureEventSourceExists & Servy.Restarter Program.cs - Logger.Error duplicates ex.Message in formatted text and again in the exception parameter (#918)
- fix(core): AppFoldersHelper.EnsureFolders - hand-rolled connection-string parser fails on quoted paths and paths containing semicolons (#922)
- fix(core): Logger.Initialize - default parameter value '10' for logRotationSizeMB hardcoded instead of DefaultLogRotationSizeMB constant (#923)
- fix(core): ServiceDtoHelper.ApplyDefaults silently clobbers RunAsLocalSystem/UserAccount/Password - XML/JSON imports always force LocalSystem (#930)
- fix(core): ProcessHelper.MaintainCache - races with GetLockForPid users; cleanup can hand out a NEW lock object for the same PID, defeating per-PID serialization (#934)
- fix(core): ServiceHelper.GetRunningServices - naive ImagePath parser splits on first space when no quotes, mangles legacy unquoted paths under 'Program Files' (#937)
- fix(core): AppConfig - DefaultStopTimeout (5s) and DefaultServiceStopTimeoutSeconds (60s) are two parallel 'stop timeout' defaults with no documented relationship; produces 12x asymmetry between Manager and Service (#942)
- fix(core): ServiceValidationRules.Validate - service-name / display-name / description length checks emit Warnings (non-blocking) for hard SCM limits, allowing invalid configs to pass validation (#943)
- fix(core): ServiceValidationRules.Validate - calls Helper.CreateParentDirectory side-effect for stdout/stderr paths during what should be a read-only validation (#944)
- fix(core): AppConfig - MaxConfigFileSizeMB (10MB) and MaxImportPayloadSizeChars (~2MB) reject the same import inconsistently (#960)
- fix(core): ServiceManager.InstallServiceAsync - EnablePreShutdown only refreshes timeout on initial create, not on existing-service update (#962)
- fix(core): ServiceManager.InstallServiceAsync - gMSA detection via EndsWith("$") misclassifies regular accounts whose name happens to end in $ (#966)
- fix(core): EventLogService.SearchAsync - sourceName constructor parameter is silently overridden by hardcoded AppConfig.EventSource filter at result-time (#969)
- fix(core): EventLogLogger.CreateScoped - every scoped logger allocates a fresh EventLog handle (resource leak across scopes) (#973)
- fix(core): ServiceManager.UninstallServiceAsync - ChangeServiceConfig called on a handle opened without SERVICE_CHANGE_CONFIG (silent ERROR_ACCESS_DENIED) (#985)
- fix(core): ServiceManager.GetAllServices - trackedTasks ConcurrentBag is declared and joined but never populated (dead safety gate) (#986)
- fix(core): ProcessKiller.KillProcessTreeAndParents(string) - root process name is not checked against CriticalSystemProcesses safelist (#990)
- fix(core): ProtectedKeyProvider.GetMachineEntropy - uses Registry.LocalMachine which silently falls back to MachineName entropy when run as 32-bit (WoW64 redirection) (#993)
- fix(core): HandleHelper.GetProcessesUsingFile - synchronous StandardOutput.ReadToEnd defeats HandleExeTimeoutMs (handle.exe hang would block forever) (#996)
- fix(core): ResourceHelper.TerminateBlockingProcesses - extension and targetFileName parameters are unused (kept for 'signature compatibility') (#999)
- fix(core): ServiceExporter.ExportJson and JsonServiceSerializer.Serialize use different JsonSerializerSettings - asymmetric JSON output (#1000)
- fix(core): SecureData.Dispose - _disposed flag set after ZeroMemory; concurrent Dispose calls can race through the guard (#1004)
- fix(core): ProcessHelper.GetProcessTreeMetrics - comment claims sum can exceed 100% but the per-process formula is normalized to whole-machine capacity (#1005)
- fix(core): NativeMethods.AtomicSecureMove - name promises atomicity but MoveFileEx falls back to copy+delete across volumes (#1014)
- fix(core): NativeMethods.GetFileIdentity - two empty catch blocks with stale 'Fallback or log failure here if necessary' TODO comments (#1015)
- fix(core): NativeMethods.ValidateCredentials - silently passes for non-gMSA accounts when password is null/empty (function name promises validation) (#1016)
- fix(core): EnvironmentVariableParser.Parse - surrounding quotes are unconditionally stripped, no way to set an env var whose literal value starts and ends with double quotes (#1074)
- fix(core): AppConfig - TFM 'net10.0-windows' hardcoded into three path constants (silently stale on TFM upgrade) (#1027)
- fix(core): AppConfig - three near-identical Get*ServicePath / GetHandleExePath methods (DRY) (#1028)
- fix(core): EventIds.ScriptInfo (1100) and EventIds.ScriptWarning (2100) constants are defined but never referenced anywhere in the codebase (#1039)
- fix(core): ServiceManager.UninstallServiceAsync - bypasses _win32ErrorProvider with direct Marshal.GetLastWin32Error() in 2 places (test-seam violation) (#1041)
- fix(core): ServiceManager - MapStartupType returns Manual for ServiceStartMode.Boot/System but GetServiceStartupType returns null (silent data drift in batch list) (#1042)
- fix(core): ProcessKiller - Process.GetCurrentProcess() handle leaked in 2 places (lines 216, 286), inconsistent with line 78 (#1045)
- fix(core): ProcessHelper.ResolvePath - Regex.Match called inline (uncompiled) on every path validation (#1046)
- fix(core): ResourceHelper - ResourceStalenessThresholdMinutes (20 min) hardcoded as private const, should live in AppConfig (#1047)
- fix(core): Servy.Core ServiceMapper.ToDto is dead code in src/ (only tests call it) (#1049)
- fix(core): Logger.cs hardcodes 'logs' subdirectory in 3 places (lines 91, 120, 349) (#1051)
- fix(core): ProcessKiller.KillProcessTreeAndParents - calls Toolhelp32 snapshot twice per invocation (BuildProcessSnapshotNative + BuildParentChildMapNative) (#1059)
- fix(core): Helper.WriteFileAtomic and Helper.WriteFileAtomicAsync are ~95% duplicated (DRY) (#1060)
- fix(core): RotatingStreamWriter - Thread.Sleep called while holding _lock blocks all writers for up to 100ms during rotation retries (#1066)
- fix(core): RotatingStreamWriter._rotationDisabled is one-way: a single non-IO exception silently disables rotation forever, file grows unbounded (#1067)
- fix(core): RotatingStreamWriter.EnforceMaxRotations regex misses double-collision filenames produced by GenerateUniqueFileName, those rotated logs accumulate forever (#1068)
- fix(core): ProtectedKeyProvider.GetKey/GetIV - no in-memory caching, full DPAPI roundtrip + 3-retry file read on every call #1069
- fix(core): ProtectedKeyProvider - [ExcludeFromCodeCoverage] on entire security class hides DPAPI/ACL regressions from coverage tooling (#1070)
- fix(core): ProtectedKeyProvider - three bare catch blocks (lines 256, 274, 310) swallow exceptions without binding the variable (#1071)
- fix(core): SecureData.Decrypt - tampered/truncated v2 payloads silently return the original ciphertext as 'plaintext', callers can't distinguish success from integrity failure (#1072)
- fix(core): SecureData.Decrypt - v1 marker path always accepts unauthenticated ...
Servy 8.3
Servy 8.3 improves the UI experience and includes many fixes. The full changelog is available below.
Full Changelog
Click to expand release notes!
- fix(service): Console.CursorVisible causes service crash when running console app as Windows service (#814)
- fix(core): ImportServiceCommandTests - MockXmlValidator/MockJsonValidator ignore isValid parameter (#738)
- fix(core): implement fine-grained per-PID locking and atomic cache pruning in ProcessHelper (#796)
- fix(core): ServiceManager.UninstallServiceAsync - stop-wait loop ignores cancellation, blocks up to ServiceStopTimeoutSeconds (#798)
- fix(core): HandleHelper.cs - regex timeout hardcoded to 1s, bypasses AppConfig.InputRegexTimeout convention (#801)
- fix(core): ServiceValidationRules.cs - Service name not validated against SCM-forbidden characters (, /) (#806)
- fix(core): ServiceManager.cs - ServiceStopTimeoutSeconds hardcoded as a local const, should live in AppConfig (#809)
- fix(core): ServiceDto.cs - ShouldSerializEnableSizeRotation typo (missing 'e') breaks conditional serialization of EnableSizeRotation (#810)
- fix(core): ServiceManager.GetAllServices - Parallel.ForEach has no per-service timeout, single hung SCM RPC blocks a worker (#819)
- fix(core): ProcessKiller.CriticalSystemProcesses - incomplete safelist; killing dwm/MsMpEng/audiodg/fontdrvhost can destabilize the host (#825)
- fix(core): ProcessHelper.GetProcessTreeMetrics - caps summed CPU at 100%, hides multi-core saturation (#829)
- fix(core): ProcessHelper.EscapeArgument - fails to double backslashes that precede an internal quote (Win32 CommandLineToArgvW corruption) (#827)
- fix(core): Logger.cs - MaxBackupFiles is hardcoded to 0 (unlimited), no admin override path (#838)
- fix(core): SecureData.Decrypt - on crypto failure of marked payload, returns the post-marker payload (not the documented 'original cipherText') (#848)
- fix(core): ProtectedKeyProvider.GetOrGenerate - comment claims 'exponential backoff' but Thread.Sleep math is linear (#852)
- fix(core): ProcessHelper.cs - EscapeProcessArgument is dead code; only the broken EscapeArgument is called (#857)
- fix(core): ProtectedKeyProvider.SaveProtected - WindowsIdentity from GetCurrent() is never disposed (handle leak per save) (#861)
- fix(core): SecurityHelper.CreateSecureDirectory - WindowsIdentity from GetCurrent() never disposed (handle leak per call) (#868)
- fix(core): LogonAsServiceGrant.Ensure - InvalidOperationException loses the underlying SID-resolution failure cause (#874)
- fix(core): ResourceHelper + ServiceExporter - non-atomic file writes can leave truncated/partial files on crash, mid-copy interruption, or disk full (#875)
- fix(core): SecurityHelper.ApplySecurityRules — comparing the current user's SID to BuiltinAdministrators/LocalSystem GROUP SIDs is meaningless (#933)
- fix(core): ProcessKiller.KillProcessTreeAndParents(string) - silent catch-all swallows ALL errors with no logging (#935)
- fix(core): ProcessKiller.KillProcessTree - uses stale 'allProcesses' snapshot during recursion; processes spawned mid-walk survive (#936)
- fix(core): ResourceHelper.TerminateBlockingProcesses - for .exe resources, kills ALL processes matching the filename, including unrelated instances belonging to other services (#938)
- fix(core): Logger.Log - bare 'catch { /* Fail-silent */ }' on the write path swallows every I/O exception with no fallback channel (#940)
- fix(core): ServiceManager.GetAllServices - orphaned PopulateNativeDetails task races with results consumer and uses scmHandle after dispose (#965)
- fix(core): EventLogReader.ReadEvents - materializes the entire result set in memory before MaxResults limit can apply (#970)
- fix(core): EventLogLogger.Info/Warn/Error - EventLog.WriteEntry calls have no try/catch and no length truncation (32766-char limit) (#971)
- fix(core): ServiceControllerWrapper.BuildDependencyTree - bare catch swallows every error with no logging, leaving '(Unavailable)' nodes indistinguishable (#972)
- fix(infra): ServiceRepository.cs - XmlSerializer instantiated per export, leaks dynamic assemblies (#816)
- fix(infra): ServiceRepository.UpsertBatchAsync - IN clause uses default BINARY collation while upsert key is LOWER(Name); ID sync misses case-mismatched rows (#820)
- fix(infra): ServiceRepository.SearchAsync - Name LIKE
@Patternis case-sensitive for non-ASCII characters; rest of the repo uses LOWER(...) = LOWER(...) (#881) - fix(service): ProcessWrapper.cs - CancelOutputRead/CancelErrorRead missing ThrowIfDisposed check (#800)
- fix(service): ServiceHelper.cs - duplicate using Servy.Core.Config directive (#802)
- fix(service): CheckHealth handler never -= from _healthCheckTimer before Dispose, delegate leak on each restart cycle (#803)
- fix(service): fileSemaphore and _healthCheckSemaphore never disposed, kernel handle leak on teardown (#807)
- fix(service): ProcessLauncher.ApplyLanguageFixes - substring match on 'python'/'java' in FileName produces false positives (#834)
- fix(service): ProcessWrapper.WaitUntilRunningAsync - always waits the full timeout, never returns 'running' early (#858)
- fix(service): ProcessLauncher.Start - TimeoutMs silently ignored when OnScmHeartbeat is null (#860)
- fix(service): truncated XML doc tag '/// <pa' on the unit-test constructor (#889)
- fix(service): ServiceHelper - SensitiveKeyWords list and MaskingRegex pattern are out of sync (CREDENTIAL / CONNECTIONSTRING / CERTIFICATE missing from regex) (#896)
- fix(service): ServiceHelper.MaskSensitiveValue - substring-based keyword match flags non-secret env-var names (e.g. MONKEY, APIPATH, PRIVATELY) (#929)
- fix(service): ProcessLauncher.Start - synchronous launch with TimeoutMs == 0 falls into unbounded WaitForExit() and can hang the service (#952)
- fix(service): ProcessExtensions.GetChildren / GetAllDescendants - bare catch swallows ALL errors with no logging (parallel to #935) (#975)
- fix(service): ProcessLaunchOptions.TimeoutMs - XML doc says 0 = infinite wait but ProcessLauncher.Start throws ArgumentException when TimeoutMs <= 0 (#976)
- fix(desktop): add None date rotation type (#812)
- fix(desktop,manager): all ViewModels coupled to Application.Current - cannot instantiate in tests (#429)
- fix(desktop,manager): ViewModels directly use Mouse.OverrideCursor - fails without WPF context (#431)
- fix(desktop,manager): FileSystemWatcher event handlers never unsubscribed before Dispose (#804)
- fix(desktop,manager): AsyncCommand.Execute - async void with no try/catch propagates exceptions to the WPF dispatcher and can crash the UI (#866)
- fix(desktop,manager): MainWindow.OnClosed (Servy + Manager) - Process.GetCurrentProcess() handle never disposed (#873)
- fix(manager): ServiceCommands.cs - SearchServicesAsync does not filter nulls returned by ToModelAsync (#797)
- fix(manager): ProcessHelper calls made without _metricsLock held, inconsistent with MainViewModel (#799)
- fix(manager): ConsoleViewModel / PerformanceViewModel - StopMonitoring hides base virtual method instead of overriding it (#808)
- fix(manager): MonitoringViewModelBase.OnTick - async void handler does not wrap OnTickAsync in try/catch; an unhandled exception will crash the WPF dispatcher (#982)
- fix(cli): ExportServiceCommand.cs - new Uri(fullPath) throws UriFormatException on legitimate paths (#815)
- fix(cli,psm1): DateRotationType.None missing from documented enum values (#812)
- fix(cli): ExportServiceCommand - ReservedPortRegex misses COM0/LPT0 (and Unicode-superscript COM¹/LPT¹/etc.) per current Microsoft naming docs (#900)
- fix(psm1): Servy.psd1 - AliasesToExport declared twice; line 85 empty array overwrites line 76 entries (#811)
- fix(psm1): Assert-Administrator - WindowsIdentity from GetCurrent() never disposed (handle leak per call) (#864)
- fix(psm1): Invoke-ServyCli - finally block calls process.WaitForExit() with no timeout; if Kill() failed, PowerShell hangs forever (#879)
- fix(psm1): Install-ServyService - dead 'if ($paramName -eq "Password") { continue }' guard (no '--password' entry exists in $paramMapping) (#891)
- fix(tests): ProcessHelper and ProcessKiller are static - cannot be mocked (#430)
- fix(tests): multiple test files - Mock-only tests verify Moq dispatch, not production code (#552)
- fix(tests): TestableService - 16 reflection hops into Service private members with silent null-forgiving (#742)
- fix(tests): test fixtures - 13 occurrences of maintainer-specific Python path (C:\Users\aelassas...) (#753)
- fix(iss): NumericVersion drops patch component, mis-classifies upgrade vs reinstall (#817)
- fix(publish): 8 publish-res scripts are structurally identical (~800 lines) (#406)
- fix(publish): signpath.ps1 - API token stored in plaintext file with no ACL guidance (#583)
- fix(notifications): Get-ServyLastErrors.ps1 - Get-WinEvent -FilterHashtable requires PS 3.0+, contradicts script's "PowerShell 2.0 or later" header (#805)
- fix(notifications): ServyFailureEmail.ps1 --claims PowerShell 2.0 compatibility but dot-sources a script that #Requires -Version 3.0 (#836)
- fix(notifications): ServyFailureEmail.ps1 / ServyFailureNotification.ps1 - masking regex lacks word boundaries, can mangle non-secret content (#837)
- fix(notifications): ServyFailureEmail.ps1 / Get-ServyLastErrors.ps1 - DateTime.Parse on watermark/parameters is culture-sensitive while the file is written invariant ISO 8601 (#863)
- fix(notifications): ServyFailureEmail.ps1 - SmtpClient is never disposed; comment about 'PS 2.0 / .NET 3.5' is incorrect (#884)
- fix(notifications): Get-ServyLastErrors.ps1 - fallback log file is named 'ServyFailureEmail.log' (copy-paste from sibling script) (#926)
- fix(notifications): ServyFailureEmail.ps1 and ServyFailureNotification.ps1 - concurrent watermark re-read uses bare [DateTime]::Parse instead of ParseExact, can silently misinterpret Kind under non-en-US culture (#945...
Servy 8.2
- fix(manager): CPU, RAM and PID resource monitoring shows "N/A" or frozen values for services (#796)
- fix(manager): set PID to N/A when service is uninstalled
- fix(core): ServiceValidationRules.cs - Missing Name/ExecutablePath reported as Warnings instead of Errors (#785)
- fix(core): RotatingStreamWriter.cs - Constructor captures _useLocalTimeForRotation before it is assigned (#791)
- fix(service): TimerAdapter.cs - Disposed-state check bypassed on event/property accessors (#786)
- fix(restarter): AppDbContext created but never disposed (#792)
- ci(setup-dotnet): dotnet-install.ps1 downloaded and executed without signature or hash verification (#787)
- ci(global.json): rollForward: latestPatch weakens build reproducibility (#790)
- ci(publish): CycloneDX CLI, Inno Setup, and 7-Zip installers downloaded and executed without integrity verification (#793)
Servy 8.1
Servy 8.1 introduces many fixes across main components. The full release notes are available in the expandable section below.
Full Changelog
Click to expand release notes!
- fix(core): Child folder ACLs in %ProgramData%\Servy break multi-account setups (#725)
- fix(service): Partial nginx shutdown leaves orphan process running (#784)
- fix(core): mixed string empty checks: IsNullOrWhiteSpace vs IsNullOrEmpty used interchangeably (#397)
- fix(core): Multiple files - Process.MainModule!.FileName! pattern repeated in 7 locations (follow-up to #724) (#757)
- fix(core): RotatingStreamWriter.cs - Synchronous Thread.Sleep retries during rotation stall stdout/stderr capture (#761)
- fix(core): Post-launch hook missing EnvironmentVariables / Stdout / Stderr / Timeout / Retry / IgnoreFailure — asymmetric with pre-launch (#762)
- fix(core): Servy Event Log - C# code (1000/2000/3000) and PowerShell scripts (9901/9903) use disjoint Event ID ranges on the same source (#764)
- fix(core): AppConfig.cs - ConfigurationAppPublishDebugPath and ManagerAppPublishDebugPath point to Release folders (#768)
- fix(core): AppConfig.cs - Inconsistent relative-path depth across Debug/Release folder constants (#769)
- fix(core): HandleHelper.cs - Silent catch swallows Kill() failure on handle.exe timeout (#771)
- fix(core): Domain/Service.cs - StartupType, Priority, and DateRotationType lack inline default initializers despite sibling properties having them (#776)
- fix(core): ServiceManager.cs - Null-forgiving operator on scmHandle then null check is self-contradictory (#781)
- fix(infra): DapperExecutor.cs - synchronous Thread.Sleep in SQLite busy-retry blocks thread pool (#759)
- fix(infra): DapperExecutor.cs - SpinWait.SpinUntil(() => false, delay) misuses SpinWait as Thread.Sleep (#779)
- fix(service): Fire-and-forget PRESHUTDOWN registration races OnStart failure (#758)
- fix(service): ProcessLauncher.cs - stdout/stderr log flush has no error handling, buffer lost on disk failure (#770)
- fix(service): EnvironmentVariableHelper.cs - MaxExpansionPasses and MaxStringLength hardcoded, should live in AppConfig (#775)
- fix(restarter): database locked on restricted accounts
- fix(desktop): MainViewModel.cs - ConfirmPassword silently overwritten with Password on configuration reload (#782)
- fix(desktop,manager): Export/Import XML vs JSON methods duplicated in both GUI projects (#407)
- fix(desktop,manager): Start/Stop/Restart boilerplate duplicated in both ServiceCommands (#408)
- fix(desktop,manager): MainViewModel.cs - IsManagerAppAvailable snapshotted at ctor, never refreshed (#783)
- fix(desktop,manager,cli): Validation logic triplicated across Servy, Manager, and CLI (#404)
- fix(manager): ConsoleViewModel.cs - LogTailer instances leaked across service switches (no store, no dispose) (#763)
- fix(manager): Strings.resx - Status_StopPending and Status_PausePending display values lack space, inconsistent with sibling statuses (#778)
- fix(psm1): stderr ArrayList capture is unbounded while stdout has a 1 MB cap (asymmetric) (#765)
- fix(notifications): ServyFailureEmail.ps1 - timestamp only persisted on email success causes event storm after SMTP outage (#760)
- fix(tests): ProcessKillerTests.Dispose - cmd cleanup loop has empty body; dead code or missing Kill() (#740)
- fix(tests): LogTailerTests - Hardcoded Task.Delay(300/1000) timing flake risk (#749)
- fix(bump-version): dead else branches in version-format logic (#772)
- fix(publish): docstring example uses -fm but actual parameter is -Tfm (#773)
- fix(bump-runtime): counters use $global: scope, pollute caller session (#774)
- ci(workflows): Multiple workflows - No permissions block, inheriting default read-write token scope (#589)
- ci(scoop): scoop.yml - git config --global injects PAT into runner globally, persists for all subsequent steps (#777)
Servy 8.0
Servy 8.0 introduces many fixes across all components. The full release notes are available in the expandable section below.
Full Changelog
Click to expand release notes!
- feat(core): allow MaxRestartAttempts to be set to 0 for unlimited restart attempts (#701)
- feat(desktop): replace process parameters inputs by a resizable textarea (#700)
- fix(core): EnableRotation is ambiguous now that date rotation exists - should be EnableSizeRotation (#381)
- fix(core): MaxRestartAttempts upper bound of 100 is too restrictive for long-running system services (#701)
- fix(core): ProtectedKeyProvider.cs - DPAPI entropy migration failure logs Warn once, stuck state invisible (#723)
- fix(core): Child folder ACLs in %ProgramData%\Servy break multi-account setups (#725)
- fix(core): Central config - Regex ReDoS timeout 200ms duplicated across 6 regexes (#726)
- fix(core): NativeMethods.cs - Inconsistent SafeHandle vs bare IntPtr across SCM/Job P/Invokes (#730)
- fix(core): ProtectedKeyProvider.SaveProtected - No explicit file ACL, non-atomic write (#731)
- fix(core): EventLogReader.cs:44 - Truncated <returns> docstring ending mid-word (#748)
- fix(service): ensure event log availability during pre-shutdown via explicit dependency
- fix(service): ProcessLauncher.cs - Duplicate FireAndForget check is dead code (#707)
- fix(service): EnvironmentVariableHelper.cs - Duplicate <summary> XML doc on ExpandWithDictionary (#708)
- fix(service): ProtectedKeyProvider: stale aes_key.dat from cloned/imaged hosts causes silent 1053, no recovery path (#712)
- fix(service): ProcessLaunchOptions.cs - DefaultWaitChunkMs and DefaultScmAdditionalTimeMs duplicated (#728)
- fix(desktop): MainViewModel.cs - Hardcoded English dialog titles in 4 SaveFile calls (#735)
- fix(desktop): ServiceConfigurationMapper - Mixed AppConfig vs hardcoded defaults within same file (#745)
- fix(desktop,manager): App.xaml.cs initialization duplicated between Servy and Manager (#405)
- fix(desktop,manager): MainWindow.xaml.cs - CreateMainViewModel is 60-line composition root in View code-behind (#649)
- fix(desktop,manager): App.xaml.cs - ContinueWith(async t) drops inner Task, startup faults lost (#714)
- fix(desktop,manager): AppBootstrapper.cs - OnExit disposes SecureData but not DbContext (#715)
- fix(desktop,manager): 14 unused resource keys across Servy and Servy.Manager (#729)
- fix(desktop,manager): ServiceCommands.ValidateFileSize duplicated across Servy and Servy.Manager (#743)
- fix(desktop,manager,cli): misleading error "Max Restart Attempts must be a number greater than or equal to 1" fires for upper-bound violations too (#702)
- fix(desktop,manager,cli): improve validation messages for numeric options (#703)
- fix(manager): MonitoringViewModels - OnTick guard + timer pattern triplicated across 3 ViewModels (#517)
- fix(manager): ViewModels - SearchServicesAsync duplicated across ConsoleViewModel, PerformanceViewModel, DependenciesViewModel (#632)
- fix(manager): MainViewModel.cs - Child ViewModels newed up directly, not injectable (#648)
- fix(manager): MainWindow.xaml.cs - GetDependenciesVm uses 'logsView' as pattern variable (copy-paste bug) (#705)
- fix(manager): MainWindow.xaml.cs - Type pattern variable shadows type name in GetPerformanceVm/GetConsoleVm (#706)
- fix(manager): XAML code-behind - async void event handlers swallow exceptions (#713)
- fix(manager): Monitoring ViewModels - no IDisposable, DispatcherTimer/CTS leak on GC race (#716)
- fix(manager): LogsViewModel - CTS + Cleanup() without IDisposable (#732)
- fix(manager): ServiceConfigurationMapper vs ServiceMapper - Divergent default-handling and enum validation (#733)
- fix(manager): LogTailer.cs - CreationTime rotation detection misses FAT32 tunneling and same-size rotation (#734)
- fix(cli): InstallServiceCommand.cs - Duplicate <summary> XML doc blocks on Execute method (#704)
- fix(cli): ConsoleHelper.cs - Duplicate <summary> XML doc on RunWithLoadingAnimation (#709)
- fix(tests): ServiceRepositoryTests - Brittle reflection-based property verification couples tests to DTO shape (#736)
- fix(tests): RotatingStreamWriterTests - DateTime.UtcNow used twice per test, midnight-boundary flake (#737)
- fix(tests): ServiceRepositoryStub - decrypt parameter ignored; DTOs asymmetric between Get methods (#739)
- fix(tests): ConsoleViewModelTests - [Fact(Skip="TODO needs to be fixed")] with no tracked follow-up (#741)
- fix(tests): tests/ConsoleApp/Program.cs - Developer-specific hardcoded Python path + dead commented code (#746)
- fix(tests): DatabaseValidatorTests - Environment-dependent Assert.Fail masquerading as unit test (#747)
- fix(psm1): [ValidateRange(1, 2147483647)] for -MaxRestartAttempts mismatches CLI limit of 100 (#703)
- fix(psm1): PS 2.0 compat claimed in comments but no #Requires -Version directive (#721)
- fix(notifications): force UTF8 encoding to fix NULL character bug
- fix(notifications): ensure timestamps are strictly increasing
- fix(notifications): handle non-English OS when querying event log
- fix(publish): publish-sc.ps1 / publish-fd.ps1 - Duplicate hardcoded Inno Setup and 7-Zip paths (#727)
- fix(publish): setup/signpath.ps1 - Install-Module -Force without -RequiredVersion (floating signing-module version) (#750)
- chore(deps): update dependencies
- ci(changelog,sbom): No permissions block (#717)
- ci(scoop): PAT inlined in git clone URL, inconsistent with earlier url.insteadOf pattern (#718)
- ci(tmp): Workflow named "tmp" with no documented purpose (#720)
- ci(setup-dotnet): uses dotnet-install -Channel (floating patch) instead of -Version (pinned) (#751)
- ci(azure-pipelines): orphaned legacy CI alongside GitHub Actions, no tests/coverage/sign (#752)
Servy 7.9
Servy 7.9 introduces a hardened security infrastructure, significant performance optimizations, and a wealth of new features. It's packed with an extensive list of improvements, and the full release notes are available in the expandable section below.
Full Changelog
Click to expand release notes!
- feat(desktop): replace process parameters input by a resizable textarea (#700)
- feat(manager): Get CPU and RAM usage of the whole process tree (#446)
- feat(core): replace WMI with native P/Invoke for improved performance and reliability (#94)
- feat(notifications): move SMTP settings to external XML config
- fix(security): ServiceHelper: debug logging can write passwords and sensitive values to plaintext log files (#161)
- fix(core): Resource leak: StringWriter not disposed in ServiceExporter.ExportXml (#95)
- fix(core): WindowsServiceApi.GetServices: ServiceController instances not disposed after enumeration (#97)
- fix(core): HandleHelper.GetProcessesUsingFile: int.Parse can throw FormatException (#98)
- fix(core): HandleHelper: command injection risk via string-interpolated Arguments (#99)
- fix(core): RotatingStreamWriter.GenerateUniqueFileName: null-forgiving operator on Path.GetDirectoryName (#101)
- fix(core): SecureData.Decrypt: silent fallback to plaintext on cryptographic errors (#103)
- fix(core): Async method naming: Task-returning methods missing Async suffix (#110)
- fix(core): IServiceManager: mixed sync and async methods in same interface (#114)
- fix(core): inconsistent return types for operation success across layers (#116)
- fix(core): ServiceHelper.GetRunningServyServices: verbose list union can be a single LINQ expression (#124)
- fix(core): EnvironmentVariableParser: list allocated before guard clause - use early return (#128)
- fix(core): ResourceHelper: verbose conditional list initialization can be a single ternary (#127)
- fix(core): StringHelper: typo in method name FormatEnvirnomentVariables - missing 'o' (#139)
- fix(core): InstallServiceAsync: SetServiceDescription called with zero handle when CreateService fails (#158)
- fix(core): SecureData: key material retained in memory indefinitely - class lacks IDisposable (#159)
- fix(core): ProtectedKeyProvider: DataProtectionScope.LocalMachine allows any local process to decrypt keys (#160)
- fix(core): ServiceManager.UninstallServiceAsync: Thread.Sleep blocks thread pool thread in async method (#162)
- fix(core): RotatingStreamWriter.Rotate: redundant nested lock - caller already holds _lock (#164)
- fix(core): Logger.Log: null check on _writer outside lock - race condition with Shutdown() (#165)
- fix(core): RotatingStreamWriter: _disposed check outside lock - race with concurrent Dispose() (#168)
- fix(core): Helper.IsValidPath: path traversal check is too broad - blocks legitimate paths (#169)
- fix(core): XXE vulnerability in XmlServiceValidator deserialization (#172)
- fix(core): SecureData missing ObjectDisposedException guard on Encrypt/Decrypt (#177)
- fix(core): PID reuse TOCTOU race condition in ProcessKiller (179)
- fix(core): Race condition between Refresh and Start in ServiceHelper.StartServices (#181)
- fix(core): unbounded loop in RotatingStreamWriter.GenerateUniqueFileName (#183)
- fix(core): Credential validation bypassed when password is empty (#182)
- fix(core): RotatingStreamWriter: uses DateTime.Now instead of UTC for rotation date tracking (#202)
- fix(core): XmlServiceValidator: XXE vulnerability - XmlDocument loaded without disabling DTD processing (#221)
- fix(core): EventLogReader: EventRecord objects may reference disposed reader resources (#223)
- fix(core): EventLogService: XPath injection risk in query filter construction (#224)
- fix(core): HandleHelper: WaitForExit without timeout - caller blocks indefinitely if handle.exe hangs (#251)
- fix(core): EventLogService.SearchAsync: unbounded result list - OOM on large event logs (#253)
- fix(core): ProtectedKeyProvider: no retry on File.ReadAllBytes - AV lock fails service startup (#255)
- fix(core): RotatingStreamWriter.Rotate: File.Move failure silently skipped - log grows unbounded (#256)
- fix(core): EventLogReader.ParseLevel: Critical events (level 1) misclassified as Information (#259)
- fix(core): ProcessHelper.CpuTimesStore: ConcurrentDictionary grows unbounded - memory leak on long-running systems (#264)
- fix(core): ILogger.Prefix is mutable - thread-safety risk when shared across services (#266)
- fix(core): HandleHelper.GetProcessesUsingFile: synchronous ReadToEnd() deadlocks when stderr buffer fills (#277)
- fix(core): ServiceHelper.StopServices: exception wrapping loses inner exception and stack trace (#279)
- fix(core): Negative RotationSize silently produces huge ulong, disabling log rotation (#280)
- fix(core): Inconsistent StartupType default: Manual in Repository vs Automatic in Mapper (#291)
- fix(core): ServiceManager bypasses injected IWin32ErrorProvider in two methods (#295)
- fix(core): Path.GetDirectoryName and FileInfo.Directory null-forgiving on multiple locations (#307)
- fix(core): Unchecked enum casts from database integers can produce undefined enum values (#319)
- fix(core): ResolvePath returns whitespace-only strings as valid paths (#320)
- fix(core): ServiceDependenciesValidator regex rejects valid service names containing dots (#324)
- fix(core): ProcessHelper._lastPruneTime: TOCTOU race allows concurrent prune execution (#332)
- fix(core): ServiceManager.GetAllServices: bare catch silently defaults startup type to Automatic (#337)
- fix(core): ServiceManager.GetServiceStartupType: catch-all returns ambiguous null (#345)
- fix(core): ServiceManager.InstallServiceAsync: UpdateServiceConfig return value explicitly discarded (#348)
- fix(core): Log injection: no newline sanitization on user-controlled strings in log messages (#354)
- fix(core): Hardcoded DPAPI entropy string in ProtectedKeyProvider weakens defense-in-depth (#355)
- fix(core): Local privilege escalation: ProgramData directories created without restrictive ACLs (#357)
- fix(core): SCM and service handles opened with ALL_ACCESS instead of minimum required permissions (#362)
- fix(core): XML/JSON import validators only check Name and Path - numeric fields not range-validated (#366)
- fix(core): Encryption key file path revealed in error log and Event Log (#377)
- fix(core): UserSession property name misleads - actually holds service account name (#380)
- fix(core): Enum default values differ between ServiceMapper and ServiceRepository for same fields (#401)
- fix(core): Dead code: IWindowsServiceProvider and WindowsServiceProvider entirely unused (#409)
- fix(core): DRY: Stop timeout calculation duplicated in 3 locations (#414)
- fix(core): ProtectedKeyProvider: silent retry loop on DPAPI migration failure - no logging (#415)
- fix(core): RotatingStreamWriter: failed rotation causes repeated retry on every subsequent write (#418)
- fix(core): Hidden side-effect: GetCpuUsage mutates global state and prunes dead processes (#432)
- fix(core): Missing comments: magic number 15 (buffer seconds) used in 5+ locations without explanation (#435)
- fix(core): Performance: Regex allocated fresh on every HandleHelper.GetProcessesUsingFile call (#438)
- fix(core): Performance: Logger.WriteLogEntry boxes enum and allocates two strings per log call (#439)
- fix(core): EventLogService.cs - Bracket heuristic for Servy logs matches unrelated events (#463)
- fix(core): ServiceConfiguration.cs - Password and ConfirmPassword serialized in plain text during export (#466)
- fix(core): KillProcessesUsingFile kills by name instead of PID (#472)
- fix(core): HandleHelper.cs - StandardError redirected but never read, deadlock risk (#475)
- fix(core): RotatingStreamWriter.cs - EnforceMaxRotations silently swallows File.Delete exceptions (#476)
- fix(core): ProcessHelper + ProcessKiller - P/Invoke declarations duplicated verbatim (#485)
- fix(core): Logger.cs - Log entry timestamps always local time regardless of rotation setting (#499)
- fix(core): Logger.cs - Initialize tears down old writer without draining in-flight writes (#500)
- fix(core): ServiceManager.cs - Parallel.ForEach parallelism uncapped at ProcessorCount (#504)
- fix(core): ServiceValidator.cs - Error message says 30s-1h but actual bounds are 1s-86400s (#511)
- fix(core): ServiceValidator.cs - ExecutablePath not validated in ValidateDto (#512)
- fix(core): ServiceManager.StartServiceAsync - No differentiation between TimeoutException and other failures (#514)
- fix(core): ServiceManager.cs - GetAllServices: deeply nested Parallel.ForEach with 3 AllocHGlobal blocks (#520)
- fix(core): ServiceExporter.cs - XML export declares UTF-16 but file is written as UTF-8 (#555)
- fix(core): ServiceValidator.cs - Missing upper-bound check on StopTimeout for import flows (#556)
- fix(core): Handle.cs - Struct wrapping IntPtr is freely copyable, risks double-close (#557)
- fix(core): NativeMethods.cs - Regex character class
[@!-]parsed as ASCII range, allows unintended characters (#563) - fix(core): ServiceManager.cs - InstallServiceAsync leaves partial service in SCM on post-create config failure (#564)
- fix(core): SecureData.cs - Decrypt silently returns plaintext when input is not Base64 (#568)
- fix(core): Credential validation triggers real domain logon, can lock out service accounts (#574)
- fix(core): BuildDependencyTree re-expands shared deps in diamond patterns (#578)
- fix(core): ServiceManager.cs - ArgumentException passes field name as message instead of paramName (#581)
- fix(core): NativeMethods.cs - ValidateCredentials uses LOGON32_LOGON_NETWORK, false negatives for restricted accounts (#582)
- fix(core): ServiceHelper.cs - StopServices throws on first failure, remaining services left running (#590)
- fix(core): ERROR_INVALID_PARAMETER ...
Servy 7.8
- feat(logger): add
LogRollingIntervalconfig for daily, weekly, or monthly log rotation - fix(logger): defer file creation until first write (lazy init)
- fix(core): replace WMI with SCM/Registry for resource refresh service queries (#48)
- fix(cli):
--versionoutput goes tostderrinstead ofstdout(#49) - fix(cli): only inject the default verb if the user didn't provide a recognized verb
- fix(psm1): misleading error message when Servy CLI is not found (#50)
- fix(psm1): Invoke-ServyCli: non-zero exit code caught by its own try/catch block (#51)
- fix(psm1): Install-ServyService: Pre-stop and Post-stop parameters missing
[string]type declaration (#52) - fix(psm1): missing module manifest (.psd1) for Servy PowerShell module (#53)
- fix(psm1): Add-Arg does not quote values containing spaces (#54)
- fix(psm1): use
System.Diagnostics.Processinstead of&in Invoke-ServyCli (#54) - fix(psm1): Uninstall-ServyService: incorrect function name in .EXAMPLE (#55)
- fix(psm1): Show-ServyVersion: inconsistent invocation pattern compared to other functions (#56)
- fix(psm1): Show-ServyHelp: .EXAMPLE does not demonstrate -Command parameter (#57)
- fix(psm1): Install-ServyService: PostLaunch missing parameters that PreLaunch has (#58)
- fix(psm1): Import-ServyServiceConfig: missing -Name parameter unlike Export-ServyServiceConfig (#59)
- perf(core): accelerate startup by replacing WMI with fast SCM/Registry queries (#48)
Servy 7.7
- feat(logger): add
EnableEventLogoption to allow disabling Windows Event Log via configuration - refactor(core): general code cleanup and refactoring
- docs(wiki): improve Advanced Configuration documentation
- chore(announcement): launch new Servy website with refreshed branding, security audits, and 100% code coverage
Servy 7.6
- feat(service): read entire service configuration from database instead of service parameters
- feat(logger): implement dual-channel LogLevel support for the Windows Event Log and local files
- feat(logger): add
LogRotationSizeMBconfiguration option (default: 10MB) - feat(logger): add
NONELog Level to disable logging - fix(logger): implement IDisposable in EventLogLogger for clean teardown
- fix(logger): prevent duplicate exception text in logs
- fix(service): use synchronous resource extraction to ensure thread safety
- fix(service): move resource refresh to service constructor to ensure it's done before any operations that rely on it
- fix(service): move logger disposal to service teardown
- fix(service): include
LogLevelsetting in .NET Framework 4.8 build - fix(service): move event source creation to constructor for self-healing initialization
- fix(service): refactor InstallService to use InstallServiceOptions class for improved maintainability
- fix(restarter): ensure logger is disposed on exit
- fix(manager): improve stability and configuration consistency
- fix(manager): ensure EventRecords are disposed to prevent memory leaks
- fix(manager): include missing
ConfigurationAppPublishPathconfiguration in .NET Framework 4.8 build - fix(manager): remove deprecated
EnableDebugLogssetting from .NET 10.0 build - fix(manager): optimize log search threading by removing redundant
Task.RunandDispatchernesting - fix(net48): rename
Servy.Restarter.exetoServy.Restarter.Net48.exeto avoid conflict with the .NET 10.0 build - ci(publish): update publish workflow to handle the new
Servy.Restarter.Net48.exefilename for the .NET Framework 4.8 build
Servy 7.5
- feat(logger): expand observability across Desktop, CLI, Manager, Service, and Restarter logs
- feat(logger): add
DEBUGlevel for more verbose output during troubleshooting - feat(logger): add LogLevel setting to dynamically adjust log verbosity at runtime
- fix(logger): prevent null entries in logs after abrupt termination
- fix(core): improve embedded resource extraction and refresh reliability after installation
- fix(core): prevent redundant resource refreshes using 20-minute timestamp delta
- fix(core): ensure reliable resource extraction on the first run after installation
- fix(cli): ensure logger is initialized before use in CLI