feat: Disable Application Experience scheduled tasks - #59
Merged
Conversation
Add a catalog-driven feature to keep the five Windows Application Experience scheduled tasks (Microsoft Compatibility Appraiser + siblings) disabled, with drift re-assert. Mirrors the existing Windows-services feature: - ScheduledTaskDefinition/ScheduledTaskCatalog (all five, RecommendedTarget=Disabled) - ScheduledTaskController: non-elevated schtasks /Query read; elevated batched /Change /Disable|/Enable with System32-absolute paths + injection guard - AppConfig.ScheduledTasks + ScheduledTaskPref - ScheduledTaskMonitor (absent task = not drift) - App.xaml.cs registration; RecommendedPreset stages all five (Recommended + Extreme), Reset un-manages - SettingDocs entries + SETTINGS-REFERENCE.md regenerated - 37 new tests (catalog, controller builder/parse/injection, monitor, config, preset, docs)
ScheduledTaskRow view-model (single 'Disable this task' checkbox + Monitor + Auto-apply + Learn more) and a 'Scheduled tasks' section rendering the five Application Experience tasks below the services list. Loads live state via ScheduledTaskController.QueryState; mirrors the service-row save/cleanup flow.
- CRITICAL: AppConfigCloner.CopyInto now copies ScheduledTasks -- without it the
feature silently no-opped on Apply (draft prefs dropped on commit). Regression
test added.
- ScheduledTaskController.QueryState: read state via schtasks /Query /XML and parse
the locale-independent <Settings><Enabled> element instead of the localized
/FO LIST 'Status:' field (was misclassifying state on non-English Windows ->
auto-apply churn). stderr no longer redirected (un-drained pipe could deadlock).
- ScheduledTaskMonitor: skip only fully-unmanaged tasks (Default + unmonitored) so
a manual Apply disables an unmonitored task ('do it now' ignores IsMonitored),
while avoiding background schtasks spawns for never-engaged tasks.
- SettingDocs: add task: branches (mechanism/verify/apply/reverse) so ApplyResults
and Learn More show real schtasks commands instead of '(unknown)'.
- Injection guard: block ( ) too.
- UI: query the five task states in parallel on Settings open.
carterscode
force-pushed
the
feat/disable-app-experience-tasks
branch
from
June 27, 2026 17:55
999d53c to
1e26775
Compare
Comment on lines
+112
to
+121
| foreach (var def in ScheduledTaskCatalog.All) | ||
| { | ||
| if (def.RecommendedTarget is not { } target) continue; | ||
| if (!draft.ScheduledTasks.TryGetValue(def.TaskPath, out var pref) || pref is null) | ||
| { | ||
| pref = new ScheduledTaskPref(); | ||
| draft.ScheduledTasks[def.TaskPath] = pref; | ||
| } | ||
| Count(SetScheduledTask(pref, $"Task: {def.DisplayName}", target, changes)); | ||
| } |
Comment on lines
+179
to
+188
| foreach (var def in ScheduledTaskCatalog.All) | ||
| { | ||
| if (def.RecommendedTarget is not { } target) continue; | ||
| if (!draft.ScheduledTasks.TryGetValue(def.TaskPath, out var pref) || pref is null) | ||
| { | ||
| pref = new ScheduledTaskPref(); | ||
| draft.ScheduledTasks[def.TaskPath] = pref; | ||
| } | ||
| Count(SetScheduledTask(pref, $"Task: {def.DisplayName}", target, changes, tag: "Extreme")); | ||
| } |
| var system32 = Environment.SystemDirectory; | ||
| var psi = new ProcessStartInfo | ||
| { | ||
| FileName = Path.Combine(system32, "schtasks.exe"), |
| if (p is null) return ScheduledTaskState.NotPresent; | ||
| var stdout = p.StandardOutput.ReadToEnd(); | ||
| p.WaitForExit(10_000); | ||
| if (!p.HasExited) { try { p.Kill(entireProcessTree: true); } catch { } return ScheduledTaskState.NotPresent; } |
| if (p is null) return ScheduledTaskState.NotPresent; | ||
| var stdout = p.StandardOutput.ReadToEnd(); | ||
| p.WaitForExit(10_000); | ||
| if (!p.HasExited) { try { p.Kill(entireProcessTree: true); } catch { } return ScheduledTaskState.NotPresent; } |
| var system32 = Environment.SystemDirectory; | ||
| var psi = new ProcessStartInfo | ||
| { | ||
| FileName = Path.Combine(system32, "cmd.exe"), |
| using var p = Process.Start(psi); | ||
| if (p is null) return false; | ||
| p.WaitForExit(15_000); | ||
| if (!p.HasExited) { try { p.Kill(entireProcessTree: true); } catch { } return false; } |
| using var p = Process.Start(psi); | ||
| if (p is null) return false; | ||
| p.WaitForExit(15_000); | ||
| if (!p.HasExited) { try { p.Kill(entireProcessTree: true); } catch { } return false; } |
|
|
||
| foreach (var def in ScheduledTaskCatalog.All) | ||
| { | ||
| Assert.True(cfg.ScheduledTasks.ContainsKey(def.TaskPath), $"missing {def.TaskPath}"); |
Comment on lines
+89
to
+98
| foreach (var def in ScheduledTaskCatalog.All) | ||
| { | ||
| // Must resolve via the exact id the ScheduledTaskMonitor emits. | ||
| var id = $"task:{def.TaskPath.ToLowerInvariant()}"; | ||
| var d = SettingDocsCatalog.Get(id); | ||
| Assert.NotNull(d); | ||
| Assert.False(string.IsNullOrWhiteSpace(d!.What), $"empty What for {id}"); | ||
| Assert.False(string.IsNullOrWhiteSpace(d.Risks), $"empty Risks for {id}"); | ||
| Assert.False(string.IsNullOrWhiteSpace(d.ReversibleVia), $"empty ReversibleVia for {id}"); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Adds a catalog-driven feature that keeps the five Windows Application Experience scheduled tasks (Microsoft Compatibility Appraiser + siblings) disabled, re-asserting on drift. These tasks drive
CompatTelRunner.exe, whose periodic compatibility/telemetry scans spike CPU and disk (the "Microsoft Compatibility Telemetry" process) — an unpredictable hitch source during gaming. Disabling is safe (Windows Update keeps working) and reversible, but feature updates re-enable them, so monitoring + auto-apply is the differentiated value. Additive to the existingDiagTracktelemetry-service disable, since the heavy scan is launched by the task.Mirrors the existing Windows-services feature: catalog → monitor → elevated controller.
Key decisions
ScheduledTaskCatalog/ScheduledTaskMonitor/ScheduledTaskControllertrio (not an overloaded service catalog) — task path vs. service name,schtasksvs.sc.schtasksmechanism, not registry: disabling the task is the only supported lever that stops the scans.schtasks /Query /XML(locale-independent<Settings><Enabled>) without elevation; disable/enable via elevatedschtasks /Change,System32-absolute paths + shell-metacharacter injection guard (mirrorsElevatedRegistry).IsMonitored); fully-unmanaged tasks (default + unmonitored) skip the background query so there's no idleschtasksspawn.What changed
ScheduledTaskDefinition+ catalog, controller,AppConfig.ScheduledTasks+ pref, monitor,App.xaml.csregistration,RecommendedPresetstaging, a "Scheduled tasks" group on the Windows services tab,SettingDocsentries (+SETTINGS-REFERENCE.mdregenerated).Testing
dotnet buildclean (warnings-as-errors),dotnet test594 passed.AppConfigCloner.CopyIntocarriesScheduledTasks.A code review (correctness + cross-file) ran on the diff; all findings were fixed in this branch — most notably a
CopyIntoomission that would have made the feature silently no-op on Apply, and locale-dependent state parsing that could cause auto-apply churn on non-English Windows.Post-Deploy Monitoring & Validation
No additional operational monitoring required — this is a local user-mode tray app with no service/runtime backend. Manual validation: open Settings → Windows services → Scheduled tasks, tick Disable + Monitor on a task, Apply (one UAC), confirm the row shows "Disabled" and
schtasks /Query /TN "\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" /XMLshows<Enabled>false</Enabled>.🤖 Generated with Claude Code