From 827a474ed31807f481742b4eb8bce2ab7d7efd20 Mon Sep 17 00:00:00 2001 From: laurentiu021 Date: Wed, 3 Jun 2026 17:22:42 +0300 Subject: [PATCH] fix: drop async-void pipe listener and sync-over-async wrappers - App.xaml.cs: rename StartPipeListener (async void) to StartPipeListenerAsync (Task); fire as _ = StartPipeListenerAsync() so escaped exceptions surface via UnobservedTaskException instead of crashing the AppDomain - StartupService: drop the SetEnabled(...) sync wrapper that called .GetAwaiter().GetResult() on SetEnabledAsync; tests migrated to call SetEnabledAsync directly - StartupService.SetTaskSchedulerEnabledAsync: replace stderrTask.Wait(timeout) + GetAwaiter().GetResult() with stderrTask.WaitAsync(timeout) for a clean async + TimeoutException fallback --- CHANGELOG.md | 7 +++++- .../SysManager.Tests/StartupToggleTests.cs | 22 +++++++++---------- SysManager/SysManager/App.xaml.cs | 12 ++++++---- .../SysManager/Services/StartupService.cs | 19 ++++++++-------- SysManager/SysManager/SysManager.csproj | 6 ++--- 5 files changed, 37 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13a4495..bc59464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,12 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -## [1.18.1] - 2026-06-03 +## [1.18.2] - 2026-06-03 + +### Fixed +- **Pipe listener no longer fire-and-forgets via `async void`.** `App.StartPipeListener` was an async-void method, meaning any exception escaping the loop would crash the process via the AppDomain handler. Renamed to `StartPipeListenerAsync` returning `Task`; `OnStartup` calls it as `_ = StartPipeListenerAsync()` so a stray exception flows through `TaskScheduler.UnobservedTaskException` (logged) instead of terminating the app. +- **StartupService — removed sync wrapper over async.** `SetEnabled` (sync) was a thin wrapper around `SetEnabledAsync` using `.GetAwaiter().GetResult()`. The wrapper is gone; tests now call `SetEnabledAsync` directly via xUnit `Task` test methods. +- **Schtasks stderr read** — replaced `stderrTask.Wait(timeout) ? .GetAwaiter().GetResult() : ""` with `await stderrTask.WaitAsync(timeout)` so the read is fully async with a clean timeout fallback. ### Fixed - **Privacy Toggles no longer write to the registry on every click.** Toggling a switch now updates local state only; the user must press **Apply** to write pending changes. A live counter shows how many changes are pending, and **Discard** reverts them without touching the registry. Prevents accidental system changes when scrolling through the toggle list. diff --git a/SysManager/SysManager.Tests/StartupToggleTests.cs b/SysManager/SysManager.Tests/StartupToggleTests.cs index 76a4021..4b159a8 100644 --- a/SysManager/SysManager.Tests/StartupToggleTests.cs +++ b/SysManager/SysManager.Tests/StartupToggleTests.cs @@ -8,13 +8,13 @@ namespace SysManager.Tests; /// -/// Tests for covering registry-based +/// Tests for covering registry-based /// and Task Scheduler entries, plus specific error messages (#159, #160). /// public class StartupToggleTests { [Fact] - public void SetEnabled_TaskScheduler_EmptyPath_ReturnsFalseWithMessage() + public async Task SetEnabledAsync_TaskScheduler_EmptyPath_ReturnsFalseWithMessage() { var entry = new StartupEntry { @@ -26,14 +26,14 @@ public void SetEnabled_TaskScheduler_EmptyPath_ReturnsFalseWithMessage() StatusText = "Enabled" }; - var result = StartupService.SetEnabled(entry, false); + var result = await StartupService.SetEnabledAsync(entry, false); Assert.False(result); Assert.Contains("task path unknown", entry.StatusText, StringComparison.OrdinalIgnoreCase); } [Fact] - public void SetEnabled_TaskScheduler_NullPath_ReturnsFalseWithMessage() + public async Task SetEnabledAsync_TaskScheduler_NullPath_ReturnsFalseWithMessage() { var entry = new StartupEntry { @@ -45,14 +45,14 @@ public void SetEnabled_TaskScheduler_NullPath_ReturnsFalseWithMessage() StatusText = "Enabled" }; - var result = StartupService.SetEnabled(entry, false); + var result = await StartupService.SetEnabledAsync(entry, false); Assert.False(result); Assert.Contains("task path unknown", entry.StatusText, StringComparison.OrdinalIgnoreCase); } [Fact] - public void SetEnabled_RegistryEntry_NeverShowsGenericAdminMessage() + public async Task SetEnabledAsync_RegistryEntry_NeverShowsGenericAdminMessage() { var entry = new StartupEntry { @@ -65,13 +65,13 @@ public void SetEnabled_RegistryEntry_NeverShowsGenericAdminMessage() StatusText = "Enabled" }; - var result = StartupService.SetEnabled(entry, false); + var result = await StartupService.SetEnabledAsync(entry, false); Assert.DoesNotContain("may need admin", entry.StatusText, StringComparison.OrdinalIgnoreCase); } [Fact] - public void SetEnabled_TaskScheduler_WithBogusPath_ReturnsFalseWithSpecificError() + public async Task SetEnabledAsync_TaskScheduler_WithBogusPath_ReturnsFalseWithSpecificError() { var entry = new StartupEntry { @@ -83,7 +83,7 @@ public void SetEnabled_TaskScheduler_WithBogusPath_ReturnsFalseWithSpecificError StatusText = "Enabled" }; - var result = StartupService.SetEnabled(entry, false); + var result = await StartupService.SetEnabledAsync(entry, false); Assert.False(result); Assert.StartsWith("Error", entry.StatusText, StringComparison.OrdinalIgnoreCase); @@ -91,7 +91,7 @@ public void SetEnabled_TaskScheduler_WithBogusPath_ReturnsFalseWithSpecificError } [Fact] - public void SetEnabled_PreservesEntryState_OnFailure() + public async Task SetEnabledAsync_PreservesEntryState_OnFailure() { var entry = new StartupEntry { @@ -103,7 +103,7 @@ public void SetEnabled_PreservesEntryState_OnFailure() StatusText = "Enabled" }; - StartupService.SetEnabled(entry, false); + await StartupService.SetEnabledAsync(entry, false); Assert.True(entry.IsEnabled); } diff --git a/SysManager/SysManager/App.xaml.cs b/SysManager/SysManager/App.xaml.cs index aac93c2..b1d83e2 100644 --- a/SysManager/SysManager/App.xaml.cs +++ b/SysManager/SysManager/App.xaml.cs @@ -78,8 +78,10 @@ protected override void OnStartup(StartupEventArgs e) ThemeService.Instance.Initialize(); - // Start listening for activation requests from subsequent instances - StartPipeListener(); + // Start listening for activation requests from subsequent instances. + // Fire-and-forget is intentional — the listener loop runs for the app + // lifetime and is cancelled via _pipeCts on OnExit. + _ = StartPipeListenerAsync(); } protected override void OnExit(ExitEventArgs e) @@ -127,9 +129,11 @@ private static void ActivateExistingInstance() /// /// Listens for named pipe connections from subsequent instances and - /// activates the main window when one connects. + /// activates the main window when one connects. Returns a Task so the + /// caller can fire-and-forget without using the async-void anti-pattern; + /// any exception escaping the loop is logged via OnTask (UnobservedTaskException). /// - private async void StartPipeListener() + private async Task StartPipeListenerAsync() { _pipeCts = new CancellationTokenSource(); var ct = _pipeCts.Token; diff --git a/SysManager/SysManager/Services/StartupService.cs b/SysManager/SysManager/Services/StartupService.cs index 9223194..dac6434 100644 --- a/SysManager/SysManager/Services/StartupService.cs +++ b/SysManager/SysManager/Services/StartupService.cs @@ -305,13 +305,6 @@ private static void ApplyApprovedState(List entries) catch (System.IO.IOException) { return null; /* I/O error reading key */ } } - /// - /// Synchronous overload — delegates to for - /// backward compatibility with existing callers and tests. - /// - public static bool SetEnabled(StartupEntry entry, bool enabled) - => SetEnabledAsync(entry, enabled).GetAwaiter().GetResult(); - /// /// Toggle a startup entry on or off by writing to the StartupApproved /// registry key. This is the same mechanism Task Manager uses. @@ -444,9 +437,15 @@ private static async Task SetTaskSchedulerEnabledAsync(StartupEntry entry, return false; } - var stderr = stderrTask.Wait(TimeSpan.FromSeconds(5)) - ? stderrTask.GetAwaiter().GetResult().Trim() - : string.Empty; + string stderr; + try + { + stderr = (await stderrTask.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false)).Trim(); + } + catch (TimeoutException) + { + stderr = string.Empty; + } if (proc.ExitCode == 0) { diff --git a/SysManager/SysManager/SysManager.csproj b/SysManager/SysManager/SysManager.csproj index bb9162b..4d3b0fa 100644 --- a/SysManager/SysManager/SysManager.csproj +++ b/SysManager/SysManager/SysManager.csproj @@ -10,9 +10,9 @@ SysManager true NU1603;NU1701 - 1.18.1 - 1.18.1.0 - 1.18.1.0 + 1.18.2 + 1.18.2.0 + 1.18.2.0 SysManager SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup. https://github.com/laurentiu021/SystemManager