-
-
Notifications
You must be signed in to change notification settings - Fork 2
feat: Windows optional features toggle tab #288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| } | ||
| } |
| 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"; | ||
| } | ||
| } |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 -100Repository: 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.csRepository: 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.csRepository: laurentiu021/SystemManager Length of output: 5469 Prevent concurrent Lines 33, 64, and 98 attach temporary event handlers during Recommended fixes:
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| 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); | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle non-exact enabled states when mapping Line 131 only treats exact 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| features.Add(new WindowsFeature | ||||||||||
| { | ||||||||||
| Name = name, | ||||||||||
| DisplayName = HumanizeName(name), | ||||||||||
| IsEnabled = isEnabled, | ||||||||||
| Category = WindowsFeature.CategorizeFeature(name) | ||||||||||
| }); | ||||||||||
| } | ||||||||||
Check noticeCode scanning / CodeQL Missed opportunity to use Where Note
This foreach loop
implicitly filters its target sequence Error loading related location Loading |
||||||||||
|
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(); | ||||||||||
| } | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Include the “Other” category in the feature docs.
Line 148 lists categories but omits the fallback
Otherbucket used by the feature model/service, so docs can diverge from what users see in the UI.Suggested doc tweak
📝 Committable suggestion
🤖 Prompt for AI Agents