-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMainViewModel.cs
More file actions
363 lines (294 loc) · 14 KB
/
Copy pathMainViewModel.cs
File metadata and controls
363 lines (294 loc) · 14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows.Threading;
using ClaudeGuardian.Models;
using ClaudeGuardian.Services;
namespace ClaudeGuardian.ViewModels;
public enum FilterMode { All, StaleOnly, Today }
public enum ToolFilterMode { All, ClaudeOnly, CodexOnly }
public sealed class MainViewModel : INotifyPropertyChanged
{
private readonly ProcessScanner _scanner = new();
private readonly SystemMemoryService _memService = new();
private readonly SettingsService _settingsService = new();
private DispatcherTimer _timer;
private DispatcherTimer _standbyCleanTimer;
public AppSettings Settings { get; private set; }
/// <summary>由 MainWindow 注入,用于弹出自绘确认框,避免 ViewModel 直接依赖具体窗口实现。</summary>
public Func<string, string, Task<bool>>? ConfirmAsync { get; set; }
/// <summary>自动清理执行后的通知回调(用于托盘气泡提示)。</summary>
public Action<string>? NotifyAutoClean { get; set; }
private bool _standbyCleanInFlight;
public ObservableCollection<ProcessInfo> Processes { get; } = new();
private FilterMode _filter = FilterMode.All;
public FilterMode Filter
{
get => _filter;
set { if (_filter != value) { _filter = value; OnPropertyChanged(); RefreshFilterView(); } }
}
private ToolFilterMode _toolFilter = ToolFilterMode.All;
public ToolFilterMode ToolFilter
{
get => _toolFilter;
set { if (_toolFilter != value) { _toolFilter = value; OnPropertyChanged(); RefreshFilterView(); } }
}
private string _totalClaudeMemory = "0 MB";
public string TotalClaudeMemory { get => _totalClaudeMemory; set { _totalClaudeMemory = value; OnPropertyChanged(); } }
private string _processCount = "0";
public string ProcessCount { get => _processCount; set { _processCount = value; OnPropertyChanged(); } }
private string _staleCount = "0";
public string StaleCount { get => _staleCount; set { _staleCount = value; OnPropertyChanged(); } }
private string _systemMemorySummary = "";
public string SystemMemorySummary { get => _systemMemorySummary; set { _systemMemorySummary = value; OnPropertyChanged(); } }
private double _systemMemoryPercent;
public double SystemMemoryPercent { get => _systemMemoryPercent; set { _systemMemoryPercent = value; OnPropertyChanged(); } }
private string _vmmemSummary = "";
public string VmmemSummary { get => _vmmemSummary; set { _vmmemSummary = value; OnPropertyChanged(); } }
private string _lastUpdated = "";
public string LastUpdated { get => _lastUpdated; set { _lastUpdated = value; OnPropertyChanged(); } }
private bool _hasSelection;
public bool HasSelection { get => _hasSelection; set { _hasSelection = value; OnPropertyChanged(); } }
public RelayCommand RefreshCommand { get; }
public RelayCommand SelectStaleCommand { get; }
public RelayCommand ClearSelectionCommand { get; }
public RelayCommand KillSelectedCommand { get; }
public RelayCommand KillOneCommand { get; }
private readonly List<ProcessInfo> _allProcesses = new();
private bool _refreshInFlight;
public MainViewModel()
{
Settings = _settingsService.Load();
RefreshCommand = new RelayCommand(async _ => await RefreshAsync());
SelectStaleCommand = new RelayCommand(_ => SelectStale());
ClearSelectionCommand = new RelayCommand(_ => ClearSelection());
KillSelectedCommand = new RelayCommand(async _ => await KillSelectedAsync(), _ => HasSelection);
KillOneCommand = new RelayCommand(async p =>
{
if (p is ProcessInfo info) await KillOneAsync(info);
});
_ = RefreshAsync();
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(Settings.PollIntervalSeconds) };
_timer.Tick += async (_, _) => await RefreshAsync();
_timer.Start();
_standbyCleanTimer = new DispatcherTimer { Interval = TimeSpan.FromMinutes(Settings.CleanStandbyIntervalMinutes) };
_standbyCleanTimer.Tick += async (_, _) => await CleanStandbyListAsync(silent: true);
if (Settings.AutoCleanStandbyList) _standbyCleanTimer.Start();
}
/// <summary>设置面板保存后调用:持久化 + 应用轮询间隔等运行时可变项。</summary>
public void ApplySettings(AppSettings updated)
{
Settings = updated;
_settingsService.Save(Settings);
_timer.Stop();
_timer.Interval = TimeSpan.FromSeconds(Settings.PollIntervalSeconds);
_timer.Start();
_standbyCleanTimer.Stop();
_standbyCleanTimer.Interval = TimeSpan.FromMinutes(Settings.CleanStandbyIntervalMinutes);
if (Settings.AutoCleanStandbyList) _standbyCleanTimer.Start();
AutoStartService.SetEnabled(Settings.AutoStartWithWindows);
_ = RefreshAsync();
}
/// <summary>
/// 清空 Windows 待机内存列表(Standby List)。这只是把系统缓存的"可立即回收的空闲内存"
/// 从任务管理器的已用数字里挪走,不会真正增加可用内存上限;副作用是之后重新打开最近
/// 用过的文件/程序会因为缓存丢失而略慢。纯粹为满足"看数字"的需求而做。
/// 每次调用都会拉起一个带 UAC 提权的自身子进程执行实际清理,主程序全程保持普通权限。
/// </summary>
public async Task CleanStandbyListAsync(bool silent)
{
if (_standbyCleanInFlight) return;
_standbyCleanInFlight = true;
try
{
var ok = await StandbyListCleaner.CleanViaElevatedChildAsync();
if (!silent)
{
var failMsg = StandbyListCleaner.LastError is { } err ? $"释放失败:{err}" : "释放失败或已取消提权";
NotifyAutoClean?.Invoke(ok ? "已释放Windows系统占用内存" : failMsg);
}
else if (ok)
{
NotifyAutoClean?.Invoke("已自动释放Windows系统占用内存");
}
}
finally
{
_standbyCleanInFlight = false;
}
}
/// <summary>
/// 供外部(托盘菜单等)同步触发一次刷新;内部转发到异步实现,不阻塞调用方线程。
/// </summary>
public void Refresh() => _ = RefreshAsync();
public async Task RefreshAsync()
{
// WMI 查询 + Process 句柄读取都是阻塞式系统调用,不能直接在 DispatcherTimer.Tick(UI 线程)里同步执行,
// 否则每次轮询都会冻结消息泵几十到上百毫秒。挪到线程池执行,只有轻量的集合合并和属性赋值留在 UI 线程。
if (_refreshInFlight) return;
_refreshInFlight = true;
try
{
var watched = Settings.WatchedProcessNames;
var staleIdleHours = Settings.StaleIdleHours;
var staleCpuThreshold = Settings.StaleCpuDeltaThreshold;
var (scanned, mem) = await Task.Run(() => (_scanner.Scan(watched), _memService.GetSnapshot()));
foreach (var p in scanned)
{
p.StaleIdleHours = staleIdleHours;
p.StaleCpuDeltaThreshold = staleCpuThreshold;
}
// 按 PID 合并进已有对象而不是整批替换:新建对象会破坏 ObservableCollection 的身份识别,
// 迫使列表在每次轮询时重建所有可见行的可视化树,滚动过程中撞上就会明显卡顿。
var scannedByPid = scanned.ToDictionary(p => p.Pid);
var existingByPid = _allProcesses.ToDictionary(p => p.Pid);
foreach (var stalePid in existingByPid.Keys.Except(scannedByPid.Keys).ToList())
{
var stale = existingByPid[stalePid];
_allProcesses.Remove(stale);
}
foreach (var fresh in scanned)
{
if (existingByPid.TryGetValue(fresh.Pid, out var existing))
{
existing.MemoryMb = fresh.MemoryMb;
existing.CpuSeconds = fresh.CpuSeconds;
existing.CpuDeltaLastPoll = fresh.CpuDeltaLastPoll;
existing.StaleIdleHours = staleIdleHours;
existing.StaleCpuDeltaThreshold = staleCpuThreshold;
}
else
{
_allProcesses.Add(fresh);
}
}
RefreshFilterView();
var totalMb = scanned.Sum(p => p.MemoryMb);
TotalClaudeMemory = totalMb >= 1024 ? $"{totalMb / 1024:0.00} GB" : $"{totalMb:0} MB";
ProcessCount = scanned.Count.ToString();
StaleCount = scanned.Count(p => p.IsStaleCandidate).ToString();
SystemMemorySummary = $"{mem.UsedGb:0.0} / {mem.TotalGb:0.0} GB";
SystemMemoryPercent = mem.UsedPercent;
VmmemSummary = mem.VmmemGb > 0 ? $"{mem.VmmemGb:0.0} GB" : "未运行";
LastUpdated = $"更新于 {DateTime.Now:HH:mm:ss}";
UpdateSelectionState();
await RunAutoCleanAsync();
}
finally
{
_refreshInFlight = false;
}
}
/// <summary>
/// 自动清理规则引擎:满足条件的进程无需人工确认直接终止。
/// 在每次 RefreshAsync 末尾运行,使用的是这一轮刚采集到的最新数据,不会误杀已经不存在的进程。
/// </summary>
private async Task RunAutoCleanAsync()
{
var rule = Settings.AutoClean;
if (!rule.Enabled) return;
var candidates = _allProcesses.Where(p =>
{
var idleEnough = p.Age.TotalHours >= rule.IdleHours && p.IsIdle;
var memEnough = rule.MemoryThresholdMb > 0 && p.MemoryMb >= rule.MemoryThresholdMb;
// “要求闲置”开启时,无论走哪个条件都必须先闲置——避免误杀仍在正常工作的进程;
// 关闭时允许仅按内存超限清理,哪怕进程当前活跃(用户明确要的激进模式)。
return rule.RequireIdle ? idleEnough || (memEnough && p.IsIdle) : idleEnough || memEnough;
}).ToList();
if (candidates.Count == 0) return;
var freedMb = candidates.Sum(c => c.MemoryMb);
foreach (var c in candidates)
{
ProcessScanner.KillProcess(c.Pid);
}
NotifyAutoClean?.Invoke($"自动清理已终止 {candidates.Count} 个进程,释放约 {freedMb / 1024:0.00} GB 内存");
await Task.Run(() => _scanner.Scan(Settings.WatchedProcessNames)); // 刷新内部 CPU 采样缓存,避免残留已退出 PID
}
private readonly HashSet<int> _subscribedPids = new();
private void RefreshFilterView()
{
IEnumerable<ProcessInfo> filtered = Filter switch
{
FilterMode.StaleOnly => _allProcesses.Where(p => p.IsStaleCandidate),
FilterMode.Today => _allProcesses.Where(p => p.CreatedAt.Date == DateTime.Today),
_ => _allProcesses.AsEnumerable()
};
filtered = ToolFilter switch
{
ToolFilterMode.ClaudeOnly => filtered.Where(p => p.Tool == ToolKind.Claude),
ToolFilterMode.CodexOnly => filtered.Where(p => p.Tool == ToolKind.Codex),
_ => filtered
};
var target = filtered.OrderByDescending(p => p.Age).ToList();
var targetPids = target.Select(p => p.Pid).ToHashSet();
// 移除不再属于当前筛选结果的行
for (int i = Processes.Count - 1; i >= 0; i--)
{
if (!targetPids.Contains(Processes[i].Pid))
{
_subscribedPids.Remove(Processes[i].Pid);
Processes.RemoveAt(i);
}
}
// 按目标顺序就地插入缺失的行,已存在的行保持原有对象实例(身份不变,ItemsControl 可以复用可视化树)
for (int i = 0; i < target.Count; i++)
{
var p = target[i];
if (i >= Processes.Count || Processes[i].Pid != p.Pid)
{
if (_subscribedPids.Add(p.Pid))
{
p.PropertyChanged += OnProcessPropertyChanged;
}
Processes.Insert(i, p);
}
}
}
private void OnProcessPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ProcessInfo.IsSelected)) UpdateSelectionState();
}
private void SelectStale()
{
foreach (var p in Processes)
{
p.IsSelected = p.IsStaleCandidate;
}
UpdateSelectionState();
}
private void ClearSelection()
{
foreach (var p in Processes) p.IsSelected = false;
UpdateSelectionState();
}
private void UpdateSelectionState() => HasSelection = Processes.Any(p => p.IsSelected);
private async Task KillSelectedAsync()
{
var targets = Processes.Where(p => p.IsSelected).ToList();
if (targets.Count == 0) return;
var confirmed = ConfirmAsync is null
|| await ConfirmAsync(
"确认清理",
$"确定要终止选中的 {targets.Count} 个进程吗?\n将释放约 {targets.Sum(t => t.MemoryMb) / 1024:0.00} GB 内存。");
if (!confirmed) return;
foreach (var t in targets)
{
ProcessScanner.KillProcess(t.Pid);
}
await RefreshAsync();
}
private async Task KillOneAsync(ProcessInfo info)
{
var confirmed = ConfirmAsync is null
|| await ConfirmAsync(
"确认终止",
$"终止进程 PID {info.Pid}(创建于 {info.CreatedAt:MM-dd HH:mm})?");
if (!confirmed) return;
ProcessScanner.KillProcess(info.Pid);
await RefreshAsync();
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}