Servy 8.4 introduces enhanced recovery orchestration, improved security protocols, and significant performance optimizations across the entire service ecosystem.
Key Highlights
- Advanced Recovery Orchestration: New configuration options to ensure critical services maintain high availability through complex failure states.
- Granular Configuration: Expanded settings for process management and lifecycle handling, offering deeper flexibility for specialized environments.
- Hardened Security & Stability: Under-the-hood improvements to core security logic and stability, ensuring safer operation in production settings.
- System Maintenance: Comprehensive bug fixes and optimizations for a more reliable developer and operator experience.
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 ciphertext, enabling silent v2->v1 downgrade by any writer of stored credentials (#1073)
- fix(core): HandleHelper.GetProcessesUsingFile - StringBuilder accessed unsynchronized after Kill on timeout, race with still-firing OutputDataReceived/ErrorDataReceived handlers (#1075)
- fix(core): ServiceMapper.ToDto omits Pid, ActiveStdoutPath, ActiveStderrPath; Domain->DTO->Domain round-trip silently drops runtime state (#1076)
- fix(core): Logger.Log - exception logged via $"{message}\nException: {ex}" then newline-sanitized to literal \r\n, stack traces become unreadable single-line escaped blobs (#1077)
- fix(core): ServiceValidationRules - service name only checks for '/' and '', misses other SCM-rejected inputs (leading/trailing whitespace, control chars, '"', '|', etc.) (#1078)
- fix(core): ResourceHelper.GetEmbeddedResourceLastWriteTimeUTC name and XML doc claim 'embedded resource' time, actually returns the exe's own File.GetLastWriteTimeUtc (#1079)
- fix(core): LogonAsServiceGrant.cs - [ExcludeFromCodeCoverage] on entire LSA-privilege class hides regressions in security-sensitive code (#1092)
- fix(core): ServiceManager.cs - three SCM-tuning consts hardcoded (ServiceStartTimeoutSeconds, ScmPollIntervalMs, MaxParallelScmQueries) and inconsistent 250ms poll in Start/Stop (#1095)
- fix(core): ServiceExporter.cs - private nested Utf8StringWriter duplicates the public Servy.Core.IO.Utf8StringWriter class (#1104)
- fix(core): EscapedTokenizer - three methods duplicate the 'count preceding backslashes' loop (DRY) (#1105)
- fix(core): EnvironmentVariablesValidator.Validate - XML doc says 'exactly one unescaped equals' but code only requires >=1, and CountUnescapedChar pass is redundant before IndexOfUnescapedChar (#1113)
- fix(core): EscapedTokenizer.Unescape - escaped newline/semicolon split asymmetry: split treats \r and \n as escaped delimiters but Unescape never strips the backslash, leaving a stray '' in the value (#1114)
- fix(core): OperationResult.Failure - XML doc claims ArgumentException is thrown only on null, but code throws on whitespace too (#1115)
- fix(core): RotatingStreamWriter.ShouldRotateByDate Weekly mode misses year-on-year rotation when both samples land in 'week 1' under ISO calendar year mismatch (#1116)
- fix(core): Logger.Log error fallback writes to logs/LoggerWriteErrors.log without first ensuring the directory exists, unlike InternalInitialize which calls SecurityHelper.CreateSecureDirectory (#1117)
- fix(core): EventLogLogger.SetIsEventLogEnabled - when InitializeEventLog fails, _isEventLogEnabled is overwritten back to the requested 'true' state, leaving _eventLog null but flag pretending logging is on (#1118)
- fix(core): EventLogReader.ReadEvents - cutoff EventRecord at maxReadCount yields break before using-block, leaking the just-fetched native handle (#1119)
- fix(core): ServiceControllerWrapper.BuildDependencyTree - currentPath retains stale entries when an exception is thrown after Add but before RemoveAt, corrupting cycle detection for siblings (#1120)
- fix(core): JsonServiceSerializer.Deserialize logs first 100 chars of malformed JSON to error log on JsonException - can leak Password if it appears early in the payload (#1121)
- fix(core): XmlServiceSerializer.Deserialize/Serialize allocate XmlSerializer per call instead of caching it statically (parallel impl of #1121 + perf hit) (#1122)
- fix(core): ServiceManager.GetAllServices - LogOnAs initialized to magic string "LocalSystem" instead of LocalSystemAccount const defined in same file (#1131)
- fix(core): ServiceManager.UninstallServiceAsync - stop wait loop uses DefaultServiceStopTimeoutSeconds instead of per-service StopTimeout (asymmetric with StopServiceAsync) (#1132)
- fix(core): ServiceManager.GetDependencies - null return is overloaded (means both "service not installed" and "unexpected error") (#1133)
- fix(core): SecurityHelper.CreateSecureDirectory - silently swallows UnauthorizedAccessException without logging, leaves no audit trail when ACL hardening fails (#1137)
- fix(core): XmlServiceValidator/JsonServiceValidator - both treat ValidationResult.Warnings as blocking failures, defeating the purpose of separate Errors/Warnings collections (#1139)
- fix(core): WindowsServiceApi - [ExcludeFromCodeCoverage] on the entire class hides regressions in the non-trivial GetServices() loop (parallel to #1070 and #1092) (#1138)
- fix(core): EventIds.cs declares only base ranges (1000/2000/3000) but specific IDs (3001/3002/3003/3103/3104) are hardcoded magic numbers in ProtectedKeyProvider.cs and the taskschd scripts (#1144)
- fix(core): Servy.Core DTOs/ServiceInfo.cs constructor - LogOnAs hardcoded to 'LocalSystem' magic string instead of ServiceAccounts.LocalSystem const (parallel to #1131) (#1160)
- fix(core): Servy.Core Native/Handle.cs - process-handle wrapper class is named 'Handle' while the three siblings (SafeScmHandle / SafeServiceHandle / SafeJobObjectHandle) all follow the SafeXxxHandle BCL convention (#1161)
- fix(core): EventLogReader.MapToDto - 'evt.TimeCreated ?? DateTime.MinValue' converts implicitly to DateTimeOffset and throws ArgumentOutOfRangeException in non-UTC time zones when TimeCreated is null (#1167)
- fix(core): ProcessHelper.cs - [ExcludeFromCodeCoverage] on entire class hides regressions in non-trivial logic (BFS process-tree walker, CPU-delta cache pruning, RAM formatter, ResolvePath regex flow); parallel to #1070 / #1092 / #1138 (#1169)
- fix(core): EventLogService.cs - MaxResults (10000) and LogName ('Application') hardcoded as private consts; LogName is also duplicated in EventLogLogger and Helper.EnsureEventSourceExists (#1170)
- fix(core): ResourceHelper.CopyEmbeddedResource - outer catch misattributes restart failures (StartServices in finally) as 'Failed to copy embedded resource' when the copy itself succeeded (#1171)
- fix(core): ServiceManager.cs - Nullable IServiceRepository? parameter contradicts ArgumentNullException guard (#1176)
- fix(core): ServiceManager.GetAllServices - ServiceController handles leak on cancellation (#1183)
- fix(core): Servy.Core.Helpers.ServiceHelper constructor silently accepts null repository, NREs at runtime (#1187)
- fix(core): Multiple files - Hardcoded 1024*1024 bytes-per-MB literal bypasses AppConfig.BytesInMegabyte / AppConfig.ToBytes (#1188)
- fix(core): AppConfig.cs - MaxMaxFailedChecks / MaxMaxRestartAttempts / MaxPreLaunchRetryAttempts set to int.MaxValue, inconsistent with other Max* constants and creates overflow risk in counters (#1193)
- fix(core): EventLogLogger.cs - SafeWriteToEventLog reads _eventLog without volatile/lock; SetIsEventLogEnabled can null it concurrently (#1205)
- fix(core): EventLogLogger.cs - EventLogMessageMaxChars (31000) hardcoded; should live in AppConfig with the other Event-Log constants (#1206)
- fix(core): Logging-&-Log-Rotation.md - Event ID 3002 is documented in the 'Error' range table but the entry itself describes a Warning, contradicting the actual EventLogEntryType.Warning emitted by the code (#1210)
- fix(core): EnvironmentVariableHelper.cs - ProtectedVariables list omits DOTNET_ROOT, DOTNET_HOST_PATH, DOTNET_BUNDLE_EXTRACT_BASE_DIR - .NET runtime hijacking via env var override (#1212)
- fix(core): ProtectedKeyProvider.cs - GetCachedOrGenerate fast-path reads cacheField without synchronization; race with InvalidateCache can NRE on Clone() (#1213)
- fix(core): SecureData.cs - IDisposable class holding raw AES/HMAC keys has no finalizer; forgotten Dispose leaves key material unwiped until GC (#1214)
- fix(core): JsonServiceValidator.cs - Copy-paste bug: warning log says 'XML Import succeeded with warnings' inside the JSON validator (#1215)
- fix(core): Logger.cs / RotatingStreamWriter.cs - Date format strings use thread-default culture; produce non-Gregorian years on Thai locale (Buddhist calendar) (#1216)
- fix(core): ProtectedKeyProvider.cs - IDisposable holding decrypted AES key/IV has no finalizer; forgotten Dispose leaves plaintext key material in managed heap until GC (#1220)
- fix(core): ProcessKiller.cs - WaitForExit timeout floor only applied to KillParentWaitMs (Math.Max 1000); KillChildWaitMs/KillTreeWaitMs honor smaller values without floor (#1222)
- fix(core): ProcessKiller.cs - KillProcessTreeAndParents(int) lacks the top-level try/catch present on the string overload, leaks exceptions to callers (#1223)
- fix(core): EventLogService.cs - EventLogMaxResults caps raw events before filtering; '[' bracket heuristic and provider-name substring filter silently drop matches, so user sees fewer hits than configured (#1228)
- fix(core): ProcessKiller.cs - KillParentProcesses recursive call passes DateTime.MinValue, disabling PID-recycling identity check for grandparents and beyond (#1231)
- fix(core): Logger.cs - _currentLogLevel read on hot logging path without volatile or lock; updates may go unobserved (#1236)
- fix(core): ServiceHelper.cs (Core) - StopServices ignores PreStopTimeoutSeconds, stop wait too short when pre-stop hook configured (#1241)
- fix(core): ServiceHelper.cs (Core) - StartServices ignores PreLaunchTimeoutSeconds, start wait too short when pre-launch hook configured (#1242)
- fix(core): ServiceHelper.cs - Hardcoded 'defaultTimeoutInSeconds = 30' should reference AppConfig.DefaultServiceStartTimeoutSeconds (#1243)
- fix(core): ServiceManager.cs - UninstallServiceAsync GetByNameAsync call drops cancellationToken (#1244)
- fix(core): ProcessKiller.cs - WalkAndKillChildren recursive walk has no cycle/visited-set protection (DFS) (#1247)
- fix(core): ResourceHelper.cs - GetHostProcessLastWriteTimeUTC fallback to UtcNow forces unconditional re-extraction every startup (#1248)
- fix(core): ResourceHelper.cs - Constructor instantiates concrete ServiceHelper instead of accepting an abstraction; tightly couples and blocks unit testing (#1249)
- fix(core): ProcessHelper.cs - ValidatePath swallows InvalidOperationException, losing diagnostic context for unexpanded environment variables (#1250)
- fix(core): ResourceHelper.cs - 'targetFileName' out parameter on ShouldCopyResource is set but never consumed by either caller (#1256)
- fix(core): ResourceHelper.cs - CopyEmbeddedResource (async) and CopyEmbeddedResourceSync are asymmetric: async stops/restarts services, sync silently doesn't (#1257)
- fix(core): Helper.cs - WriteFileAtomic (sync) calls async core via .GetAwaiter().GetResult(); deadlock risk on captured SynchronizationContext (#1259)
- fix(core): Logger.cs - FormatException recurses on InnerException with no depth limit; pathological chains can blow the stack (#1260)
- fix(core): AppConfig.cs - DefaultDesktopAppPublishPath / DefaultManagerAppPublishPath use './X.exe' which is CWD-dependent, not BaseDirectory-anchored (#1266)
- fix(core): AppConfig.cs - Five-level '..........' relative paths to sibling project bin folders are fragile and break under publish/single-file/custom output (#1268)
- fix(core): ServiceExporter.cs - ExportXml string vs file overloads diverge in null handling and UTF-8 BOM behavior (#1269)
- fix(core): Helper.cs - IsServiceNameValid does not enforce the Windows SCM 256-character maximum service name length (#1279)
- fix(core): ProcessHelper.cs - MaintainCache catches only ArgumentException; Win32Exception/InvalidOperationException from Process.HasExited terminates the prune loop early (#1281)
- fix(core): XmlServiceValidator.cs / JsonServiceValidator.cs - Redundant ValidatePath call after _serviceValidationRules.Validate (#1285)
- fix(core): NativeMethods.cs - ValidateCredentials .\AccountName forms throw SecurityException because Translate runs before built-in bypass (#1286)
- fix(core): NativeMethods.cs - FILE_IDENTITY.IsDifferentFrom does asymmetric comparison when handle-info presence differs between the two probes (#1287)
- fix(core): ServiceDto.cs - Clone() omits EnableConsoleUI; cloned DTOs silently lose console-UI flag (#1290)
- fix(core): ServiceDtoHelper.cs - ApplyDefaults skips EnableConsoleUI while filling all other nullable bool defaults (#1292)
- fix(core): ServiceControllerProvider.cs - Constructor accepts factory without null guard, breaks sibling-component convention (#1293)
- fix(core): ServiceDependenciesValidator.cs - Error message and class comment omit 'periods', contradicting the regex that allows them (#1294)
- fix(core): EventLogReader.cs - MapToDto's FormatDescription() call has no try/catch; one event with a missing provider aborts the entire enumeration (#1295)
- fix(core): EnvironmentVariableParser.cs - Structural-quote strip eats trailing escaped \", contradicting the documented 'escaped quotes survive' contract (#1296)
- fix(core): ProcessHelper.cs - GetProcessTreeMetrics does not cap CPU at 100% despite ProcessMetrics XML doc promising 'capped at 100.0' for trees (#1297)
- fix(core): Logger.cs - _logRotationSizeMB field is long but every public setter takes int; the extra width is dead (#1308)
- fix(core): RotatingStreamWriter.cs - InitializeWriter omits FileShare.Delete; same root cause as #1306, affects log rotation file (#1314)
- fix(core): RotatingStreamWriter.cs - EnforceMaxRotations does not catch IO/Unauthorized exceptions from Directory.GetFiles; one stale lock terminates retention enforcement (#1315)
- fix(core): NativeMethods.cs - Multiple critical P/Invoke declarations missing SetLastError=true; callers cannot diagnose failures (#1316)
- fix(core): Helper.cs - WriteFileAtomicCore AV-retry catch on Win32Exception is dead code; File.Move throws IOException/UnauthorizedAccessException, never Win32Exception (#1317)
- fix(core): ProcessKiller.cs - Failed-kill diagnostics emitted at Debug level; production logs hide why processes survived termination requests (#1318)
- fix(core): ServiceValidationRules.cs - Service name length check is dead code; Helper.IsServiceNameValid already returns early on length violation (#1319)
- fix(core): ProtectedKeyProvider.cs - GetOrGenerate retry loop catches only IOException; UnauthorizedAccessException (AV lock) bypasses backoff (#1323)
- fix(core): ServiceHelper.cs (Core) - Misleading 'Fail-safe' comment contradicts the throw on the next line (#1328)
- fix(core): ServiceManager.cs - MapStartupType returns ServiceStartType.Manual on query failure but ServiceStartType.Unknown on unmapped enum values (#1336)
- fix(core): ConfigParser.cs - ParseInt uses culture-dependent TryParse; centralized parser should be invariant (#1341)
- fix(core): StringHelper.cs - NormalizeString joins on raw ';' without escaping pre-existing semicolons in env-var values, breaking PATH-style values (#1347)
- fix(core): AppConfig.cs / XmlServiceValidator.cs / JsonServiceValidator.cs - 'MaxConfigFileSizeMB' uses decimal MB (1,000,000) for char check but 1024*1024 binary MB for file size check (#1353)
- fix(core): Helper.cs - 'ReservedNames' is a mutable public static field; any caller can clear or mutate it (#1355)
- fix(core): Helper.cs - WriteFileAtomic uses deterministic temp file name 'path + .tmp', concurrent writers to the same path collide (#1356)
- fix(core): RotatingStreamWriter.cs - Race window: writer can re-attach to soon-to-be-moved file because PerformPhysicalRotation runs outside the lock (#1357)
- fix(core): ProtectedKeyProvider.cs - GetCachedOrGenerate fast-path snapshot is NOT immune to InvalidateCache's ZeroMemory; comment claims immunity it does not deliver (#1358)
- fix(core): ProtectedKeyProvider.cs - Library code mutates Environment.ExitCode = 13; cross-cutting global side-effect inside a key provider (#1359)
- fix(core): NativeMethods.cs - ValidateCredentials accepts non-service identities ('Everyone', 'Authenticated Users', 'Anonymous Logon') as built-in passwordless accounts (#1363)
- fix(core): NativeMethods.cs - ValidateCredentials regex check '!isBuiltIn' is dead; built-in branch already returned earlier (#1364)
- fix(core): NativeMethods.cs - AtomicSecureMove lacks null/empty validation for source and destination, breaking sibling-API guard convention (#1365)
- fix(core): rocessKiller.cs - BuildSnapshotAndChildMapNative compares against 'new IntPtr(-1)' instead of the central INVALID_HANDLE_VALUE constant (#1366)
- fix(core): ProcessHelper.cs - ResolvePath UnexpandedEnvVarRegex false-positives on legitimate filesystem paths containing literal '%' (#1367)
- fix(core): ProcessHelper.cs - FormatCpuUsage near-zero shortcut returns '0' but non-zero values use '0.0' format, breaking visual alignment (#1369)
- fix(core): AppConfig.cs - TargetFramework metadata fallback hardcoded to 'net10.0-windows' silently drives DEBUG path resolution to a wrong folder (#1370)
- fix(core): SecureData.cs - Decrypt silently returns the encrypted base64 payload as 'plaintext' when AllowLegacyV1Decryption is false (#1371)
- fix(core): ResourceHelper.cs - GetHostProcessLastWriteTimeUTC XML doc claims fallback is 'DateTime.UtcNow' but implementation returns DateTime.MinValue, flipping the staleness decision (#1372)
- fix(core): Logger.cs - LogLevel fallback uses ToUpper() instead of ToUpperInvariant(), breaks under Turkish locale (#1373)
- fix(core): RotatingStreamWriter.cs - Hardcoded rotation timing constants (RotationCooldownMs / CriticalFailureCooldownMs / SyncRotationRetry) bypass AppConfig pattern (#1376)
- fix(core): RotatingStreamWriter.cs - Timestamp format 'yyyyMMdd_HHmmss' duplicated as literal length 16 magic check in GenerateUniqueFileName (#1377)
- fix(core): RotatingStreamWriter.cs - 'Disk space growth is no longer bounded' error log omits FullName, hampering operator triage (#1378)
- fix(core): Logger.cs - Hardcoded MaxInnerExceptionDepth (16) and MaxFormattedExceptionLength (16384) bypass AppConfig central-config pattern (#1379)
- fix(core): SecureData.cs - 'Unsupported encryption version marker' exception echoes full ciphertext into the exception message and logs (#1382)
- fix(core): AppFoldersHelper.cs - 'isChildOfRoot' check uses raw StartsWith on unnormalized paths; '..' segments can fool inheritance-preservation logic (#1386)
- fix(core): ServiceHelper.cs - StartServices/StopServices asymmetric handling of TimeoutException, no CancellationToken on either (#1388)
- fix(core): Logger.cs - LoggerInitializationErrors.log and LoggerWriteErrors.log fallback files grow unbounded (no rotation) (#1389)
- fix(core): Logger.cs - Info(message) is the only level missing the Exception overload (Debug/Warn/Error all accept ex) (#1392)
- fix(core): ProtectedKeyProvider.cs - SaveProtected uses deterministic temp path 'path + .tmp' (same family as #1356) (#1397)
- fix(core): ServiceControllerWrapper.cs - GetDependencies returns empty subtree on second visit (diamond/shared dependency in tree) (#1400)
- fix(core): ServiceValidationRules.cs - wrapperExePath uses File.Exists while every other path field uses _processHelper.ValidatePath (asymmetric env-var expansion) (#1401)
- fix(core): ProcessKiller.cs - KillParentProcesses uses raw '1000' floor instead of SafeWait(AppConfig.MinKillWaitMs); deviates from sibling kill paths (#1403)
- fix(core): ProcessKiller.cs - KillProcessesUsingFile returns true after Logger.Error when file is missing (success contradicts error log) (#1404)
- fix(core): SecureData.cs - DecryptV2 wraps CryptographicException as SecureDataIntegrityException but drops the inner exception (#1405)
- fix(core): Helper.cs - WriteFileAtomicCore (async) uses non-unique '.tmp' suffix while sync sibling uses GUID; concurrent async writes to same path collide (#1406)
- fix(core): AppConfig.cs - UpdateCheckTimeoutSeconds <= UpdateCheckHttpTimeoutSeconds invariant only enforced via XML comment, no compile-time/static check (#1415)
- fix(core): EventLogService.cs - Stale 'using block / ObjectDisposedException' comment; loop iterates DTOs, not EventRecord (#1416)
- fix(core): EventLogService.cs - Hardcoded 5x cushion multiplier on AppConfig.EventLogMaxResults read should be a named constant (#1417)
- fix(core): LoggerConfigurator.cs - ConfigureFromAppSettings recreates the underlying writer up to 3x on every config load (#1418)
- fix(core): EventLogLogger.cs - Scoped logger SetLogLevel/SetIsEventLogEnabled mutate the parent's global state (#1419)
- fix(core): IServyLogger.cs - Info/Warn lack optional Exception parameter while Debug/Error accept one (asymmetric API) (#1420)
- fix(core): AppFoldersHelper.cs - Path comparison in EnsureFolders is not normalized; aesKeyFolder == aesIVFolder duplicates SecureDirectory call (#1428)
- fix(core): ResourceHelper.cs - Comment 'fallback to UtcNow' contradicts actual return DateTime.MinValue (#1429)
- fix(core): SERVY_PASSWORD environment variable name hardcoded across CLI / PowerShell module / help text (#1432)
- fix(core): EnvironmentVariableParser.Parse - Documented quote-escape example KEY=""value"" produces ""value" (extra leading quote, missing trailing) (#1442)
- fix(core): EventLogLogger.cs - ScopedEventLogLogger Info/Warn/Error level filter is overridden by parent when event log is enabled (#1443)
- fix(core): ProcessKiller.cs - BuildSnapshotAndChildMapNative silently returns empty maps when CreateToolhelp32Snapshot fails; callers cannot tell (#1450)
- fix(core): NativeMethods.FILE_IDENTITY.IsDifferentFrom returns false (same) when both probes are undeterminable, masking rotations on hostile file systems (#1456)
- fix(core): ServiceHelper.GetRunningServices - substring fallback can falsely match unrelated services (#1461)
- fix(core): RotatingStreamWriter.PrepareRotation - _lastRotationDate set BEFORE physical move; failed date-based rotations skipped until next interval (#1463)
- fix(core): RotatingStreamWriter.PerformPhysicalRotation - UnauthorizedAccessException trips circuit breaker permanently instead of retrying like IOException (#1469)
- fix(core): ProcessKiller.KillParentProcesses - missing cycle/visited guard can cause StackOverflowException on PID-reuse cycles (#1470)
- fix(core): ProcessHelper.GetProcessTree - silently returns rootPid only when CreateToolhelp32Snapshot fails (same family as #1450) (#1471)
- fix(core): ServiceManager.StartServiceAsync / StopServiceAsync swallow OperationCanceledException; inconsistent with UninstallServiceAsync which re-throws (#1475)
- fix(core): NativeMethods.GetFileIdentity - file position not restored on Read/Seek exception, leaves caller stream at offset 0 (#1479)
- fix(core): ServiceHelper.StartServices/StopServices - sc.WaitForStatus inside Task.Run is non-cancellable mid-wait; cancellation only honored between services (#1480)
- fix(core): Helper.WriteFileAtomicAsync - FlushAsync called twice (caller's lambda flushes, then WriteFileAtomicCore flushes again) (#1507)
- fix(core): ServiceControllerWrapper.cs - [ExcludeFromCodeCoverage] hides BuildDependencyTree's recursion logic from coverage (#1520)
- fix(core): Logger.cs - [ExcludeFromCodeCoverage] hides rotation, formatting, sanitization, and exception-truncation logic from coverage (#1529)
- fix(core): EventIds.cs values duplicated as hardcoded magic numbers in PowerShell scripts (3103, 3104) (#1535)
- fix(core): Logger.Initialize - inconsistent parameter name for LogLevel ('initialLevel' vs 'logLevel') across the two overloads (#1538)
- fix(core): EventLogLogger.cs - [ExcludeFromCodeCoverage] hides level filtering, message truncation and ScopedEventLogLogger logic from coverage (same family as #1529, #1520) (#1548)
- fix(core): ServiceManager.UninstallServiceAsync - stop wait loop swallows timeout, leading to a misleading 'Failed to uninstall' error if the service didn't actually stop (#1560)
- fix(core): ResourceHelper.GetHostProcessLastWriteTimeUTC - AppDomain fallback uses FriendlyName which often lacks the .exe suffix on .NET 5+, so File.Exists silently returns false (#1564)
- fix(infra): ServiceRepository.UpsertBatchAsync - IEnumerable parameter enumerated 3+ times (.Any(), .Select(...).ToList(), .ToList()) (#821)
- fix(infra): ServiceRepository.ExportJsonAsync - bypasses injected IJsonServiceSerializer, asymmetric with ImportJsonAsync (#822)
- fix(infra): ServiceRepository.GetServicePidAsync / GetServiceConsoleStateAsync - Name parameter not trimmed (inconsistent with GetByName / DeleteAsync / etc.) (#880)
- fix(infra): SQLiteDbInitializer.GetSqlType - 13 columns declared NOT NULL in the schema but defined as nullable (int?/bool?) on ServiceDto (#897)
- fix(infra): ServiceRepository.ExportXmlAsync - bypasses injected IXmlServiceSerializer, asymmetric with ImportXmlAsync (parallel to #822) (#968)
- fix(infra): DapperExecutor - ThreadLocal should be Random.Shared on .NET 6+ (correlated jitter, unnecessary allocation) (#994)
- fix(infra): ServiceRepository.ExportXmlAsync - XML preamble declares 'utf-16' but the file is written as UTF-8 (encoding mismatch in exports) (#995)
- fix(infra): ServiceRepository.ImportXml/JsonAsync - UPSERT clobbers Pid / ActiveStdoutPath / ActiveStderrPath of a running service (Manager loses tracking) (#997)
- fix(infra): DapperExecutor - sync ExecuteWithRetry inlines backoff calculation while async path uses CalculateBackoff helper (DRY) (#1085)
- fix(infra): ServiceRepository - private static XmlSerializer ServiceDtoSerializer field declared but never used (dead code) (#1123)
- fix(infra): DatabaseInitializer.InitializeDatabase - XML doc is missing the 'initializer' parameter and the matching ArgumentNullException case for it (#1124)
- fix(infra): SQLiteDbInitializer - only the unversioned-legacy path uses GetExpectedColumns to ALTER missing columns; once a DB reaches Version 1, future SqlConstants additions need a manual ApplyVersionN migration or the DB silently lacks the column (#1143)
- fix(infra): SQLiteDbInitializer.cs - ApplyVersion4 leaves PRAGMA foreign_keys=OFF on the pooled connection if migration throws (#1194)
- fix(infra): ServiceRepository.cs - PatchRuntimeStateAsync / PatchRuntimeState are duplicated and the encrypted/decrypted field list is repeated in two methods (#1204)
- fix(infra): SQLiteDbInitializer.cs - ReconcileSchema runs ALTER TABLE statements without a transaction; partial schema state survives if one ALTER fails mid-loop (#1232)
- fix(infra): SQLiteDbInitializer.cs - GetSqlType silently maps unknown column names to TEXT; a future INTEGER added to SqlConstants but not to nullableInts is created with the wrong affinity (#1233)
- fix(infra): ServiceRepository.cs - DecryptDto null-check is inconsistent between sync and async overloads (#1251)
- fix(infra): DapperExecutor.cs - CalculateBackoff exponential backoff has silent int-overflow path and no upper cap (#1307)
- fix(infra): SQLiteDbInitializer.cs - ApplyVersion1 lacks a transaction; partial failure leaves DB unrecoverable (#1327)
- fix(infra): DapperExecutor.cs - Async retry path silently rethrows on exhaustion while sync path logs Warn (asymmetric retry diagnostics) (#1380)
- fix(infra): DapperExecutor.cs - XML doc claims 'Uses SpinWait' but ExecuteWithRetry actually uses Thread.Sleep (#1381)
- fix(infra): ServiceRepository.UpsertBatchAsync - idMap.TryGetValue(service.Name, ...) throws ArgumentNullException when DTO.Name is null (#1434)
- fix(infra): ServiceRepository.Update - sync-over-async (.GetAwaiter().GetResult()) on PatchRuntimeStateAsync risks UI deadlock (#1435)
- fix(infra): DapperExecutor.cs - Doc-comment typo 'Uses Uses Thread.Sleep' (duplicate word) (#1440)
- fix(infra): ServiceRepository.cs - ImportXmlAsync/ImportJsonAsync swallow OperationCanceledException, returning false instead of propagating cancellation (#1451)
- fix(infra): ServiceRepository.cs - 'updateRuntimeState' parameter name is inverted: false means update, true means skip (#1452)
- fix(infra): DapperExecutor.cs - QueryAsync(string) and QuerySingleOrDefaultAsync(string) overloads missing null check on sql parameter (#1459)
- fix(infra): DapperExecutor.cs - [ExcludeFromCodeCoverage] hides ExecuteWithRetry/ExecuteWithRetryAsync, CalculateBackoff, and FormatSqlForLog logic from coverage (same family as #1529, #1520, #1548) (#1554)
- fix(infra): SQLiteDbInitializer.cs - [ExcludeFromCodeCoverage] hides migration sequencing, table-rebuild (V4), legacy upgrade and ReconcileSchema self-healing logic from coverage (same family as #1529, #1520, #1548, #1554) (#1557)
- fix(service): prevent recovery counter jump by locking gate during terminal actions
- fix(service): ServiceHelper.LogStartupArguments - sensitive env vars are formatted unconditionally even when EnableDebugLogs is false (#824)
- fix(service): ServiceHelper.GetSanitizedArgs - production dead code, only invoked from a single test (#828)
- fix(service): ProcessLauncher.cs - synchronous mode buffers entire stdout/stderr in memory until exit (#846)
- fix(service): Service.cs HandleLogWriters - XML doc comment buried inside method body becomes a no-op (#859)
- fix(service): ServiceHelper.RestartService - log message hardcodes '4 minutes' instead of using RestarterExeMaxWaitMs constant (#928)
- fix(service): ProcessLauncher.Start - appends empty content to StdOutPath/StdErrPath when redirectOutput is false, creating spurious zero-byte log files (#953)
- fix(service): RunFailureProgram bypasses ProcessLauncher.Start centralized utility (#959)
- fix(service): ProcessWrapper.Stop and ProcessWrapper.StopPrivate are ~90% duplicated stop sequences (#961)
- fix(service): ServiceHelper.ValidateStartupOptions - Pre-Stop and Post-Stop paths/working dirs are not validated, while Pre-Launch and Post-Launch are (#967)
- fix(service): EventLogLogger.CreateScoped - every scoped logger allocates a fresh EventLog handle (resource leak across scopes) (#973)
- fix(service): StartOptionsParser.Parse - RecoveryAction default is hardcoded RecoveryAction.None instead of AppConfig.DefaultRecoveryAction (parallel to #893 / #892) (#974)
- fix(service): ProcessLauncher.Start - process wrapper leaked on timeout / log-writer failure (no dispose path when exception thrown) (#989)
- fix(service): OnCustomCommand fallback teardown calls Environment.Exit(1) - overrides distinct exit codes set by ProtectedKeyProvider (e.g. 13) (#1003)
- fix(service): ServiceHelper.InitializeStartup is dead production code - public method not in IServiceHelper interface, only called by unit tests (#1007)
- fix(service): OnOutputDataReceived/OnErrorDataReceived - IsNullOrWhiteSpace silently drops blank/whitespace-only lines from child output (#1040)
- fix(service): ServiceHelper SensitiveKeyWords contains overly-broad 'KEY' entry that masks legitimate values like FOREIGN_KEY/PRIMARY_KEY/SSH_KEY in connection-strings logs (#1055)
- fix(service): ServiceHelper.RestarterExeMaxWaitMs (240000ms) hardcoded as public const, should live in AppConfig (#1053)
- fix(service): ProcessLauncher.Start - TimeoutMs <= 0 guard fires AFTER process started and writers opened (line 154) (#1056)
- fix(service): ServiceHelper.MaskSensitiveValue - KeyMatcherRegex.IsMatch has no RegexMatchTimeoutException catch (inconsistent with MaskRawArguments) (#1081)
- fix(service): ProcessLauncher.Start - child process leaks if writer setup throws after process.Start(): finally only Disposes the wrapper, the started process keeps running orphaned (#1086)
- fix(service): ProcessExtensions.GetAllDescendants - recursive call takes a fresh Toolhelp32 snapshot per node, so a tree of N processes does N system-wide snapshots (#1088)
- fix(service): ProcessExtensions.Format - only catches InvalidOperationException, will throw Win32Exception ('Access denied') on protected processes (#1089)
- fix(service): StartOptionsParser.Parse uses unchecked enum casts (Priority, DateRotationType, RecoveryAction) - bypasses ConfigParser.ParseEnum used by ServiceMapper.ToDomain on the same DTO (#1090)
- fix(service): Service.cs ConditionalResetRestartAttemptsAsync - _fileSemaphore.WaitAsync() called without CancellationToken (inconsistent with read/write helpers) (#1093)
- fix(service): PreShutdownWaitHintMs (30000) and ScmStartupRequestThresholdSeconds (20) hardcoded as private consts, should live in AppConfig (#1094)
- fix(service): ServiceHelper.ValidateStartupOptions - 13 nearly-identical path/working-dir guard blocks (DRY) (#1102)
- fix(service): Servy.Service Hook.Dispose - '_disposed = true' set inside the 'if (disposing)' block, inconsistent with every other Dispose pattern in the codebase (#1106)
- fix(service): Service.cs OnStart - ExitCode 1064 commented as ERROR_SERVICE_SPECIFIC_ERROR but Win32 1064 is actually ERROR_EXCEPTION_IN_SERVICE (line 416 comments same constant correctly) (#1134)
- fix(service): ProcessLauncher.ApplyLanguageFixes - Python overrides silently win over user EnvironmentVariables, but Java's -Dfile.encoding respects user value (asymmetric) (#1141)
- fix(service): Service.cs constructor - AppDbContext created but never Disposed (parallel to #1127) (#1145)
- fix(service): ConsoleAppDetector.CheckPEHeaderForConsole - 'magic' is read from the optional header but never used; PE32/PE32+ magic word is referenced in comments but not validated (#1168)
- fix(service): Service.cs - Misleading comment: 1066 is ERROR_SERVICE_SPECIFIC_ERROR, not ERROR_EXCEPTION_IN_SERVICE (#1174)
- fix(service): missing string interpolation in pre-launch failure log (literal {attempt} in output) (#1175)
- fix(service): ProcessLauncher.cs - StandardOutputEncoding set without RedirectStandardOutput throws InvalidOperationException (#1177)
- fix(service): Service.cs - Missing string interpolation in teardown error log (literal {reason} in output) (#1184)
- fix(service): Helper.cs / Service.cs - Reserved Windows device names (CON/PRN/AUX/NUL/COMn/LPTn) not rejected by IsServiceNameValid or MakeFilenameSafe (#1189)
- fix(service): Service.cs - _cancellationSource disposed without first calling Cancel(), causes ObjectDisposedException for in-flight tasks (#1190)
- fix(service): Service.cs - CheckHealth logs ExitCode read failure at Debug level (silent in production), inconsistent with OnProcessExited which logs at Warn (#1192)
- fix(service): Service.cs - WriteAttemptsInternalAsync uses non-atomic File.WriteAllTextAsync; power loss during write resets restart-attempt counter to 0 (#1195)
- fix(service): ProcessWrapper.cs - SendCtrlC mutates process-wide console state (FreeConsole / AttachConsole / SetConsoleCtrlHandler) without a process-wide lock (#1207)
- fix(service): ProcessLauncher.cs - OutputDataReceived/ErrorDataReceived handlers can rethrow into Process pipe-drain thread, surfacing as AppDomain.UnhandledException on transient log-write failures (#1224)
- fix(service): ProcessLauncher.cs - WaitForExitWithHeartbeat ignores TimeoutMs for the first WaitChunkMs slice; small TimeoutMs gets silently rounded up (#1225)
- fix(service): ProcessWrapper.cs - StopTree hardcodes 3000ms postKillWaitMs while Stop honors operator StopTimeout; descendants ignore configured timeout (#1227)
- fix(service): Service.cs - CheckHealth catch and Cleanup hook loop use static Logger instead of scoped _logger, dropping service-name prefix (#1229)
- fix(service): Service.cs - ConditionalResetRestartAttemptsAsync 'maintain previous session' branch never re-resets after the first cross-boot transit; restart counter sticks indefinitely (#1230)
- fix(service): ProcessLauncher.cs - Java -Dfile.encoding detection IndexOf can false-positive on paths or jar names containing the literal (#1234)
- fix(service): ProcessLauncher.cs - Unbounded WaitForExit() after timeout-aware loop can hang the synchronous launch path (#1235)
- fix(service): ProcessExtensions.cs - GetAllDescendants lacks cycle protection (visited-set), unlike ProcessHelper.GetProcessTree (#1246)
- fix(service): ProcessLauncher.cs - pathsMatch uses raw string comparison; equivalent paths with different normalization create two writers for the same file (#1282)
- fix(service): ProcessLauncher.cs - Hook stdout/stderr FileStreams omit FileShare.Delete; blocks external rotation/delete (LogTailer.cs:108 explicitly does the opposite) (#1306)
- fix(service): EnvironmentVariableHelper.cs - ExpandWithDictionary silently truncates on length cap; outer loop logs, inner loop does not (#1325)
- fix(service): ProcessExtensions.cs - GetChildren/GetAllDescendants leak Process handles when StartTime throws after GetProcessById succeeds (#1326)
- fix(service): Service.cs - SafeKillProcess collapses null Stop() result to true, masking timeouts as graceful cancellation (#1334)
- fix(service): Service.cs - OnStart Stop() paths leave ExitCode=0, SCM treats failed startup as graceful stop (#1337)
- fix(service): Service.cs - public SetProcessPriority dereferences _childProcess with null-forgiving operator and no guard (#1338)
- fix(service): ProcessLauncher.cs - WaitForExitWithHeartbeat enters tight CPU spin when WaitChunkMs is 0 (#1339)
- fix(service): ProcessLauncher.cs - Java -Dfile.encoding regex lacks RegexMatchTimeout, deviates from project pattern (ReDoS surface in launch path) (#1374)
- fix(service): ServiceHelper.cs (Servy.Service) - RestartProcess logs 'Process restarted' even when startProcess delegate is null (false success log) (#1375)
- fix(service): ServiceHelper.cs - EnvironmentVariablesToString throws if any variable Name is null (NRE during startup logging) (#1393)
- fix(service): Servy.Service ServiceHelper.cs - EnsureValidWorkingDirectory silently overrides operator config and can pass null path to Path.GetDirectoryName (#1398)
- fix(service): StartOptionsParser.cs - MapPriority silently defaults unrecognized ProcessPriority enum values to Normal (no log) (#1399)
- fix(service): StartOptions.cs vs Service.cs - Property defaults diverge from AppConfig (Priority, RecoveryAction, DateRotationType, HeartbeatInterval, StartTimeout, StopTimeout, MaxFailedChecks, EnableSizeRotation, EnableDateRotation, PreLaunchIgnoreFailure) (#1407)
- fix(service): Service.cs - Integer overflow in detectionWindowSeconds when HeartbeatInterval and MaxFailedChecks are at validated maximums (#1433)
- fix(service): StartOptionsParser.MapPriority - Normal case falls through to default and triggers misleading 'Unknown ProcessPriority' warning on every service start (#1437)
- fix(service): EnvironmentVariableHelper.cs - Custom env vars with empty values silently dropped; references like %EMPTY_VAR% remain literal in output (#1447)
- fix(service): ProcessLauncher.Start - WaitForExit(timeout) does not drain async output handlers; comment is misleading (#1485)
- fix(service): ProcessLauncher.Start - lazy stdout/stderr writer init retried on every output line when file open fails, flooding logs (#1489)
- fix(service): Service.cs OnStart - 'nint.Zero' used in a single line while the rest of the file uses 'IntPtr.Zero' (#1506)
- fix(service): Service.cs OnStart - ConditionalResetRestartAttempts task captures _cancellationSource before it is initialized; cancellation effectively unreachable in the typical timing (#1511)
- fix(service): Service.cs - background ConditionalReset continuation logs only InnerException.Message (no type/stack, drops siblings) (#1524)
- fix(service): Service.cs Cleanup - tracked hook Process.Kill(entireProcessTree:true) is synchronous with no timeout (#1558)
- fix(service,restarter): invalid EnableEventLog config value (e.g. 'yes', '1') silently disables event log; asymmetric with the documented '?? "true"' default and with neighbouring TryParse fallbacks in the same method (#1166)
- fix(restarter): ServiceController.Status - missing ThrowIfDisposed check, inconsistent with all other public members (#948)
- fix(restarter): ServiceRestarter.RestartService - TimeoutException thrown AFTER controller.Start() with misleading 'before the service could be started' wording (#949)
- fix(restarter): Program.Main mixes static Logger.Info with scopedLogger.Error - info events skip the service-name scope (#1001)
- fix(restarter): ServiceRestarter.RestartService - variable named 'startRemaining' is used during the STOP phase (line 44-48), misleads readers about which phase is racing the deadline (#1084)
- fix(restarter): Program.cs - Duplicate scopedLogger.SetLogLevel call (#1178)
- fix(restarter): ServiceRestarter.cs - HandleTransitionalError passes potentially negative TimeSpan to WaitForStatus (#1179)
- fix(restarter): Program.cs - protectedKeyProvider disposed before secureData (reverse-of-construction order) (#1202)
- fix(restarter): ServiceRestarter.cs - Stop-phase timeout exception incorrectly says 'No time remaining to start service' (#1263)
- fix(restarter): HandleTransitionalError 'Running' branch is dead code; Start phase lacks the same transitional-race handling Stop has (#1300)
- fix(restarter): HandleTransitionalError TimeoutException messages omit service name, breaking correlation in Event Log (#1301)
- fix(desktop): ServiceConfigurationValidator.Validate (Servy desktop) - 'checkServiceStatus' parameter is documented but never used in the body (dead parameter or unimplemented feature) (#899)
- fix(desktop): ServiceConfiguration.cs (Servy desktop) - multiple hardcoded default values bypass AppConfig.Default* constants (#981)
- fix(desktop): Servy.Mappers.ServiceConfigurationMapper - entire class is dead code (no callers) and silently drops EnableConsoleUI / EnableDebugLogs (#998)
- fix(desktop): MainViewModel parameterless constructor always throws (chained ctor rejects null) - body is dead code (#1011)
- fix(desktop): MainViewModel - constructor and ClearForm duplicate ~60 lines of default initialization (DRY) (#1013)
- fix(desktop): Servy ServiceCommands.InstallService rebuilds Config->DTO mapping (drifts from MainViewModel.ModelToServiceDto, different sentinel values) (#1021)
- fix(desktop): Servy ServiceCommands.cs - XML doc references parameter 'serviceRepository' that does not exist (#1022)
- fix(desktop): ServiceCommands - cancellationToken parameter accepted but never propagated to InstallServiceAsync / File.ReadAllTextAsync (#1023)
- fix(desktop): MainViewModel.cs - Import/Export commands don't set IsBusy, breaking command-availability invariants for parallel clicks (#1270)
- fix(desktop): MainViewModel.cs - BindServiceDtoToModel XML doc claims Password and ConfirmPassword are 'set to the same value' but ConfirmPassword is reset to string.Empty (#1335)
- fix(desktop): PasswordBoxHelper.cs - IsUpdating flag leaks if UpdateSource() throws (no try/finally) (#1425)
- fix(desktop,manager): Nullable reference types enabled in 5 projects (Core/CLI/Infra/Restarter/Service) but disabled in the 3 WPF projects (Servy / Servy.Manager / Servy.UI) (#903)
- fix(desktop,manager): MainWindow.xaml.cs OnClosing - Application.Current.Shutdown() called unconditionally, ignoring e.Cancel (#905)
- fix(desktop,manager): AppBootstrapper.cs InitializeAppAsync - 'processHelper' parameter is unused (the field _processHelper is used instead) (#907)
- fix(desktop,manager): AppBootstrapper.cs - fatal/unexpected error MessageBoxes use hardcoded English strings, bypassing Strings.resx (#908)
- fix(desktop,manager): Servy/App.xaml.cs and Servy.Manager/App.xaml.cs - ~80% duplicated code (StartAvailabilityMonitor, OnStartup, OnExit, faults, CTOR boilerplate) (#909)
- fix(desktop,manager): BulkObservableCollection - AddRange/TrimToSize fire CollectionChanged(Reset) but NOT PropertyChanged for Count and Item[] (#914)
- fix(desktop,manager): HelpService.cs - GitHub API URL hardcoded inline instead of in AppConfig (alongside DocumentationLink, LatestReleaseLink) (#915)
- fix(desktop,manager): Servy.UI/Helpers/Helper.cs GetRowsInfo - i18n bug: surrounding 'No', 'Loaded', 'in' words are hardcoded English even though rowText is localized (#916)
- fix(desktop,manager): Servy/App.xaml.cs and Servy.Manager/App.xaml.cs StartAvailabilityMonitor - Directory.CreateDirectory side-effect creates the optional executable's parent directory (#921)
- fix(desktop,manager): Servy.UI AsyncCommand.ExecuteAsync - TOCTOU race between CanExecute() and _isExecuting=true allows two parallel executions when called off the UI thread (#1125)
- fix(desktop,manager): ImportGuard.ValidateFileSizeAsync - file-not-found error is hardcoded English '[Import] File not found:' while size error uses localized format string (parallels #908) (#1140)
- fix(desktop,manager): Servy.UI InverseBooleanConverter.ConvertBack - bare 'catch { b = false; }' silently swallows all exceptions, fabricates an inverted value on garbage input (parallel to #1071) (#1162)
- fix(desktop,manager): Servy.UI DesignTimeFileDialogService - OpenXml / OpenJson doc-comments are stale copy-paste from OpenExecutable ('Simulates selecting an executable file' on the XML and JSON variants) (#1163)
- fix(desktop,manager): Servy.UI FileDialogService - every filter string and dialog title is hardcoded English ('Executable files', 'Select XML file', 'Select startup directory') bypassing Strings.resx (parallel to #908 / #1140) (#1164)
- fix(desktop,manager): Servy.UI HelpService.CheckUpdates - 10-second cancellation timeout (and the static 20-second HttpClient.Timeout) are private magic numbers, should live in AppConfig (#1165)
- fix(desktop,manager): Nullable IServiceRepository? parameters contradict ArgumentNullException guards (#1185)
- fix(desktop,manager): AppBootstrapper.cs - ProtectedKeyProvider created and never disposed; cached AES key material lingers in heap until GC (#1208)
- fix(desktop,manager): AppBootstrapper.cs - StartAvailabilityMonitor outer catch kills watcher permanently after single transient error (#1218)
- fix(desktop,manager): AppBootstrapper.cs - StartAvailabilityMonitor outer catch kills watcher permanently after single transient error (#1219)
- fix(desktop,manager): AppBootstrapper.cs - OnExit disposes _appLifetimeCts while async-void StartAvailabilityMonitor still observes its Token; ObjectDisposedException promoted to AppDomain.UnhandledException (#1221)
- fix(desktop,manager): AppBootstrapper.cs - SplashWindowFactory always invoked even when showSplash=false; resulting Window leaks until GC (#1238)
- fix(desktop,manager): AppBootstrapper.cs - OnExit double-disposes _availabilityWatcher (CleanupAvailabilityWatcher already nulls it out) (#1239)
- fix(desktop,manager): AppBootstrapper.cs - When OnStartup calls app.Shutdown() for security/SQLite checks, caller still proceeds to StartAvailabilityMonitor + InitializeAppWithFaultHandlingAsync (#1278)
- fix(desktop,manager): BulkObservableCollection.cs - TrimToSize(maxItems) throws ArgumentException when maxItems is negative (#1329)
- fix(desktop,manager): WpfUiDispatcher.cs - YieldAsync uses Dispatcher.CurrentDispatcher (per-thread), not the UI dispatcher; latent bug for any future background-thread caller (#1305)
- fix(desktop,manager): WpfUiDispatcher.cs - InvokeAsync(Action) silently no-ops on null while InvokeAsync(Func) throws ArgumentNullException (#1333)
- fix(desktop,manager): HelpService.cs - CheckUpdates ReadAsStringAsync ignores cts.Token, body-stage stall bypasses timeout (#1349)
- fix(desktop,manager): FileDialogService.cs - OpenFolder uses WinForms FolderBrowserDialog (Description is body label, not title); WPF Microsoft.Win32.OpenFolderDialog available since net8.0 (#1350)
- fix(desktop,manager): AppBootstrapper.cs - StartAvailabilityMonitor is 'async void' on a non-event-handler method; unobserved exceptions can crash the process (#1361)
- fix(desktop,manager): AppBootstrapper.cs - SQLite version-check abort uses app.Shutdown() with default exit code 0; sibling admin-check uses Shutdown(1) (#1362)
- fix(desktop,manager): AsyncCommand.cs - Generic 'AsyncCommand execution failed' log entry omits command identity, blocks 3am triage in a WPF app with many commands (#1383)
- fix(desktop,manager): HelpService.cs - Asymmetric handling of null Process.Start result between OpenDocumentation and CheckUpdates (#1387)
- fix(desktop,manager): Manager vs WPF ServiceConfigurationValidator - Reverse warning/error order; Manager comment contradicts code (#1426)
- fix(desktop,manager): AppBootstrapper.cs - Connection string read from wrong config key, silently ignores user override (#1431)
- fix(desktop,manager): AppBootstrapper.cs - AppDomain.UnhandledException handler calls MessageBox.Show; non-UI threads can deadlock or crash (#1499)
- fix(desktop,manager): AppBootstrapper.StartAvailabilityMonitorAsync - FileSystemWatcher EnableRaisingEvents = true is set in the object initializer before event handlers are subscribed (event-loss race window) (#1553)
- fix(manager,cli,service,restarter): Hardcoded delay literals (Task.Delay / Thread.Sleep) bypass the AppConfig timing-constants pattern (#1186)
- fix(desktop,manager,cli,service,restarter): Multiple Program.cs files - Duplicated logger/config bootstrap block across CLI, Restarter, Service, and Manager entry points (#1201)
- fix(desktop,manager,cli,service,restarter): appsettings*.json - DefaultConnection / AESKey / AESIV paths duplicated across 5 files (and re-defined in AppConfig.cs) (#1427)
- fix(manager): ServiceCommands.ExecuteLockedAsync - eager eviction races with GetOrAdd, allows two threads to run the same service operation in parallel (#831)
- fix(manager): LogTailer.RunFromPosition - XML doc references non-existent parameter 'token' (actual name is 'externalToken') (#890)
- fix(manager): ConsoleViewModel.cs - case-sensitive path comparisons trigger spurious resume on Windows (paths are case-insensitive) (#904)
- fix(manager): MainWindow.xaml.cs MainTabControl_SelectionChangedAsync - exception not logged when MessageBoxService is available, silently lost on dialog dismiss (#906)
- fix(manager): App.xaml.cs - typo 'Dependencis' in DependenciesRefreshIntervalInMs XML doc (#910)
- fix(manager): DependenciesViewModel.cs SetExpansion - recursive traversal has no cycle guard, infinite recursion / stack overflow if tree contains a cycle (#912)
- fix(manager): DependenciesViewModel.cs duplicates SearchServicesAsync from ServiceSearchViewModelBase (DRY) (#913)
- fix(manager): ServiceMapper.ToModel - three near-identical overloads (Performance/Console/Dependency Service) instead of one base-class mapping (#919)
- fix(manager): ServiceCommands.ImportXmlConfigAsync/ImportJsonConfigAsync - instantiates serializers directly instead of using validators-style DI (#832)
- fix(manager): ServiceCommands.CopyPid - Clipboard.SetText called without ensuring STA Dispatcher context (#833)
- fix(manager): ServiceCommands.InstallServiceAsync - 'if (wrapperExeDir == null)' is always true (dead defensive check + CS8600 candidate) (#894)
- fix(manager): DependenciesViewModel.cs - repeated 'depdency treer' typo in three command XML doc-comments (#911)
- fix(manager): Servy.Manager ServiceMapper - '?? string.Empty' is unreachable because GetLogOnAsDisplayName never returns null (#920)
- fix(manager): LogsViewModel.cs - magic number 3 for default 'last N days' filter on first load (#924)
- fix(manager): ServiceRowViewModel.Service_PropertyChanged - explicit switch cases are redundant; default branch already forwards every PropertyName 1:1 (#925)
- fix(manager): LogTailer.LoadHistory - historical lines synthesize timestamps 1ms apart, capping the rendered time-spread of any backlog at maxLines milliseconds (#939)
- fix(manager): ServiceSearchViewModelBase.SearchServicesAsync - Helper.IsRunningInUnitTest() branch in production code path is a layering violation (#983)
- fix(manager): ServiceMapper.GetLogOnAsDisplayName - magic string "LocalSystem" duplicated instead of using a named SCM constant (#984)
- fix(manager): ServiceMapper.GetLogOnAsDisplayName - XML doc typo 'ser session display name' (should be 'User') (#1006)
- fix(manager): MainViewModel parameterless constructor always throws (chained ctor rejects null) - same dead-code pattern as Servy (#1017)
- fix(manager): MainViewModel - six occurrences of 'if (ServiceCommands == null) throw new InvalidOperationException' (DRY) (#1018)
- fix(manager): MainViewModel - typo 'Setp 5' in step comment (line 583) (#1019)
- fix(manager): MainViewModel - MaxBulkOperationParallelism = 8 is a private const, should live in AppConfig (#1020)
- fix(manager): ServiceCommands.StartProcess - XML doc references parameter 'app' that does not exist (#1024)
- fix(manager): ServiceCommands.RefreshServices - typo 'resfresh' in XML doc, plus dead null check on injected callback (#1025)
- fix(manager): ConsoleViewModel - SearchDebounceDelayMs = 300 hardcoded as private const, should live in AppConfig (#1026)
- fix(manager): PerformanceViewModel.Dispose duplicates the _monitoringCts cleanup already performed by MonitoringViewModelBase.Dispose (#1109)
- fix(manager): CpuUsageConverter & RamUsageConverter - stale 'this will safely return false' comment doesn't match the string-returning code (#1110)
- fix(manager): ServiceConfigurationValidator depends on concrete ServiceValidationRules instead of IServiceValidationRules (parallel to CLI) (#1111)
- fix(manager): ServiceConfigurationValidator.Validate shows first Warning before any Error, hiding the actually-blocking issue (parallel impl of #1108) (#1126)
- fix(manager): PidConverter.Convert - dead null check (early return + redundant '?.' / '??') (#1154)
- fix(manager): Converters - Pid/Message/CpuUsage/RamUsage throw NotImplementedException in ConvertBack while StatusConverter/StartupTypeConverter return Binding.DoNothing (inconsistent) (#1155)
- fix(manager): CpuUsageConverter / RamUsageConverter constructor throws InvalidOperationException at design time when App.Services is null (breaks XAML designer) (#1156)
- fix(manager): LogEntryModel.LevelIcon - magic-string level comparison ('Information'/'Warning'/'Error') and four hardcoded pack-URI strings (EventLogLevel enum already exists) (#1157)
- fix(manager): Models inconsistency - Service.cs uses SetProperty/CallerMemberName helper, LogEntryModel.cs hand-rolls INotifyPropertyChanged setters with nameof (DRY) (#1158)
- fix(manager): Models Service.cs - backing field '_userSession' for LogOnAs property is inconsistent with every other field in the file (#1159)
- fix(manager): LogTailer.cs - Hardcoded MaxSafeLines / LogBatchFlushThreshold / FileRetryDelayMs / 1000ms recovery delay should live in AppConfig (#1203)
- fix(manager): LogTailer.cs - lastPosition tracked via fs.Position overshoots StreamReader buffer; transient errors skip log content on reopen (#1245)
- fix(manager): PerformanceViewModel.cs - 'Pre-allocated to avoid GC pressure' comment is misleading; Clone() still allocates a new PointCollection on every tick (#1264)
- fix(manager): PerformanceViewModel.cs - hardcoded magic '100.0' for stepX should derive from PerformanceHistoryCapacity (#1265)
- fix(manager): LogsViewModel.cs - eventLogService constructor parameter is not null-guarded; sibling deps are (#1267)
- fix(manager): MainViewModel.cs (Manager) è Dispose(bool) skips most cleanup that Cleanup() performs (timer, CTS, ServiceCommands, busy cursor) (#1271)
- fix(manager): MainViewModel.cs (Manager) - SearchServicesAsync uses ServiceCommands?.SearchServicesAsync but follow-up lines presume non-null result, NREs if guard ever triggers (#1272)
- fix(manager): ServiceCommands.cs - CopyPid uses Thread.Sleep on the WPF Dispatcher; clipboard contention freezes the UI for up to 250ms (#1273)
- fix(manager): MainViewModel.cs - Cleanup() disposes shared ServiceCommands on tab switch; subsequent tab returns leak semaphores via the still-shared disposed engine (#1274)
- fix(manager): MainWindow.xaml.cs (Manager) - Five fire-and-forget '_ = AsyncMethod()' wrappers swallow exceptions silently; only one wrapper has a try/catch (#1275)
- fix(manager): App.xaml.cs - CustomConfigAction parses RefreshIntervalInSeconds/etc. without bounds-checking; zero or negative values crash the timer-creating VMs at startup (#1276)
- fix(manager): ConsoleViewModel.cs - Mixes injected IUiDispatcher with direct Application.Current.Dispatcher reads, breaking testability (#1277)
- fix(manager): WpfUiDispatcher.cs - YieldAsync uses Dispatcher.CurrentDispatcher (per-thread), not the UI dispatcher; latent bug for any future background-thread caller (#1305)
- fix(manager): ServiceCommands.cs - ArgumentException thrown for null arguments instead of ArgumentNullException (Export/RemoveService) (#1321)
- fix(manager): Manager MainWindow.xaml.cs - Three Handle*TabSelected helpers declared async without any await (CS1998) (#1322)
- fix(manager): MonitoringViewModelBase.cs - StopMonitoring 'clearView' parameter declared but ignored in base implementation (#1330)
- fix(manager): ServiceRowViewModel.cs - CanExecuteServiceCommand ignores Service.Status while Status changes still trigger RaiseCanExecuteChanged (#1331)
- fix(manager): DependenciesViewModel.cs - Constructor XML doc omits messageBoxService and contains 'depdency' typos in command summaries (#1332)
- fix(manager): RamUsageConverter.cs - Convert() docs/comment claim 'PID' and 'double?' but value is the RAM usage as long (#1348)
- fix(manager): ServiceCommands.cs - ExecuteLockedAsync semaphore acquisition ignores caller's CancellationToken (#1352)
- fix(manager): ServiceMapper.cs (Manager) - ToModelAsync ignores CancellationToken; bulk search keeps churning processHelper after cancel (#1354)
- fix(manager): App.xaml.cs - Enum.TryParse for LogLevel accepts numeric strings as valid (same family as #1283/#1284/#1289/#1324) (#1390)
- fix(manager): StatusConverter.cs - Convert/ConvertBack maps duplicated; null/unknown value masquerades as 'Not Installed' (#1421)
- fix(manager): App.xaml.cs (Manager) - ConsoleMaxLines upper bound is DefaultConsoleMaxLines*2 magic factor; deviates from sibling GetConfigInt calls (#1422)
- fix(manager): LogTailer.cs - File-not-found uses 3 different retry delays (1000ms / 500ms / 200ms) for similar transient conditions (#1423)
- fix(manager): LogLine.cs / LogTailer.cs - Timestamp Kind drifts: live tailing uses DateTime.Now (Local), history uses LastWriteTimeUtc (Utc) (#1424)
- fix(manager): LogTailer.cs - RunFromPosition byte tracking assumes Environment.NewLine, drifts on LF-only log files (#1455)
- fix(manager): Servy.Manager.Services.ServiceCommands.ImportConfigAsync - no CancellationToken throughout import chain; large/slow-network XML/JSON imports cannot be cancelled (#1483)
- fix(manager): LogTailer.LoadHistory - off-by-one drops the last N lines when file lacks a trailing newline (#1515)
- fix(manager): ServiceCommands.ExecuteServiceCommandAsync - Task.Run drops cancellationToken (inconsistent with InstallServiceAsync/UninstallServiceAsync siblings) (#1551)
- fix(cli): InstallServiceCommand - four ParseEnumOption defaults hardcoded as enum literals instead of AppConfig.Default* constants (#895)
- fix(cli): InstallServiceOptions.cs --params HelpText example uses singular --param= which the CLI does not accept (#964)
- fix(cli): ExportServiceCommand.SaveFile - reserved device name check bypassed by extra extension (e.g. NUL.config.json passes) (#991)
- fix(cli): ExportServiceCommand.SaveFile - comment says '8. Final Atomic Write' but File.WriteAllText is not atomic; Helper.WriteFileAtomic exists (#992)
- fix(cli): InstallServiceCommand - nine int.TryParse fallback lines should call ConfigParser.ParseInt (DRY) (#1035)
- fix(cli): ImportServiceCommand.ValidateServicePaths - hardcoded list of 11 path fields will silently miss new fields added to ServiceDto (#1036)
- fix(cli): ServiceInstallValidator depends on concrete ServiceValidationRules instead of IServiceValidationRules (#1107)
- fix(cli): ervy.CLI ServiceInstallValidator.Validate - reports first Warning before any Error, hiding the actually-blocking issue (#1108)
- fix(cli): Program.Main - AppDbContext is created but never Disposed; finally only disposes _secureData and Logger (#1127)
- fix(cli): Program.Main - Environment.Exit(1064) on SQLite version mismatch reuses Windows-service exit code (ERROR_EXCEPTION_IN_SERVICE) for a console-app exit (#1142)
- fix(cli): Program.cs - SQLite version-fail path uses Environment.Exit, skipping the finally block that flushes the logger (#1280)
- fix(cli): InstallServiceOptions.cs - Multiple copy-paste errors in XML doc summaries (process vs pre-launch/post-launch/pre-stop/post-stop) (#1288)
- fix(cli): ImportServiceCommand.cs - TryParseFileType accepts numeric strings like '0'/'1' for ConfigFileType (#1283)
- fix(cli): InstallServiceCommand.cs - ParseEnumOption accepts undefined numeric enum values (#1284)
- fix(cli): ServiceInstallValidator.cs - MapEnum accepts numeric strings (e.g. '99') as valid enum values (#1289)
- fix(cli): ServiceInstallValidator.cs - TryMapToDto omits EnableConsoleUI; validator DTO drifts from desktop/CLI install paths (#1298)
- fix(cli): ExportServiceCommand.cs - Enum.TryParse accepts numeric strings ('0'/'1') for ConfigFileType (same defect as #1283) (#1324)
- fix(cli): Commands - 7 of 8 Execute methods drop CancellationToken; only UninstallServiceCommand accepts one (#1408)
- fix(cli): BaseCommand.cs - ExecuteWithHandling and ExecuteWithHandlingAsync are near-identical 30-line duplicates (DRY) (#1430)
- fix(cli): ImportServiceCommand.ProcessImportInternalAsync - File.ReadAllTextAsync ignores cancellationToken; cannot abort on slow/network paths (#1436)
- fix(cli): ImportServiceCommand.ProcessImportInternalAsync - DB import happens before path validation; failed --installService leaves DB in dirty state (#1439)
- fix(cli): ImportServiceCommand.TryInstallServiceAsync / GetByNameAsync don't accept or propagate the import's CancellationToken (#1466)
- fix(cli): StartServiceCommand.ExecuteAsync - cancellationToken not propagated to StartServiceAsync (inconsistent with Stop/Restart) (#1474)
- fix(cli): ImportServiceCommand.ProcessXmlAsync - CancellationToken not forwarded; XML imports/installs are not cancellable while JSON ones are (#1540)
- fix(cli): ExportServiceCommand.ExecuteAsync - GetByNameAsync existence check missing CancellationToken (inconsistent with ExportXml/JsonAsync) (#1541)
- fix(cli,restarter,service): ProtectedKeyProvider is never disposed in CLI / Restarter / Service composition roots (cached unprotected key material leaks) (#1182)
- fix(cli,restarter,service): linker.xml duplicated across Servy.Service / Servy.Restarter (and 90% overlapping with Servy.CLI) (#1569)
- fix(psm1): Format-SecureLogMessage - docstring example output does not match actual output (says space-separated, but actual is '=' separated) (#951)
- fix(psm1): Invoke-ServyCli - synchronous stdout read can defeat ServyTimeoutSeconds (CLI may hang past timeout) (#987)
- fix(psm1): Export-ServyServiceConfig and Import-ServyServiceConfig - docstring claims 'Requires Administrator privileges' but Assert-Administrator is never called (#988)
- fix(psm1): identical ValidatePattern regex duplicated for EnvVars and PreLaunchEnv (DRY) (#1029)
- fix(psm1): $script:ServyTimeoutSeconds (600s) and $script:ServyMaxBufferChars (1MB) are hardcoded with no public way to override (#1043)
- fix(psm1): Format-SecureLogMessage line 186 inline regex comment has typo ("[]" should be "[^"]") (#1052)
- fix(psm1): $script:EnvVarValidationPattern is a ReDoS-prone alternation regex with no timeout (PS 2.0 .NET regex doesn't support timeouts) (#1091)
- fix(psm1): Add-Arg ArrayList 'performance' optimization is defeated by PowerShell pipeline unrolling on return (#1237)
- fix(psm1): Logging-&-Log-Rotation.md - Documents -DateRotationType 'None' option, but Install-ServyService [ValidateSet] only accepts Daily/Weekly/Monthly (#1240)
- fix(psm1): Invoke-ServyCli scrubs stderr (success path) and both streams (catch path) but leaves success-path stdout unscrubbed (#1299)
- fix(psm1): Buffer field named ByteCount actually accumulates char count (.Length), making truncation cap unit-ambiguous (#1402)
- fix(tests): CLI command tests - Hardcoded English message assertions break on any Strings.resx edit (#744)
- fix(publish): Five per-project publish.ps1 scripts each contain a near-identical Check-LastExitCode function (DRY) (#977)
- fix(publish): (Servy / Servy.CLI / Servy.Manager) - if ($null -ne $signPath) is unreachable; Join-Path never returns null (#978)
- fix(publish): setup/publish-sc.ps1 and setup/publish-fd.ps1 are ~70% identical (Inno-Setup retry loop, Tool Discovery, Check-LastExitCode, Remove-ItemSafely, package construction) (#1030)
- fix(publish): setup/signpath.ps1 - line 167 contains invalid UTF-8 byte (0x97 / Windows-1252 em-dash) in comment (#1063)
- fix(publish): framework-dependent (publish-fd.ps1) invocation is commented out, leaving FD build orchestration dead code in the master entry script (#1149)
- fix(publish): signpath.ps1 - while-loop unloading the SignPath module relies on Remove-Module silently succeeding, infinite loop if removal is suppressed by -ErrorAction SilentlyContinue (#1150)
- fix(publish): setup/winget/manifests/.../1.0/aelassas.servy.installer.yaml - version+SHA+TFM hardcoded and bump-version.ps1 does NOT update them (#1061)
- fix(publish): setup/scoop/servy.json - version 4.0 + sha256 hardcoded; bump-version.ps1 does NOT update it (#1062)
- fix(publish): publish-common.ps1 - Copy-Item -Recurse -Exclude does not match files in subdirectories; smtp-cred.xml could be packaged into installer (#1200)
- fix(publish): publish-sc.ps1 / publish-fd.ps1 - Inconsistent error termination ('return' vs 'exit 1') between sister scripts (#1255)
- fix(publish): publish-common.ps1 - Check-LastExitCode uses unapproved verb (PSScriptAnalyzer warning) (#1411)
- fix(publish): build-common.ps1 vs publish-common.ps1 - Two divergent definitions of Check-LastExitCode (one exits, one doesn't) (#1412)
- fix(publish): signpath.ps1 - Config key uppercased with culture-dependent ToUpper() (Turkish locale breaks 'I' keys) (#1413)
- fix(publish): tools-config.ps1 - Resolve-Tool returns env-var path without Test-Path validation, may return non-existent path (#1414)
- fix(bump-version): Get-ChildItem -Recurse on $baseDir without bin/obj/.git/packages exclusion (same root cause as #1258, different file) (#1302)
- fix(bump-runtime): Get-ChildItem -Recurse on $baseDir without bin/obj/.git/packages exclusion can rewrite vendor and build-output files (#1258)
- fix(notifications): setup/taskschd/*.ps1 - dead PS 2.0 $PSScriptRoot fallback in scripts that already declare #Requires -Version 3.0+ (#955)
- fix(notifications): ServyFailureEmail.ps1 - $serviceName not HTML-encoded in email body (only $logText is); subject also unscrubbed (#1031)
- fix(notifications): ServyFailureEmail.ps1 - EnableSsl hardcoded to $true; no way to use internal SMTP relays that don't support TLS (#1032)
- fix(notifications): ServyFailureNotification.ps1 and ServyFailureEmail.ps1 share ~50 lines of identical watermark/event-processing logic (DRY) (#1034)
- fix(notifications): ServyFailureEmail.ps1 - SmtpClient.Send has no Timeout set, defaults to 100s blocking the scheduled task (#1147)
- fix(notifications): ServyFailureNotification.ps1 - Toast Tag/Group both set to 'Servy' causes new toasts to silently replace earlier ones in Action Center (#1044)
- fix(notifications): ServyFailureNotification.ps1 - '#requires -Version 5.1' makes the PS 2.0 fallback at lines 126-132 unreachable (#1057)
- fix(notifications): ServySecurity.ps1 - broad 'KEY' keyword masks legitimate values like FOREIGN_KEY/PRIMARY_KEY (parallel impl of #1055) (#1058)
- fix(notifications): ServyFailureEmail.ps1 - Protect-SensitiveString runs AFTER HTML encoding, defeating the quoted-string regex branch and leaking partial credentials (#1101)
- fix(notifications): Get-ServyLastErrors.ps1 - DateTime.Parse uses current-culture, locale-dependent date parsing of LastProcessed (#1146)
- fix(notifications): Get-ServyLastErrors.ps1 / ServyFailureEmail.ps1 / ServyFailureNotification.ps1 - fallback .log files use Out-File without -Encoding and grow unbounded forever (#1148)
- fix(notifications): ServyFailureNotification.ps1 - Show-Notification references caller-scope $evt without declaring it as a parameter (#1180)
- fix(notifications): ServySecurity.ps1 - Protect-SensitiveString claims parity with C# MaskingRegex but is missing the space-separator and '/' branches; secrets in CLI args leak into email/toast notifications (#1196)
- fix(notifications): Servy-Watermark.psm1 - Update-Watermark guards $null against a [DateTime] (value type) parameter, which can never be null (#1199)
- fix(notifications): ServyFailureEmail.ps1 - Watermark advances even when SMTP send fails; transient outages permanently lose alerts (#1252)
- fix(notifications): ServyFailureNotification.ps1 - Watermark advances even when toast notification fails; transient errors permanently lose alerts (#1253)
- fix(notifications): Get-ServyLastErrors.ps1 / Servy-Watermark.psm1 - EVENT_ID_ERROR=3103 duplicated; consumers hardcode 3104 instead of using exported constants (#1254)
- fix(notifications): ServyFailureEmail.ps1 - 'break' inside switch inside foreach exits switch only, foreach keeps processing on transient SMTP failure (#1344)
- fix(notifications): taskschd .vbs and .xml files hardcode 'C:\Program Files\Servy' - tasks break on custom install paths (#1345)
- fix(notifications): ServySecurity.ps1 - Protect-SensitiveString masking regex has no timeout, vulnerable to ReDoS via nested quantifiers (#1368)
- fix(notifications): ServyFailureEmail.ps1 - Catch-all classifies permanent SmtpExceptions (auth failure, invalid recipient) as TransientFailure, causing watermark stall (#1384)
- fix(notifications): Servy-Watermark.psm1 - Silently loads with Write-Warning when Get-ServyLastErrors.ps1 / Write-ServyLog.ps1 are missing, causing 'command not found' at runtime (#1385)
- fix(notifications): Servy-Watermark.psm1 - $EVENT_ID_ERROR_DEP declared but never used (#1394)
- fix(notifications): Get-ServyLastErrors.ps1 - References $EVENT_ID_ERROR from parent module scope without declaring it as a parameter (same family as #1180) (#1395)
- fix(notifications): Write-ServyLog.ps1 - rotatedFilePath variable computed but never used (dead code) (#1410)
- fix(Get-FileEncoding): Get-FileEncoding.ps1 - ReadAllBytes loads entire file into memory just to inspect 3-byte BOM (#1136)
- fix(bump-runtime): Get-FileEncoding only detects UTF-8 BOM, while bump-version.ps1's identical-name helper also detects UTF-16 LE/BE BOMs (#979)
- fix(bump-runtime): .EXAMPLE comments reference 'update-runtime.ps1' but actual file name is 'bump-runtime.ps1' (#1135)
- fix(bump-runtime): regex 'net\d+.\d+' will partially replace inside three-segment versions like 'net10.0.1', mangling the version (#1532)
- fix(bump-runtime): bin/obj exclusion regex '[/]' never matches Windows backslash paths (#1572)
- fix(bump-version): local $matches assignment shadows PowerShell automatic variable (#950)
- fix(bump-version): setup/publish-sc.ps1 and publish-fd.ps1 - default $Version stuck at "1.0" while bump-version.ps1 only updates setup/publish.ps1 (#956)
- fix(bump-version): csproj loop is silent on no-match (unlike Update-FileContent helper) (#1002)
- fix(bump-version): Write-Error does not terminate the script, allowing silent partial failures across version bump steps (#1198)
- fix(docs): Environment-Variables.md - verification recipe uses 'cmd.exe /c set > ... && pause' which makes the service hang in Session 0 (no console = pause never returns) (#1151)
- ci(test): Notify Codecov/Coveralls comments are posted on CLOSED issues, so maintainers receive no notification (#1096)
- ci(test): test.yml - Invoke-Expression to run dotnet test command is unnecessary and an injection-prone anti-pattern (#1262)
- ci(publish): 7-Zip and CycloneDX CLI binaries downloaded without Authenticode signature verification (#849)
- ci(publish): 'dotnet tool install --global CycloneDX' has no version pin (#851)
- ci(publish): unit tests run in Debug while artifacts ship from Release; release-only regressions slip through CI (#862)
- ci(publish): 'Cool down API' step uses legacy 'powershell' shell instead of 'pwsh' (inconsistent with every other step) (#1082)
- ci(publish): initial 'Build Servy projects' phase rebuilds every project that is then re-built via 'dotnet publish' later (wasted work + risk of stale artifacts) (#1083)
- ci(publish): Initial dotnet build runs in Debug while rest of workflow is Release (wasted CI time) (#1181)
- fix(publish): publish-common.ps1 - New-PortablePackage comment 'compress contents of the folder, not the folder itself' contradicts the actual 7-Zip invocation which includes the folder itself (#1226)
- ci(setup-dotnet): .github/actions/setup-dotnet/action.yml - 'version' input + DOTNET_VERSION env are dead; install version actually comes from global.json (#1261)
- ci(publish): publish.yml - Copy-Item -Recurse -Exclude does not match files in subdirectories (same bug as #1200, different file; also packages portable 7z) (#1303)
- ci(choco): chocolateyuninstall.ps1 - 'Servy*' wildcard lookup mis-handles multi-match case (silent partial uninstall) (#1340)
- ci(security): 'Scan for vulnerable packages' uses legacy 'powershell' shell instead of 'pwsh' (parallels #1082) (#1097)
- ci(security): Concurrency block commented out (lines 26-28); either delete or activate (#1098)
- ci(release): 'changelog' workflow triggers this run but is missing from the validation list (asymmetry) (#1099)
- ci(dotnet-reflection): comment claims 'disabled' but workflow_dispatch is still active; also typo 'worflow' and commented-out triggers (#1100)
- ci(sbom): sbom.yml - Generate SBOM step skips exit-code checks between five dotnet-CycloneDX invocations; partial-failure completes with success status (#1312)
- ci(sonar): sonar.yml - Missing top-level permissions block leaves GITHUB_TOKEN at repo defaults; PR trigger silently commented out (#1310)
- ci(loc): loc.yml - 'Create total LoC badge' step uses 'curl -s' without -f or exit-code check; shields.io HTTP errors are silently published as the LoC total badge (#1313)
- ci: five workflow files saved as Windows-1252 (Non-ISO extended-ASCII) instead of UTF-8, breaking YAML 1.2 spec and rendering em-dashes as mojibake (#954)
- chore(deps): update dependencies