github aelassas/servy v10.0
Servy 10.0

5 hours ago

This major release includes new backup and restore features, a hardened PowerShell module, bug fixes, security patches, code quality improvements, and documentation updates. The full changelog is listed below.

Full Changelog

Click to expand release notes!
  • feat: add Servy-Dump.ps1 and Servy-Restore.ps1 PowerShell backup and recovery scripts
  • fix(security): break inheritance and enforce read-only ACLs on configuration files
  • fix(security): Set-ServyExePermissions.ps1 - Grant Delete permission strictly to restarter executable
  • fix(security): Set-ServyExePermissions.ps1 - binaries absent at run time are skipped in grey text, reported as "complete", and inherit Modify when Servy creates them later (#6201)
  • fix(security): Set-ServyExePermissions.ps1 - a present-but-FAILED binary lands in the same skipped bucket as a missing one, and a wholly missing directory exits 0 (residual of #6201) (#6397)
  • fix(security): Set-ServyExePermissions.ps1 - explicit ACEs of third-party principals survive hardening and ResourceHelper re-applies them on every re-extract (sibling of fixed #1291) (#6398)
  • fix(security): Set-ServyExePermissions.ps1 - explicit Deny ACEs for the target survive the purge the comment claims, and the audit prints them under [Target Granted] (#6412)
  • fix(security): Set-ServyExePermissions.ps1 - the step-5 audit translates ACEs to NTAccount in bulk, so one orphaned SID reports a successfully hardened binary as FAILED (#6411)
  • fix(security): Set-ServyExePermissions.ps1 - -TargetAccount SYSTEM or Administrators makes step 4 replace the FullControl step 3 just granted (#6438)
  • fix(security): Set-ServyExePermissions.ps1 - the unconditional owner rewrite is absent from both the header and the step-5 audit (#6439)
  • fix(security): Set-ServyExePermissions.ps1 - the [Target Granted] if-guard wraps an already empty-safe foreach, unlike its two header-gating siblings (sibling of #6421) (#6440)
  • fix(security): Set-ServyExePermissions.ps1 - the [Owner] replaced audit line prints before Set-Acl commits, so a failed hardening still logs an owner replacement that never happened (#6465)
  • fix(security): Set-ServyExePermissions.ps1 - the SID-to-name translate-with-fallback expression is maintained twice, once per audit fix (#6467)
  • fix(security): Set-ServyExePermissions.ps1 - -TargetAccount Everyone, Users or Authenticated Users makes step 4 re-grant the broad group step 2 just purged (sibling of #6438) (#6468)
  • fix(security): Set-ServyExePermissions.ps1 - the ARM64 probe reads process-architecture env vars, so an emulated x64 shell on ARM64 hardens the wrong handle binary and exits 2 forever (#6472)
  • fix(security): Set-ServyExePermissions.ps1 - an unresolvable -TargetAccount leaves through throw and exits 1, the code the header reserves for 'Privilege Error' (sibling of #6466) (#6536)
  • fix(security): Set-ServyExePermissions.ps1 - a target account that is a MEMBER of Administrators keeps FullControl through the group, and the audit reports the ReadAndExecute ACE as if it were the effective access (#6680)
  • fix(security): Set-ServyExePermissions.ps1 - Test-ServyAdminGroupMember's ADSI fallback returns $false for a nested-group admin, so the #6680 warning never fires (#6697)
  • fix(core): clarify CPU affinity error messages
  • fix(core): AppConfig.cs - ServyServiceCLIFileName and AppFolderName are still static readonly after #3181 converted every other plain literal to const (#5501)
  • fix(core): AffinityHelper.cs - ValidateAffinity returns the raw exception message, so every CPU affinity validation error shows "(Parameter 'affinityInput')" to the user (#5502)
  • fix(core): Helper.cs - IsValidPath and ParseVersion null-check their argument but declare it non-nullable, unlike the seven siblings in the same file that do the same guard (#5510)
  • fix(core): ProcessHelper.cs - Math.Max(totalCpu, 0.0) in GetProcessTreeMetrics is provably a no-op; every GetProcessMetrics path returns a non-negative CpuUsage (#5513)
  • fix(core): ProcessKiller.cs - KillProcessTreeAndParents(int) catches only ArgumentException from GetProcessById where its string sibling also catches InvalidOperationException, turning an exited target into a logged failure (#5514)
  • fix(core): ResourceHelper.cs - a failed host-exe timestamp probe always trips the #4803 downgrade warning, so the log reports a version downgrade that did not happen (#5516)
  • fix(core): ResourceHelper.cs / Service.cs (Core) / LogTailer.cs - the last seven unsuffixed async methods in src/; CopyEmbeddedResource is bare while its sync sibling is named ForceSync (#5517)
  • fix(core): Helper.cs / ServiceDependenciesValidator.cs - a service name Servy will install can be un-referenceable as a dependency; the charset rules diverge (residual of #5038) (#5518)
  • fix(core): EventLogLogger.cs - the constructor prefix bypasses SanitizePrefixSegment, so a top-level prefix can still forge bracket nesting (residual of #2166) (#5521)
  • fix(core): EventLogReader.cs - SafeToOffset guards the near-MinValue overflow but not its near-MaxValue mirror, so west-of-UTC hosts can still abort the whole enumeration (residual of #2170) (#5522)
  • fix(core): Logger.cs - FormatException's depth cap prunes the exception tree silently while its length cap marks the cut, so a truncated chain reads as complete (residual of #1260) (#5524)
  • fix(core): RotatingStreamWriter.cs - Dispose and Flush skip the _rotationInProgress gate WriteInternal waits on, so Shutdown returns mid-rotation and Flush silently no-ops (residual of #1357) (#5526)
  • fix(core): NativeMethods.cs - the #3224 fix removed CreateFile and the std-handle imports but left their five constants, LOGON32_LOGON_INTERACTIVE and the single-part 'partial' modifier behind (#5536)
  • fix(core): NativeMethods.cs - PROCESSENTRY32 is still CharSet.Auto after #3223 pinned its two functions to the W exports, the two QueryServiceConfig2 overloads bind one export with two CharSets, and CloseHandle carries one it cannot use (#5537)
  • fix(core): Strings.resx (Servy.Core) - the #5163 fix lowercased three sentence openers and added its terminal periods only to the four entries it quoted (#5538)
  • fix(core): Strings.resx (Servy.Core) - the #5370 fix removed the only consumer of Msg_ImportReadFailure but left the string and its generated accessor shipped (#5539)
  • fix(core): Strings.resx (Servy.Core) - the 18 Msg_Security* values announce one outcome with five different prefixes (#5540)
  • fix(core): IEventLogService.cs / LogsViewModel.cs - the tailored 'run as Administrator' and 'Event Log service not running' messages never reach the user; the interface documents no exceptions and the handler shows Msg_UnexpectedError (#5548)
  • fix(core): AppConfig.cs / Servy.Core.csproj - the missing-BuiltWithFramework error tells the developer to add an item, but the project declares it as an with _Parameter1/_Parameter2 (#5551)
  • fix(core): ServiceManager.cs - OpenSCManager/CreateService results carry an unreachable null term at five sites while the five OpenService results, same non-nullable SafeHandle contract, carry none (#5553)
  • fix(core): ServiceManager.cs - the uninstall ChangeServiceConfig passes eleven positional arguments, six of them null, where its install sibling and CreateService name every one (#5555)
  • fix(core): ImportServiceCommand.cs / ServiceValidationRules.cs - the CLI import ignores [ServicePath].ErrorResourceKey that all 12 ServiceDto properties declare, so 11 of 12 path failures get a generic, half-localized message (#5569)
  • fix(core): AppFoldersHelper.cs - a relative Data Source or AES path override is anchored to the current working directory, so the CLI and the service silently use different vaults (#5912)
  • fix(core): Helper.cs - a failed WriteFileAtomic leaves the destination with its ReadOnly attribute stripped; PrepareDestinationForMove clears it and nothing restores it (#5916)
  • fix(core): ProcessKiller.cs - a third dead term #5515 missed: exactStartTime == DateTime.MinValue cannot fire because line 533 reads StartTime directly, not through SafeStartTime (residue of #3205) (#5920)
  • fix(core): SecurityHelper.cs - ApplySecurityRules takes IdentityReference but compares against SecurityIdentifier-typed rules, so an NTAccount argument silently disables the anti-squatting purge while the grant still lands (#5959)
  • fix(core): IServiceControllerProvider.cs - GetServices hands the caller several hundred IDisposable wrappers holding live SCM handles, and neither the returns nor the remarks says the caller owns them (#5969)
  • fix(core): ServiceManager.cs - QueryServiceConfigString's 'return null' is unreachable: a size probe that sets ERROR_INSUFFICIENT_BUFFER always reports a positive bytesNeeded, so the outer guard and the inner test cannot both hold (#5971)
  • fix(core): AppFoldersHelper.cs - rootVaultPath is the one path parameter EnsureFolders does not guard, so a blank value reaches SecurityHelper and fails with ParamName 'path' (#6103)
  • fix(core): NativeMethodsHelpers.cs - ForbiddenGroupIdentities pairs every name with an NT AUTHORITY\ form but carries no BUILTIN\ form, so BUILTIN\Administrators reaches LogonUser and is reported as a wrong password (#6122)
  • fix(core): ProcessKiller.cs - WalkAndKillChildren logs a benign exited-child race as a kill failure, unlike the string overload's handle-open path (sibling of #5514) (#6344)
  • fix(core): RotatingStreamWriter.cs - EnforceMaxRotations null-coalesces two NotNullIfNotNull Path APIs and comments a GetFileName null that cannot occur (#6349)
  • fix(core): NativeMethods.cs - SERVICE_QUERY_STATUS and SERVICE_QUERY_CONFIG sit in the SCM Access Rights region their prefix and usage contradict (residual of #3227) (#6363)
  • fix(core): Strings.resx (Servy.Core) - three pre-launch/pre-stop validation messages start lowercase against every capitalized sibling (#6365)
  • fix(core): Strings.resx (Servy.Core) - Msg_ImportXmlFailed carries a trailing period its JSON twin and the ': {0}' template family do not (#6366)
  • fix(core): ServiceManager.cs / ServiceCommands.cs - the #4235 orphan-row repair lives only in the CLI, so the Manager lists a DB-only service it can never uninstall (#6374)
  • fix(core): ServiceManager.cs - uninstalling an already-stopped service logs a spurious Win32-error warning on every run (residual of #1607) (#6377)
  • fix(core): ServicePathValidator.cs - the unchecked 'as string' turns a misapplied [ServicePath] on a non-string property into a silent no-op or a false 'missing' (#6387)
  • fix(core): PathSecurityGuard.cs - IsDirectoryAclHardened tests rights against Write|Modify|FullControl, a mask equal to FullControl, so the default Users:ReadAndExecute ACE under Program Files is reported as Write/Modify on every startup (residual of #6602) (#6628)
  • fix(core): PathSecurityGuard.cs - IsSafelyContainedWithinAppDirectory documents rejecting reparse points but walks ancestors only, the one of the three #6602/#6525 guards without the file-level symlink check (#6629)
  • fix(core): ProcessLauncher.cs / Helper.cs - HasAncestorReparsePoint cannot tell a volume mount point from a junction, so since #6525 a service logging below a mounted volume has its output discarded after one Error line naming a junction nobody created (#6635)
  • fix(infra): SQLiteDbInitializer.cs - the LatestSchemaVersion sync check can only fire when the database is NEWER than the build, yet its warning tells the reader to wire a missing migration block (#5570)
  • fix(infra): SQLiteDbInitializer.cs - a database newer than LatestSchemaVersion is reconciled and ALTERed instead of refused, and the single warning names the wrong cause (#5991)
  • fix(service): Service.cs / ProcessLaunchOptions.cs - the 5 AuditContext values restored by #5234 use 4 different conventions, so Pre-Launch and post-launch bracket differently in the same log (#5624)
  • fix(service): ProcessLauncher.cs - Start never cancels the async read pumps or unsubscribes its handlers before the finally disposes the writers they hold, the #262 race in the 1 of 3 Begin sites that only got the #1485 drain (#5628)
  • fix(service): Service.cs - OnStart re-derives the cancellation token from the field at two sites after capturing capturedToken for exactly that reason (residual of #2222) (#5632)
  • fix(service): Service.cs - SafeKillProcess abandons the timed-out stop task with no orphan-observing continuation, unlike FlushAndShutdownLogger in the same file (#5635)
  • fix(service): ServiceHelper.cs - the startup dump records 40 of the 41 StartOptions properties; the one it omits is EnableDebugLogs, the flag that decides whether its own second half is written (#6038)
  • fix(service): ServiceHelper.cs - the Pre-Stop and Post-Stop banners in the startup dump are 29 and 30 characters where the other twelve are all 31, so the last two sections step out of the ruler (#6039)
  • fix(service): Service.cs - the #5250 fix added the ct local to InitiateRecoveryAsync but passed it to one of the three calls it named; both SaveRestartAttemptsAsync sites are still tokenless (#6046)
  • fix(service): Service.cs - EmitHeartbeatPing's second enableFlags test is provably always true: the line-1402 guard already pinned it, and line 1418 writes the same predicate without it (#6049)
  • fix(service): ProcessHelper.cs (Service) - ExpandAndAudit runs the unexpanded-placeholder audit over the whole inherited environment, so a machine-wide Path with a literal %VAR% is reported as the service's own misconfigured variable on every launch (#6506)
  • fix(service): ServiceHelper.cs / ServySecurity.ps1 - a sensitive keyword followed by a letter is never masked, so AZURE_CREDENTIALS, SECRETS, TOKENS and PASSWORDS leak in full (suffix mirror of #5877) (#6511)
  • fix(service): ProcessExtensions.cs / ProcessWrapper.cs - GetChildren returns the same empty list for "did not look" and "no children", so a failed lineage capture logs "No active descendants found for PID 0" (sibling of #4639) (#6512)
  • fix(service): ServiceHelper.cs / ServySecurity.ps1 - the #6511 plural S? is accepted only after the last underscore segment, so SECRETS_FILE, TOKENS_PATH and DB_PASSWORDS_ENC leak in full while SECRET_FILE is masked (residual of #6511) (#6627)
  • fix(service): Service.cs - SharedPingClient has an infinite PooledConnectionLifetime, so a heartbeat host that changes address is never re-resolved while pings keep the connection warm (#6545)
  • fix(service): Service.cs - the three _fileSemaphore.Release() calls are still unguarded against teardown disposal, the pattern #4424 fixed for _healthCheckSemaphore (#6546)
  • fix(service): Service.cs - StartPreStopProcess null-tests a non-nullable parameter its only caller guarded fourteen lines earlier (class of #5791 / #6516) (#6547)
  • fix(service): Service.cs - EnsureRestartAttemptsFileAsync returns 0 for 'could not read', so an unreadable attempts file (a 229+ character service name, or a denied ACL) silently makes MaxRestartAttempts unlimited (#6550)
  • fix(service): ProcessLauncher.cs - the #6525 handle-path verification is skipped silently when the fixed 1024-char resolver fails, the fail-open shape #3523 fixed in PathSecurityGuard, beside a second copy of its prefix stripping (#6630)
  • fix(service): Service.cs - the restart counter is written with InvariantCulture and read with culture-sensitive int.TryParse, and line 1420 is the only EndsWith in src/ without a StringComparison; InvariantGlobalization in the csproj is what keeps them correct (#6551)
  • fix(service): Service.cs - OnProcessExited declares a non-nullable sender against EventHandler's object?, so all three subscription sites carry a null-forgiving ! on the method group; CheckHealth in the same file already uses object? (#6556)
  • fix(restarter): ServiceRestarter.cs - the class doing the entire restart has no logger, so 8 swallowed SCM exceptions and every retry are invisible between Program.cs's two log lines (#5619)
  • fix(restarter): ServiceRestarter.cs - a permanent SCM refusal (disabled service, logon failure, access denied) is retried as a pending transition for the full timeout and surfaces as a TimeoutException with no inner cause (sibling of #5116) (#6505)
  • fix(ui): BulkObservableCollection.cs - AddRange and TrimToSize bypass CheckReentrancy(), so a mutation during a Reset notification corrupts silently instead of throwing (#5639)
  • fix(ui): AppBootstrapper.cs - the last three production CancellationToken.None sites opt a 25.8 MB extraction and a service-stop loop out of shutdown cancellation (family of #4434/#5250) (#5640)
  • fix(ui): AppBootstrapper.cs - EnsureEventSourceExists runs on the UI thread and outside the splash stopwatch, so the splash freezes and the minimum-display floor is measured from the wrong origin (residual of #6053) (#6205)
  • fix(ui): AppBootstrapper.cs - the error-dialog debounce keys on the exception message, so a varying PID or path in the text reopens the modal spam #1762 closed (#6206)
  • fix(ui): AppBootstrapper.cs - the admin and SQLite failure dialogs are the one option set left outside the #5248 validation, so a missing text shows a blank refusal modal (#6243)
  • fix(ui): HelpService.cs - a 200 response with no tag_name is reported as 'No updates currently available' with nothing logged, the misleading outcome #2128 removed for an unparseable tag (#6566)
  • fix(ui): RelayCommand.cs - a non-null parameter of the wrong type is coerced to default(T) exactly like null, the comment describes only the null case, and all 19 instantiations are RelayCommand so the value-type guard has no caller (#6641)
  • fix(ui): HelpService.cs - OpenExternalUrl disposes the launched Process through an empty using block, eight lines and two nesting levels for what process.Dispose() says directly (#6644)
  • fix(ui): HelpService.cs - the update-check error message's tagName ?? "" fallback only fires for a null tag, not the empty-string case IsNullOrEmpty(tagName) also guards (#6741)
  • fix(desktop): App.xaml.cs (Servy) - MessageBoxService still gets a new WpfUiDispatcher(), the line #2696 fixed in the Manager twin, and IUiDispatcher is never registered here at all (#5648)
  • fix(desktop): MainViewModel.cs / ServiceConfigurationValidator.cs (Servy) - the four import/export handlers are the only ones not wrapped in Task.Run, and they are the ones whose validation makes a domain logon on the UI thread (#5657)
  • fix(desktop): StringHelper.cs / MainViewModel.cs - FormatEnvironmentVariables propagates FormatException into BindServiceDtoToModel, which has no catch, so a legacy row leaves the form half-bound; the ?? that hides it is dead since #1842 (#5923)
  • fix(desktop): MainViewModel.cs / MainWindow.xaml (Servy) - IsBusy disables the commands and nothing else, so the form stays editable while Install runs from a snapshot taken on a thread-pool thread, and nothing on screen shows it is busy (#6591)
  • fix(desktop): MainWindow.xaml.cs (Servy) - MainViewModel.Dispose() is not called from OnClosed or anywhere else, so the unsubscribe #3851 was closed for is unreachable again (the Manager twin disposes at OnClosed) (#6597)
  • fix(desktop): appsettings.desktop.json / App.xaml.cs / ServiceCommands.cs (Servy) - the elevated desktop app launches whatever ManagerAppPublishPath names after a File.Exists check, so the install directory's ACL is the only control (#6602)
  • fix(desktop): ServiceCommands.cs (Servy) - OpenSecurityHardeningGuideAsync keeps the empty-using disposal shape #6644 names in HelpService.cs, while OpenManagerAsync twelve lines above and both Manager launch sites use the inline form (#6648)
  • fix(manager): ServiceCommands.cs (Manager) - ImportConfigAsync is the one cancellation catch of eight that logs nothing, so a cancelled import leaves no trace at all (#5586)
  • fix(manager): MainViewModel.cs - HandleSearchExceptionAsync still shows a Warning dialog, the sibling #4355 fixed in LogsViewModel; and that sibling now double-logs (#5598)
  • fix(manager): HistoryResult.cs / LogTailer.cs - the rotation timestamp must be UTC and is compared Kind-blind, but none of its four API surfaces says so (#5602)
  • fix(manager): PerformanceViewModel.cs - AddPoint's two Math clamp terms on line 356 are provably dead, and the headroom remark describes the clamp that cannot fire (residual of #4373) (#5603)
  • fix(manager): LogLine.cs - the ctor's 'normalized to UTC' promise shifts a Kind.Unspecified timestamp by the local offset, the one Kind a log-line parser produces (#5998)
  • fix(manager): ConsoleViewModel.cs / DependenciesViewModel.cs / PerformanceViewModel.cs - only 1 of the 3 CreateServiceItem overrides keeps the Pid it is handed, so 2 tabs show N/A and disable Copy PID for a full refresh interval (#6002)
  • fix(manager): LogsView.xaml - the Message column is the only header-bearing column in the Manager with no SortMemberPath, so its header is clickable and inert (#6023)
  • fix(manager): ServiceSearchUserControl.cs / MainWindow.xaml.cs - the three 'search if empty' guards treat an empty result as 'never searched', so a search that matches nothing re-runs on every tab switch (#6029)
  • fix(manager): DependenciesView.xaml / MainWindow.xaml - the dependency TreeView is the only items control with no local IsVirtualizing, so it inherits False from DependenciesTab and builds every node eagerly (#6034)
  • fix(manager): App.xaml.cs (Manager) - the bootstrapper's parsed service-name argument is accepted by the factory and silently dropped, unlike the Servy twin that loads it (#6396)
  • fix(manager): LogTailer.cs - the threshold-flush torn-line guard still trusts the buffered fs.Position, so a complete line mid-buffer is held back and merged with its successor (residual of #5200/#3475) (#6471)
  • fix(manager): MainViewModel.cs (Manager) - RemoveService matches the row name with a case-sensitive == while every other name lookup in the file is OrdinalIgnoreCase (#6481)
  • fix(manager): ConsoleView.xaml.cs / ServiceCommands.cs (Manager) - the #1320 fix added catch (ExternalException) beside catch (COMException) instead of replacing it; COMException derives from ExternalException, so both sites carry a subsumed arm with an identical body (#6489)
  • fix(manager): MainViewModel.cs (Manager) - the post-search refresh is dropped, not deferred, when a timer refresh is in flight, and nothing cancels that tick, so new rows wait a full interval (#6491)
  • fix(manager): MainViewModel.cs (Manager) - RefreshAllServicesAsync enumerates the SCM and decrypts every DB row before checking whether the snapshot is empty (#6492)
  • fix(manager): MainViewModel.cs (Manager) - SearchText and IsConfiguratorEnabled hand-roll the compare-and-raise idiom while every sibling property in the hierarchy uses ViewModelBase.Set (#6494)
  • fix(manager): ServiceCommands.cs (Servy + Manager) - a domain-validation rejection on import is a Warn line in the desktop twin and silent in the Manager twin, the one exit of five in that method the Manager log cannot show (#6676)
  • fix(manager): ConsoleView.xaml.cs - Clipboard copy failure after exhausted retries is silent, no user feedback (#6714)
  • fix(cli): Servy.CLI.csproj - DebuggerSupport is false here but true in the two sibling trimmed executables, and those are the only three files that set it (#5491)
  • fix(cli): InstallServiceOptions.cs - four HelpTexts state their default but heartbeatUrlTimeoutSeconds, priority and startupType omit theirs (sibling of #4709) (#6320)
  • fix(cli): Program.cs (Servy.CLI) - the unknown-command guard null-checks firstArg on the branch that proves it non-null (#6321)
  • fix(cli): Strings.resx (Servy.CLI) - the two '{0}: ' log templates disagree on capitalization after the shared prefix (#6322)
  • fix(cli): UninstallServiceCommand.cs / BaseCommand.cs - the shared IsServiceInstalled pre-flight blocks the #6374 orphan-row repair on the CLI path, reintroducing #4235 (#6405)
  • fix(cli): InstallServiceOptions.cs - the --user help text's hardening-guide URL is split mid-token by the console wrapper (#6702)
  • fix(psm1): Servy.psd1 / Directory.Build.props - the module manifest ships "(c)" with no year while every assembly ships "Copyright © 2026" (#6203)
  • fix(psm1): Servy.psm1 - the -Deps ValidatePattern rejects the '+' load-order prefix the CLI accepts (residual of #5547; third instance after #1694/#2312) (#6323)
  • fix(psm1): Servy.psm1 - Invoke-ServyCli's EndOfStream drain blocks on a silent pipe, so a quietly hung CLI still defeats ServyTimeoutSeconds (residual of #987) (#6324)
  • fix(psm1): Servy.psm1 - Get-ServyServiceStatus help omits Unknown from the possible status results (residual of #4256) (#6325)
  • fix(psm1): Servy.psm1 - the rewritten Invoke-ServyCli drain still blocks on a silent or stderr-heavy CLI, and its timeout break lands in an unbounded ReadToEnd (residual of #6324) (#6372)
  • fix(psm1): Servy.psm1 - the new per-chunk UTF8.GetString decode corrupts multibyte sequences split across reads, and the #1715 StandardOutputEncoding fix is now inert (residual of #6372) (#6401)
  • fix(psm1): Servy.psm1 - the final drain reads at most one chunk per stream and polls IsCompleted once, so a timeout that lands in the exit window returns truncated output as success (residual of #6372) (#6402)
  • fix(psm1): Servy.psm1 - the catch block's partial-output report reads stdoutLines, which the #6372 rewrite now fills only on the success path, so a timeout error carries no captured output (#6403)
  • fix(psm1): Servy.psm1 - the timeout kill path invokes taskkill.exe by bare name from an elevated session, resolving through PATH (sibling of fixed #6380) (#6404)
  • fix(psm1): Servy.psm1 - the #6402 final drain resets its timeout per chunk, so a trickling inherited pipe holds Invoke-ServyCli open unboundedly (residual of #6324/#6372) (#6419)
  • fix(psm1): Servy.psm1 - the #6401 stateful decoders are never flushed at end of stream, silently dropping a final split multibyte character (residual of #6401) (#6420)
  • fix(psm1): Servy.psm1 - both #6402 drain loops are wrapped in an if on the identical condition the while re-tests (sibling of #6313/#6317/#6339) (#6421)
  • fix(psm1): Servy.psd1 / bump-version.ps1 - the #6203 fix puts a raw © into a BOM-less manifest, which Windows PowerShell decodes as ANSI, so the Copyright field reads 'Copyright © 2026' (#6474)
  • fix(psm1): Servy.psm1 - the #6419 drain deadline gates the loop but not the WaitOne, so the 5s final drain can hold for ~20s (residual of #6419) (#6446)
  • fix(psm1): Servy.psm1 - Set-ServyHardenedFileAcl never sets the owner, so a pre-existing file owned by a non-admin keeps implicit WRITE_DAC and can undo the Admin-only hardening (#6692)
  • fix(setup): publish-common.ps1 - Copy-CommonArtifacts step 1 copies Set-ServyExePermissions.ps1 with no existence guard, while step 3's comment advertises the guards it got from #550 (#5469)
  • fix(setup): servy.iss - GetUninstallString and GetInstalledVersion are the same 20-line cross-arch AppId sweep with one literal changed, and have already drifted on Result initialisation (residual of #4990) (#5471)
  • fix(setup): servy.iss - ShouldAddCurrentUser drops the C# rule's administrator condition, so every elevated install leaves a permanent personal Full Control ACE on the Servy data vault (#6207)
  • fix(setup): servy.iss - ShouldAddCurrentUser is always False under PrivilegesRequired=admin, so the grant-current-user step and its Win32 plumbing are unreachable (residual of #6207) (#6370)
  • fix(setup): servy.iss - the #6207 fix is forward-only: upgrades never remove the personal Full Control ACE earlier installers already planted on the data vault (#6371)
  • fix(setup): servy.iss - icacls.exe and taskkill are invoked by bare filename from an elevated process, resolving through the CreateProcess search path (#6380)
  • fix(setup): servy.iss - the icacls hardening steps ignore exit codes, so a failed DACL reset leaves the data vault on its default permissive ACL with no signal (#6381)
  • fix(setup): servy.iss - the PATH-entry match predicate lives in both PathContainsFolder and RemoveFromPath, the exact drift class #5467 already paid for (#6382)
  • fix(setup): servy.iss - trailing spaces and a spaces-only line introduced by the #6371 fix block (#6383)
  • fix(setup): servy.iss - InitializeSetup deletes the remembered Setup Type / Components values even when the user cancels, and before Inno ever reads them (#6399)
  • fix(setup): servy.iss - AddToPath shows an unguarded modal MsgBox on failure, blocking silent installs, while PrepareToInstall checks WizardSilent (#6400)
  • fix(setup): servy.iss - SetupMutex is per-arch while everything else treats both arches as one product, so x64 and arm64 setups can run concurrently on ARM64 (residual of #4990) (#6443)
  • fix(setup): servy.iss - the ACL-hardening failure dialog is mbInformation while the PATH failure dialog is mbError, ranking the failures backwards (#6444)
  • fix(notifications): Servy-Watermark.psm1 - the only '-ne $null' in the repo; 35 other sites put $null on the left (PSPossibleIncorrectComparisonWithNull) (#5481)
  • fix(notifications): Write-ServyLog.ps1 - the taskschd file logger diverges from Logger.cs / RotatingStreamWriter.cs on clock base, timezone marker, line format, rotated-filename stamp and default rotation size (#5880)
  • fix(notifications): ServyFailureEmail.ps1 - Port is range-checked but TimeoutMs is not, so a negative TimeoutMs makes every queued alert fail with a generic ArgumentOutOfRangeException and advance the watermark (#5881)
  • fix(notifications): ServyFailureEmail.xml / ServyFailureNotification.xml - the task action names wscript.exe without a path while its argument is absolute; resolution is left to the search order at trigger time (#6613)
  • fix(notifications): ServyFailureEmail.vbs / ServyFailureNotification.vbs - the #6613 fix pins wscript.exe in the task action, but the script it runs still launches powershell.exe by bare name, so resolution is left to the search order one hop later (residual of #6613) (#6634)
  • ci(choco.yml): servy.nuspec - packageSourceUrl is the one Chocolatey metadata field left unset, so nothing on chocolatey.org points at setup/choco/servy (#5866)
  • ci(changelog.yml): CHANGELOG.md - 460 KiB and about 5 releases away from GitHub's 512 KiB markdown-render cutoff (#6218)
  • ci(changelog.yml): CHANGELOG-v1.0-v5.2.md - the archive links nowhere: the #6218 split's pointer is one-directional (#6425)
  • ci(changelog.yml): changelog.yml - the auto-split path lacks the reset path's cleanup, so the next re-chunk (due: main is already 412 KB) strands CHANGELOG-v1.0-v5.2.md orphaned and duplicated (#6435)

Don't miss a new servy release

NewReleases is sending notifications on new releases.