Skip to content

Commit fc67841

Browse files
committed
feat: Windows services tab — stop + disable unneeded services
Adds a curated catalog of 10 Windows services that are commonly disabled for gaming setups (DiagTrack, MapsBroker, WMPNetworkSvc, Fax, RetailDemo, WerSvc, lfsvc, plus opt-ins for WSearch, SysMain, TabletInputService). For each service the Settings UI shows display name + service ID, what it does, the Windows default start type, the current start type+status, and reboot/recommended/not-installed badges. Per-service the user chooses Want=Disabled or Default, can toggle Monitor, and can enable Auto-apply (re-applies via UAC if Windows or another app re-enables a disabled service). A 'Gaming optimized' / 'Default' radio at the top of the section is a one-click preset that flips Want across the recommended subset. WindowsServiceController wraps sc.exe via Verb=runas; DisableElevated chains stop + config disabled into a single UAC prompt. Reads use ServiceController + the registry (no elevation). WindowsServiceMonitor implements IMonitoredSetting, one instance per ServiceDefinition, registered in App.xaml.cs from ServiceCatalog.All. Drift is only reported when the user opted into Want=Disabled (current != Disabled) or Want=Default-and-currently-Disabled — Manual->Auto trigger transitions don't churn the monitor. ServicePref ('Monitor', 'DesiredDisabled', 'AutoApply') is keyed by service name in AppConfig.Services so adding services to the catalog doesn't migrate existing configs.
1 parent ea477ee commit fc67841

10 files changed

Lines changed: 631 additions & 1 deletion

src/GamerGuardian/App.xaml.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ protected override void OnStartup(StartupEventArgs e)
5252
TempCleanup.Run();
5353

5454
_notifier = new Notifier();
55-
_allMonitors = new IMonitoredSetting[]
55+
var fixedMonitors = new IMonitoredSetting[]
5656
{
5757
new HdrMonitor(),
5858
new RefreshRateMonitor(),
@@ -70,6 +70,9 @@ protected override void OnStartup(StartupEventArgs e)
7070
new FullscreenOptimizationsMonitor(),
7171
new PowerPlanMonitor(),
7272
};
73+
var serviceMonitors = GamerGuardian.Services.ServiceCatalog.All
74+
.Select(d => (IMonitoredSetting)new WindowsServiceMonitor(d));
75+
_allMonitors = fixedMonitors.Concat(serviceMonitors).ToArray();
7376
_monitor = new MonitorService(_store, _allMonitors, report => _notifier.ShowAsync(report));
7477
_monitor.AutoAppliedRebootRequired += items =>
7578
{

src/GamerGuardian/GamerGuardian.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
</PropertyGroup>
2424
<ItemGroup>
2525
<PackageReference Include="WPF-UI" Version="3.0.5" />
26+
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
2627
</ItemGroup>
2728
<ItemGroup>
2829
<Resource Include="Assets\AppIcon.ico" />

src/GamerGuardian/Models/AppConfig.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ public sealed class AppConfig
1313

1414
public Dictionary<string, DisplayPreference> Displays { get; set; } = new();
1515
public GlobalPreferences Global { get; set; } = new();
16+
public Dictionary<string, ServicePref> Services { get; set; } = new();
17+
}
18+
19+
public sealed class ServicePref
20+
{
21+
public bool Monitor { get; set; } = false;
22+
public bool DesiredDisabled { get; set; } = false;
23+
public bool AutoApply { get; set; } = false;
1624
}
1725

1826
[JsonConverter(typeof(JsonStringEnumConverter))]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace GamerGuardian.Models;
2+
3+
/// <summary>
4+
/// Static metadata for a Windows service that GamerGuardian knows about.
5+
/// Lives in <see cref="GamerGuardian.Services.ServiceCatalog"/>; per-user prefs
6+
/// live in <see cref="AppConfig.Services"/> keyed by <see cref="Name"/>.
7+
/// </summary>
8+
public sealed record ServiceDefinition(
9+
string Name,
10+
string DisplayName,
11+
string Description,
12+
ServiceStartType DefaultStartType,
13+
bool RequiresReboot = false,
14+
bool RecommendedDisable = false);
15+
16+
public enum ServiceStartType
17+
{
18+
Unknown = 0,
19+
Boot = 1,
20+
System = 2,
21+
Automatic = 3,
22+
Manual = 4,
23+
Disabled = 5,
24+
AutomaticDelayed = 6,
25+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using GamerGuardian.Models;
2+
using GamerGuardian.Services;
3+
4+
namespace GamerGuardian.Monitors;
5+
6+
/// <summary>
7+
/// Monitors a single Windows service start type against the user's preference.
8+
/// One instance per <see cref="ServiceDefinition"/>; registered N times in
9+
/// <c>App.xaml.cs</c> from <see cref="ServiceCatalog.All"/>.
10+
/// </summary>
11+
public sealed class WindowsServiceMonitor : IMonitoredSetting
12+
{
13+
private readonly ServiceDefinition _def;
14+
15+
public WindowsServiceMonitor(ServiceDefinition def) { _def = def; }
16+
17+
public string Id => $"service:{_def.Name.ToLowerInvariant()}";
18+
19+
public IEnumerable<DriftItem> CheckDrift(AppConfig config)
20+
{
21+
if (!WindowsServiceController.Exists(_def.Name)) yield break;
22+
if (!config.Services.TryGetValue(_def.Name, out var pref) || pref is null) yield break;
23+
24+
var current = WindowsServiceController.ReadStartType(_def.Name);
25+
if (current == ServiceStartType.Unknown) yield break;
26+
27+
var desired = pref.DesiredDisabled ? ServiceStartType.Disabled : _def.DefaultStartType;
28+
if (current == desired) yield break;
29+
30+
// For "default" preference we only flag drift if the current state is *Disabled* —
31+
// otherwise we'd churn on services Windows naturally promotes Manual → Automatic
32+
// via triggers. The user explicitly chose to disable; the user explicitly chose
33+
// not to. Anything in between is fine.
34+
if (!pref.DesiredDisabled && current != ServiceStartType.Disabled) yield break;
35+
36+
bool desiredDisabled = pref.DesiredDisabled;
37+
yield return new DriftItem(
38+
SettingId: Id,
39+
DisplayKey: "service",
40+
DisplayLabel: _def.DisplayName,
41+
Description: $"{_def.DisplayName}{(desiredDisabled ? "stop and disable" : $"restore to {DescribeStart(_def.DefaultStartType)}")}",
42+
CurrentValue: DescribeStart(current),
43+
DesiredValue: DescribeStart(desired),
44+
AutoApply: pref.AutoApply,
45+
Apply: () => Task.Run(() =>
46+
{
47+
if (desiredDisabled)
48+
WindowsServiceController.DisableElevated(_def.Name);
49+
else
50+
WindowsServiceController.RestoreDefaultElevated(_def.Name, _def.DefaultStartType);
51+
}),
52+
RequiresReboot: _def.RequiresReboot,
53+
IsMonitored: pref.Monitor,
54+
RawBefore: ((int)current).ToString(),
55+
RawDesired: ((int)desired).ToString());
56+
}
57+
58+
public static string DescribeStart(ServiceStartType s) => s switch
59+
{
60+
ServiceStartType.Boot => "Boot",
61+
ServiceStartType.System => "System",
62+
ServiceStartType.Automatic => "Automatic",
63+
ServiceStartType.AutomaticDelayed => "Automatic (Delayed)",
64+
ServiceStartType.Manual => "Manual",
65+
ServiceStartType.Disabled => "Disabled",
66+
_ => "unknown",
67+
};
68+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using GamerGuardian.Models;
2+
3+
namespace GamerGuardian.Services;
4+
5+
/// <summary>
6+
/// Curated list of Windows services that are commonly disabled for gaming
7+
/// setups. Conservative picks only — services here either have no impact on a
8+
/// typical desktop gaming user or have a well-understood tradeoff. Items with
9+
/// <see cref="ServiceDefinition.RecommendedDisable"/> = true are toggled by the
10+
/// "Gaming optimized" preset; the rest are listed but require explicit opt-in.
11+
/// </summary>
12+
public static class ServiceCatalog
13+
{
14+
public static IReadOnlyList<ServiceDefinition> All { get; } = new ServiceDefinition[]
15+
{
16+
new(
17+
Name: "DiagTrack",
18+
DisplayName: "Connected User Experiences and Telemetry",
19+
Description: "Sends diagnostic and usage data to Microsoft. Disabling cuts background CPU/network and is safe on consumer Windows.",
20+
DefaultStartType: ServiceStartType.Automatic,
21+
RecommendedDisable: true),
22+
23+
new(
24+
Name: "MapsBroker",
25+
DisplayName: "Downloaded Maps Manager",
26+
Description: "Background downloads for offline maps. Useless if you don't use the Maps app.",
27+
DefaultStartType: ServiceStartType.AutomaticDelayed,
28+
RecommendedDisable: true),
29+
30+
new(
31+
Name: "WMPNetworkSvc",
32+
DisplayName: "Windows Media Player Network Sharing",
33+
Description: "Shares Windows Media Player libraries on the network. Almost no one uses this in 2026.",
34+
DefaultStartType: ServiceStartType.Manual,
35+
RecommendedDisable: true),
36+
37+
new(
38+
Name: "Fax",
39+
DisplayName: "Fax",
40+
Description: "Sends and receives faxes via a connected fax machine. Disable unless you actually fax.",
41+
DefaultStartType: ServiceStartType.Manual,
42+
RecommendedDisable: true),
43+
44+
new(
45+
Name: "RetailDemo",
46+
DisplayName: "Retail Demo Service",
47+
Description: "Powers Windows demo mode in retail stores. Has no purpose on a personal machine.",
48+
DefaultStartType: ServiceStartType.Manual,
49+
RecommendedDisable: true),
50+
51+
new(
52+
Name: "WerSvc",
53+
DisplayName: "Windows Error Reporting",
54+
Description: "Collects crash data and sends it to Microsoft. Crashes still happen; only the upload is suppressed.",
55+
DefaultStartType: ServiceStartType.Manual,
56+
RecommendedDisable: true),
57+
58+
new(
59+
Name: "lfsvc",
60+
DisplayName: "Geolocation Service",
61+
Description: "Provides location data to apps. Disable if no app on your machine needs your location.",
62+
DefaultStartType: ServiceStartType.Manual,
63+
RecommendedDisable: true),
64+
65+
new(
66+
Name: "WSearch",
67+
DisplayName: "Windows Search",
68+
Description: "Indexes files for fast Start-menu/Explorer search. Disabling drops background I/O but breaks search-as-you-type. Off by default in the preset — opt in if you don't rely on it.",
69+
DefaultStartType: ServiceStartType.AutomaticDelayed,
70+
RecommendedDisable: false),
71+
72+
new(
73+
Name: "SysMain",
74+
DisplayName: "SysMain (Superfetch)",
75+
Description: "Pre-loads frequently-used apps into RAM. Controversial on SSDs — some report better latency disabled, others see slower app launches. Off by default in the preset.",
76+
DefaultStartType: ServiceStartType.Automatic,
77+
RecommendedDisable: false),
78+
79+
new(
80+
Name: "TabletInputService",
81+
DisplayName: "Touch Keyboard and Handwriting Panel",
82+
Description: "On-screen keyboard and pen input. Useless on a non-touch desktop.",
83+
DefaultStartType: ServiceStartType.Manual,
84+
RecommendedDisable: false),
85+
};
86+
}

src/GamerGuardian/Services/SettingDocs.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ public static string MechanismFor(string settingId)
1212
if (settingId.StartsWith("hdr:")) return "DisplayConfigSetDeviceInfo (CCD API)";
1313
if (settingId.StartsWith("refresh:")) return "ChangeDisplaySettingsEx (DEVMODE.dmDisplayFrequency)";
1414
if (settingId.StartsWith("resolution:")) return "ChangeDisplaySettingsEx (DEVMODE.dmPelsWidth/Height)";
15+
if (settingId.StartsWith("service:"))
16+
{
17+
var name = settingId["service:".Length..];
18+
return $"sc.exe stop / config (writes HKLM\\SYSTEM\\CurrentControlSet\\Services\\{name}\\Start)";
19+
}
1520
return settingId switch
1621
{
1722
"hags" => @"HKLM\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\HwSchMode (DWORD)",
@@ -34,6 +39,11 @@ public static string VerifyCommandFor(string settingId)
3439
{
3540
if (settingId.StartsWith("hdr:") || settingId.StartsWith("refresh:") || settingId.StartsWith("resolution:"))
3641
return "Open Settings → System → Display, or run: dxdiag";
42+
if (settingId.StartsWith("service:"))
43+
{
44+
var name = settingId["service:".Length..];
45+
return $"sc qc \"{name}\" # look for START_TYPE";
46+
}
3747
return settingId switch
3848
{
3949
"hags" => @"(Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\GraphicsDrivers' -Name HwSchMode).HwSchMode",
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using System.ComponentModel;
2+
using System.Diagnostics;
3+
using System.ServiceProcess;
4+
using GamerGuardian.Models;
5+
6+
namespace GamerGuardian.Services;
7+
8+
/// <summary>
9+
/// Read/modify Windows service start type and running state.
10+
///
11+
/// Reads use <see cref="ServiceController"/> for status and the registry for
12+
/// start type (no elevation needed). Writes use sc.exe via Verb=runas — same
13+
/// pattern as <see cref="ElevatedRegistry"/>, one UAC prompt per call.
14+
/// </summary>
15+
public static class WindowsServiceController
16+
{
17+
public static bool Exists(string serviceName)
18+
{
19+
try
20+
{
21+
using var sc = new ServiceController(serviceName);
22+
// Touching Status forces a query that throws if the service isn't installed.
23+
_ = sc.Status;
24+
return true;
25+
}
26+
catch { return false; }
27+
}
28+
29+
public static ServiceStartType ReadStartType(string serviceName)
30+
{
31+
try
32+
{
33+
using var k = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(
34+
$@"SYSTEM\CurrentControlSet\Services\{serviceName}", writable: false);
35+
if (k is null) return ServiceStartType.Unknown;
36+
if (k.GetValue("Start") is not int start) return ServiceStartType.Unknown;
37+
// 2 = Automatic; check DelayedAutostart for the "Automatic (Delayed)" variant.
38+
if (start == 2)
39+
{
40+
var delayed = k.GetValue("DelayedAutostart") is int d && d == 1;
41+
return delayed ? ServiceStartType.AutomaticDelayed : ServiceStartType.Automatic;
42+
}
43+
return start switch
44+
{
45+
0 => ServiceStartType.Boot,
46+
1 => ServiceStartType.System,
47+
3 => ServiceStartType.Manual,
48+
4 => ServiceStartType.Disabled,
49+
_ => ServiceStartType.Unknown,
50+
};
51+
}
52+
catch { return ServiceStartType.Unknown; }
53+
}
54+
55+
public static ServiceControllerStatus? ReadStatus(string serviceName)
56+
{
57+
try
58+
{
59+
using var sc = new ServiceController(serviceName);
60+
return sc.Status;
61+
}
62+
catch { return null; }
63+
}
64+
65+
/// <summary>
66+
/// Stop the service (if running) and set start type to Disabled. Single
67+
/// UAC prompt; tolerates "already stopped" / "stop pending" exit codes
68+
/// from sc.exe so the configure step still runs.
69+
/// </summary>
70+
public static bool DisableElevated(string serviceName) =>
71+
RunChained(
72+
$"sc stop \"{serviceName}\"",
73+
$"sc config \"{serviceName}\" start= disabled");
74+
75+
/// <summary>
76+
/// Restore a service's start type to its default. Does not start it — the
77+
/// next reboot or trigger will pick that up if/when needed.
78+
/// </summary>
79+
public static bool RestoreDefaultElevated(string serviceName, ServiceStartType defaultStart)
80+
{
81+
var startArg = defaultStart switch
82+
{
83+
ServiceStartType.Boot => "boot",
84+
ServiceStartType.System => "system",
85+
ServiceStartType.Automatic => "auto",
86+
ServiceStartType.AutomaticDelayed => "delayed-auto",
87+
ServiceStartType.Manual => "demand",
88+
ServiceStartType.Disabled => "disabled",
89+
_ => "demand",
90+
};
91+
return Run($"config \"{serviceName}\" start= {startArg}");
92+
}
93+
94+
private static bool Run(string scArgs)
95+
{
96+
var psi = new ProcessStartInfo
97+
{
98+
FileName = "sc.exe",
99+
Arguments = scArgs,
100+
Verb = "runas",
101+
UseShellExecute = true,
102+
CreateNoWindow = true,
103+
WindowStyle = ProcessWindowStyle.Hidden,
104+
};
105+
try
106+
{
107+
using var p = Process.Start(psi);
108+
if (p is null) return false;
109+
p.WaitForExit(10_000);
110+
return p.HasExited && p.ExitCode == 0;
111+
}
112+
catch (Win32Exception)
113+
{
114+
return false;
115+
}
116+
}
117+
118+
private static bool RunChained(params string[] scCommands)
119+
{
120+
// Combine into one cmd /c call so the user only sees a single UAC prompt.
121+
// The first command (typically `sc stop`) is allowed to fail — `&` (not `&&`)
122+
// means cmd executes the second command regardless.
123+
var joined = string.Join(" & ", scCommands);
124+
var psi = new ProcessStartInfo
125+
{
126+
FileName = "cmd.exe",
127+
Arguments = $"/c {joined}",
128+
Verb = "runas",
129+
UseShellExecute = true,
130+
CreateNoWindow = true,
131+
WindowStyle = ProcessWindowStyle.Hidden,
132+
};
133+
try
134+
{
135+
using var p = Process.Start(psi);
136+
if (p is null) return false;
137+
p.WaitForExit(15_000);
138+
// Last command's exit code wins — that's the `sc config`, which is the one we care about.
139+
return p.HasExited && p.ExitCode == 0;
140+
}
141+
catch (Win32Exception)
142+
{
143+
return false;
144+
}
145+
}
146+
}

0 commit comments

Comments
 (0)