Skip to content

Servy 8.4

Choose a tag to compare

@aelassas aelassas released this 11 May 12:15
· 1467 commits to main since this release

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 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): ProcessKiller.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): 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): 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): Servy.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)
  • test: 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.yml): Notify Codecov/Coveralls comments are posted on CLOSED issues, so maintainers receive no notification (#1096)
  • ci(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