Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.46.0] - 2026-05-13

### Added
- **Windows Features tab** — list, enable, and disable Windows optional
features (Hyper-V, WSL, .NET 3.5, Telnet, etc.) directly from SysManager.
Features are categorized (Virtualization, Networking, Development, Media,
Legacy). Toggle requires admin. Shows reboot-required status. Includes
search/filter. Closes #5.

## [0.45.0] - 2026-05-13

### Added
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ implemented; 12 are work-in-progress placeholders marked with ⚙️:
| Group | Tabs |
|-------|------|
| 🏠 Dashboard | Dashboard |
| 🔧 System | System Health · Windows Update · Performance Mode · Services · Startup Manager · Windows Features ⚙️ |
| 🔧 System | System Health · Windows Update · Performance Mode · Services · Startup Manager · Windows Features |
| 📊 Monitor | Process Manager · Resource History ⚙️ · App Alerts · Privacy Monitor ⚙️ |
| 🧹 Cleanup | Quick Cleanup · Deep Cleanup · Shortcut Cleaner · File Shredder ⚙️ |
| 💾 Storage | Disk Analyzer · Duplicate Finder |
Expand Down Expand Up @@ -142,6 +142,14 @@ long-running operation, so you always know which tab is working.
- Shows name, publisher, command, and enabled/disabled status
- Open file location in Explorer

### Windows Features
- Lists all Windows optional features with current state (Enabled/Disabled)
- Toggle enable/disable per feature with confirmation dialog
- Categorized: Virtualization, Networking, Development, Media & Print, Legacy
- Shows reboot-required status after toggling
- Search/filter across all features
- Requires administrator privileges for modifications

Comment on lines +145 to +152

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Include the “Other” category in the feature docs.

Line 148 lists categories but omits the fallback Other bucket used by the feature model/service, so docs can diverge from what users see in the UI.

Suggested doc tweak
-- Categorized: Virtualization, Networking, Development, Media & Print, Legacy
+- Categorized: Virtualization, Networking, Development, Media & Print, Legacy, Other
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Windows Features
- Lists all Windows optional features with current state (Enabled/Disabled)
- Toggle enable/disable per feature with confirmation dialog
- Categorized: Virtualization, Networking, Development, Media & Print, Legacy
- Shows reboot-required status after toggling
- Search/filter across all features
- Requires administrator privileges for modifications
### Windows Features
- Lists all Windows optional features with current state (Enabled/Disabled)
- Toggle enable/disable per feature with confirmation dialog
- Categorized: Virtualization, Networking, Development, Media & Print, Legacy, Other
- Shows reboot-required status after toggling
- Search/filter across all features
- Requires administrator privileges for modifications
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 145 - 152, Update the "Windows Features" docs to
include the fallback "Other" category so docs match the feature model/service;
specifically edit the "### Windows Features" section where the categories are
listed (the line that currently reads "Categorized: Virtualization, Networking,
Development, Media & Print, Legacy") and append or insert "Other" (e.g.,
"Categorized: Virtualization, Networking, Development, Media & Print, Legacy,
Other") and add a brief note that "Other" is the fallback bucket used by the
feature model/service for uncategorized items.

### Duplicate File Finder
- Two-pass scan: group by size, then SHA-256 hash only size-matched files
- Duplicate groups sorted by wasted space (descending)
Expand Down
132 changes: 132 additions & 0 deletions SysManager/SysManager.Tests/WindowsFeaturesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// SysManager · WindowsFeaturesTests
// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager
// License: MIT

using SysManager.Models;
using SysManager.Services;
using SysManager.ViewModels;
using Xunit;

namespace SysManager.Tests;

/// <summary>
/// Tests for <see cref="WindowsFeaturesService"/> and <see cref="WindowsFeaturesViewModel"/>.
/// </summary>
public class WindowsFeaturesTests
{
// ── ParseFeatureList ──

[Fact]
public void ParseFeatureList_EmptyInput_ReturnsEmpty()
{
var result = WindowsFeaturesService.ParseFeatureList(new List<string>());
Assert.Empty(result);
}

[Fact]
public void ParseFeatureList_ValidLines_ParsesCorrectly()
{
var lines = new List<string>
{
"Microsoft-Hyper-V-All|Enabled",
"TelnetClient|Disabled",
"NetFx3|Enabled"
};

var result = WindowsFeaturesService.ParseFeatureList(lines);

Assert.Equal(3, result.Count);
var hyperV = result.First(f => f.Name == "Microsoft-Hyper-V-All");
Assert.True(hyperV.IsEnabled);
Assert.Equal("Virtualization", hyperV.Category);

var telnet = result.First(f => f.Name == "TelnetClient");
Assert.False(telnet.IsEnabled);
Assert.Equal("Networking", telnet.Category);
}

[Fact]
public void ParseFeatureList_SkipsBlankLines()
{
var lines = new List<string>
{
"",
" ",
"SomeFeature|Disabled",
""
};

var result = WindowsFeaturesService.ParseFeatureList(lines);
Assert.Single(result);
}

[Fact]
public void ParseFeatureList_SkipsInvalidLines()
{
var lines = new List<string>
{
"NoSeparator",
"|Enabled",
"ValidFeature|Disabled"
};

var result = WindowsFeaturesService.ParseFeatureList(lines);
Assert.Single(result);
Assert.Equal("ValidFeature", result[0].Name);
}

// ── CategorizeFeature ──

[Theory]
[InlineData("Microsoft-Hyper-V-All", "Virtualization")]
[InlineData("VirtualMachinePlatform", "Virtualization")]
[InlineData("Microsoft-Windows-Subsystem-Linux", "Virtualization")]
[InlineData("Containers", "Virtualization")]
[InlineData("Windows-Sandbox", "Virtualization")]
[InlineData("TelnetClient", "Networking")]
[InlineData("IIS-WebServer", "Networking")]
[InlineData("SMB1Protocol", "Networking")]
[InlineData("NetFx3", "Development")]
[InlineData("Microsoft-Windows-Developer-Mode", "Development")]
[InlineData("OpenSSH-Client", "Development")]
[InlineData("MediaPlayback", "Media & Print")]
[InlineData("Printing-XPSServices-Features", "Media & Print")]
[InlineData("DirectPlay", "Legacy")]
[InlineData("SomeRandomFeature", "Other")]
public void CategorizeFeature_AssignsCorrectCategory(string featureName, string expected)
{
Assert.Equal(expected, WindowsFeature.CategorizeFeature(featureName));
}

[Fact]
public void CategorizeFeature_NullOrEmpty_ReturnsOther()
{
Assert.Equal("Other", WindowsFeature.CategorizeFeature(null!));
Assert.Equal("Other", WindowsFeature.CategorizeFeature(""));
Assert.Equal("Other", WindowsFeature.CategorizeFeature(" "));
}

// ── HumanizeName ──

[Theory]
[InlineData("Microsoft-Hyper-V-All", "Microsoft Hyper V All")]
[InlineData("TelnetClient", "TelnetClient")]
[InlineData("Some_Feature_Name", "Some Feature Name")]
public void HumanizeName_ReplacesDelimiters(string input, string expected)
{
Assert.Equal(expected, WindowsFeaturesService.HumanizeName(input));
}

// ── ViewModel ──

[Fact]
public void ViewModel_InitialState_IsCorrect()
{
var vm = new WindowsFeaturesViewModel(new PowerShellRunner());
Assert.Empty(vm.AllFeatures);
Assert.Empty(vm.FilteredFeatures);
Assert.Equal("", vm.FilterText);
Assert.Equal(0, vm.FeatureCount);
Assert.False(vm.PendingReboot);
}
}
55 changes: 55 additions & 0 deletions SysManager/SysManager/Models/WindowsFeature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SysManager · WindowsFeature — model for Windows optional features
// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager
// License: MIT

using CommunityToolkit.Mvvm.ComponentModel;

namespace SysManager.Models;

/// <summary>
/// Represents a Windows optional feature with its current state.
/// </summary>
public partial class WindowsFeature : ObservableObject
{
[ObservableProperty] private string _name = "";
[ObservableProperty] private string _displayName = "";
[ObservableProperty] private bool _isEnabled;
[ObservableProperty] private bool _requiresReboot;
[ObservableProperty] private string _category = "Other";
[ObservableProperty] private string _status = "";

/// <summary>Category assignment based on feature name patterns.</summary>
public static string CategorizeFeature(string featureName)
{
if (string.IsNullOrWhiteSpace(featureName)) return "Other";

var name = featureName.ToUpperInvariant();

if (name.Contains("HYPER") || name.Contains("VIRTUAL") ||
name.Contains("CONTAINER") || name.Contains("SANDBOX") ||
name.Contains("WSL") || name.Contains("LINUX"))
return "Virtualization";

if (name.Contains("IIS") || name.Contains("INTERNET") ||
name.Contains("TFTP") || name.Contains("TELNET") ||
name.Contains("SMB") || name.Contains("NFS") ||
name.Contains("SNMP") || name.Contains("RIP"))
return "Networking";

if (name.Contains("NETFX") || name.Contains(".NET") ||
name.Contains("DEVELOPER") || name.Contains("POWERSHELL") ||
name.Contains("OPENSSH") || name.Contains("BASH"))
return "Development";

if (name.Contains("MEDIA") || name.Contains("PRINT") ||
name.Contains("XPS") || name.Contains("PDF") ||
name.Contains("SCAN") || name.Contains("FAX"))
return "Media & Print";

if (name.Contains("LEGACY") || name.Contains("DIRECTPLAY") ||
name.Contains("INDEXING") || name.Contains("WORK"))
return "Legacy";

return "Other";
}
}
2 changes: 2 additions & 0 deletions SysManager/SysManager/ServiceRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public static IServiceCollection ConfigureServices(this IServiceCollection servi
services.AddSingleton<ServicesViewModel>();
services.AddSingleton<AppAlertsViewModel>();
services.AddSingleton<ShortcutCleanerViewModel>();
services.AddSingleton<WindowsFeaturesService>();
services.AddSingleton<WindowsFeaturesViewModel>();
services.AddSingleton<AppBlockerViewModel>();

return services;
Expand Down
161 changes: 161 additions & 0 deletions SysManager/SysManager/Services/WindowsFeaturesService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// SysManager · WindowsFeaturesService — list and toggle Windows optional features
// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager
// License: MIT

using System.Text.RegularExpressions;
using Serilog;
using SysManager.Models;

namespace SysManager.Services;

/// <summary>
/// Wraps PowerShell commands to list, enable, and disable Windows optional features.
/// Requires administrator privileges for enable/disable operations.
/// </summary>
public sealed partial class WindowsFeaturesService
{
private readonly PowerShellRunner _runner;

public WindowsFeaturesService(PowerShellRunner runner) => _runner = runner;

/// <summary>
/// Lists all Windows optional features with their current state.
/// Uses Get-WindowsOptionalFeature -Online.
/// </summary>
public async Task<List<WindowsFeature>> ListFeaturesAsync(CancellationToken ct = default)
{
var captured = new List<string>();
void Collect(PowerShellLine l)
{
if (l.Kind == OutputKind.Output) captured.Add(l.Text);
}

_runner.LineReceived += Collect;
try
{
await _runner.RunProcessAsync("powershell",
"-NoProfile -Command \"Get-WindowsOptionalFeature -Online | " +
"Select-Object FeatureName, State | " +
"ForEach-Object { $_.FeatureName + '|' + $_.State }\"", ct);
}
finally { _runner.LineReceived -= Collect; }
Comment on lines +33 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect PowerShellRunner implementation for serialization and event dispatch semantics
fd -i "PowerShellRunner.cs" --exec sed -n '1,320p' {}

# 2) Locate synchronization primitives and LineReceived usage
rg -nP --type=cs -C3 'class\s+PowerShellRunner|event\s+.*LineReceived|RunProcessAsync\s*\(|SemaphoreSlim|lock\s*\('

# 3) Confirm whether any callers assume exclusive output capture via event subscription
rg -nP --type=cs -C2 'LineReceived\s*\+=|LineReceived\s*-='

Repository: laurentiu021/SystemManager

Length of output: 50382


🏁 Script executed:

# Check if WindowsFeaturesService or its callers use OperationLockService
rg -nP --type=cs -C3 "WindowsFeaturesService|OperationLockService" | head -100

Repository: laurentiu021/SystemManager

Length of output: 9427


🏁 Script executed:

# Search for actual concurrent calls to RunProcessAsync in the codebase
# Look for any async methods calling RunProcessAsync without await or Task.WhenAll scenarios
rg -nP --type=cs -B2 -A2 "RunProcessAsync" SysManager/SysManager/Services/WindowsFeaturesService.cs

Repository: laurentiu021/SystemManager

Length of output: 815


🏁 Script executed:

# Check if WindowsFeaturesService methods are decorated with synchronization attributes or use locks
sed -n '1,150p' SysManager/SysManager/Services/WindowsFeaturesService.cs

Repository: laurentiu021/SystemManager

Length of output: 5469


Prevent concurrent LineReceived event capture across multiple async operations on the shared PowerShellRunner instance.

Lines 33, 64, and 98 attach temporary event handlers during RunProcessAsync calls; if multiple service methods execute concurrently (which is currently possible since WindowsFeaturesViewModel does not use OperationLockService), output from unrelated processes will bleed between collectors, causing silent parsing failures or data corruption.

Recommended fixes:

  • Add OperationLockService synchronization in WindowsFeaturesViewModel (pattern used consistently elsewhere in codebase), or
  • Introduce a SemaphoreSlim in WindowsFeaturesService to serialize its public methods
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SysManager/SysManager/Services/WindowsFeaturesService.cs` around lines 33 -
41, The shared PowerShellRunner.LineReceived event handler (attached in
WindowsFeaturesService methods around RunProcessAsync with the Collect handler)
can be invoked concurrently across multiple async calls causing cross-talk;
serialize access by either acquiring the existing OperationLockService from
WindowsFeaturesViewModel before invoking WindowsFeaturesService methods (follow
the same lock pattern used elsewhere) or add a private SemaphoreSlim (e.g.,
_runSemaphore) to WindowsFeaturesService and await
_runSemaphore.WaitAsync()/Release() around the sections that attach/detach
LineReceived and call RunProcessAsync so only one Collect handler is active at a
time.


return ParseFeatureList(captured);
}

/// <summary>
/// Enables a Windows optional feature. Requires admin.
/// Returns true if successful, false otherwise.
/// </summary>
public async Task<(bool Success, bool RebootRequired)> EnableFeatureAsync(
string featureName, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(featureName) || !FeatureNamePattern().IsMatch(featureName))
throw new ArgumentException("Invalid feature name.", nameof(featureName));

Log.Information("Enabling Windows feature: {Feature}", featureName);

var captured = new List<string>();
void Collect(PowerShellLine l)
{
if (l.Kind == OutputKind.Output) captured.Add(l.Text);
}

_runner.LineReceived += Collect;
try
{
var code = await _runner.RunProcessAsync("powershell",
$"-NoProfile -Command \"Enable-WindowsOptionalFeature -Online " +
$"-FeatureName '{featureName}' -NoRestart -All | " +
$"Select-Object RestartNeeded | ForEach-Object {{ $_.RestartNeeded }}\"", ct);

var reboot = captured.Any(l =>
l.Contains("True", StringComparison.OrdinalIgnoreCase));

return (code == 0, reboot);
}
finally { _runner.LineReceived -= Collect; }
}

/// <summary>
/// Disables a Windows optional feature. Requires admin.
/// Returns true if successful, false otherwise.
/// </summary>
public async Task<(bool Success, bool RebootRequired)> DisableFeatureAsync(
string featureName, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(featureName) || !FeatureNamePattern().IsMatch(featureName))
throw new ArgumentException("Invalid feature name.", nameof(featureName));

Log.Information("Disabling Windows feature: {Feature}", featureName);

var captured = new List<string>();
void Collect(PowerShellLine l)
{
if (l.Kind == OutputKind.Output) captured.Add(l.Text);
}

_runner.LineReceived += Collect;
try
{
var code = await _runner.RunProcessAsync("powershell",
$"-NoProfile -Command \"Disable-WindowsOptionalFeature -Online " +
$"-FeatureName '{featureName}' -NoRestart | " +
$"Select-Object RestartNeeded | ForEach-Object {{ $_.RestartNeeded }}\"", ct);

var reboot = captured.Any(l =>
l.Contains("True", StringComparison.OrdinalIgnoreCase));

return (code == 0, reboot);
}
finally { _runner.LineReceived -= Collect; }
}

/// <summary>Parses the pipe-delimited feature list output.</summary>
internal static List<WindowsFeature> ParseFeatureList(List<string> lines)
{
var features = new List<WindowsFeature>();

foreach (var line in lines)
{
if (string.IsNullOrWhiteSpace(line)) continue;

var parts = line.Split('|', 2);
if (parts.Length < 2) continue;

var name = parts[0].Trim();
var state = parts[1].Trim();

if (string.IsNullOrWhiteSpace(name)) continue;

var isEnabled = state.Equals("Enabled", StringComparison.OrdinalIgnoreCase);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle non-exact enabled states when mapping IsEnabled.

Line 131 only treats exact "Enabled" as enabled; states like "Enabled with Payload Removed" (and pending variants) will be shown as disabled.

Suggested parsing adjustment
- var isEnabled = state.Equals("Enabled", StringComparison.OrdinalIgnoreCase);
+ var isEnabled =
+     state.StartsWith("Enabled", StringComparison.OrdinalIgnoreCase) ||
+     state.StartsWith("Enable Pending", StringComparison.OrdinalIgnoreCase);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var isEnabled = state.Equals("Enabled", StringComparison.OrdinalIgnoreCase);
var isEnabled =
state.StartsWith("Enabled", StringComparison.OrdinalIgnoreCase) ||
state.StartsWith("Enable Pending", StringComparison.OrdinalIgnoreCase);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SysManager/SysManager/Services/WindowsFeaturesService.cs` at line 131, The
current mapping sets isEnabled only when state.Equals("Enabled", ...), which
misses values like "Enabled with Payload Removed" or pending variants; update
the logic in WindowsFeaturesService.cs where the local variable isEnabled is set
from state to consider any state that starts with or contains "Enabled" (e.g.,
use state?.Trim().StartsWith("Enabled", StringComparison.OrdinalIgnoreCase) or
state?.IndexOf("Enabled", StringComparison.OrdinalIgnoreCase) >= 0) and ensure
you null-check state before calling methods so those variants are treated as
enabled.


features.Add(new WindowsFeature
{
Name = name,
DisplayName = HumanizeName(name),
IsEnabled = isEnabled,
Category = WindowsFeature.CategorizeFeature(name)
});
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
Comment on lines +119 to +140

return features.OrderBy(f => f.Category)
.ThenBy(f => f.DisplayName)
.ToList();
}

/// <summary>
/// Converts PascalCase feature names to human-readable form.
/// e.g. "Microsoft-Hyper-V-All" → "Microsoft Hyper-V All"
/// </summary>
internal static string HumanizeName(string featureName)
{
return featureName.Replace('-', ' ').Replace('_', ' ');
}

/// <summary>
/// Validates feature names: alphanumeric, hyphens, underscores, dots. Max 128 chars.
/// </summary>
[GeneratedRegex(@"^[\w.\-]{1,128}$")]
private static partial Regex FeatureNamePattern();
}
Loading
Loading