Skip to content

Commit e2fcaf6

Browse files
committed
added scheduler inspection and display to tweak details
Introduces scheduler (task) inspection to the backend and frontend, including new types and logic for detecting and displaying scheduler mismatches. Updates the backup info and tweak inspection models to include scheduler snapshot and inspection results, and enhances the TweakDetailsModal UI to show scheduler check results alongside registry and service checks.
1 parent 182d9a8 commit e2fcaf6

6 files changed

Lines changed: 181 additions & 9 deletions

File tree

src-tauri/src/commands/backup.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub struct BackupInfo {
1111
pub windows_version: u32,
1212
pub registry_values_count: usize,
1313
pub service_snapshots_count: usize,
14+
pub scheduler_snapshots_count: usize,
1415
}
1516

1617
/// Check if a tweak has a snapshot (is applied)
@@ -36,6 +37,7 @@ pub fn get_backup_info(tweak_id: String) -> Result<Option<BackupInfo>> {
3637
windows_version: snapshot.windows_version,
3738
registry_values_count: snapshot.registry_snapshots.len(),
3839
service_snapshots_count: snapshot.service_snapshots.len(),
40+
scheduler_snapshots_count: snapshot.scheduler_snapshots.len(),
3941
})),
4042
None => Ok(None),
4143
}

src-tauri/src/models/inspection.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ pub struct ServiceMismatch {
2121
pub is_match: bool,
2222
}
2323

24+
#[derive(Debug, Clone, Serialize, Deserialize)]
25+
pub struct SchedulerMismatch {
26+
pub task_path: String,
27+
pub task_name: String,
28+
pub expected_state: String,
29+
pub actual_state: Option<String>,
30+
pub description: String,
31+
pub is_match: bool,
32+
}
33+
2434
#[derive(Debug, Clone, Serialize, Deserialize)]
2535
pub struct OptionInspection {
2636
pub option_index: usize,
@@ -29,6 +39,7 @@ pub struct OptionInspection {
2939
pub is_pending: bool,
3040
pub registry_results: Vec<RegistryMismatch>,
3141
pub service_results: Vec<ServiceMismatch>,
42+
pub scheduler_results: Vec<SchedulerMismatch>,
3243
pub all_match: bool,
3344
}
3445

src-tauri/src/services/backup/inspection.rs

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ use super::capture::read_registry_value;
22
use super::helpers::values_match;
33
use crate::error::Error;
44
use crate::models::{
5-
OptionInspection, RegistryAction, RegistryMismatch, ServiceMismatch, TweakDefinition,
6-
TweakInspection, TweakOption,
5+
OptionInspection, RegistryAction, RegistryMismatch, SchedulerAction, SchedulerMismatch,
6+
ServiceMismatch, TweakDefinition, TweakInspection, TweakOption,
77
};
8-
use crate::services::{registry_service, service_control};
8+
use crate::services::{registry_service, scheduler_service, service_control};
99
use rayon::prelude::*;
1010

1111
/// Inspect a tweak to find exact system state vs expected state for all options
@@ -53,9 +53,13 @@ fn inspect_option(
5353
// Check service changes
5454
let service_results = inspect_service_changes(option)?;
5555

56+
// Check scheduler changes
57+
let scheduler_results = inspect_scheduler_changes(option)?;
58+
5659
// Determine if everything matches
57-
let all_match =
58-
registry_results.iter().all(|r| r.is_match) && service_results.iter().all(|s| s.is_match);
60+
let all_match = registry_results.iter().all(|r| r.is_match)
61+
&& service_results.iter().all(|s| s.is_match)
62+
&& scheduler_results.iter().all(|s| s.is_match);
5963

6064
Ok(OptionInspection {
6165
option_index: index,
@@ -64,6 +68,7 @@ fn inspect_option(
6468
is_pending,
6569
registry_results,
6670
service_results,
71+
scheduler_results,
6772
all_match,
6873
})
6974
}
@@ -202,3 +207,117 @@ fn inspect_service_changes(option: &TweakOption) -> Result<Vec<ServiceMismatch>,
202207

203208
Ok(results)
204209
}
210+
211+
fn inspect_scheduler_changes(option: &TweakOption) -> Result<Vec<SchedulerMismatch>, Error> {
212+
let mut results = Vec::new();
213+
214+
for change in &option.scheduler_changes {
215+
// Handle pattern-based task matching
216+
if let Some(pattern) = &change.task_name_pattern {
217+
// For patterns, we need to list matching tasks and check each one
218+
let matching_tasks =
219+
scheduler_service::find_tasks_by_pattern(&change.task_path, pattern)
220+
.unwrap_or_default();
221+
222+
if matching_tasks.is_empty() && change.ignore_not_found {
223+
// Skip if no tasks found and ignore_not_found is true
224+
continue;
225+
}
226+
227+
for task_info in matching_tasks {
228+
let actual_state = match &task_info.state {
229+
scheduler_service::TaskState::Ready => Some("Ready".to_string()),
230+
scheduler_service::TaskState::Disabled => Some("Disabled".to_string()),
231+
scheduler_service::TaskState::Running => Some("Running".to_string()),
232+
scheduler_service::TaskState::NotFound => None,
233+
scheduler_service::TaskState::Unknown(s) => Some(s.clone()),
234+
};
235+
236+
let (expected_state, is_match) = match change.action {
237+
SchedulerAction::Enable => {
238+
let expected = "Ready";
239+
let matches = matches!(
240+
task_info.state,
241+
scheduler_service::TaskState::Ready
242+
| scheduler_service::TaskState::Running
243+
);
244+
(expected, matches)
245+
}
246+
SchedulerAction::Disable => {
247+
let expected = "Disabled";
248+
let matches =
249+
matches!(task_info.state, scheduler_service::TaskState::Disabled);
250+
(expected, matches)
251+
}
252+
SchedulerAction::Delete => {
253+
let expected = "Deleted";
254+
let matches =
255+
matches!(task_info.state, scheduler_service::TaskState::NotFound);
256+
(expected, matches)
257+
}
258+
};
259+
260+
results.push(SchedulerMismatch {
261+
task_path: change.task_path.clone(),
262+
task_name: task_info.name,
263+
expected_state: expected_state.to_string(),
264+
actual_state,
265+
description: format!("{:?} task (pattern: {})", change.action, pattern),
266+
is_match,
267+
});
268+
}
269+
} else if let Some(task_name) = &change.task_name {
270+
// Single task inspection
271+
let task_state = scheduler_service::get_task_state(&change.task_path, task_name)
272+
.unwrap_or(scheduler_service::TaskState::Unknown("Error".to_string()));
273+
274+
// Handle not found case
275+
if matches!(task_state, scheduler_service::TaskState::NotFound)
276+
&& change.ignore_not_found
277+
{
278+
continue;
279+
}
280+
281+
let actual_state = match &task_state {
282+
scheduler_service::TaskState::Ready => Some("Ready".to_string()),
283+
scheduler_service::TaskState::Disabled => Some("Disabled".to_string()),
284+
scheduler_service::TaskState::Running => Some("Running".to_string()),
285+
scheduler_service::TaskState::NotFound => None,
286+
scheduler_service::TaskState::Unknown(s) => Some(s.clone()),
287+
};
288+
289+
let (expected_state, is_match) = match change.action {
290+
SchedulerAction::Enable => {
291+
let expected = "Ready";
292+
let matches = matches!(
293+
task_state,
294+
scheduler_service::TaskState::Ready | scheduler_service::TaskState::Running
295+
);
296+
(expected, matches)
297+
}
298+
SchedulerAction::Disable => {
299+
let expected = "Disabled";
300+
let matches = matches!(task_state, scheduler_service::TaskState::Disabled);
301+
(expected, matches)
302+
}
303+
SchedulerAction::Delete => {
304+
let expected = "Deleted";
305+
let matches = matches!(task_state, scheduler_service::TaskState::NotFound);
306+
(expected, matches)
307+
}
308+
};
309+
310+
results.push(SchedulerMismatch {
311+
task_path: change.task_path.clone(),
312+
task_name: task_name.clone(),
313+
expected_state: expected_state.to_string(),
314+
actual_state,
315+
description: format!("{:?} task", change.action),
316+
is_match,
317+
});
318+
}
319+
// If neither task_name nor task_name_pattern is set, skip this change
320+
}
321+
322+
Ok(results)
323+
}

src/lib/api/tweaks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ export interface BackupInfo {
142142
windows_version: number;
143143
registry_values_count: number;
144144
service_snapshots_count: number;
145+
scheduler_snapshots_count: number;
145146
}
146147

147148
/**

src/lib/components/TweakDetailsModal.svelte

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,16 @@
9898
9999
const matchedOption = inspection.options.find((o) => o.all_match);
100100
const totalChecks = inspection.options.reduce(
101-
(sum, o) => sum + o.registry_results.length + o.service_results.length,
101+
(sum, o) => sum + o.registry_results.length + o.service_results.length + o.scheduler_results.length,
102102
0,
103103
);
104104
105105
// Count mismatches for the current/pending option or first option
106106
const relevantOption = inspection.options.find((o) => o.is_current || o.is_pending) ?? inspection.options[0];
107107
const mismatches = relevantOption
108108
? relevantOption.registry_results.filter((r) => !r.is_match).length +
109-
relevantOption.service_results.filter((s) => !s.is_match).length
109+
relevantOption.service_results.filter((s) => !s.is_match).length +
110+
relevantOption.scheduler_results.filter((s) => !s.is_match).length
110111
: 0;
111112
112113
return {
@@ -333,7 +334,31 @@
333334
</div>
334335
{/each}
335336

336-
{#if opt.registry_results.length === 0 && opt.service_results.length === 0}
337+
{#each opt.scheduler_results as task}
338+
<div class="flex items-start gap-2 rounded-lg px-2 py-1.5 {task.is_match ? '' : 'bg-error/5'}">
339+
<Icon
340+
icon={task.is_match ? "mdi:check-circle" : "mdi:close-circle"}
341+
width="14"
342+
class="mt-0.5 shrink-0 {task.is_match ? 'text-success' : 'text-error'}"
343+
/>
344+
<div class="min-w-0 flex-1 text-xs">
345+
<div class="font-medium text-foreground">Task: {task.task_name}</div>
346+
<div class="truncate text-[10px] text-foreground-muted/70">{task.task_path}</div>
347+
{#if !task.is_match}
348+
<div class="mt-1 flex gap-4 font-mono text-[11px]">
349+
<span class="text-foreground-muted">
350+
Expected: <span class="text-success">{task.expected_state}</span>
351+
</span>
352+
<span class="text-foreground-muted">
353+
Actual: <span class="text-error">{task.actual_state ?? "Not Found"}</span>
354+
</span>
355+
</div>
356+
{/if}
357+
</div>
358+
</div>
359+
{/each}
360+
361+
{#if opt.registry_results.length === 0 && opt.service_results.length === 0 && opt.scheduler_results.length === 0}
337362
<div class="px-2 py-1.5 text-xs text-foreground-muted italic">
338363
No detectable changes for this Windows version
339364
</div>
@@ -363,9 +388,13 @@
363388
— {snapshotInfo.registry_values_count} registry
364389
{snapshotInfo.registry_values_count === 1 ? "value" : "values"}
365390
{#if snapshotInfo.service_snapshots_count > 0}
366-
and {snapshotInfo.service_snapshots_count}
391+
, {snapshotInfo.service_snapshots_count}
367392
{snapshotInfo.service_snapshots_count === 1 ? "service" : "services"}
368393
{/if}
394+
{#if snapshotInfo.scheduler_snapshots_count > 0}
395+
, {snapshotInfo.scheduler_snapshots_count}
396+
{snapshotInfo.scheduler_snapshots_count === 1 ? "task" : "tasks"}
397+
{/if}
369398
captured
370399
</span>
371400
</div>

src/lib/types/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,23 @@ export interface ServiceMismatch {
177177
is_match: boolean;
178178
}
179179

180+
export interface SchedulerMismatch {
181+
task_path: string;
182+
task_name: string;
183+
expected_state: string;
184+
actual_state?: string;
185+
description: string;
186+
is_match: boolean;
187+
}
188+
180189
export interface OptionInspection {
181190
option_index: number;
182191
label: string;
183192
is_current: boolean;
184193
is_pending: boolean;
185194
registry_results: RegistryMismatch[];
186195
service_results: ServiceMismatch[];
196+
scheduler_results: SchedulerMismatch[];
187197
all_match: boolean;
188198
}
189199

0 commit comments

Comments
 (0)