├── EdgeMonitor
├── Resources
│ ├── README.txt
│ └── about.gif
├── icon.ico
├── icon.jpg
├── App.xaml
├── Services
│ ├── IConfigurationService.cs
│ ├── InputDialog.xaml.cs
│ ├── DataService.cs
│ ├── DialogService.cs
│ ├── InputDialog.xaml
│ ├── IStartupService.cs
│ ├── ILogService.cs
│ ├── VersionHelper.cs
│ ├── PrivilegeService.cs
│ ├── ConfigurationService.cs
│ ├── LogService.cs
│ ├── TrayService.cs
│ ├── EdgeMonitorService.cs
│ └── StartupService.cs
├── appsettings.json
├── Enums
│ └── CloseActionEnums.cs
├── Converters
│ ├── EnumToBooleanConverter.cs
│ └── ValueConverters.cs
├── CloseOptionsDialog.xaml.cs
├── EdgeMonitor.csproj
├── Commands
│ └── RelayCommand.cs
├── CloseOptionsDialog.xaml
├── Views
│ ├── AboutWindow.xaml
│ └── AboutWindow.xaml.cs
├── App.xaml.cs
├── MainWindow.xaml.cs
├── Styles
│ └── AppStyles.xaml
├── MainWindow.xaml
└── ViewModels
│ └── MainViewModel.cs
├── image.png
├── .gitignore
├── README.md
├── EdgeMonitor.sln
├── LICENSE
└── VersionExtractionTest.cs
/EdgeMonitor/Resources/README.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrelinaMontelli/Edge-Monitor/HEAD/image.png
--------------------------------------------------------------------------------
/EdgeMonitor/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrelinaMontelli/Edge-Monitor/HEAD/EdgeMonitor/icon.ico
--------------------------------------------------------------------------------
/EdgeMonitor/icon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrelinaMontelli/Edge-Monitor/HEAD/EdgeMonitor/icon.jpg
--------------------------------------------------------------------------------
/EdgeMonitor/Resources/about.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PrelinaMontelli/Edge-Monitor/HEAD/EdgeMonitor/Resources/about.gif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | obj/
3 | *.user
4 | *.suo
5 | .vs/
6 | packages/
7 | *.log
8 | *.tmp
9 | *.cache
10 | *.DotSettings
11 | .vscode/
12 | .idea/
13 | *.userprefs
14 | *.pidb
15 | *.userprefs
16 | *.DS_Store
17 | Thumbs.db
18 | EdgeMonitorV1.1.zip
19 | Prelina Montelli_0xE53985E5_public.asc
20 | EdgeMonitorV1.1/
21 | publish/
22 | update-version.ps1
23 | UPDATE_v1.3.1.md
24 |
--------------------------------------------------------------------------------
/EdgeMonitor/App.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/EdgeMonitor/Services/IConfigurationService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 |
3 | namespace EdgeMonitor.Services
4 | {
5 | public interface IConfigurationService
6 | {
7 | ///
8 | /// 获取配置值
9 | ///
10 | T GetValue(string key);
11 |
12 | ///
13 | /// 设置配置值
14 | ///
15 | void SetValue(string key, object value);
16 |
17 | ///
18 | /// 保存配置到文件
19 | ///
20 | Task SaveAsync();
21 |
22 | ///
23 | /// 重新加载配置
24 | ///
25 | void Reload();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/EdgeMonitor/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "ApplicationSettings": {
10 | "DefaultMonitorInterval": 5,
11 | "AutoSaveEnabled": true,
12 | "NotificationsEnabled": true,
13 | "LogRetentionDays": 30,
14 | "MaxLogEntries": 1000
15 | },
16 | "EdgeMonitoring": {
17 | "CpuThreshold": 30.0,
18 | "MemoryThresholdMB": 2048,
19 | "CheckInterval": 5,
20 | "EnableAutoKill": true,
21 | "RequireAdminRights": true
22 | },
23 | "UI": {
24 | "Theme": "Light",
25 | "Language": "zh-CN",
26 | "WindowWidth": 800,
27 | "WindowHeight": 600,
28 | "CloseToTray": null,
29 | "RememberCloseChoice": false,
30 | "StartupHideToTray": false
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/EdgeMonitor/Services/InputDialog.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace EdgeMonitor.Services
4 | {
5 | public partial class InputDialog : Window
6 | {
7 | public string InputText { get; set; } = "";
8 |
9 | public InputDialog(string title, string prompt, string defaultValue = "")
10 | {
11 | InitializeComponent();
12 | Title = title;
13 | PromptLabel.Content = prompt;
14 | InputTextBox.Text = defaultValue;
15 | InputText = defaultValue;
16 | }
17 |
18 | private void OkButton_Click(object sender, RoutedEventArgs e)
19 | {
20 | InputText = InputTextBox.Text;
21 | DialogResult = true;
22 | }
23 |
24 | private void CancelButton_Click(object sender, RoutedEventArgs e)
25 | {
26 | DialogResult = false;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Edge Monitor
2 |
3 | 主播发现自己的电脑在启动一些软件或游戏时Edge会在后台自动启动,占用大量内存和CPU时间,故编写本程序来解决这个问题。
4 |
5 | 这个程序治标不治本,只能结束Edge,无法完全修复自动启动和异常占用的问题
6 |
7 | 
8 |
9 | ## 条件
10 |
11 | 程序会在同时满足以下两个条件时自动终止Edge进程:
12 |
13 | 1. **后台运行状态**:Edge进程正在运行,但没有可见的用户界面窗口
14 | 2. **资源占用异常**:满足以下任一条件
15 | - CPU使用率超过 30%
16 | - 内存使用量超过 2GB
17 |
18 |
19 | ## 技术栈
20 |
21 | - .NET 7
22 | - WPF (Windows Presentation Foundation)
23 | - Microsoft Extensions (DI, Logging, Configuration, Hosting)
24 | - MVVM 模式
25 |
26 |
27 | ## 许可证
28 |
29 | 本项目采用 CC BY-NC 4.0(Creative Commons Attribution-NonCommercial 4.0 International)许可证。
30 |
31 | **阁下可以:**
32 | - ✅ 分享 — 在任何媒介以任何形式复制、发行本作品
33 | - ✅ 演绎 — 修改、转换或以本作品为基础进行创作
34 |
35 | **惟须遵守下列条件:**
36 | - 📝 **署名** — 阁下必须给出适当的署名,提供指向本许可协议的链接,同时标明是否(对原始作品)作了修改
37 | - 🚫 **非商业性使用** — 阁下不得将本作品用于商业目的
38 |
39 | **原作者:** Prelina Montelli
40 |
41 | 详细许可条款请参见:https://creativecommons.org/licenses/by-nc/4.0/
42 |
--------------------------------------------------------------------------------
/EdgeMonitor.sln:
--------------------------------------------------------------------------------
1 | Microsoft Visual Studio Solution File, Format Version 12.00
2 | # Visual Studio Version 17
3 | VisualStudioVersion = 17.0.31903.59
4 | MinimumVisualStudioVersion = 10.0.40219.1
5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdgeMonitor", "EdgeMonitor\EdgeMonitor.csproj", "{A1B2C3D4-E5F6-7890-ABCD-123456789ABC}"
6 | EndProject
7 | Global
8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
9 | Debug|Any CPU = Debug|Any CPU
10 | Release|Any CPU = Release|Any CPU
11 | EndGlobalSection
12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
13 | {A1B2C3D4-E5F6-7890-ABCD-123456789ABC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
14 | {A1B2C3D4-E5F6-7890-ABCD-123456789ABC}.Debug|Any CPU.Build.0 = Debug|Any CPU
15 | {A1B2C3D4-E5F6-7890-ABCD-123456789ABC}.Release|Any CPU.ActiveCfg = Release|Any CPU
16 | {A1B2C3D4-E5F6-7890-ABCD-123456789ABC}.Release|Any CPU.Build.0 = Release|Any CPU
17 | EndGlobalSection
18 | EndGlobal
19 |
--------------------------------------------------------------------------------
/EdgeMonitor/Enums/CloseActionEnums.cs:
--------------------------------------------------------------------------------
1 | namespace EdgeMonitor
2 | {
3 | ///
4 | /// 窗口关闭行为枚举
5 | ///
6 | public enum CloseAction
7 | {
8 | ///
9 | /// 每次询问用户
10 | ///
11 | Ask,
12 |
13 | ///
14 | /// 最小化到托盘
15 | ///
16 | MinimizeToTray,
17 |
18 | ///
19 | /// 直接退出程序
20 | ///
21 | Exit
22 | }
23 |
24 | ///
25 | /// 关闭选项对话框的选择结果
26 | ///
27 | public enum CloseOption
28 | {
29 | ///
30 | /// 取消关闭
31 | ///
32 | Cancel,
33 |
34 | ///
35 | /// 最小化到托盘
36 | ///
37 | MinimizeToTray,
38 |
39 | ///
40 | /// 退出程序
41 | ///
42 | Exit
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Creative Commons Attribution-NonCommercial 4.0 International License
2 |
3 | Copyright (c) 2025 Prelina Montelli
4 |
5 | This work is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License.
6 |
7 | You are free to:
8 | - Share — copy and redistribute the material in any medium or format
9 | - Adapt — remix, transform, and build upon the material
10 |
11 | Under the following terms:
12 | - Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
13 | - NonCommercial — You may not use the material for commercial purposes.
14 |
15 | No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
16 |
17 | To view a copy of this license, visit:
18 | https://creativecommons.org/licenses/by-nc/4.0/
19 |
20 | Original Author: Prelina Montelli
21 | Project: Edge Monitor - A program to monitor and terminate Microsoft Edge processes when running abnormally in the background.
22 |
--------------------------------------------------------------------------------
/EdgeMonitor/Converters/EnumToBooleanConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Windows.Data;
4 |
5 | namespace EdgeMonitor.Converters
6 | {
7 | public class EnumToBooleanConverter : IValueConverter
8 | {
9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
10 | {
11 | if (value == null || parameter == null)
12 | return false;
13 |
14 | string enumValue = value.ToString();
15 | string targetValue = parameter.ToString();
16 | return enumValue?.Equals(targetValue, StringComparison.InvariantCultureIgnoreCase) == true;
17 | }
18 |
19 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
20 | {
21 | if (value == null || parameter == null)
22 | return Binding.DoNothing;
23 |
24 | bool useValue = (bool)value;
25 | string targetValue = parameter.ToString();
26 | if (useValue)
27 | {
28 | return Enum.Parse(targetType, targetValue);
29 | }
30 | return Binding.DoNothing;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/EdgeMonitor/CloseOptionsDialog.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace EdgeMonitor
4 | {
5 | public partial class CloseOptionsDialog : Window
6 | {
7 | public CloseOption SelectedOption { get; private set; } = CloseOption.Cancel;
8 | public bool RememberChoice { get; private set; } = false;
9 |
10 | public CloseOptionsDialog()
11 | {
12 | InitializeComponent();
13 | }
14 |
15 | private void OkButton_Click(object sender, RoutedEventArgs e)
16 | {
17 | if (MinimizeToTrayRadio.IsChecked == true)
18 | {
19 | SelectedOption = CloseOption.MinimizeToTray;
20 | }
21 | else if (ExitRadio.IsChecked == true)
22 | {
23 | SelectedOption = CloseOption.Exit;
24 | }
25 |
26 | RememberChoice = RememberChoiceCheckBox.IsChecked == true;
27 |
28 | DialogResult = true;
29 | Close();
30 | }
31 |
32 | private void CancelButton_Click(object sender, RoutedEventArgs e)
33 | {
34 | SelectedOption = CloseOption.Cancel;
35 | DialogResult = false;
36 | Close();
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/EdgeMonitor/Services/DataService.cs:
--------------------------------------------------------------------------------
1 | namespace EdgeMonitor.Services
2 | {
3 | public interface IDataService
4 | {
5 | Task LoadDataAsync();
6 | Task SaveDataAsync(string data);
7 | Task> GetLogEntriesAsync();
8 | Task AddLogEntryAsync(string entry);
9 | }
10 |
11 | public class DataService : IDataService
12 | {
13 | private readonly List _logEntries = new();
14 |
15 | public async Task LoadDataAsync()
16 | {
17 | // 模拟异步数据加载
18 | await Task.Delay(100);
19 | return "示例数据已加载";
20 | }
21 |
22 | public async Task SaveDataAsync(string data)
23 | {
24 | // 模拟异步数据保存
25 | await Task.Delay(100);
26 | await AddLogEntryAsync($"数据已保存: {data.Length} 字符");
27 | }
28 |
29 | public async Task> GetLogEntriesAsync()
30 | {
31 | await Task.Delay(50);
32 | return new List(_logEntries);
33 | }
34 |
35 | public async Task AddLogEntryAsync(string entry)
36 | {
37 | await Task.Delay(10);
38 | _logEntries.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {entry}");
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/EdgeMonitor/Services/DialogService.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace EdgeMonitor.Services
4 | {
5 | public interface IDialogService
6 | {
7 | void ShowMessage(string title, string message);
8 | bool ShowConfirmation(string title, string message);
9 | string? ShowInputDialog(string title, string prompt, string defaultValue = "");
10 | }
11 |
12 | public class DialogService : IDialogService
13 | {
14 | public void ShowMessage(string title, string message)
15 | {
16 | MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Information);
17 | }
18 |
19 | public bool ShowConfirmation(string title, string message)
20 | {
21 | var result = MessageBox.Show(message, title, MessageBoxButton.YesNo, MessageBoxImage.Question);
22 | return result == MessageBoxResult.Yes;
23 | }
24 |
25 | public string? ShowInputDialog(string title, string prompt, string defaultValue = "")
26 | {
27 | // 简单的输入对话框实现
28 | var inputDialog = new InputDialog(title, prompt, defaultValue);
29 | if (inputDialog.ShowDialog() == true)
30 | {
31 | return inputDialog.InputText;
32 | }
33 | return null;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/EdgeMonitor/Converters/ValueConverters.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using System.Windows.Data;
3 |
4 | namespace EdgeMonitor.Converters
5 | {
6 | public class BooleanToStringConverter : IValueConverter
7 | {
8 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
9 | {
10 | if (value is bool boolValue)
11 | {
12 | return boolValue ? "运行中" : "已停止";
13 | }
14 | return "未知";
15 | }
16 |
17 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
18 | {
19 | throw new NotImplementedException();
20 | }
21 | }
22 |
23 | public class InverseBooleanConverter : IValueConverter
24 | {
25 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
26 | {
27 | if (value is bool boolValue)
28 | {
29 | return !boolValue;
30 | }
31 | return true;
32 | }
33 |
34 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
35 | {
36 | if (value is bool boolValue)
37 | {
38 | return !boolValue;
39 | }
40 | return false;
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/EdgeMonitor/Services/InputDialog.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
24 |
30 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/EdgeMonitor/Services/IStartupService.cs:
--------------------------------------------------------------------------------
1 | namespace EdgeMonitor.Services
2 | {
3 | ///
4 | /// 开机自启动服务接口
5 | ///
6 | public interface IStartupService
7 | {
8 | ///
9 | /// 检查是否已启用开机自启动
10 | ///
11 | /// 如果已启用开机自启动则返回true
12 | bool IsStartupEnabled();
13 |
14 | ///
15 | /// 检查是否已启用管理员权限的开机自启动
16 | ///
17 | /// 如果已启用管理员权限的开机自启动则返回true
18 | bool IsAdminStartupEnabled();
19 |
20 | ///
21 | /// 启用开机自启动
22 | ///
23 | /// 是否以管理员权限启动
24 | /// 是否启动时隐藏到托盘
25 | /// 如果成功启用则返回true
26 | Task EnableStartupAsync(bool runAsAdmin = false, bool hideToTray = false);
27 |
28 | ///
29 | /// 启用开机后自动在托盘监测
30 | ///
31 | /// 如果成功启用则返回true
32 | Task EnableTrayMonitorStartupAsync();
33 |
34 | ///
35 | /// 检查是否已启用开机后自动在托盘监测
36 | ///
37 | /// 如果已启用则返回true
38 | bool IsTrayMonitorStartupEnabled();
39 |
40 | ///
41 | /// 禁用开机自启动
42 | ///
43 | /// 如果成功禁用则返回true
44 | Task DisableStartupAsync();
45 |
46 | ///
47 | /// 检查启动任务状态并在需要时重新安排
48 | ///
49 | void StartupCheck();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/EdgeMonitor/Services/ILogService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.ObjectModel;
2 |
3 | namespace EdgeMonitor.Services
4 | {
5 | public interface ILogService
6 | {
7 | ///
8 | /// 获取内存中的日志条目(最新300条)
9 | ///
10 | ObservableCollection LogEntries { get; }
11 |
12 | ///
13 | /// 获取内存中的监控数据(最新300条)
14 | ///
15 | ObservableCollection MonitorEntries { get; }
16 |
17 | ///
18 | /// 添加日志条目
19 | ///
20 | Task AddLogEntryAsync(string message);
21 |
22 | ///
23 | /// 添加监控数据条目
24 | ///
25 | Task AddMonitorEntryAsync(string message);
26 |
27 | ///
28 | /// 清除内存中的日志(文件中的不删除)
29 | ///
30 | void ClearMemoryLogs();
31 |
32 | ///
33 | /// 获取日志文件路径
34 | ///
35 | string GetLogFilePath();
36 |
37 | ///
38 | /// 清理超过指定天数的日志文件
39 | ///
40 | Task CleanupOldLogFilesAsync();
41 |
42 | ///
43 | /// 获取日志统计信息
44 | ///
45 | Task GetLogStatisticsAsync();
46 | }
47 |
48 | public class LogStatistics
49 | {
50 | public int TotalFiles { get; set; }
51 | public long TotalSizeBytes { get; set; }
52 | public double TotalSizeMB { get; set; }
53 | public int MemoryLogCount { get; set; }
54 | public int MemoryMonitorCount { get; set; }
55 | public string LogDirectory { get; set; } = "";
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/EdgeMonitor/Services/VersionHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 |
3 | namespace EdgeMonitor.Services
4 | {
5 | public static class VersionHelper
6 | {
7 | ///
8 | /// 获取当前应用程序的版本号
9 | ///
10 | /// 格式化的版本号字符串
11 | public static string GetVersionString()
12 | {
13 | var version = Assembly.GetExecutingAssembly().GetName().Version;
14 | if (version != null)
15 | {
16 | // 如果构建版本是0,只显示主版本.次版本
17 | if (version.Build == 0)
18 | {
19 | return $"v{version.Major}.{version.Minor}";
20 | }
21 | // 否则显示主版本.次版本.构建版本
22 | return $"v{version.Major}.{version.Minor}.{version.Build}";
23 | }
24 | return "v1.0";
25 | }
26 |
27 | ///
28 | /// 获取完整的版本信息
29 | ///
30 | /// 完整版本号
31 | public static string GetFullVersionString()
32 | {
33 | var version = Assembly.GetExecutingAssembly().GetName().Version;
34 | return version?.ToString() ?? "1.0.0.0";
35 | }
36 |
37 | ///
38 | /// 获取程序集文件版本
39 | ///
40 | /// 文件版本号
41 | public static string GetFileVersionString()
42 | {
43 | var assembly = Assembly.GetExecutingAssembly();
44 | var fileVersionInfo = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location);
45 | return fileVersionInfo.FileVersion ?? "1.0.0.0";
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/EdgeMonitor/EdgeMonitor.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | net7.0-windows
6 | true
7 | enable
8 | enable
9 | Edge Monitor
10 | Edge Monitor Application
11 | Prelina_Montelli
12 | Edge Monitor
13 | 1.3.3.0
14 | 1.3.3.0
15 | icon.ico
16 |
17 |
18 | true
19 | win-x64
20 | true
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Always
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/EdgeMonitor/Commands/RelayCommand.cs:
--------------------------------------------------------------------------------
1 | using System.Windows.Input;
2 |
3 | namespace EdgeMonitor.Commands
4 | {
5 | public class RelayCommand : ICommand
6 | {
7 | private readonly Action _execute;
8 | private readonly Func? _canExecute;
9 |
10 | public RelayCommand(Action execute, Func? canExecute = null)
11 | {
12 | _execute = execute ?? throw new ArgumentNullException(nameof(execute));
13 | _canExecute = canExecute;
14 | }
15 |
16 | public event EventHandler? CanExecuteChanged
17 | {
18 | add { CommandManager.RequerySuggested += value; }
19 | remove { CommandManager.RequerySuggested -= value; }
20 | }
21 |
22 | public bool CanExecute(object? parameter)
23 | {
24 | return _canExecute?.Invoke() ?? true;
25 | }
26 |
27 | public void Execute(object? parameter)
28 | {
29 | _execute();
30 | }
31 | }
32 |
33 | public class RelayCommand : ICommand
34 | {
35 | private readonly Action _execute;
36 | private readonly Predicate? _canExecute;
37 |
38 | public RelayCommand(Action execute, Predicate? canExecute = null)
39 | {
40 | _execute = execute ?? throw new ArgumentNullException(nameof(execute));
41 | _canExecute = canExecute;
42 | }
43 |
44 | public event EventHandler? CanExecuteChanged
45 | {
46 | add { CommandManager.RequerySuggested += value; }
47 | remove { CommandManager.RequerySuggested -= value; }
48 | }
49 |
50 | public bool CanExecute(object? parameter)
51 | {
52 | return _canExecute?.Invoke((T?)parameter) ?? true;
53 | }
54 |
55 | public void Execute(object? parameter)
56 | {
57 | _execute((T?)parameter);
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/EdgeMonitor/Services/PrivilegeService.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Principal;
2 | using System.Diagnostics;
3 | using System.Windows;
4 |
5 | namespace EdgeMonitor.Services
6 | {
7 | public interface IPrivilegeService
8 | {
9 | bool IsRunningAsAdministrator();
10 | void RestartAsAdministrator();
11 | void ShowAdministratorRequiredMessage();
12 | }
13 |
14 | public class PrivilegeService : IPrivilegeService
15 | {
16 | public bool IsRunningAsAdministrator()
17 | {
18 | try
19 | {
20 | var identity = WindowsIdentity.GetCurrent();
21 | var principal = new WindowsPrincipal(identity);
22 | return principal.IsInRole(WindowsBuiltInRole.Administrator);
23 | }
24 | catch
25 | {
26 | return false;
27 | }
28 | }
29 |
30 | public void RestartAsAdministrator()
31 | {
32 | try
33 | {
34 | var processInfo = new ProcessStartInfo
35 | {
36 | UseShellExecute = true,
37 | WorkingDirectory = Environment.CurrentDirectory,
38 | FileName = Environment.ProcessPath ?? Process.GetCurrentProcess().MainModule?.FileName,
39 | Verb = "runas"
40 | };
41 |
42 | Process.Start(processInfo);
43 | Application.Current.Shutdown();
44 | }
45 | catch (Exception ex)
46 | {
47 | MessageBox.Show($"无法以管理员身份重新启动程序: {ex.Message}",
48 | "错误",
49 | MessageBoxButton.OK,
50 | MessageBoxImage.Error);
51 | }
52 | }
53 |
54 | public void ShowAdministratorRequiredMessage()
55 | {
56 | var result = MessageBox.Show(
57 | "此程序需要管理员权限才能正常运行。\n\n是否要以管理员身份重新启动程序?",
58 | "需要管理员权限",
59 | MessageBoxButton.YesNo,
60 | MessageBoxImage.Warning);
61 |
62 | if (result == MessageBoxResult.Yes)
63 | {
64 | RestartAsAdministrator();
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/VersionExtractionTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.RegularExpressions;
3 |
4 | namespace EdgeMonitor.Tests
5 | {
6 | public static class VersionExtractionTest
7 | {
8 | public static void TestVersionExtraction()
9 | {
10 | var testCases = new[]
11 | {
12 | ("EdgeMonitor-v1.3", "v1.3"),
13 | ("EdgeMonitor-v1.3.0", "v1.3.0"),
14 | ("EM-v1.4", "v1.4"),
15 | ("EM-v1.4.2", "v1.4.2"),
16 | ("Edge Monitor-v2.0", "v2.0"),
17 | ("MyApp-v3.1.5", "v3.1.5"),
18 | ("v1.5", "v1.5"),
19 | ("v2.0.1", "v2.0.1"),
20 | ("1.6", "v1.6"),
21 | ("2.1.0", "v2.1.0"),
22 | ("Release", null), // 应该返回null
23 | ("", null), // 应该返回null
24 | };
25 |
26 | Console.WriteLine("版本号提取测试:");
27 | Console.WriteLine("================");
28 |
29 | foreach (var (input, expected) in testCases)
30 | {
31 | var result = ExtractVersionFromReleaseName(input);
32 | var status = result == expected ? "✅ PASS" : "❌ FAIL";
33 | Console.WriteLine($"{status} \"{input}\" → \"{result}\" (期望: \"{expected}\")");
34 | }
35 | }
36 |
37 | private static string? ExtractVersionFromReleaseName(string? releaseName)
38 | {
39 | if (string.IsNullOrEmpty(releaseName)) return null;
40 |
41 | try
42 | {
43 | // 尝试匹配各种可能的版本号格式
44 | var patterns = new[]
45 | {
46 | @"EdgeMonitor-v(\d+\.\d+(?:\.\d+)?)", // EdgeMonitor-v1.3 或 EdgeMonitor-v1.3.0
47 | @"EM-v(\d+\.\d+(?:\.\d+)?)", // EM-v1.4 或 EM-v1.4.0
48 | @"Edge\s*Monitor-v(\d+\.\d+(?:\.\d+)?)", // Edge Monitor-v1.3 (带空格)
49 | @"[A-Za-z\s]*-v(\d+\.\d+(?:\.\d+)?)", // 任意前缀-v1.3 (通用匹配)
50 | @"v(\d+\.\d+(?:\.\d+)?)", // v1.3 或 v1.3.0
51 | @"(\d+\.\d+(?:\.\d+)?)" // 1.3 或 1.3.0 (纯数字)
52 | };
53 |
54 | foreach (var pattern in patterns)
55 | {
56 | var match = Regex.Match(releaseName, pattern, RegexOptions.IgnoreCase);
57 | if (match.Success)
58 | {
59 | return "v" + match.Groups[1].Value;
60 | }
61 | }
62 | }
63 | catch
64 | {
65 | // 解析失败时返回null
66 | }
67 |
68 | return null;
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/EdgeMonitor/CloseOptionsDialog.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
16 |
23 |
29 |
30 |
31 |
32 |
38 |
39 |
40 |
41 |
42 |
43 |
48 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
67 |
71 |
72 |
73 |
78 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/EdgeMonitor/Views/AboutWindow.xaml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
34 |
35 |
36 |
37 |
38 |
43 |
44 |
49 |
50 |
55 |
56 |
57 |
60 |
61 |
65 |
66 |
67 |
68 |
69 |
70 |
75 |
76 |
77 |
81 |
87 |
88 |
89 |
90 |
93 |
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/EdgeMonitor/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Hosting;
3 | using Microsoft.Extensions.Logging;
4 | using Microsoft.Extensions.Configuration;
5 | using System.Windows;
6 | using EdgeMonitor.ViewModels;
7 | using EdgeMonitor.Services;
8 | using System.IO;
9 |
10 | namespace EdgeMonitor
11 | {
12 | ///
13 | /// Interaction logic for App.xaml
14 | ///
15 | public partial class App : Application
16 | {
17 | private IHost? _host;
18 |
19 | protected override async void OnStartup(StartupEventArgs e)
20 | {
21 | try
22 | {
23 | _host = CreateHostBuilder(e.Args).Build();
24 |
25 | // 启动服务
26 | await _host.StartAsync();
27 |
28 | // Check startup modes
29 | bool isTrayMonitorMode = e.Args.Contains("--tray-monitor");
30 | bool isStartupHideToTray = e.Args.Contains("--startup-hide-tray");
31 |
32 | // 检查管理员权限
33 | var privilegeService = _host.Services.GetRequiredService();
34 | if (!privilegeService.IsRunningAsAdministrator())
35 | {
36 | if (!isTrayMonitorMode)
37 | {
38 | privilegeService.ShowAdministratorRequiredMessage();
39 | }
40 | Shutdown();
41 | return;
42 | }
43 |
44 | // 执行启动检查
45 | var startupService = _host.Services.GetRequiredService();
46 | startupService.StartupCheck();
47 |
48 | // 获取托盘服务
49 | var trayService = _host.Services.GetRequiredService();
50 |
51 | // 初始化托盘服务
52 | trayService.InitializeTray();
53 |
54 | if (isTrayMonitorMode)
55 | {
56 | // Tray monitor mode - hide window and auto start monitoring
57 | var mainViewModel = _host.Services.GetRequiredService();
58 |
59 | // Show tray icon
60 | trayService.ShowTray();
61 |
62 | // Start tray monitoring mode
63 | mainViewModel.StartTrayMonitoring();
64 |
65 | // Don't set MainWindow in tray mode
66 | }
67 | else if (isStartupHideToTray)
68 | {
69 | // Startup hide to tray mode - create window but hide it
70 | var mainWindow = _host.Services.GetRequiredService();
71 |
72 | // Set as main window but don't show
73 | MainWindow = mainWindow;
74 |
75 | // Show tray icon
76 | trayService.ShowTray();
77 |
78 | // Hide to tray without showing window
79 | mainWindow.Hide();
80 | }
81 | else
82 | {
83 | // Normal mode - show main window
84 | var mainWindow = _host.Services.GetRequiredService();
85 | mainWindow.Show();
86 |
87 | // Set as main window
88 | MainWindow = mainWindow;
89 | }
90 |
91 | base.OnStartup(e);
92 | }
93 | catch (Exception ex)
94 | {
95 | MessageBox.Show($"应用程序启动时发生严重错误: {ex.Message}\n\n{ex.ToString()}",
96 | "启动失败",
97 | MessageBoxButton.OK,
98 | MessageBoxImage.Error);
99 | Shutdown();
100 | }
101 | }
102 |
103 | private IHostBuilder CreateHostBuilder(string[] args)
104 | {
105 | return Host.CreateDefaultBuilder(args)
106 | .ConfigureServices((context, services) =>
107 | {
108 | services.AddSingleton();
109 | services.AddSingleton();
110 | services.AddSingleton();
111 | services.AddSingleton();
112 | services.AddSingleton();
113 | services.AddSingleton();
114 | services.AddSingleton();
115 | services.AddSingleton();
116 | services.AddSingleton();
117 | services.AddSingleton();
118 | })
119 | .ConfigureLogging(logging =>
120 | {
121 | logging.AddConsole();
122 | });
123 | }
124 |
125 | protected override async void OnExit(ExitEventArgs e)
126 | {
127 | if (_host != null)
128 | {
129 | await _host.StopAsync();
130 | _host.Dispose();
131 | }
132 |
133 | base.OnExit(e);
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/EdgeMonitor/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 | using System.Windows;
3 | using EdgeMonitor.Services;
4 | using EdgeMonitor.ViewModels;
5 | using Microsoft.Extensions.Configuration;
6 | using Microsoft.Extensions.Logging;
7 |
8 | namespace EdgeMonitor
9 | {
10 | ///
11 | /// Interaction logic for MainWindow.xaml
12 | ///
13 | public partial class MainWindow : Window
14 | {
15 | private readonly MainViewModel _viewModel;
16 | private readonly ITrayService _trayService;
17 | private readonly IDialogService _dialogService;
18 | private readonly IConfiguration _configuration;
19 | private readonly IConfigurationService _configService;
20 | private readonly ILogger _logger;
21 | private bool _isReallyClosing = false;
22 |
23 | public MainWindow(MainViewModel viewModel, ITrayService trayService, IDialogService dialogService, IConfiguration configuration, IConfigurationService configService, ILogger logger)
24 | {
25 | InitializeComponent();
26 | _viewModel = viewModel;
27 | _trayService = trayService;
28 | _dialogService = dialogService;
29 | _configuration = configuration;
30 | _configService = configService;
31 | _logger = logger;
32 |
33 | DataContext = _viewModel;
34 |
35 | // 处理窗口关闭事件
36 | this.Closing += MainWindow_Closing;
37 |
38 | _logger.LogInformation("主窗口已初始化,托盘功能已启用");
39 | }
40 |
41 | private void MainWindow_Closing(object? sender, CancelEventArgs e)
42 | {
43 | // 如果已经决定真正关闭,不再询问
44 | if (_isReallyClosing)
45 | {
46 | _trayService.Dispose();
47 | return;
48 | }
49 |
50 | // 取消关闭事件
51 | e.Cancel = true;
52 |
53 | // 使用ViewModel中的CloseAction设置
54 | var closeAction = _viewModel.CloseAction;
55 |
56 | switch (closeAction)
57 | {
58 | case CloseAction.MinimizeToTray:
59 | MinimizeToTray();
60 | break;
61 | case CloseAction.Exit:
62 | ReallyClose();
63 | break;
64 | case CloseAction.Ask:
65 | // 显示选择对话框
66 | var result = ShowCloseOptionsDialog();
67 | HandleCloseDialogResult(result);
68 | break;
69 | }
70 | }
71 |
72 | private (CloseOption Option, bool RememberChoice) ShowCloseOptionsDialog()
73 | {
74 | var dialog = new CloseOptionsDialog();
75 | dialog.Owner = this;
76 | var result = dialog.ShowDialog();
77 |
78 | if (result == true)
79 | {
80 | return (dialog.SelectedOption, dialog.RememberChoice);
81 | }
82 |
83 | return (CloseOption.Cancel, false);
84 | }
85 |
86 | private void HandleCloseDialogResult((CloseOption Option, bool RememberChoice) result)
87 | {
88 | var option = result.Option;
89 |
90 | // 如果用户选择记住选择,更新ViewModel的CloseAction并保存
91 | if (result.RememberChoice && option != CloseOption.Cancel)
92 | {
93 | var closeAction = option switch
94 | {
95 | CloseOption.MinimizeToTray => CloseAction.MinimizeToTray,
96 | CloseOption.Exit => CloseAction.Exit,
97 | _ => CloseAction.Ask
98 | };
99 |
100 | // 更新ViewModel的设置(这会自动触发保存)
101 | _viewModel.CloseAction = closeAction;
102 | }
103 |
104 | // 执行相应的关闭操作
105 | switch (option)
106 | {
107 | case CloseOption.MinimizeToTray:
108 | MinimizeToTray();
109 | break;
110 | case CloseOption.Exit:
111 | ReallyClose();
112 | break;
113 | case CloseOption.Cancel:
114 | // 什么都不做,继续显示窗口
115 | break;
116 | }
117 | }
118 |
119 | private async Task SaveCloseChoiceAsync(CloseOption option)
120 | {
121 | try
122 | {
123 | var closeToTray = option == CloseOption.MinimizeToTray ? "true" : "false";
124 |
125 | _configService.SetValue("UI:CloseToTray", closeToTray);
126 | _configService.SetValue("UI:RememberCloseChoice", true);
127 |
128 | await _configService.SaveAsync();
129 |
130 | _logger.LogInformation($"保存关闭选择: {option}");
131 | }
132 | catch (Exception ex)
133 | {
134 | _logger.LogError($"保存关闭选择失败: {ex.Message}");
135 | }
136 | }
137 |
138 | private void SaveCloseChoice(CloseOption option)
139 | {
140 | try
141 | {
142 | // 注意:这里只是演示,实际应用中需要更复杂的配置保存机制
143 | // 因为 IConfiguration 通常是只读的
144 | var closeToTray = option == CloseOption.MinimizeToTray ? "true" : "false";
145 | _logger.LogInformation($"保存关闭选择: {option}");
146 |
147 | // 这里可以实现配置文件的实际写入逻辑
148 | // 或者使用用户配置存储(Registry、用户文件夹等)
149 | }
150 | catch (Exception ex)
151 | {
152 | _logger.LogError($"保存关闭选择失败: {ex.Message}");
153 | }
154 | }
155 |
156 | private void MinimizeToTray()
157 | {
158 | this.Hide();
159 | _trayService.ShowTray();
160 | _trayService.ShowNotification("Edge Monitor", "程序已最小化到托盘,双击托盘图标可恢复窗口");
161 | _logger.LogInformation("程序已最小化到托盘");
162 | }
163 |
164 | private void ReallyClose()
165 | {
166 | _isReallyClosing = true;
167 | _trayService.Dispose();
168 | _logger.LogInformation("程序正在退出");
169 | Application.Current.Shutdown();
170 | }
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/EdgeMonitor/Services/ConfigurationService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 | using Microsoft.Extensions.Logging;
3 | using System.Text.Json;
4 | using System.IO;
5 |
6 | namespace EdgeMonitor.Services
7 | {
8 | public class ConfigurationService : IConfigurationService
9 | {
10 | private readonly ILogger _logger;
11 | private readonly string _configFilePath;
12 | private Dictionary _configData = new();
13 |
14 | public ConfigurationService(ILogger logger)
15 | {
16 | _logger = logger;
17 | _configFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "appsettings.json");
18 | LoadConfiguration();
19 | }
20 |
21 | public T GetValue(string key)
22 | {
23 | try
24 | {
25 | var keys = key.Split(':');
26 | var current = _configData;
27 |
28 | for (int i = 0; i < keys.Length - 1; i++)
29 | {
30 | if (current.TryGetValue(keys[i], out var value) && value is Dictionary dict)
31 | {
32 | current = dict;
33 | }
34 | else
35 | {
36 | return default(T);
37 | }
38 | }
39 |
40 | if (current.TryGetValue(keys[^1], out var finalValue))
41 | {
42 | if (finalValue is JsonElement element)
43 | {
44 | return JsonSerializer.Deserialize(element.GetRawText());
45 | }
46 | return (T)Convert.ChangeType(finalValue, typeof(T));
47 | }
48 |
49 | return default(T);
50 | }
51 | catch (Exception ex)
52 | {
53 | _logger.LogError($"获取配置值失败 '{key}': {ex.Message}");
54 | return default(T);
55 | }
56 | }
57 |
58 | public void SetValue(string key, object value)
59 | {
60 | try
61 | {
62 | var keys = key.Split(':');
63 | var current = _configData;
64 |
65 | // 导航到正确的嵌套字典
66 | for (int i = 0; i < keys.Length - 1; i++)
67 | {
68 | if (!current.TryGetValue(keys[i], out var existingValue) || existingValue is not Dictionary)
69 | {
70 | current[keys[i]] = new Dictionary();
71 | }
72 | current = (Dictionary)current[keys[i]];
73 | }
74 |
75 | // 设置最终值
76 | current[keys[^1]] = value;
77 | _logger.LogInformation($"配置值已更新 '{key}': {value}");
78 | }
79 | catch (Exception ex)
80 | {
81 | _logger.LogError($"设置配置值失败 '{key}': {ex.Message}");
82 | }
83 | }
84 |
85 | public async Task SaveAsync()
86 | {
87 | try
88 | {
89 | var options = new JsonSerializerOptions
90 | {
91 | WriteIndented = true
92 | // 移除PropertyNamingPolicy,保持原始键名
93 | };
94 |
95 | var json = JsonSerializer.Serialize(_configData, options);
96 | await File.WriteAllTextAsync(_configFilePath, json);
97 | _logger.LogInformation("配置文件已保存");
98 | }
99 | catch (Exception ex)
100 | {
101 | _logger.LogError($"保存配置文件失败: {ex.Message}");
102 | }
103 | }
104 |
105 | public void Reload()
106 | {
107 | LoadConfiguration();
108 | }
109 |
110 | private void LoadConfiguration()
111 | {
112 | try
113 | {
114 | if (File.Exists(_configFilePath))
115 | {
116 | var json = File.ReadAllText(_configFilePath);
117 | var jsonDoc = JsonDocument.Parse(json);
118 | _configData = ConvertJsonElementToDictionary(jsonDoc.RootElement);
119 | }
120 | else
121 | {
122 | _configData = new Dictionary();
123 | }
124 | _logger.LogInformation("配置文件已加载");
125 | }
126 | catch (Exception ex)
127 | {
128 | _logger.LogError($"加载配置文件失败: {ex.Message}");
129 | _configData = new Dictionary();
130 | }
131 | }
132 |
133 | private Dictionary ConvertJsonElementToDictionary(JsonElement element)
134 | {
135 | var result = new Dictionary();
136 |
137 | foreach (var property in element.EnumerateObject())
138 | {
139 | result[property.Name] = ConvertJsonElementToObject(property.Value);
140 | }
141 |
142 | return result;
143 | }
144 |
145 | private object ConvertJsonElementToObject(JsonElement element)
146 | {
147 | switch (element.ValueKind)
148 | {
149 | case JsonValueKind.Object:
150 | return ConvertJsonElementToDictionary(element);
151 | case JsonValueKind.Array:
152 | return element.EnumerateArray().Select(ConvertJsonElementToObject).ToArray();
153 | case JsonValueKind.String:
154 | return element.GetString() ?? "";
155 | case JsonValueKind.Number:
156 | if (element.TryGetInt32(out int intValue))
157 | return intValue;
158 | if (element.TryGetDouble(out double doubleValue))
159 | return doubleValue;
160 | return element.GetDecimal();
161 | case JsonValueKind.True:
162 | return true;
163 | case JsonValueKind.False:
164 | return false;
165 | case JsonValueKind.Null:
166 | return null;
167 | default:
168 | return element.ToString();
169 | }
170 | }
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/EdgeMonitor/Services/LogService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.ObjectModel;
2 | using System.IO;
3 | using Microsoft.Extensions.Logging;
4 | using Microsoft.Extensions.Configuration;
5 |
6 | namespace EdgeMonitor.Services
7 | {
8 | public class LogService : ILogService
9 | {
10 | private readonly ILogger _logger;
11 | private readonly IConfiguration _configuration;
12 | private const int MAX_MEMORY_ENTRIES = 300;
13 | private readonly string _logDirectory;
14 | private readonly string _logFilePath;
15 | private readonly string _monitorFilePath;
16 | private readonly object _logLock = new object();
17 | private readonly object _monitorLock = new object();
18 |
19 | public ObservableCollection LogEntries { get; } = new ObservableCollection();
20 | public ObservableCollection MonitorEntries { get; } = new ObservableCollection();
21 |
22 | public LogService(ILogger logger, IConfiguration configuration)
23 | {
24 | _logger = logger;
25 | _configuration = configuration;
26 |
27 | // 创建日志目录
28 | _logDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "EdgeMonitor", "Logs");
29 | Directory.CreateDirectory(_logDirectory);
30 |
31 | // 设置日志文件路径
32 | var today = DateTime.Now.ToString("yyyy-MM-dd");
33 | _logFilePath = Path.Combine(_logDirectory, $"EdgeMonitor-{today}.log");
34 | _monitorFilePath = Path.Combine(_logDirectory, $"EdgeMonitor-Monitor-{today}.log");
35 |
36 | _logger.LogInformation($"日志服务已初始化,日志目录: {_logDirectory}");
37 | }
38 |
39 | public async Task AddLogEntryAsync(string message)
40 | {
41 | var timestampedMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}";
42 |
43 | // 异步写入文件
44 | await WriteToFileAsync(_logFilePath, timestampedMessage);
45 |
46 | // 在UI线程上更新内存集合
47 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
48 | {
49 | lock (_logLock)
50 | {
51 | LogEntries.Add(timestampedMessage);
52 |
53 | // 保持内存中只有最新的300条记录
54 | while (LogEntries.Count > MAX_MEMORY_ENTRIES)
55 | {
56 | LogEntries.RemoveAt(0);
57 | }
58 | }
59 | });
60 | }
61 |
62 | public async Task AddMonitorEntryAsync(string message)
63 | {
64 | var timestampedMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}";
65 |
66 | // 异步写入文件
67 | await WriteToFileAsync(_monitorFilePath, timestampedMessage);
68 |
69 | // 在UI线程上更新内存集合
70 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
71 | {
72 | lock (_monitorLock)
73 | {
74 | MonitorEntries.Add(timestampedMessage);
75 |
76 | // 保持内存中只有最新的300条记录
77 | while (MonitorEntries.Count > MAX_MEMORY_ENTRIES)
78 | {
79 | MonitorEntries.RemoveAt(0);
80 | }
81 | }
82 | });
83 | }
84 |
85 | public void ClearMemoryLogs()
86 | {
87 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
88 | {
89 | lock (_logLock)
90 | {
91 | LogEntries.Clear();
92 | }
93 | lock (_monitorLock)
94 | {
95 | MonitorEntries.Clear();
96 | }
97 | });
98 |
99 | _logger.LogInformation("内存日志已清除(文件日志保留)");
100 | }
101 |
102 | public string GetLogFilePath()
103 | {
104 | return _logDirectory;
105 | }
106 |
107 | private async Task WriteToFileAsync(string filePath, string message)
108 | {
109 | try
110 | {
111 | // 确保文件所在目录存在
112 | var directory = Path.GetDirectoryName(filePath);
113 | if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
114 | {
115 | Directory.CreateDirectory(directory);
116 | }
117 |
118 | // 异步追加写入文件
119 | using (var writer = new StreamWriter(filePath, append: true, encoding: System.Text.Encoding.UTF8))
120 | {
121 | await writer.WriteLineAsync(message);
122 | await writer.FlushAsync();
123 | }
124 | }
125 | catch (Exception ex)
126 | {
127 | _logger.LogError($"写入日志文件失败: {ex.Message}");
128 | }
129 | }
130 |
131 | ///
132 | /// 清理超过指定天数的日志文件
133 | ///
134 | public async Task CleanupOldLogFilesAsync()
135 | {
136 | try
137 | {
138 | var retentionDays = _configuration.GetValue("ApplicationSettings:LogRetentionDays", 30);
139 | var cutoffDate = DateTime.Now.AddDays(-retentionDays);
140 |
141 | await Task.Run(() =>
142 | {
143 | var logFiles = Directory.GetFiles(_logDirectory, "*.log");
144 | var deletedCount = 0;
145 |
146 | foreach (var file in logFiles)
147 | {
148 | var fileInfo = new FileInfo(file);
149 | if (fileInfo.CreationTime < cutoffDate)
150 | {
151 | try
152 | {
153 | File.Delete(file);
154 | deletedCount++;
155 | _logger.LogInformation($"已删除过期日志文件: {Path.GetFileName(file)}");
156 | }
157 | catch (Exception ex)
158 | {
159 | _logger.LogWarning($"删除日志文件失败 {Path.GetFileName(file)}: {ex.Message}");
160 | }
161 | }
162 | }
163 |
164 | if (deletedCount > 0)
165 | {
166 | _logger.LogInformation($"日志清理完成,共删除 {deletedCount} 个过期文件");
167 | }
168 | });
169 | }
170 | catch (Exception ex)
171 | {
172 | _logger.LogError($"清理日志文件时发生错误: {ex.Message}");
173 | }
174 | }
175 |
176 | ///
177 | /// 获取日志统计信息
178 | ///
179 | public async Task GetLogStatisticsAsync()
180 | {
181 | return await Task.Run(() =>
182 | {
183 | try
184 | {
185 | var logFiles = Directory.GetFiles(_logDirectory, "*.log");
186 | var totalSize = logFiles.Sum(f => new FileInfo(f).Length);
187 | var totalFiles = logFiles.Length;
188 |
189 | return new LogStatistics
190 | {
191 | TotalFiles = totalFiles,
192 | TotalSizeBytes = totalSize,
193 | TotalSizeMB = totalSize / (1024.0 * 1024.0),
194 | MemoryLogCount = LogEntries.Count,
195 | MemoryMonitorCount = MonitorEntries.Count,
196 | LogDirectory = _logDirectory
197 | };
198 | }
199 | catch (Exception ex)
200 | {
201 | _logger.LogError($"获取日志统计信息失败: {ex.Message}");
202 | return new LogStatistics
203 | {
204 | LogDirectory = _logDirectory,
205 | MemoryLogCount = LogEntries.Count,
206 | MemoryMonitorCount = MonitorEntries.Count
207 | };
208 | }
209 | });
210 | }
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/EdgeMonitor/Services/TrayService.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 | using Hardcodet.Wpf.TaskbarNotification;
3 | using Microsoft.Extensions.Logging;
4 | using System.IO;
5 | using System.Drawing;
6 | using Microsoft.Extensions.DependencyInjection;
7 |
8 | namespace EdgeMonitor.Services
9 | {
10 | public interface ITrayService
11 | {
12 | ///
13 | /// 初始化托盘图标
14 | ///
15 | void InitializeTray();
16 |
17 | ///
18 | /// 显示托盘图标
19 | ///
20 | void ShowTray();
21 |
22 | ///
23 | /// 隐藏托盘图标
24 | ///
25 | void HideTray();
26 |
27 | ///
28 | /// 显示托盘通知
29 | ///
30 | void ShowNotification(string title, string message);
31 |
32 | ///
33 | /// 更新托盘状态
34 | ///
35 | /// 是否正在监控
36 | void UpdateTrayStatus(bool isMonitoring);
37 |
38 | ///
39 | /// 释放托盘资源
40 | ///
41 | void Dispose();
42 | }
43 |
44 | public class TrayService : ITrayService, IDisposable
45 | {
46 | private readonly ILogger _logger;
47 | private readonly IServiceProvider _serviceProvider;
48 | private TaskbarIcon? _taskbarIcon;
49 | private bool _disposed = false;
50 |
51 | public TrayService(ILogger logger, IServiceProvider serviceProvider)
52 | {
53 | _logger = logger;
54 | _serviceProvider = serviceProvider;
55 | }
56 |
57 | public void InitializeTray()
58 | {
59 | try
60 | {
61 | _taskbarIcon = new TaskbarIcon();
62 |
63 | // 设置托盘图标 - 使用应用程序图标
64 | try
65 | {
66 | // 方法1:尝试从应用程序主程序集获取图标
67 | var assembly = System.Reflection.Assembly.GetExecutingAssembly();
68 | var iconStream = assembly.GetManifestResourceStream("EdgeMonitor.icon.ico");
69 |
70 | if (iconStream != null)
71 | {
72 | _taskbarIcon.Icon = new System.Drawing.Icon(iconStream);
73 | _logger.LogInformation("托盘图标从嵌入资源加载成功");
74 | }
75 | else
76 | {
77 | // 方法2:尝试从应用程序当前目录加载
78 | var iconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "icon.ico");
79 | if (File.Exists(iconPath))
80 | {
81 | _taskbarIcon.Icon = new System.Drawing.Icon(iconPath);
82 | _logger.LogInformation($"托盘图标从文件路径加载成功: {iconPath}");
83 | }
84 | else
85 | {
86 | // 方法3:尝试使用应用程序的图标
87 | var appIcon = System.Drawing.Icon.ExtractAssociatedIcon(System.Reflection.Assembly.GetExecutingAssembly().Location);
88 | if (appIcon != null)
89 | {
90 | _taskbarIcon.Icon = appIcon;
91 | _logger.LogInformation("托盘图标从应用程序图标加载成功");
92 | }
93 | else
94 | {
95 | // 方法4:使用系统默认应用程序图标
96 | _taskbarIcon.Icon = SystemIcons.Application;
97 | _logger.LogWarning("使用系统默认图标,未找到自定义图标文件");
98 | }
99 | }
100 | }
101 | }
102 | catch (Exception ex)
103 | {
104 | _logger.LogError($"加载托盘图标失败: {ex.Message},使用系统默认图标");
105 | try
106 | {
107 | _taskbarIcon.Icon = SystemIcons.Application;
108 | }
109 | catch
110 | {
111 | _logger.LogError("连系统默认图标都无法加载");
112 | }
113 | }
114 |
115 | _taskbarIcon.ToolTipText = "Edge Monitor - 就绪";
116 |
117 | // 双击托盘图标显示主窗口
118 | _taskbarIcon.TrayMouseDoubleClick += (s, e) =>
119 | {
120 | ShowMainWindow();
121 | };
122 |
123 | // 单击托盘图标也显示主窗口
124 | _taskbarIcon.TrayLeftMouseUp += (s, e) =>
125 | {
126 | ShowMainWindow();
127 | };
128 |
129 | // 创建右键菜单
130 | var contextMenu = new System.Windows.Controls.ContextMenu();
131 |
132 | var showMenuItem = new System.Windows.Controls.MenuItem
133 | {
134 | Header = "显示主窗口"
135 | };
136 | showMenuItem.Click += (s, e) => ShowMainWindow();
137 |
138 | var exitMenuItem = new System.Windows.Controls.MenuItem
139 | {
140 | Header = "退出程序"
141 | };
142 | exitMenuItem.Click += (s, e) => ExitApplication();
143 |
144 | contextMenu.Items.Add(showMenuItem);
145 | contextMenu.Items.Add(new System.Windows.Controls.Separator());
146 | contextMenu.Items.Add(exitMenuItem);
147 |
148 | _taskbarIcon.ContextMenu = contextMenu;
149 |
150 | _logger.LogInformation("托盘图标已初始化");
151 | }
152 | catch (Exception ex)
153 | {
154 | _logger.LogError($"初始化托盘图标失败: {ex.Message}");
155 | }
156 | }
157 |
158 | public void ShowTray()
159 | {
160 | if (_taskbarIcon != null)
161 | {
162 | _taskbarIcon.Visibility = Visibility.Visible;
163 | _logger.LogInformation("托盘图标已显示");
164 | }
165 | }
166 |
167 | public void HideTray()
168 | {
169 | if (_taskbarIcon != null)
170 | {
171 | _taskbarIcon.Visibility = Visibility.Hidden;
172 | _logger.LogInformation("托盘图标已隐藏");
173 | }
174 | }
175 |
176 | public void ShowNotification(string title, string message)
177 | {
178 | if (_taskbarIcon != null)
179 | {
180 | _taskbarIcon.ShowBalloonTip(title, message, BalloonIcon.Info);
181 | _logger.LogInformation($"显示托盘通知: {title} - {message}");
182 | }
183 | }
184 |
185 | public void UpdateTrayStatus(bool isMonitoring)
186 | {
187 | if (_taskbarIcon != null)
188 | {
189 | var statusText = isMonitoring ? "Edge Monitor - 监控中" : "Edge Monitor - 就绪";
190 | _taskbarIcon.ToolTipText = statusText;
191 | _logger.LogInformation($"托盘状态已更新: {statusText}");
192 | }
193 | }
194 |
195 | private void ShowMainWindow()
196 | {
197 | try
198 | {
199 | var mainWindow = Application.Current?.MainWindow;
200 |
201 | // 如果主窗口不存在,创建一个新的
202 | if (mainWindow == null && Application.Current != null)
203 | {
204 | _logger.LogInformation("主窗口不存在,正在创建新的主窗口");
205 | mainWindow = _serviceProvider.GetRequiredService();
206 | Application.Current.MainWindow = mainWindow;
207 | }
208 |
209 | if (mainWindow != null)
210 | {
211 | mainWindow.Show();
212 | mainWindow.WindowState = WindowState.Normal;
213 | mainWindow.Activate();
214 | mainWindow.Topmost = true; // 确保窗口置顶
215 | mainWindow.Topmost = false; // 然后取消置顶,让它正常显示
216 | _logger.LogInformation("主窗口已从托盘恢复");
217 | }
218 | else
219 | {
220 | _logger.LogError("无法创建或获取主窗口");
221 | }
222 | }
223 | catch (Exception ex)
224 | {
225 | _logger.LogError(ex, "显示主窗口时发生错误");
226 | }
227 | }
228 |
229 | private void ExitApplication()
230 | {
231 | _logger.LogInformation("从托盘退出应用程序");
232 | Application.Current?.Shutdown();
233 | }
234 |
235 | public void Dispose()
236 | {
237 | if (!_disposed)
238 | {
239 | _taskbarIcon?.Dispose();
240 | _disposed = true;
241 | _logger.LogInformation("托盘服务已释放");
242 | }
243 | }
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/EdgeMonitor/Styles/AppStyles.xaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
26 |
27 |
28 |
35 |
36 |
37 |
45 |
46 |
47 |
80 |
81 |
82 |
118 |
119 |
120 |
131 |
132 |
133 |
139 |
140 |
141 |
146 |
147 |
176 |
177 |
178 |
--------------------------------------------------------------------------------
/EdgeMonitor/Views/AboutWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Net.Http;
3 | using System.Reflection;
4 | using System.Text.Json;
5 | using System.Windows;
6 | using System.Windows.Navigation;
7 | using EdgeMonitor.Services;
8 |
9 | namespace EdgeMonitor.Views
10 | {
11 | public partial class AboutWindow : Window
12 | {
13 | private static readonly HttpClient _httpClient = new HttpClient();
14 |
15 | public AboutWindow()
16 | {
17 | InitializeComponent();
18 | LoadVersionInfo();
19 | }
20 |
21 | private void LoadVersionInfo()
22 | {
23 | // 使用版本辅助类自动获取版本号
24 | VersionTextBlock.Text = VersionHelper.GetVersionString();
25 | }
26 |
27 | private void CloseButton_Click(object sender, RoutedEventArgs e)
28 | {
29 | this.Close();
30 | }
31 |
32 | private async void CheckUpdateButton_Click(object sender, RoutedEventArgs e)
33 | {
34 | try
35 | {
36 | // 禁用按钮防止重复点击
37 | var button = sender as System.Windows.Controls.Button;
38 | if (button != null)
39 | {
40 | button.IsEnabled = false;
41 | button.Content = "检查中...";
42 | }
43 |
44 | // 配置HttpClient请求头
45 | _httpClient.DefaultRequestHeaders.Clear();
46 | _httpClient.DefaultRequestHeaders.Add("User-Agent", "EdgeMonitor/1.3.0");
47 |
48 | // 从GitHub API获取最新release信息
49 | string apiUrl = "https://api.github.com/repos/PrelinaMontelli/Edge-Monitor/releases/latest";
50 |
51 | var response = await _httpClient.GetAsync(apiUrl);
52 |
53 | if (button != null)
54 | {
55 | button.IsEnabled = true;
56 | button.Content = "检查更新";
57 | }
58 |
59 | if (response.IsSuccessStatusCode)
60 | {
61 | var responseContent = await response.Content.ReadAsStringAsync();
62 | var releaseInfo = JsonDocument.Parse(responseContent);
63 |
64 | // 优先使用 name 字段,如果不可用则使用 tag_name
65 | var latestVersionFromName = releaseInfo.RootElement.GetProperty("name").GetString();
66 | var latestVersionFromTag = releaseInfo.RootElement.GetProperty("tag_name").GetString();
67 | var currentVersion = VersionHelper.GetVersionString();
68 |
69 | // 尝试从 name 字段提取版本号(如 "EdgeMonitor-v1.3" -> "v1.3")
70 | string? latestVersion = ExtractVersionFromReleaseName(latestVersionFromName) ?? latestVersionFromTag;
71 |
72 | // 比较版本号
73 | if (!string.IsNullOrEmpty(latestVersion) && IsNewerVersion(latestVersion, currentVersion))
74 | {
75 | var result = MessageBox.Show(
76 | $"发现新版本: {latestVersion}\n当前版本: {currentVersion}\n\n是否前往下载页面?",
77 | "发现更新",
78 | MessageBoxButton.YesNo,
79 | MessageBoxImage.Information);
80 |
81 | if (result == MessageBoxResult.Yes)
82 | {
83 | // 打开GitHub release页面
84 | Process.Start(new ProcessStartInfo
85 | {
86 | FileName = "https://github.com/PrelinaMontelli/Edge-Monitor/releases/tag/Release",
87 | UseShellExecute = true
88 | });
89 | }
90 | }
91 | else
92 | {
93 | MessageBox.Show(
94 | $"当前已是最新版本: {currentVersion}\n(最新发布: {latestVersionFromName ?? latestVersionFromTag})",
95 | "检查更新",
96 | MessageBoxButton.OK,
97 | MessageBoxImage.Information);
98 | }
99 | }
100 | else
101 | {
102 | // API请求失败,提供备用方案
103 | var result = MessageBox.Show(
104 | $"无法自动检查更新(HTTP {(int)response.StatusCode})\n\n是否直接打开GitHub发布页面查看最新版本?",
105 | "检查更新",
106 | MessageBoxButton.YesNo,
107 | MessageBoxImage.Warning);
108 |
109 | if (result == MessageBoxResult.Yes)
110 | {
111 | Process.Start(new ProcessStartInfo
112 | {
113 | FileName = "https://github.com/PrelinaMontelli/Edge-Monitor/releases",
114 | UseShellExecute = true
115 | });
116 | }
117 | }
118 | }
119 | catch (Exception ex)
120 | {
121 | if (sender is System.Windows.Controls.Button btn)
122 | {
123 | btn.IsEnabled = true;
124 | btn.Content = "检查更新";
125 | }
126 |
127 | // 提供备用方案
128 | var result = MessageBox.Show(
129 | $"检查更新失败: {ex.Message}\n\n是否直接打开GitHub发布页面查看最新版本?",
130 | "错误",
131 | MessageBoxButton.YesNo,
132 | MessageBoxImage.Error);
133 |
134 | if (result == MessageBoxResult.Yes)
135 | {
136 | try
137 | {
138 | Process.Start(new ProcessStartInfo
139 | {
140 | FileName = "https://github.com/PrelinaMontelli/Edge-Monitor/releases",
141 | UseShellExecute = true
142 | });
143 | }
144 | catch (Exception browserEx)
145 | {
146 | MessageBox.Show(
147 | $"无法打开浏览器: {browserEx.Message}\n\n请手动访问: https://github.com/PrelinaMontelli/Edge-Monitor/releases",
148 | "错误",
149 | MessageBoxButton.OK,
150 | MessageBoxImage.Error);
151 | }
152 | }
153 | }
154 | }
155 |
156 | private string? ExtractVersionFromReleaseName(string? releaseName)
157 | {
158 | if (string.IsNullOrEmpty(releaseName)) return null;
159 |
160 | try
161 | {
162 | // 尝试匹配各种可能的版本号格式
163 | var patterns = new[]
164 | {
165 | @"EdgeMonitor-v(\d+\.\d+(?:\.\d+)?)", // EdgeMonitor-v1.3 或 EdgeMonitor-v1.3.0
166 | @"EM-v(\d+\.\d+(?:\.\d+)?)", // EM-v1.4 或 EM-v1.4.0
167 | @"Edge\s*Monitor-v(\d+\.\d+(?:\.\d+)?)", // Edge Monitor-v1.3 (带空格)
168 | @"[A-Za-z\s]*-v(\d+\.\d+(?:\.\d+)?)", // 任意前缀-v1.3 (通用匹配)
169 | @"v(\d+\.\d+(?:\.\d+)?)", // v1.3 或 v1.3.0
170 | @"(\d+\.\d+(?:\.\d+)?)" // 1.3 或 1.3.0 (纯数字)
171 | };
172 |
173 | foreach (var pattern in patterns)
174 | {
175 | var match = System.Text.RegularExpressions.Regex.Match(releaseName, pattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
176 | if (match.Success)
177 | {
178 | return "v" + match.Groups[1].Value;
179 | }
180 | }
181 | }
182 | catch
183 | {
184 | // 解析失败时返回null
185 | }
186 |
187 | return null;
188 | }
189 |
190 | private bool IsNewerVersion(string? latestVersion, string currentVersion)
191 | {
192 | if (string.IsNullOrEmpty(latestVersion)) return false;
193 |
194 | try
195 | {
196 | // 移除版本号中的 'v' 前缀
197 | var latest = latestVersion.TrimStart('v');
198 | var current = currentVersion.TrimStart('v');
199 |
200 | var latestParts = latest.Split('.').Select(int.Parse).ToArray();
201 | var currentParts = current.Split('.').Select(int.Parse).ToArray();
202 |
203 | // 确保两个版本号都有相同的部分数量
204 | var maxLength = Math.Max(latestParts.Length, currentParts.Length);
205 | var latestExpanded = latestParts.Concat(Enumerable.Repeat(0, maxLength - latestParts.Length)).ToArray();
206 | var currentExpanded = currentParts.Concat(Enumerable.Repeat(0, maxLength - currentParts.Length)).ToArray();
207 |
208 | for (int i = 0; i < maxLength; i++)
209 | {
210 | if (latestExpanded[i] > currentExpanded[i])
211 | return true;
212 | if (latestExpanded[i] < currentExpanded[i])
213 | return false;
214 | }
215 |
216 | return false; // 版本号相同
217 | }
218 | catch
219 | {
220 | return false; // 解析失败时假设没有新版本
221 | }
222 | }
223 |
224 | private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
225 | {
226 | try
227 | {
228 | Process.Start(new ProcessStartInfo
229 | {
230 | FileName = e.Uri.AbsoluteUri,
231 | UseShellExecute = true
232 | });
233 | e.Handled = true;
234 | }
235 | catch (Exception ex)
236 | {
237 | MessageBox.Show(
238 | $"无法打开链接: {ex.Message}\n\n链接地址: {e.Uri.AbsoluteUri}",
239 | "错误",
240 | MessageBoxButton.OK,
241 | MessageBoxImage.Error);
242 | }
243 | }
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/EdgeMonitor/Services/EdgeMonitorService.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Runtime.InteropServices;
3 | using Microsoft.Extensions.Logging;
4 |
5 | namespace EdgeMonitor.Services
6 | {
7 | public interface IEdgeMonitorService
8 | {
9 | Task GetEdgeProcessesAsync();
10 | Task HasVisibleWindowsAsync();
11 | Task KillAllEdgeProcessesAsync();
12 | bool IsEdgeRunningInBackground(EdgeProcessInfo[] processes);
13 | bool HasAbnormalResourceUsage(EdgeProcessInfo[] processes, double cpuThreshold = 30.0, long memoryThreshold = 2048);
14 | }
15 |
16 | public class EdgeProcessInfo
17 | {
18 | public int ProcessId { get; set; }
19 | public string ProcessName { get; set; } = "";
20 | public double CpuUsage { get; set; }
21 | public long MemoryUsageMB { get; set; }
22 | public int WindowCount { get; set; }
23 | public string CommandLine { get; set; } = "";
24 | public DateTime StartTime { get; set; }
25 | }
26 |
27 | public class EdgeMonitorService : IEdgeMonitorService
28 | {
29 | private readonly ILogger _logger;
30 | private readonly Dictionary _previousCpuTimes = new();
31 | private readonly Dictionary _previousSampleTimes = new();
32 |
33 | // Windows API 声明
34 | [DllImport("user32.dll")]
35 | private static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam);
36 |
37 | [DllImport("user32.dll")]
38 | private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
39 |
40 | [DllImport("user32.dll")]
41 | private static extern bool IsWindowVisible(IntPtr hWnd);
42 |
43 | [DllImport("user32.dll")]
44 | private static extern int GetWindowTextLength(IntPtr hWnd);
45 |
46 | [DllImport("user32.dll")]
47 | private static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count);
48 |
49 | private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
50 |
51 | public EdgeMonitorService(ILogger logger)
52 | {
53 | _logger = logger;
54 | }
55 |
56 | public async Task GetEdgeProcessesAsync()
57 | {
58 | return await Task.Run(() =>
59 | {
60 | var startTime = DateTime.Now;
61 | var edgeProcesses = new List();
62 |
63 | // 修改为只搜索现代Edge进程以提高性能
64 | _logger.LogInformation("开始搜索Edge进程");
65 |
66 | var processes = Process.GetProcessesByName("msedge");
67 | _logger.LogInformation($"找到 {processes.Length} 个 msedge 进程");
68 |
69 | // 获取所有Edge进程的窗口信息(一次性处理,避免重复枚举)
70 | var processWindowCounts = GetAllProcessWindowCounts(processes.Select(p => p.Id).ToArray());
71 |
72 | // 并行处理进程信息以提高性能
73 | var processInfos = processes.AsParallel().WithDegreeOfParallelism(Environment.ProcessorCount)
74 | .Select(process =>
75 | {
76 | try
77 | {
78 | var processInfo = new EdgeProcessInfo
79 | {
80 | ProcessId = process.Id,
81 | ProcessName = process.ProcessName,
82 | MemoryUsageMB = process.WorkingSet64 / (1024 * 1024),
83 | StartTime = process.StartTime,
84 | WindowCount = processWindowCounts.GetValueOrDefault(process.Id, 0),
85 | CommandLine = "已省略" // 移除耗时的命令行获取
86 | };
87 |
88 | // 计算CPU使用率
89 | processInfo.CpuUsage = CalculateCpuUsage(process);
90 |
91 | _logger.LogDebug($"处理进程: PID={processInfo.ProcessId}, 内存={processInfo.MemoryUsageMB}MB, 窗口数={processInfo.WindowCount}");
92 | return processInfo;
93 | }
94 | catch (Exception ex)
95 | {
96 | _logger.LogWarning($"无法获取进程 {process.ProcessName} (PID: {process.Id}) 的信息: {ex.Message}");
97 | return null;
98 | }
99 | finally
100 | {
101 | process.Dispose();
102 | }
103 | })
104 | .Where(info => info != null)
105 | .ToList();
106 |
107 | edgeProcesses.AddRange(processInfos!);
108 |
109 | var endTime = DateTime.Now;
110 | var elapsed = (endTime - startTime).TotalMilliseconds;
111 | _logger.LogInformation($"Edge进程扫描完成,耗时: {elapsed:F0}ms,找到 {edgeProcesses.Count} 个进程");
112 |
113 | return edgeProcesses.ToArray();
114 | });
115 | }
116 |
117 | public async Task HasVisibleWindowsAsync()
118 | {
119 | return await Task.Run(() =>
120 | {
121 | var edgeProcesses = Process.GetProcessesByName("msedge");
122 | var hasVisibleWindows = false;
123 |
124 | var processIds = edgeProcesses.Select(p => (uint)p.Id).ToHashSet();
125 |
126 | EnumWindows((hWnd, lParam) =>
127 | {
128 | if (IsWindowVisible(hWnd))
129 | {
130 | GetWindowThreadProcessId(hWnd, out uint processId);
131 | if (processIds.Contains(processId))
132 | {
133 | // 检查窗口是否有标题(通常表示是真正的用户界面窗口)
134 | int length = GetWindowTextLength(hWnd);
135 | if (length > 0)
136 | {
137 | var title = new System.Text.StringBuilder(length + 1);
138 | GetWindowText(hWnd, title, title.Capacity);
139 |
140 | // 过滤掉一些系统窗口或隐藏窗口
141 | var titleText = title.ToString();
142 | if (!string.IsNullOrWhiteSpace(titleText) &&
143 | !titleText.Contains("Microsoft Edge WebView2") &&
144 | !titleText.Contains("DevTools"))
145 | {
146 | hasVisibleWindows = true;
147 | return false; // 停止枚举
148 | }
149 | }
150 | }
151 | }
152 | return true; // 继续枚举
153 | }, IntPtr.Zero);
154 |
155 | foreach (var process in edgeProcesses)
156 | {
157 | process.Dispose();
158 | }
159 |
160 | return hasVisibleWindows;
161 | });
162 | }
163 |
164 | public async Task KillAllEdgeProcessesAsync()
165 | {
166 | await Task.Run(() =>
167 | {
168 | // 只终止现代Edge进程,提高性能
169 | var processes = Process.GetProcessesByName("msedge");
170 | int killedCount = 0;
171 |
172 | foreach (var process in processes)
173 | {
174 | try
175 | {
176 | _logger.LogInformation($"正在终止 Edge 进程: {process.ProcessName} (PID: {process.Id})");
177 | process.Kill();
178 | process.WaitForExit(3000); // 减少等待时间到3秒
179 | killedCount++;
180 | }
181 | catch (Exception ex)
182 | {
183 | _logger.LogError($"无法终止进程 {process.ProcessName} (PID: {process.Id}): {ex.Message}");
184 | }
185 | finally
186 | {
187 | process.Dispose();
188 | }
189 | }
190 |
191 | _logger.LogInformation($"已终止 {killedCount} 个 Edge 进程");
192 | });
193 | }
194 |
195 | public bool IsEdgeRunningInBackground(EdgeProcessInfo[] processes)
196 | {
197 | return processes.Any() && processes.All(p => p.WindowCount == 0);
198 | }
199 |
200 | public bool HasAbnormalResourceUsage(EdgeProcessInfo[] processes, double cpuThreshold = 30.0, long memoryThreshold = 2048)
201 | {
202 | if (!processes.Any()) return false;
203 |
204 | var totalCpu = processes.Sum(p => p.CpuUsage);
205 | var totalMemory = processes.Sum(p => p.MemoryUsageMB);
206 |
207 | return totalCpu > cpuThreshold || totalMemory > memoryThreshold;
208 | }
209 |
210 | private Dictionary GetAllProcessWindowCounts(int[] processIds)
211 | {
212 | var windowCounts = new Dictionary();
213 | var processIdSet = processIds.ToHashSet();
214 |
215 | // 初始化所有进程的窗口计数为0
216 | foreach (var pid in processIds)
217 | {
218 | windowCounts[pid] = 0;
219 | }
220 |
221 | // 一次性枚举所有窗口,避免重复枚举
222 | EnumWindows((hWnd, lParam) =>
223 | {
224 | if (IsWindowVisible(hWnd))
225 | {
226 | GetWindowThreadProcessId(hWnd, out uint processId);
227 | if (processIdSet.Contains((int)processId))
228 | {
229 | int length = GetWindowTextLength(hWnd);
230 | if (length > 0)
231 | {
232 | windowCounts[(int)processId]++;
233 | }
234 | }
235 | }
236 | return true;
237 | }, IntPtr.Zero);
238 |
239 | return windowCounts;
240 | }
241 |
242 | private int GetWindowCountForProcess(int processId)
243 | {
244 | int windowCount = 0;
245 |
246 | EnumWindows((hWnd, lParam) =>
247 | {
248 | GetWindowThreadProcessId(hWnd, out uint winProcessId);
249 | if (winProcessId == processId && IsWindowVisible(hWnd))
250 | {
251 | int length = GetWindowTextLength(hWnd);
252 | if (length > 0)
253 | {
254 | windowCount++;
255 | }
256 | }
257 | return true;
258 | }, IntPtr.Zero);
259 |
260 | return windowCount;
261 | }
262 |
263 | private double CalculateCpuUsage(Process process)
264 | {
265 | try
266 | {
267 | var currentTime = DateTime.UtcNow;
268 | var currentCpuTime = process.TotalProcessorTime.TotalMilliseconds;
269 |
270 | if (_previousCpuTimes.ContainsKey(process.Id) && _previousSampleTimes.ContainsKey(process.Id))
271 | {
272 | var previousCpuTime = _previousCpuTimes[process.Id];
273 | var previousSampleTime = _previousSampleTimes[process.Id];
274 |
275 | var cpuTimeDelta = currentCpuTime - previousCpuTime;
276 | var realTimeDelta = (currentTime - previousSampleTime).TotalMilliseconds;
277 |
278 | if (realTimeDelta > 0)
279 | {
280 | var cpuUsage = (cpuTimeDelta / realTimeDelta) * 100.0 / Environment.ProcessorCount;
281 |
282 | _previousCpuTimes[process.Id] = currentCpuTime;
283 | _previousSampleTimes[process.Id] = currentTime;
284 |
285 | return Math.Max(0, Math.Min(100, cpuUsage));
286 | }
287 | }
288 |
289 | // 第一次检测时,记录初始值
290 | _previousCpuTimes[process.Id] = currentCpuTime;
291 | _previousSampleTimes[process.Id] = currentTime;
292 |
293 | // 快速估算:第一次检测时返回0,避免耗时的进程年龄计算
294 | return 0.0;
295 | }
296 | catch (Exception ex)
297 | {
298 | _logger.LogWarning($"计算进程 {process.Id} CPU使用率失败: {ex.Message}");
299 | return 0.0;
300 | }
301 | }
302 | }
303 | }
304 |
--------------------------------------------------------------------------------
/EdgeMonitor/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
51 |
52 |
53 |
54 |
57 |
58 |
63 |
64 |
69 |
70 |
71 |
72 |
73 |
74 |
78 |
79 |
83 |
84 |
88 |
89 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
115 |
116 |
117 |
118 |
123 |
124 |
125 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
140 |
145 |
146 |
147 |
150 |
151 |
152 |
153 |
156 |
161 |
162 |
166 |
171 |
172 |
175 |
180 |
181 |
182 |
183 |
184 |
185 |
189 |
193 |
197 |
198 |
199 |
204 |
205 |
206 |
207 |
208 |
212 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
--------------------------------------------------------------------------------
/EdgeMonitor/Services/StartupService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 | using Microsoft.Win32;
3 | using Microsoft.Win32.TaskScheduler;
4 | using System.Diagnostics;
5 | using System.IO;
6 | using System.Security.Principal;
7 | using System.Threading.Tasks;
8 |
9 | namespace EdgeMonitor.Services
10 | {
11 | ///
12 | /// Windows系统启动管理服务实现
13 | ///
14 | public class StartupService : IStartupService
15 | {
16 | private readonly ILogger _logger;
17 | private const string TASK_NAME = "EdgeMonitor";
18 | private const string TRAY_TASK_NAME = "EdgeMonitorTrayMonitor";
19 | private const string REGISTRY_KEY = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
20 | private const string APP_NAME = "EdgeMonitor";
21 | private const string TRAY_APP_NAME = "EdgeMonitorTrayMonitor";
22 |
23 | private static string ExecutablePath => Process.GetCurrentProcess().MainModule?.FileName ?? "";
24 |
25 | public StartupService(ILogger logger)
26 | {
27 | _logger = logger;
28 | }
29 |
30 | public bool IsStartupEnabled()
31 | {
32 | try
33 | {
34 | // 首先检查任务计划程序
35 | if (IsScheduledTask())
36 | return true;
37 |
38 | // 然后检查注册表启动项
39 | using var key = Registry.CurrentUser.OpenSubKey(REGISTRY_KEY, false);
40 | return key?.GetValue(APP_NAME) != null;
41 | }
42 | catch (Exception ex)
43 | {
44 | _logger.LogError(ex, "检查开机自启动状态时发生错误");
45 | return false;
46 | }
47 | }
48 |
49 | public bool IsAdminStartupEnabled()
50 | {
51 | return IsScheduledTask();
52 | }
53 |
54 | public bool IsTrayMonitorStartupEnabled()
55 | {
56 | try
57 | {
58 | // 检查托盘监测的任务计划程序
59 | if (IsTrayMonitorScheduledTask())
60 | return true;
61 |
62 | // 检查托盘监测的注册表启动项
63 | using var key = Registry.CurrentUser.OpenSubKey(REGISTRY_KEY, false);
64 | return key?.GetValue(TRAY_APP_NAME) != null;
65 | }
66 | catch (Exception ex)
67 | {
68 | _logger.LogError(ex, "检查托盘监测开机自启动状态时发生错误");
69 | return false;
70 | }
71 | }
72 |
73 | private bool IsScheduledTask()
74 | {
75 | try
76 | {
77 | using var taskService = new TaskService();
78 | return taskService.RootFolder.AllTasks.Any(t => t.Name == TASK_NAME);
79 | }
80 | catch (Exception ex)
81 | {
82 | _logger.LogError(ex, "检查任务计划程序启动状态时发生错误");
83 | return false;
84 | }
85 | }
86 |
87 | private bool IsTrayMonitorScheduledTask()
88 | {
89 | try
90 | {
91 | using var taskService = new TaskService();
92 | return taskService.RootFolder.AllTasks.Any(t => t.Name == TRAY_TASK_NAME);
93 | }
94 | catch (Exception ex)
95 | {
96 | _logger.LogError(ex, "检查托盘监测任务计划程序启动状态时发生错误");
97 | return false;
98 | }
99 | }
100 |
101 | public async Task EnableStartupAsync(bool runAsAdmin = false, bool hideToTray = false)
102 | {
103 | try
104 | {
105 | // 先清除现有的启动配置
106 | await DisableStartupAsync();
107 |
108 | if (runAsAdmin)
109 | {
110 | return CreateScheduledTask(hideToTray);
111 | }
112 | else
113 | {
114 | return CreateRegistryStartup(hideToTray);
115 | }
116 | }
117 | catch (Exception ex)
118 | {
119 | _logger.LogError(ex, "启用开机自启动时发生错误");
120 | return false;
121 | }
122 | }
123 |
124 | public async Task EnableTrayMonitorStartupAsync()
125 | {
126 | try
127 | {
128 | // 先清除现有的启动配置
129 | await DisableStartupAsync();
130 |
131 | // 创建带有托盘监测参数的启动项(使用管理员权限以确保功能正常)
132 | return CreateTrayMonitorScheduledTask();
133 | }
134 | catch (Exception ex)
135 | {
136 | _logger.LogError(ex, "启用托盘监测开机自启动时发生错误");
137 | return false;
138 | }
139 | }
140 |
141 | public Task DisableStartupAsync()
142 | {
143 | var success = true;
144 |
145 | // 删除任务计划程序任务
146 | try
147 | {
148 | if (IsScheduledTask())
149 | {
150 | RemoveScheduledTask();
151 | _logger.LogInformation("已删除任务计划程序启动项");
152 | }
153 | }
154 | catch (Exception ex)
155 | {
156 | _logger.LogError(ex, "删除任务计划程序启动项时发生错误");
157 | success = false;
158 | }
159 |
160 | // 删除托盘监测任务计划程序任务
161 | try
162 | {
163 | if (IsTrayMonitorScheduledTask())
164 | {
165 | RemoveTrayMonitorScheduledTask();
166 | _logger.LogInformation("已删除托盘监测任务计划程序启动项");
167 | }
168 | }
169 | catch (Exception ex)
170 | {
171 | _logger.LogError(ex, "删除托盘监测任务计划程序启动项时发生错误");
172 | success = false;
173 | }
174 |
175 | // 删除注册表启动项
176 | try
177 | {
178 | using var key = Registry.CurrentUser.OpenSubKey(REGISTRY_KEY, true);
179 | if (key?.GetValue(APP_NAME) != null)
180 | {
181 | key.DeleteValue(APP_NAME, false);
182 | _logger.LogInformation("已删除注册表启动项");
183 | }
184 | if (key?.GetValue(TRAY_APP_NAME) != null)
185 | {
186 | key.DeleteValue(TRAY_APP_NAME, false);
187 | _logger.LogInformation("已删除托盘监测注册表启动项");
188 | }
189 | }
190 | catch (Exception ex)
191 | {
192 | _logger.LogError(ex, "删除注册表启动项时发生错误");
193 | success = false;
194 | }
195 |
196 | return System.Threading.Tasks.Task.FromResult(success);
197 | }
198 |
199 | private bool CreateRegistryStartup(bool hideToTray = false)
200 | {
201 | try
202 | {
203 | using var key = Registry.CurrentUser.OpenSubKey(REGISTRY_KEY, true);
204 | if (key != null)
205 | {
206 | var arguments = hideToTray ? " --startup-hide-tray" : "";
207 | key.SetValue(APP_NAME, $"\"{ExecutablePath}\"{arguments}");
208 | _logger.LogInformation($"Registry startup entry created: {ExecutablePath}{arguments}");
209 | return true;
210 | }
211 | else
212 | {
213 | _logger.LogError("Unable to open registry startup key");
214 | return false;
215 | }
216 | }
217 | catch (Exception ex)
218 | {
219 | _logger.LogError(ex, "Failed to create registry startup entry");
220 | return false;
221 | }
222 | }
223 |
224 | private bool CreateScheduledTask(bool hideToTray = false)
225 | {
226 | try
227 | {
228 | if (string.IsNullOrEmpty(ExecutablePath))
229 | {
230 | _logger.LogError("Unable to get application executable path");
231 | return false;
232 | }
233 |
234 | using var taskDefinition = TaskService.Instance.NewTask();
235 |
236 | taskDefinition.RegistrationInfo.Description = "Edge Monitor Auto Startup";
237 |
238 | // Trigger on user logon
239 | var logonTrigger = new LogonTrigger
240 | {
241 | UserId = WindowsIdentity.GetCurrent().Name,
242 | Delay = TimeSpan.FromSeconds(3)
243 | };
244 | taskDefinition.Triggers.Add(logonTrigger);
245 |
246 | // Execute action
247 | var arguments = hideToTray ? "--startup-hide-tray" : "";
248 | taskDefinition.Actions.Add(ExecutablePath, arguments);
249 |
250 | // Run with highest privileges (admin rights)
251 | taskDefinition.Principal.RunLevel = TaskRunLevel.Highest;
252 | taskDefinition.Principal.LogonType = TaskLogonType.InteractiveToken;
253 |
254 | // Set other options
255 | taskDefinition.Settings.StopIfGoingOnBatteries = false;
256 | taskDefinition.Settings.DisallowStartIfOnBatteries = false;
257 | taskDefinition.Settings.ExecutionTimeLimit = TimeSpan.Zero;
258 | taskDefinition.Settings.AllowDemandStart = true;
259 | taskDefinition.Settings.StartWhenAvailable = true;
260 |
261 | // Register task
262 | TaskService.Instance.RootFolder.RegisterTaskDefinition(TASK_NAME, taskDefinition);
263 |
264 | _logger.LogInformation($"Scheduled task startup entry created: {ExecutablePath} {arguments}");
265 | return true;
266 | }
267 | catch (Exception ex)
268 | {
269 | _logger.LogError(ex, "Failed to create scheduled task startup entry");
270 | return false;
271 | }
272 | }
273 |
274 | private bool RemoveScheduledTask()
275 | {
276 | try
277 | {
278 | using var taskService = new TaskService();
279 | var task = taskService.RootFolder.AllTasks.FirstOrDefault(t => t.Name == TASK_NAME);
280 | if (task != null)
281 | {
282 | taskService.RootFolder.DeleteTask(TASK_NAME);
283 | _logger.LogInformation($"已删除任务计划程序启动项: {TASK_NAME}");
284 | }
285 | return true;
286 | }
287 | catch (Exception ex)
288 | {
289 | _logger.LogError(ex, "删除任务计划程序启动项时发生错误");
290 | return false;
291 | }
292 | }
293 |
294 | private bool CreateTrayMonitorScheduledTask()
295 | {
296 | try
297 | {
298 | if (string.IsNullOrEmpty(ExecutablePath))
299 | {
300 | _logger.LogError("无法获取应用程序可执行文件路径");
301 | return false;
302 | }
303 |
304 | using var taskDefinition = TaskService.Instance.NewTask();
305 |
306 | taskDefinition.RegistrationInfo.Description = "Edge Monitor 托盘自动监测";
307 |
308 | // 用户登录时触发
309 | var logonTrigger = new LogonTrigger
310 | {
311 | UserId = WindowsIdentity.GetCurrent().Name,
312 | Delay = TimeSpan.FromSeconds(5) // 稍微延迟启动
313 | };
314 | taskDefinition.Triggers.Add(logonTrigger);
315 |
316 | // 执行操作,添加 --tray-monitor 参数
317 | taskDefinition.Actions.Add(ExecutablePath, "--tray-monitor");
318 |
319 | // 设置为最高权限运行(管理员权限)
320 | taskDefinition.Principal.RunLevel = TaskRunLevel.Highest;
321 | taskDefinition.Principal.LogonType = TaskLogonType.InteractiveToken;
322 |
323 | // 设置其他选项
324 | taskDefinition.Settings.StopIfGoingOnBatteries = false;
325 | taskDefinition.Settings.DisallowStartIfOnBatteries = false;
326 | taskDefinition.Settings.ExecutionTimeLimit = TimeSpan.Zero;
327 | taskDefinition.Settings.AllowDemandStart = true;
328 | taskDefinition.Settings.StartWhenAvailable = true;
329 | taskDefinition.Settings.Hidden = true; // 隐藏运行
330 |
331 | // 注册任务
332 | TaskService.Instance.RootFolder.RegisterTaskDefinition(TRAY_TASK_NAME, taskDefinition);
333 |
334 | _logger.LogInformation($"已创建托盘监测任务计划程序启动项: {ExecutablePath} --tray-monitor");
335 | return true;
336 | }
337 | catch (Exception ex)
338 | {
339 | _logger.LogError(ex, "创建托盘监测任务计划程序启动项时发生错误");
340 | return false;
341 | }
342 | }
343 |
344 | private bool RemoveTrayMonitorScheduledTask()
345 | {
346 | try
347 | {
348 | using var taskService = new TaskService();
349 | var task = taskService.RootFolder.AllTasks.FirstOrDefault(t => t.Name == TRAY_TASK_NAME);
350 | if (task != null)
351 | {
352 | taskService.RootFolder.DeleteTask(TRAY_TASK_NAME);
353 | _logger.LogInformation($"已删除托盘监测任务计划程序启动项: {TRAY_TASK_NAME}");
354 | }
355 | return true;
356 | }
357 | catch (Exception ex)
358 | {
359 | _logger.LogError(ex, "删除托盘监测任务计划程序启动项时发生错误");
360 | return false;
361 | }
362 | }
363 |
364 | ///
365 | /// 检查启动任务状态并在需要时重新安排
366 | ///
367 | public void StartupCheck()
368 | {
369 | try
370 | {
371 | using var taskService = new TaskService();
372 | var task = taskService.RootFolder.AllTasks.FirstOrDefault(t => t.Name == TASK_NAME);
373 | if (task != null)
374 | {
375 | try
376 | {
377 | var action = task.Definition.Actions.FirstOrDefault()?.ToString()?.Trim();
378 | if (!string.IsNullOrEmpty(action) &&
379 | !ExecutablePath.Equals(action.Trim('"'), StringComparison.OrdinalIgnoreCase) &&
380 | !File.Exists(action.Trim('"')))
381 | {
382 | _logger.LogInformation($"启动任务路径已过期: {action},重新创建: {ExecutablePath}");
383 | System.Threading.Tasks.Task.Run(async () =>
384 | {
385 | await DisableStartupAsync();
386 | await EnableStartupAsync(true);
387 | });
388 | }
389 | }
390 | catch (Exception ex)
391 | {
392 | _logger.LogError(ex, "检查启动任务时发生错误");
393 | }
394 | }
395 | }
396 | catch (Exception ex)
397 | {
398 | _logger.LogError(ex, "启动检查时发生错误");
399 | }
400 | }
401 | }
402 | }
403 |
--------------------------------------------------------------------------------
/EdgeMonitor/ViewModels/MainViewModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 | using System.Runtime.CompilerServices;
3 | using System.Windows.Input;
4 | using Microsoft.Extensions.Logging;
5 | using EdgeMonitor.Services;
6 | using EdgeMonitor.Commands;
7 |
8 | namespace EdgeMonitor.ViewModels
9 | {
10 | public class MainViewModel : INotifyPropertyChanged
11 | {
12 | private readonly ILogger _logger;
13 | private readonly IDataService _dataService;
14 | private readonly IDialogService _dialogService;
15 | private readonly IPrivilegeService _privilegeService;
16 | private readonly IEdgeMonitorService _edgeMonitorService;
17 | private readonly ILogService _logService;
18 | private readonly IConfigurationService _configService;
19 |
20 | private readonly IStartupService _startupService;
21 | private readonly ITrayService _trayService;
22 |
23 | private string _statusMessage = "就绪";
24 | private int _monitorInterval = 5;
25 | private bool _autoSaveEnabled = true;
26 | private DateTime _currentTime = DateTime.Now;
27 | private string _windowTitle = "Edge Monitor";
28 | private bool _isMonitoring = false;
29 | private System.Windows.Threading.DispatcherTimer? _monitorTimer;
30 | private bool _isCurrentlyMonitoring = false; // 防止重叠检查
31 | private CloseAction _closeAction = CloseAction.Ask;
32 | private bool _isStartupEnabled = false;
33 | private bool _isTrayMonitorStartupEnabled = false;
34 | private bool _isStartupHideToTray = false;
35 |
36 | public MainViewModel(
37 | ILogger logger,
38 | IDataService dataService,
39 | IDialogService dialogService,
40 | IPrivilegeService privilegeService,
41 | IEdgeMonitorService edgeMonitorService,
42 | ILogService logService,
43 | IConfigurationService configService,
44 | IStartupService startupService,
45 | ITrayService trayService)
46 | {
47 | _logger = logger;
48 | _dataService = dataService;
49 | _dialogService = dialogService;
50 | _privilegeService = privilegeService;
51 | _edgeMonitorService = edgeMonitorService;
52 | _logService = logService;
53 | _configService = configService;
54 | _startupService = startupService;
55 | _trayService = trayService;
56 |
57 | // 订阅日志集合变化事件
58 | _logService.LogEntries.CollectionChanged += (s, e) => NotifyLogPropertiesChanged();
59 | _logService.MonitorEntries.CollectionChanged += (s, e) => NotifyLogPropertiesChanged();
60 |
61 | InitializeCommands();
62 | StartTimeUpdater();
63 | UpdateWindowTitle();
64 | LoadCloseActionSettings();
65 | LoadStartupSettings();
66 | LoadStartupHideToTraySettings();
67 |
68 | // 启动时清理过期日志文件
69 | _ = Task.Run(async () => await _logService.CleanupOldLogFilesAsync());
70 | }
71 |
72 | #region Properties
73 |
74 | public string StatusMessage
75 | {
76 | get => _statusMessage;
77 | set => SetProperty(ref _statusMessage, value);
78 | }
79 |
80 | public string MonitorData
81 | {
82 | get => string.Join("\n", _logService.MonitorEntries);
83 | }
84 |
85 | public string LogData
86 | {
87 | get => string.Join("\n", _logService.LogEntries);
88 | }
89 |
90 | ///
91 | /// 通知日志属性已更改
92 | ///
93 | private void NotifyLogPropertiesChanged()
94 | {
95 | OnPropertyChanged(nameof(MonitorData));
96 | OnPropertyChanged(nameof(LogData));
97 | }
98 |
99 | public int MonitorInterval
100 | {
101 | get => _monitorInterval;
102 | set
103 | {
104 | // 验证输入值,确保在合理范围内
105 | var newValue = Math.Max(1, Math.Min(3600, value)); // 限制在1-3600秒之间
106 | if (SetProperty(ref _monitorInterval, newValue))
107 | {
108 | // 如果监控正在运行,更新定时器间隔
109 | if (IsMonitoring && _monitorTimer != null)
110 | {
111 | _monitorTimer.Interval = TimeSpan.FromSeconds(_monitorInterval);
112 | _logger.LogInformation($"监控间隔已更新为: {_monitorInterval}秒");
113 | }
114 | }
115 | }
116 | }
117 |
118 | public bool AutoSaveEnabled
119 | {
120 | get => _autoSaveEnabled;
121 | set => SetProperty(ref _autoSaveEnabled, value);
122 | }
123 |
124 | public DateTime CurrentTime
125 | {
126 | get => _currentTime;
127 | set => SetProperty(ref _currentTime, value);
128 | }
129 |
130 | public string WindowTitle
131 | {
132 | get => _windowTitle;
133 | set => SetProperty(ref _windowTitle, value);
134 | }
135 |
136 | public bool IsMonitoring
137 | {
138 | get => _isMonitoring;
139 | set => SetProperty(ref _isMonitoring, value);
140 | }
141 |
142 | public CloseAction CloseAction
143 | {
144 | get => _closeAction;
145 | set
146 | {
147 | if (SetProperty(ref _closeAction, value))
148 | {
149 | // 异步保存关闭行为设置
150 | _ = Task.Run(async () => await SaveCloseActionAsync(value));
151 | }
152 | }
153 | }
154 |
155 | public bool IsStartupEnabled
156 | {
157 | get => _isStartupEnabled;
158 | set
159 | {
160 | if (SetProperty(ref _isStartupEnabled, value))
161 | {
162 | // 如果启用开机自启,则禁用托盘监测启动
163 | if (value && _isTrayMonitorStartupEnabled)
164 | {
165 | _isTrayMonitorStartupEnabled = false;
166 | OnPropertyChanged(nameof(IsTrayMonitorStartupEnabled));
167 | }
168 | // 异步调用启动服务
169 | _ = Task.Run(async () => await UpdateStartupStatusAsync(value));
170 | }
171 | }
172 | }
173 |
174 | public bool IsTrayMonitorStartupEnabled
175 | {
176 | get => _isTrayMonitorStartupEnabled;
177 | set
178 | {
179 | if (SetProperty(ref _isTrayMonitorStartupEnabled, value))
180 | {
181 | // 如果启用托盘监测启动,则禁用开机自启
182 | if (value && _isStartupEnabled)
183 | {
184 | _isStartupEnabled = false;
185 | OnPropertyChanged(nameof(IsStartupEnabled));
186 | }
187 | // 异步调用托盘监测启动服务
188 | _ = Task.Run(async () => await UpdateTrayMonitorStartupStatusAsync(value));
189 | }
190 | }
191 | }
192 |
193 | public bool IsStartupHideToTray
194 | {
195 | get => _isStartupHideToTray;
196 | set
197 | {
198 | if (SetProperty(ref _isStartupHideToTray, value))
199 | {
200 | // Save setting when changed
201 | _ = Task.Run(async () => await SaveStartupHideToTraySettingsAsync(value));
202 |
203 | // If startup is enabled, recreate startup entry with new setting
204 | if (IsStartupEnabled)
205 | {
206 | _ = Task.Run(async () => await UpdateStartupStatusAsync(true));
207 | }
208 | }
209 | }
210 | }
211 |
212 | #endregion
213 |
214 | #region Commands
215 |
216 | public ICommand AboutCommand { get; private set; } = null!;
217 | public ICommand StartMonitoringCommand { get; private set; } = null!;
218 | public ICommand StopMonitoringCommand { get; private set; } = null!;
219 | public ICommand ClearLogsCommand { get; private set; } = null!;
220 | public ICommand CheckAdminCommand { get; private set; } = null!;
221 | public ICommand TestEdgeDetectionCommand { get; private set; } = null!;
222 | public ICommand ForceKillEdgeCommand { get; private set; } = null!;
223 | public ICommand ViewLogStatsCommand { get; private set; } = null!;
224 | public ICommand OpenLogFolderCommand { get; private set; } = null!;
225 | public ICommand ResetCloseChoiceCommand { get; private set; } = null!;
226 |
227 | #endregion
228 |
229 | private void InitializeCommands()
230 | {
231 | AboutCommand = new RelayCommand(ExecuteAbout);
232 | StartMonitoringCommand = new RelayCommand(ExecuteStartMonitoring);
233 | StopMonitoringCommand = new RelayCommand(ExecuteStopMonitoring);
234 | ClearLogsCommand = new RelayCommand(ExecuteClearLogs);
235 | CheckAdminCommand = new RelayCommand(ExecuteCheckAdmin);
236 | TestEdgeDetectionCommand = new RelayCommand(ExecuteTestEdgeDetection);
237 | ForceKillEdgeCommand = new RelayCommand(ExecuteForceKillEdge);
238 | ViewLogStatsCommand = new RelayCommand(ExecuteViewLogStats);
239 | OpenLogFolderCommand = new RelayCommand(ExecuteOpenLogFolder);
240 | ResetCloseChoiceCommand = new RelayCommand(ExecuteResetCloseChoice);
241 | }
242 |
243 | private void StartTimeUpdater()
244 | {
245 | var timer = new System.Windows.Threading.DispatcherTimer
246 | {
247 | Interval = TimeSpan.FromSeconds(1)
248 | };
249 | timer.Tick += (s, e) => CurrentTime = DateTime.Now;
250 | timer.Start();
251 | }
252 |
253 | private void UpdateWindowTitle()
254 | {
255 | var baseTitle = "Edge Monitor";
256 | if (_privilegeService.IsRunningAsAdministrator())
257 | {
258 | WindowTitle = $"{baseTitle} - 管理员";
259 | _logger.LogInformation("应用程序正在以管理员权限运行");
260 | }
261 | else
262 | {
263 | WindowTitle = baseTitle;
264 | _logger.LogWarning("应用程序未以管理员权限运行");
265 | }
266 | }
267 |
268 | #region Command Implementations
269 |
270 | private void ExecuteAbout()
271 | {
272 | _logger.LogInformation("显示关于对话框");
273 | var aboutWindow = new EdgeMonitor.Views.AboutWindow
274 | {
275 | Owner = System.Windows.Application.Current.MainWindow
276 | };
277 | aboutWindow.ShowDialog();
278 | }
279 |
280 | private async void ExecuteStartMonitoring()
281 | {
282 | if (IsMonitoring)
283 | {
284 | _logger.LogWarning("监控已在运行中");
285 | return;
286 | }
287 |
288 | _logger.LogInformation("开始Edge监控");
289 | IsMonitoring = true;
290 | StatusMessage = "Edge监控已启动";
291 |
292 | // 更新托盘状态
293 | _trayService.UpdateTrayStatus(true);
294 |
295 | var message = $"Edge监控已启动 - 检查间隔: {MonitorInterval}秒";
296 | await _logService.AddMonitorEntryAsync(message);
297 | await _logService.AddLogEntryAsync("INFO: Edge监控服务已启动");
298 |
299 | // 启动监控定时器
300 | _monitorTimer = new System.Windows.Threading.DispatcherTimer
301 | {
302 | Interval = TimeSpan.FromSeconds(MonitorInterval)
303 | };
304 | _monitorTimer.Tick += MonitorTimer_Tick;
305 | _monitorTimer.Start();
306 |
307 | // 立即执行一次检查(同步方式,避免定时器冲突)
308 | _logger.LogInformation("立即执行首次Edge检查");
309 | _ = Task.Run(async () =>
310 | {
311 | await Task.Delay(1000); // 短暂延迟让界面更新
312 | await PerformEdgeMonitoringAsync();
313 | });
314 | }
315 |
316 | private async void MonitorTimer_Tick(object? sender, EventArgs e)
317 | {
318 | // 防止重叠检查
319 | if (_isCurrentlyMonitoring)
320 | {
321 | _logger.LogWarning("上一次监控检查尚未完成,跳过本次检查");
322 | return;
323 | }
324 |
325 | _logger.LogInformation("定时器触发Edge监控检查");
326 | await PerformEdgeMonitoringAsync();
327 | }
328 |
329 | private async void ExecuteStopMonitoring()
330 | {
331 | if (!IsMonitoring)
332 | {
333 | _logger.LogWarning("监控未在运行");
334 | return;
335 | }
336 |
337 | _logger.LogInformation("停止Edge监控");
338 | IsMonitoring = false;
339 | StatusMessage = "Edge监控已停止";
340 |
341 | // 更新托盘状态
342 | _trayService.UpdateTrayStatus(false);
343 |
344 | _monitorTimer?.Stop();
345 | _monitorTimer = null;
346 |
347 | var message = "Edge监控已停止";
348 | await _logService.AddMonitorEntryAsync(message);
349 | await _logService.AddLogEntryAsync("INFO: Edge监控服务已停止");
350 | }
351 |
352 | ///
353 | /// 启动托盘监测模式
354 | ///
355 | public async void StartTrayMonitoring()
356 | {
357 | try
358 | {
359 | _logger.LogInformation("启动托盘监测模式");
360 |
361 | // 直接开始监测,不显示窗口
362 | await Task.Delay(2000); // 等待系统完全启动
363 | ExecuteStartMonitoring();
364 |
365 | _logger.LogInformation("托盘监测模式已启动");
366 | }
367 | catch (Exception ex)
368 | {
369 | _logger.LogError(ex, "启动托盘监测模式时发生错误");
370 | }
371 | }
372 |
373 | private void ExecuteClearLogs()
374 | {
375 | _logger.LogInformation("清除内存日志");
376 | StatusMessage = "内存日志已清除";
377 | _logService.ClearMemoryLogs();
378 | }
379 |
380 | private void ExecuteCheckAdmin()
381 | {
382 | var isAdmin = _privilegeService.IsRunningAsAdministrator();
383 | var status = isAdmin ? "是" : "否";
384 | _dialogService.ShowMessage("管理员权限检查", $"当前是否以管理员身份运行: {status}");
385 |
386 | if (!isAdmin)
387 | {
388 | var result = _dialogService.ShowConfirmation("权限提升", "是否要以管理员身份重新启动程序?");
389 | if (result)
390 | {
391 | _privilegeService.RestartAsAdministrator();
392 | }
393 | }
394 | }
395 |
396 | private async void ExecuteTestEdgeDetection()
397 | {
398 | _logger.LogInformation("手动执行Edge检测测试");
399 | StatusMessage = "正在执行Edge检测测试...";
400 |
401 | try
402 | {
403 | var edgeProcesses = await _edgeMonitorService.GetEdgeProcessesAsync();
404 | var hasVisibleWindows = await _edgeMonitorService.HasVisibleWindowsAsync();
405 | var hasAbnormalUsage = _edgeMonitorService.HasAbnormalResourceUsage(edgeProcesses, 30.0, 2048);
406 |
407 | var totalCpu = edgeProcesses.Sum(p => p.CpuUsage);
408 | var totalMemory = edgeProcesses.Sum(p => p.MemoryUsageMB);
409 |
410 | var testResult = $"=== Edge检测测试结果 ===\n" +
411 | $"进程数量: {edgeProcesses.Length}\n" +
412 | $"总CPU使用: {totalCpu:F1}%\n" +
413 | $"总内存使用: {totalMemory}MB\n" +
414 | $"有可见窗口: {hasVisibleWindows}\n" +
415 | $"资源异常: {hasAbnormalUsage}\n" +
416 | $"满足终止条件: {!hasVisibleWindows && hasAbnormalUsage}\n" +
417 | $"==================";
418 |
419 | await _logService.AddLogEntryAsync(testResult);
420 |
421 | foreach (var process in edgeProcesses)
422 | {
423 | var processInfo = $"进程 {process.ProcessId}: {process.ProcessName}, " +
424 | $"CPU: {process.CpuUsage:F1}%, 内存: {process.MemoryUsageMB}MB, " +
425 | $"窗口数: {process.WindowCount}";
426 | await _logService.AddLogEntryAsync(processInfo);
427 | }
428 |
429 | StatusMessage = $"测试完成 - 进程:{edgeProcesses.Length}, CPU:{totalCpu:F1}%, 内存:{totalMemory}MB";
430 | }
431 | catch (Exception ex)
432 | {
433 | _logger.LogError($"Edge检测测试失败: {ex.Message}");
434 | await _logService.AddLogEntryAsync($"测试失败: {ex.Message}");
435 | StatusMessage = "测试失败";
436 | }
437 | }
438 |
439 | private async void ExecuteForceKillEdge()
440 | {
441 | _logger.LogInformation("手动强制终止Edge进程");
442 |
443 | var result = _dialogService.ShowConfirmation("强制终止Edge", "确定要强制终止所有Edge进程吗?");
444 | if (!result) return;
445 |
446 | try
447 | {
448 | StatusMessage = "正在强制终止Edge进程...";
449 | await _edgeMonitorService.KillAllEdgeProcessesAsync();
450 |
451 | var message = "手动强制终止Edge进程完成";
452 | await _logService.AddMonitorEntryAsync(message);
453 | await _logService.AddLogEntryAsync(message);
454 | StatusMessage = "Edge进程已被手动终止";
455 |
456 | _dialogService.ShowMessage("操作完成", "所有Edge进程已被强制终止");
457 | }
458 | catch (Exception ex)
459 | {
460 | _logger.LogError($"强制终止Edge进程失败: {ex.Message}");
461 | await _logService.AddLogEntryAsync($"强制终止失败: {ex.Message}");
462 | StatusMessage = "强制终止失败";
463 | _dialogService.ShowMessage("操作失败", $"强制终止Edge进程失败: {ex.Message}");
464 | }
465 | }
466 |
467 | private async void ExecuteViewLogStats()
468 | {
469 | try
470 | {
471 | var stats = await _logService.GetLogStatisticsAsync();
472 | var message = $"日志统计信息:\n" +
473 | $"日志文件数量: {stats.TotalFiles}\n" +
474 | $"总文件大小: {stats.TotalSizeMB:F2} MB\n" +
475 | $"内存中日志条数: {stats.MemoryLogCount}\n" +
476 | $"内存中监控数据条数: {stats.MemoryMonitorCount}\n" +
477 | $"日志目录: {stats.LogDirectory}";
478 |
479 | _dialogService.ShowMessage("日志统计", message);
480 | }
481 | catch (Exception ex)
482 | {
483 | _logger.LogError($"获取日志统计失败: {ex.Message}");
484 | _dialogService.ShowMessage("错误", $"获取日志统计失败: {ex.Message}");
485 | }
486 | }
487 |
488 | private void ExecuteOpenLogFolder()
489 | {
490 | try
491 | {
492 | var logPath = _logService.GetLogFilePath();
493 | System.Diagnostics.Process.Start("explorer.exe", logPath);
494 | _logger.LogInformation($"打开日志文件夹: {logPath}");
495 | }
496 | catch (Exception ex)
497 | {
498 | _logger.LogError($"打开日志文件夹失败: {ex.Message}");
499 | _dialogService.ShowMessage("错误", $"打开日志文件夹失败: {ex.Message}");
500 | }
501 | }
502 |
503 | private async void ExecuteResetCloseChoice()
504 | {
505 | try
506 | {
507 | _configService.SetValue("UI:RememberCloseChoice", false);
508 | _configService.SetValue("UI:CloseToTray", "");
509 | await _configService.SaveAsync();
510 |
511 | CloseAction = CloseAction.Ask;
512 |
513 | _logger.LogInformation("关闭选择已重置");
514 | _dialogService.ShowMessage("成功", "关闭选择已重置,下次关闭时将重新询问。");
515 | }
516 | catch (Exception ex)
517 | {
518 | _logger.LogError($"重置关闭选择失败: {ex.Message}");
519 | _dialogService.ShowMessage("错误", $"重置关闭选择失败: {ex.Message}");
520 | }
521 | }
522 |
523 | private void LoadCloseActionSettings()
524 | {
525 | try
526 | {
527 | var rememberChoice = _configService.GetValue("UI:RememberCloseChoice");
528 | var savedChoice = _configService.GetValue("UI:CloseToTray");
529 |
530 | if (rememberChoice && !string.IsNullOrEmpty(savedChoice))
531 | {
532 | CloseAction = savedChoice.ToLower() switch
533 | {
534 | "true" => CloseAction.MinimizeToTray,
535 | "false" => CloseAction.Exit,
536 | _ => CloseAction.Ask
537 | };
538 | }
539 | else
540 | {
541 | CloseAction = CloseAction.Ask;
542 | }
543 |
544 | _logger.LogInformation($"加载关闭行为设置: {CloseAction}");
545 | }
546 | catch (Exception ex)
547 | {
548 | _logger.LogError($"加载关闭行为设置失败: {ex.Message}");
549 | CloseAction = CloseAction.Ask;
550 | }
551 | }
552 |
553 | private async Task SaveCloseActionAsync(CloseAction action)
554 | {
555 | try
556 | {
557 | _logger.LogInformation($"保存关闭行为设置: {action}");
558 |
559 | if (action == CloseAction.Ask)
560 | {
561 | // 如果选择"每次询问",清除记住的选择
562 | _configService.SetValue("UI:RememberCloseChoice", false);
563 | _configService.SetValue("UI:CloseToTray", "");
564 | }
565 | else
566 | {
567 | // 保存具体的选择
568 | var closeToTray = action == CloseAction.MinimizeToTray ? "true" : "false";
569 | _configService.SetValue("UI:CloseToTray", closeToTray);
570 | _configService.SetValue("UI:RememberCloseChoice", true);
571 | }
572 |
573 | await _configService.SaveAsync();
574 |
575 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
576 | {
577 | StatusMessage = $"已保存关闭行为设置: {GetCloseActionDisplayName(action)}";
578 | });
579 |
580 | _logger.LogInformation($"关闭行为设置已保存: {action}");
581 | }
582 | catch (Exception ex)
583 | {
584 | _logger.LogError(ex, "保存关闭行为设置时发生错误");
585 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
586 | {
587 | StatusMessage = $"保存关闭行为设置失败: {ex.Message}";
588 | });
589 | }
590 | }
591 |
592 | private static string GetCloseActionDisplayName(CloseAction action)
593 | {
594 | return action switch
595 | {
596 | CloseAction.Exit => "关闭程序",
597 | CloseAction.MinimizeToTray => "最小化到托盘",
598 | CloseAction.Ask => "每次询问",
599 | _ => "未知"
600 | };
601 | }
602 |
603 | private async Task PerformEdgeMonitoringAsync()
604 | {
605 | // 设置监控标志,防止重叠
606 | if (_isCurrentlyMonitoring)
607 | {
608 | _logger.LogWarning("监控检查已在进行中,忽略重复调用");
609 | return;
610 | }
611 |
612 | _isCurrentlyMonitoring = true;
613 | var startTime = DateTime.Now;
614 |
615 | try
616 | {
617 | _logger.LogInformation($"[{startTime:HH:mm:ss.fff}] 开始执行Edge监控检查");
618 |
619 | // 获取Edge进程信息
620 | var processStartTime = DateTime.Now;
621 | var edgeProcesses = await _edgeMonitorService.GetEdgeProcessesAsync();
622 | var processEndTime = DateTime.Now;
623 | _logger.LogInformation($"[{processEndTime:HH:mm:ss.fff}] 获取Edge进程耗时: {(processEndTime - processStartTime).TotalMilliseconds}ms,检测到 {edgeProcesses.Length} 个Edge进程");
624 |
625 | if (!edgeProcesses.Any())
626 | {
627 | var message = "未检测到Edge进程";
628 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
629 | {
630 | StatusMessage = "未检测到Edge进程";
631 | });
632 | await _logService.AddMonitorEntryAsync(message);
633 | _logger.LogInformation("未检测到Edge进程,跳过检查");
634 | return;
635 | }
636 |
637 | // 检查是否有可见窗口
638 | var windowStartTime = DateTime.Now;
639 | var hasVisibleWindows = await _edgeMonitorService.HasVisibleWindowsAsync();
640 | var windowEndTime = DateTime.Now;
641 | var isRunningInBackground = !hasVisibleWindows && edgeProcesses.Any();
642 | _logger.LogInformation($"[{windowEndTime:HH:mm:ss.fff}] 窗口检查耗时: {(windowEndTime - windowStartTime).TotalMilliseconds}ms,结果: 可见窗口={hasVisibleWindows}, 后台运行={isRunningInBackground}");
643 |
644 | // 检查资源使用情况
645 | var resourceStartTime = DateTime.Now;
646 | var hasAbnormalUsage = _edgeMonitorService.HasAbnormalResourceUsage(edgeProcesses, 30.0, 2048);
647 | var resourceEndTime = DateTime.Now;
648 |
649 | var totalCpu = edgeProcesses.Sum(p => p.CpuUsage);
650 | var totalMemory = edgeProcesses.Sum(p => p.MemoryUsageMB);
651 | _logger.LogInformation($"[{resourceEndTime:HH:mm:ss.fff}] 资源检查耗时: {(resourceEndTime - resourceStartTime).TotalMilliseconds}ms,使用情况: CPU={totalCpu:F1}%, 内存={totalMemory}MB, 异常={hasAbnormalUsage}");
652 |
653 | var statusInfo = $"Edge进程: {edgeProcesses.Length}个 | CPU: {totalCpu:F1}% | 内存: {totalMemory}MB | 窗口: {(hasVisibleWindows ? "有" : "无")}";
654 |
655 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
656 | {
657 | StatusMessage = statusInfo;
658 | });
659 |
660 | await _logService.AddMonitorEntryAsync(statusInfo);
661 |
662 | // 记录详细信息
663 | foreach (var process in edgeProcesses)
664 | {
665 | var processDetail = $"进程详情: PID={process.ProcessId}, " +
666 | $"CPU={process.CpuUsage:F1}%, 内存={process.MemoryUsageMB}MB, " +
667 | $"窗口数={process.WindowCount}";
668 |
669 | await _logService.AddLogEntryAsync(processDetail);
670 |
671 | _logger.LogDebug($"进程 {process.ProcessId}: CPU={process.CpuUsage:F1}%, 内存={process.MemoryUsageMB}MB, 窗口={process.WindowCount}");
672 | }
673 |
674 | // 判断是否需要终止Edge
675 | _logger.LogInformation($"判断条件: 后台运行={isRunningInBackground}, 异常使用={hasAbnormalUsage}");
676 | if (isRunningInBackground && hasAbnormalUsage)
677 | {
678 | _logger.LogWarning($"满足终止条件! Edge异常检测 - CPU: {totalCpu:F1}%, 内存: {totalMemory}MB, 后台运行: {isRunningInBackground}");
679 |
680 | var killMessage = "检测到Edge在后台运行且资源占用异常,正在终止...";
681 | await _logService.AddMonitorEntryAsync(killMessage);
682 | await _logService.AddLogEntryAsync($"WARNING: Edge异常检测 - CPU: {totalCpu:F1}%, 内存: {totalMemory}MB");
683 |
684 | // 执行终止操作
685 | var killStartTime = DateTime.Now;
686 | await _edgeMonitorService.KillAllEdgeProcessesAsync();
687 | var killEndTime = DateTime.Now;
688 | _logger.LogInformation($"[{killEndTime:HH:mm:ss.fff}] Edge进程终止耗时: {(killEndTime - killStartTime).TotalMilliseconds}ms");
689 |
690 | var killedMessage = "Edge进程已被终止";
691 | await _logService.AddMonitorEntryAsync(killedMessage);
692 | await _logService.AddLogEntryAsync("INFO: Edge进程已被终止");
693 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
694 | {
695 | StatusMessage = "Edge进程已被终止";
696 | });
697 |
698 | // 静默终止,不显示通知
699 | _logger.LogInformation($"Edge进程已静默终止 - CPU: {totalCpu:F1}%, 内存: {totalMemory}MB");
700 | }
701 | else
702 | {
703 | _logger.LogInformation("不满足终止条件,继续监控");
704 | await _logService.AddLogEntryAsync("INFO: 检查完成 - 不满足终止条件");
705 | }
706 |
707 | var endTime = DateTime.Now;
708 | _logger.LogInformation($"[{endTime:HH:mm:ss.fff}] Edge监控检查完成,总耗时: {(endTime - startTime).TotalMilliseconds}ms");
709 | }
710 | catch (Exception ex)
711 | {
712 | var endTime = DateTime.Now;
713 | _logger.LogError($"[{endTime:HH:mm:ss.fff}] Edge监控过程中发生错误(耗时: {(endTime - startTime).TotalMilliseconds}ms): {ex.Message}");
714 | _logger.LogError($"错误堆栈: {ex.StackTrace}");
715 |
716 | var errorMessage = $"监控错误: {ex.Message}";
717 | await _logService.AddLogEntryAsync(errorMessage);
718 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
719 | {
720 | StatusMessage = "监控出现错误";
721 | });
722 | }
723 | finally
724 | {
725 | // 重置监控标志
726 | _isCurrentlyMonitoring = false;
727 | }
728 | }
729 |
730 | private void LoadStartupSettings()
731 | {
732 | try
733 | {
734 | IsStartupEnabled = _startupService.IsStartupEnabled();
735 | IsTrayMonitorStartupEnabled = _startupService.IsTrayMonitorStartupEnabled();
736 | _logger.LogInformation($"当前开机自启动状态: {IsStartupEnabled}");
737 | _logger.LogInformation($"当前托盘监测开机自启动状态: {IsTrayMonitorStartupEnabled}");
738 | }
739 | catch (Exception ex)
740 | {
741 | _logger.LogError(ex, "加载开机自启动设置时发生错误");
742 | IsStartupEnabled = false;
743 | IsTrayMonitorStartupEnabled = false;
744 | }
745 | }
746 |
747 | private async Task UpdateStartupStatusAsync(bool enable)
748 | {
749 | try
750 | {
751 | _logger.LogInformation($"更新开机自启动状态: {enable}");
752 |
753 | bool success;
754 | if (enable)
755 | {
756 | // Enable startup with admin privileges and tray hide option
757 | success = await _startupService.EnableStartupAsync(true, IsStartupHideToTray);
758 | if (success)
759 | {
760 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
761 | {
762 | StatusMessage = "已启用开机自启动";
763 | });
764 | _logger.LogInformation("Startup enabled with admin privileges");
765 | }
766 | else
767 | {
768 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
769 | {
770 | StatusMessage = "启用开机自启动失败";
771 | // Reset UI state if enable failed
772 | _isStartupEnabled = false;
773 | OnPropertyChanged(nameof(IsStartupEnabled));
774 | });
775 | _logger.LogError("Failed to enable startup");
776 | }
777 | }
778 | else
779 | {
780 | // 禁用开机自启动
781 | success = await _startupService.DisableStartupAsync();
782 | if (success)
783 | {
784 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
785 | {
786 | StatusMessage = "已禁用开机自启动";
787 | });
788 | _logger.LogInformation("开机自启动已禁用");
789 | }
790 | else
791 | {
792 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
793 | {
794 | StatusMessage = "禁用开机自启动失败";
795 | // 如果禁用失败,需要重置UI状态
796 | _isStartupEnabled = true;
797 | OnPropertyChanged(nameof(IsStartupEnabled));
798 | });
799 | _logger.LogError("禁用开机自启动失败");
800 | }
801 | }
802 | }
803 | catch (Exception ex)
804 | {
805 | _logger.LogError(ex, "更新开机自启动状态时发生错误");
806 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
807 | {
808 | StatusMessage = $"开机自启动操作失败: {ex.Message}";
809 | // 发生异常时,重置为原来的状态
810 | _isStartupEnabled = !enable;
811 | OnPropertyChanged(nameof(IsStartupEnabled));
812 | });
813 | }
814 | }
815 |
816 | private async Task UpdateTrayMonitorStartupStatusAsync(bool enable)
817 | {
818 | try
819 | {
820 | _logger.LogInformation($"更新托盘监测开机自启动状态: {enable}");
821 |
822 | bool success;
823 | if (enable)
824 | {
825 | // 启用托盘监测开机自启动
826 | success = await _startupService.EnableTrayMonitorStartupAsync();
827 | if (success)
828 | {
829 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
830 | {
831 | StatusMessage = "已启用开机后自动在托盘监测";
832 | });
833 | _logger.LogInformation("托盘监测开机自启动已启用");
834 | }
835 | else
836 | {
837 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
838 | {
839 | StatusMessage = "启用托盘监测开机自启动失败";
840 | // 如果启用失败,需要重置UI状态
841 | _isTrayMonitorStartupEnabled = false;
842 | OnPropertyChanged(nameof(IsTrayMonitorStartupEnabled));
843 | });
844 | _logger.LogError("启用托盘监测开机自启动失败");
845 | }
846 | }
847 | else
848 | {
849 | // 禁用托盘监测开机自启动
850 | success = await _startupService.DisableStartupAsync();
851 | if (success)
852 | {
853 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
854 | {
855 | StatusMessage = "已禁用托盘监测开机自启动";
856 | });
857 | _logger.LogInformation("托盘监测开机自启动已禁用");
858 | }
859 | else
860 | {
861 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
862 | {
863 | StatusMessage = "禁用托盘监测开机自启动失败";
864 | // 如果禁用失败,需要重置UI状态
865 | _isTrayMonitorStartupEnabled = true;
866 | OnPropertyChanged(nameof(IsTrayMonitorStartupEnabled));
867 | });
868 | _logger.LogError("禁用托盘监测开机自启动失败");
869 | }
870 | }
871 | }
872 | catch (Exception ex)
873 | {
874 | _logger.LogError(ex, "更新托盘监测开机自启动状态时发生错误");
875 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
876 | {
877 | StatusMessage = $"托盘监测开机自启动操作失败: {ex.Message}";
878 | // 发生异常时,重置为原来的状态
879 | _isTrayMonitorStartupEnabled = !enable;
880 | OnPropertyChanged(nameof(IsTrayMonitorStartupEnabled));
881 | });
882 | }
883 | }
884 |
885 | private void LoadStartupHideToTraySettings()
886 | {
887 | try
888 | {
889 | IsStartupHideToTray = _configService.GetValue("UI:StartupHideToTray");
890 | _logger.LogInformation($"Load startup hide to tray setting: {IsStartupHideToTray}");
891 | }
892 | catch (Exception ex)
893 | {
894 | _logger.LogError(ex, "Failed to load startup hide to tray setting");
895 | IsStartupHideToTray = false;
896 | }
897 | }
898 |
899 | private async Task SaveStartupHideToTraySettingsAsync(bool hide)
900 | {
901 | try
902 | {
903 | _logger.LogInformation($"Save startup hide to tray setting: {hide}");
904 | _configService.SetValue("UI:StartupHideToTray", hide);
905 | await _configService.SaveAsync();
906 | _logger.LogInformation($"Startup hide to tray setting saved: {hide}");
907 | }
908 | catch (Exception ex)
909 | {
910 | _logger.LogError(ex, "Failed to save startup hide to tray setting");
911 | System.Windows.Application.Current.Dispatcher.Invoke(() =>
912 | {
913 | StatusMessage = $"保存开机自启动隐藏到托盘状态失败: {ex.Message}";
914 | });
915 | }
916 | }
917 |
918 | #endregion
919 |
920 | #region INotifyPropertyChanged
921 |
922 | public event PropertyChangedEventHandler? PropertyChanged;
923 |
924 | protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
925 | {
926 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
927 | }
928 |
929 | protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null)
930 | {
931 | if (EqualityComparer.Default.Equals(field, value)) return false;
932 | field = value;
933 | OnPropertyChanged(propertyName);
934 | return true;
935 | }
936 |
937 | #endregion
938 | }
939 | }
940 |
--------------------------------------------------------------------------------