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