From f3578b9def2c01bb8c3db55b56b4621bb4dbedb6 Mon Sep 17 00:00:00 2001 From: Wang Haoyu Date: Mon, 2 Mar 2026 16:28:50 +0800 Subject: [PATCH 1/2] fix: extend DeleteAction folder delete timeout to 10 minutes --- Actions/CloneDisplayAction.cs | 31 ++------ Actions/CopyAction.cs | 56 +++++--------- Actions/DeleteAction.cs | 49 ++++-------- Actions/DisableMouseAction.cs | 26 ++----- Actions/EnableMouseAction.cs | 26 ++----- Actions/ExtendDisplayAction.cs | 31 ++------ Actions/ExternalDisplayAction.cs | 31 ++------ Actions/InternalDisplayAction.cs | 31 ++------ Actions/MoveAction.cs | 57 +++++--------- Actions/SimulateMouseAction.cs | 23 ++---- CODE_REVIEW_REPORT.md | 63 +++++++++++++++ Controls/FaceRecognitionAuthorizer.axaml.cs | 25 +++--- Plugin.cs | 1 + Services/CameraCaptureService.cs | 50 ++++++++++-- Services/IProcessRunner.cs | 19 +++++ Services/ProcessRunner.cs | 86 +++++++++++++++++++++ 16 files changed, 332 insertions(+), 273 deletions(-) create mode 100644 CODE_REVIEW_REPORT.md create mode 100644 Services/IProcessRunner.cs create mode 100644 Services/ProcessRunner.cs diff --git a/Actions/CloneDisplayAction.cs b/Actions/CloneDisplayAction.cs index bd95df7..cb7f7f3 100644 --- a/Actions/CloneDisplayAction.cs +++ b/Actions/CloneDisplayAction.cs @@ -4,16 +4,15 @@ using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using SystemTools.Services; namespace SystemTools.Actions; -/// -/// 复制屏幕 -/// [ActionInfo("SystemTools.CloneDisplay", "复制屏幕", "\uE635", false)] -public class CloneDisplayAction(ILogger logger) : ActionBase +public class CloneDisplayAction(ILogger logger, IProcessRunner processRunner) : ActionBase { private readonly ILogger _logger = logger; + private readonly IProcessRunner _processRunner = processRunner; protected override async Task OnInvoke() { @@ -28,26 +27,12 @@ protected override async Task OnInvoke() CreateNoWindow = true, UseShellExecute = false, RedirectStandardOutput = true, - RedirectStandardError = true + RedirectStandardError = true, + WindowStyle = ProcessWindowStyle.Hidden }; - using var process = Process.Start(processInfo); - if (process != null) - { - string output = await process.StandardOutput.ReadToEndAsync(); - string error = await process.StandardError.ReadToEndAsync(); - - await process.WaitForExitAsync(); - - _logger.LogInformation("复制屏幕命令已执行,退出码: {ExitCode}", process.ExitCode); - - if (!string.IsNullOrEmpty(error)) - _logger.LogWarning("错误输出: {Error}", error); - } - else - { - throw new Exception("无法启动 DisplaySwitch.exe 进程"); - } + await _processRunner.RunAsync(processInfo, "复制屏幕(DisplaySwitch)"); + _logger.LogInformation("复制屏幕命令执行完成"); } catch (Exception ex) { @@ -57,4 +42,4 @@ protected override async Task OnInvoke() await base.OnInvoke(); } -} \ No newline at end of file +} diff --git a/Actions/CopyAction.cs b/Actions/CopyAction.cs index 7a39422..3487521 100644 --- a/Actions/CopyAction.cs +++ b/Actions/CopyAction.cs @@ -5,14 +5,16 @@ using System.Diagnostics; using System.IO; using System.Threading.Tasks; +using SystemTools.Services; using SystemTools.Settings; namespace SystemTools.Actions; [ActionInfo("SystemTools.Copy", "复制", "\uE6AB", false)] -public class CopyAction(ILogger logger) : ActionBase +public class CopyAction(ILogger logger, IProcessRunner processRunner) : ActionBase { private readonly ILogger _logger = logger; + private readonly IProcessRunner _processRunner = processRunner; protected override async Task OnInvoke() { @@ -25,16 +27,6 @@ protected override async Task OnInvoke() return; } - var psi = new ProcessStartInfo - { - FileName = "cmd.exe", - CreateNoWindow = true, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - WindowStyle = ProcessWindowStyle.Hidden - }; - try { var sourcePath = Settings.SourcePath.TrimEnd('\\'); @@ -60,16 +52,7 @@ protected override async Task OnInvoke() Directory.CreateDirectory(destDir); } - try - { - await Task.Run(() => File.Copy(sourcePath, destPath, true)); - } - catch (Exception ex) - { - _logger.LogError(ex, "文件复制失败"); - throw new Exception($"复制失败: {ex}"); - } - + await Task.Run(() => File.Copy(sourcePath, destPath, true)); _logger.LogInformation("文件复制成功: {Source} -> {Destination}", sourcePath, destPath); } else @@ -93,21 +76,22 @@ protected override async Task OnInvoke() Directory.Delete(finalDestPath, true); } - psi.FileName = "robocopy.exe"; - psi.Arguments = $"\"{sourcePath}\" \"{finalDestPath}\" /e /copyall /r:3 /w:3 /mt:4 /nfl /ndl /np"; - _logger.LogInformation("执行命令: robocopy \"{Source}\" \"{Destination}\"", sourcePath, finalDestPath); - - using var process = Process.Start(psi) ?? throw new Exception("无法启动进程"); - string output = await process.StandardOutput.ReadToEndAsync(); - string error = await process.StandardError.ReadToEndAsync(); - await process.WaitForExitAsync(); - - if (process.ExitCode >= 8) + var psi = new ProcessStartInfo { - _logger.LogError("robocopy 失败,退出码: {ExitCode}, 输出: {Output}, 错误: {Error}", - process.ExitCode, output, error); - throw new Exception($"robocopy 失败,退出码: {process.ExitCode}"); - } + FileName = "robocopy.exe", + Arguments = $"\"{sourcePath}\" \"{finalDestPath}\" /e /copyall /r:3 /w:3 /mt:4 /nfl /ndl /np", + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + WindowStyle = ProcessWindowStyle.Hidden + }; + + await _processRunner.RunAsync( + psi, + operationName: "复制文件夹(robocopy)", + successExitCodes: new[] { 0, 1, 2, 3, 4, 5, 6, 7 }, + timeout: TimeSpan.FromMinutes(10)); _logger.LogInformation("文件夹复制成功: {Source} -> {Destination}", sourcePath, finalDestPath); } @@ -121,4 +105,4 @@ protected override async Task OnInvoke() await base.OnInvoke(); _logger.LogDebug("CopyAction OnInvoke 完成"); } -} \ No newline at end of file +} diff --git a/Actions/DeleteAction.cs b/Actions/DeleteAction.cs index 2c514ba..0fb6282 100644 --- a/Actions/DeleteAction.cs +++ b/Actions/DeleteAction.cs @@ -5,14 +5,16 @@ using System.Diagnostics; using System.IO; using System.Threading.Tasks; +using SystemTools.Services; using SystemTools.Settings; namespace SystemTools.Actions; [ActionInfo("SystemTools.Delete", "删除", "\uE61D", false)] -public class DeleteAction(ILogger logger) : ActionBase +public class DeleteAction(ILogger logger, IProcessRunner processRunner) : ActionBase { private readonly ILogger _logger = logger; + private readonly IProcessRunner _processRunner = processRunner; protected override async Task OnInvoke() { @@ -24,16 +26,6 @@ protected override async Task OnInvoke() return; } - var psi = new ProcessStartInfo - { - FileName = "cmd.exe", - CreateNoWindow = true, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - WindowStyle = ProcessWindowStyle.Hidden - }; - try { var targetPath = Settings.TargetPath.TrimEnd('\\'); @@ -46,16 +38,7 @@ protected override async Task OnInvoke() throw new FileNotFoundException("文件不存在", targetPath); } - try - { - await Task.Run(() => File.Delete(targetPath)); - } - catch (Exception ex) - { - _logger.LogError(ex, "文件移动失败"); - throw new Exception($"移动失败: {ex}"); - } - + await Task.Run(() => File.Delete(targetPath)); _logger.LogInformation("文件删除成功: {Path}", targetPath); } else @@ -66,20 +49,18 @@ protected override async Task OnInvoke() throw new DirectoryNotFoundException($"文件夹不存在: {targetPath}"); } - psi.Arguments = $"/c rmdir /s /q \"{targetPath}\""; - _logger.LogInformation("执行命令: {Command}", psi.Arguments); - - using var process = Process.Start(psi) ?? throw new Exception("无法启动进程"); - string output = await process.StandardOutput.ReadToEndAsync(); - string error = await process.StandardError.ReadToEndAsync(); - await process.WaitForExitAsync(); - - if (process.ExitCode != 0) + var psi = new ProcessStartInfo { - _logger.LogError("删除失败,退出码: {ExitCode}, 错误: {Error}", process.ExitCode, error); - throw new Exception($"删除失败: {error}"); - } + FileName = "cmd.exe", + Arguments = $"/c rmdir /s /q \"{targetPath}\"", + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + WindowStyle = ProcessWindowStyle.Hidden + }; + await _processRunner.RunAsync(psi, "删除文件夹(rmdir)", timeout: TimeSpan.FromMinutes(10)); _logger.LogInformation("文件夹删除成功: {Path}", targetPath); } } @@ -92,4 +73,4 @@ protected override async Task OnInvoke() await base.OnInvoke(); _logger.LogDebug("DeleteAction OnInvoke 完成"); } -} \ No newline at end of file +} diff --git a/Actions/DisableMouseAction.cs b/Actions/DisableMouseAction.cs index 804b4c0..4669789 100644 --- a/Actions/DisableMouseAction.cs +++ b/Actions/DisableMouseAction.cs @@ -5,13 +5,15 @@ using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using SystemTools.Services; namespace SystemTools.Actions; [ActionInfo("SystemTools.DisableMouse", "禁用鼠标", "\uE5C7", false)] -public class DisableMouseAction(ILogger logger) : ActionBase +public class DisableMouseAction(ILogger logger, IProcessRunner processRunner) : ActionBase { private readonly ILogger _logger = logger; + private readonly IProcessRunner _processRunner = processRunner; protected override async Task OnInvoke() { @@ -23,7 +25,7 @@ protected override async Task OnInvoke() if (string.IsNullOrEmpty(pluginDir)) { _logger.LogError("无法获取程序集位置"); - throw new FileNotFoundException($"无法获取程序集位置"); + throw new FileNotFoundException("无法获取程序集位置"); } var batchPath = Path.Combine(pluginDir, "jinyongshubiao.bat"); @@ -34,8 +36,6 @@ protected override async Task OnInvoke() throw new FileNotFoundException($"找不到禁用鼠标批处理文件: {batchPath}"); } - _logger.LogInformation("正在运行禁用鼠标批处理: {Path}", batchPath); - var psi = new ProcessStartInfo { FileName = batchPath, @@ -47,21 +47,7 @@ protected override async Task OnInvoke() WindowStyle = ProcessWindowStyle.Hidden }; - using var process = Process.Start(psi) ?? throw new Exception("无法启动批处理进程"); - string output = await process.StandardOutput.ReadToEndAsync(); - string error = await process.StandardError.ReadToEndAsync(); - await process.WaitForExitAsync(); - - _logger.LogInformation("禁用鼠标批处理执行完成,退出码: {ExitCode}", process.ExitCode); - if (!string.IsNullOrWhiteSpace(output)) - _logger.LogDebug("批处理输出: {Output}", output); - if (!string.IsNullOrWhiteSpace(error)) - _logger.LogWarning("批处理错误: {Error}", error); - - if (process.ExitCode != 0) - { - _logger.LogWarning("批处理返回非零退出码: {ExitCode}", process.ExitCode); - } + await _processRunner.RunAsync(psi, "禁用鼠标批处理", timeout: TimeSpan.FromMinutes(2)); } catch (Exception ex) { @@ -72,4 +58,4 @@ protected override async Task OnInvoke() await base.OnInvoke(); _logger.LogDebug("DisableMouseAction OnInvoke 完成"); } -} \ No newline at end of file +} diff --git a/Actions/EnableMouseAction.cs b/Actions/EnableMouseAction.cs index fb8b2d6..2c8a995 100644 --- a/Actions/EnableMouseAction.cs +++ b/Actions/EnableMouseAction.cs @@ -5,13 +5,15 @@ using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using SystemTools.Services; namespace SystemTools.Actions; [ActionInfo("SystemTools.EnableMouse", "启用鼠标", "\uE5BF", false)] -public class EnableMouseAction(ILogger logger) : ActionBase +public class EnableMouseAction(ILogger logger, IProcessRunner processRunner) : ActionBase { private readonly ILogger _logger = logger; + private readonly IProcessRunner _processRunner = processRunner; protected override async Task OnInvoke() { @@ -23,7 +25,7 @@ protected override async Task OnInvoke() if (string.IsNullOrEmpty(pluginDir)) { _logger.LogError("无法获取程序集位置"); - throw new FileNotFoundException($"无法获取程序集位置"); + throw new FileNotFoundException("无法获取程序集位置"); } var batchPath = Path.Combine(pluginDir, "huifu.bat"); @@ -34,8 +36,6 @@ protected override async Task OnInvoke() throw new FileNotFoundException($"找不到启用鼠标批处理文件: {batchPath}"); } - _logger.LogInformation("正在运行启用鼠标批处理: {Path}", batchPath); - var psi = new ProcessStartInfo { FileName = batchPath, @@ -47,21 +47,7 @@ protected override async Task OnInvoke() WindowStyle = ProcessWindowStyle.Hidden }; - using var process = Process.Start(psi) ?? throw new Exception("无法启动批处理进程"); - string output = await process.StandardOutput.ReadToEndAsync(); - string error = await process.StandardError.ReadToEndAsync(); - await process.WaitForExitAsync(); - - _logger.LogInformation("启用鼠标批处理执行完成,退出码: {ExitCode}", process.ExitCode); - if (!string.IsNullOrWhiteSpace(output)) - _logger.LogDebug("批处理输出: {Output}", output); - if (!string.IsNullOrWhiteSpace(error)) - _logger.LogWarning("批处理错误: {Error}", error); - - if (process.ExitCode != 0) - { - _logger.LogWarning("批处理返回非零退出码: {ExitCode}", process.ExitCode); - } + await _processRunner.RunAsync(psi, "启用鼠标批处理", timeout: TimeSpan.FromMinutes(2)); } catch (Exception ex) { @@ -72,4 +58,4 @@ protected override async Task OnInvoke() await base.OnInvoke(); _logger.LogDebug("EnableMouseAction OnInvoke 完成"); } -} \ No newline at end of file +} diff --git a/Actions/ExtendDisplayAction.cs b/Actions/ExtendDisplayAction.cs index a181836..dea5454 100644 --- a/Actions/ExtendDisplayAction.cs +++ b/Actions/ExtendDisplayAction.cs @@ -4,16 +4,15 @@ using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using SystemTools.Services; namespace SystemTools.Actions; -/// -/// 扩展屏幕 -/// [ActionInfo("SystemTools.ExtendDisplay", "扩展屏幕", "\uE647", false)] -public class ExtendDisplayAction(ILogger logger) : ActionBase +public class ExtendDisplayAction(ILogger logger, IProcessRunner processRunner) : ActionBase { private readonly ILogger _logger = logger; + private readonly IProcessRunner _processRunner = processRunner; protected override async Task OnInvoke() { @@ -28,26 +27,12 @@ protected override async Task OnInvoke() CreateNoWindow = true, UseShellExecute = false, RedirectStandardOutput = true, - RedirectStandardError = true + RedirectStandardError = true, + WindowStyle = ProcessWindowStyle.Hidden }; - using var process = Process.Start(processInfo); - if (process != null) - { - string output = await process.StandardOutput.ReadToEndAsync(); - string error = await process.StandardError.ReadToEndAsync(); - - await process.WaitForExitAsync(); - - _logger.LogInformation("扩展屏幕命令已执行,退出码: {ExitCode}", process.ExitCode); - - if (!string.IsNullOrEmpty(error)) - _logger.LogWarning("错误输出: {Error}", error); - } - else - { - throw new Exception("无法启动 DisplaySwitch.exe 进程"); - } + await _processRunner.RunAsync(processInfo, "扩展屏幕(DisplaySwitch)"); + _logger.LogInformation("扩展屏幕命令执行完成"); } catch (Exception ex) { @@ -57,4 +42,4 @@ protected override async Task OnInvoke() await base.OnInvoke(); } -} \ No newline at end of file +} diff --git a/Actions/ExternalDisplayAction.cs b/Actions/ExternalDisplayAction.cs index ed6b986..98c1bbf 100644 --- a/Actions/ExternalDisplayAction.cs +++ b/Actions/ExternalDisplayAction.cs @@ -4,16 +4,15 @@ using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using SystemTools.Services; namespace SystemTools.Actions; -/// -/// 仅第二屏幕 -/// [ActionInfo("SystemTools.ExternalDisplay", "仅第二屏幕", "\uE641", false)] -public class ExternalDisplayAction(ILogger logger) : ActionBase +public class ExternalDisplayAction(ILogger logger, IProcessRunner processRunner) : ActionBase { private readonly ILogger _logger = logger; + private readonly IProcessRunner _processRunner = processRunner; protected override async Task OnInvoke() { @@ -28,26 +27,12 @@ protected override async Task OnInvoke() CreateNoWindow = true, UseShellExecute = false, RedirectStandardOutput = true, - RedirectStandardError = true + RedirectStandardError = true, + WindowStyle = ProcessWindowStyle.Hidden }; - using var process = Process.Start(processInfo); - if (process != null) - { - string output = await process.StandardOutput.ReadToEndAsync(); - string error = await process.StandardError.ReadToEndAsync(); - - await process.WaitForExitAsync(); - - _logger.LogInformation("仅第二屏幕命令已执行,退出码: {ExitCode}", process.ExitCode); - - if (!string.IsNullOrEmpty(error)) - _logger.LogWarning("错误输出: {Error}", error); - } - else - { - throw new Exception("无法启动 DisplaySwitch.exe 进程"); - } + await _processRunner.RunAsync(processInfo, "仅第二屏幕(DisplaySwitch)"); + _logger.LogInformation("仅第二屏幕命令执行完成"); } catch (Exception ex) { @@ -57,4 +42,4 @@ protected override async Task OnInvoke() await base.OnInvoke(); } -} \ No newline at end of file +} diff --git a/Actions/InternalDisplayAction.cs b/Actions/InternalDisplayAction.cs index 2d8440f..bff9d12 100644 --- a/Actions/InternalDisplayAction.cs +++ b/Actions/InternalDisplayAction.cs @@ -4,16 +4,15 @@ using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using SystemTools.Services; namespace SystemTools.Actions; -/// -/// 仅电脑屏幕 -/// [ActionInfo("SystemTools.InternalDisplay", "仅电脑屏幕", "\uE62F", false)] -public class InternalDisplayAction(ILogger logger) : ActionBase +public class InternalDisplayAction(ILogger logger, IProcessRunner processRunner) : ActionBase { private readonly ILogger _logger = logger; + private readonly IProcessRunner _processRunner = processRunner; protected override async Task OnInvoke() { @@ -28,26 +27,12 @@ protected override async Task OnInvoke() CreateNoWindow = true, UseShellExecute = false, RedirectStandardOutput = true, - RedirectStandardError = true + RedirectStandardError = true, + WindowStyle = ProcessWindowStyle.Hidden }; - using var process = Process.Start(processInfo); - if (process != null) - { - string output = await process.StandardOutput.ReadToEndAsync(); - string error = await process.StandardError.ReadToEndAsync(); - - await process.WaitForExitAsync(); - - _logger.LogInformation("仅电脑屏幕命令已执行,退出码: {ExitCode}", process.ExitCode); - - if (!string.IsNullOrEmpty(error)) - _logger.LogWarning("错误输出: {Error}", error); - } - else - { - throw new Exception("无法启动 DisplaySwitch.exe 进程"); - } + await _processRunner.RunAsync(processInfo, "仅电脑屏幕(DisplaySwitch)"); + _logger.LogInformation("仅电脑屏幕命令执行完成"); } catch (Exception ex) { @@ -57,4 +42,4 @@ protected override async Task OnInvoke() await base.OnInvoke(); } -} \ No newline at end of file +} diff --git a/Actions/MoveAction.cs b/Actions/MoveAction.cs index 47708e9..9d11692 100644 --- a/Actions/MoveAction.cs +++ b/Actions/MoveAction.cs @@ -5,14 +5,16 @@ using System.Diagnostics; using System.IO; using System.Threading.Tasks; +using SystemTools.Services; using SystemTools.Settings; namespace SystemTools.Actions; [ActionInfo("SystemTools.Move", "移动", "\uE6E7", false)] -public class MoveAction(ILogger logger) : ActionBase +public class MoveAction(ILogger logger, IProcessRunner processRunner) : ActionBase { private readonly ILogger _logger = logger; + private readonly IProcessRunner _processRunner = processRunner; protected override async Task OnInvoke() { @@ -25,16 +27,6 @@ protected override async Task OnInvoke() return; } - var psi = new ProcessStartInfo - { - FileName = "cmd.exe", - CreateNoWindow = true, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - WindowStyle = ProcessWindowStyle.Hidden - }; - try { var sourcePath = Settings.SourcePath.TrimEnd('\\'); @@ -60,16 +52,7 @@ protected override async Task OnInvoke() Directory.CreateDirectory(destDir); } - try - { - await Task.Run(() => File.Move(sourcePath, destPath)); - } - catch (Exception ex) - { - _logger.LogError(ex, "文件移动失败"); - throw new Exception($"移动失败: {ex}"); - } - + await Task.Run(() => File.Move(sourcePath, destPath)); _logger.LogInformation("文件移动成功: {Source} -> {Destination}", sourcePath, destPath); } else @@ -93,22 +76,22 @@ protected override async Task OnInvoke() Directory.Delete(finalDestPath, true); } - psi.FileName = "robocopy.exe"; - psi.Arguments = $"\"{sourcePath}\" \"{finalDestPath}\" /e /move /copyall /r:3 /w:3 /mt:4 /nfl /ndl /np"; - _logger.LogInformation("执行命令: robocopy \"{Source}\" \"{Destination}\" /move", sourcePath, - finalDestPath); - - using var process = Process.Start(psi) ?? throw new Exception("无法启动进程"); - string output = await process.StandardOutput.ReadToEndAsync(); - string error = await process.StandardError.ReadToEndAsync(); - await process.WaitForExitAsync(); - - if (process.ExitCode >= 8) + var psi = new ProcessStartInfo { - _logger.LogError("robocopy 移动失败,退出码: {ExitCode}, 输出: {Output}, 错误: {Error}", - process.ExitCode, output, error); - throw new Exception($"robocopy 移动失败,退出码: {process.ExitCode}"); - } + FileName = "robocopy.exe", + Arguments = $"\"{sourcePath}\" \"{finalDestPath}\" /e /move /copyall /r:3 /w:3 /mt:4 /nfl /ndl /np", + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + WindowStyle = ProcessWindowStyle.Hidden + }; + + await _processRunner.RunAsync( + psi, + operationName: "移动文件夹(robocopy)", + successExitCodes: new[] { 0, 1, 2, 3, 4, 5, 6, 7 }, + timeout: TimeSpan.FromMinutes(10)); _logger.LogInformation("文件夹移动成功: {Source} -> {Destination}", sourcePath, finalDestPath); } @@ -122,4 +105,4 @@ protected override async Task OnInvoke() await base.OnInvoke(); _logger.LogDebug("MoveAction OnInvoke 完成"); } -} \ No newline at end of file +} diff --git a/Actions/SimulateMouseAction.cs b/Actions/SimulateMouseAction.cs index 70bbeff..a0d63b8 100644 --- a/Actions/SimulateMouseAction.cs +++ b/Actions/SimulateMouseAction.cs @@ -6,15 +6,17 @@ using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; +using SystemTools.Services; using SystemTools.Settings; using Windows.Win32; namespace SystemTools.Actions; [ActionInfo("SystemTools.SimulateMouse", "模拟鼠标", "\uE5C1", false)] -public class SimulateMouseAction(ILogger logger) : ActionBase +public class SimulateMouseAction(ILogger logger, IProcessRunner processRunner) : ActionBase { private readonly ILogger _logger = logger; + private readonly IProcessRunner _processRunner = processRunner; private const int MOUSE_DELAY = 20; private const int SCROLL_DELAY = 50; private bool _isLeftButtonDown = false; @@ -213,21 +215,10 @@ private async Task ExecuteBatchFile(string batchFileName, string operation) WindowStyle = ProcessWindowStyle.Hidden }; - using var process = Process.Start(psi) ?? throw new Exception("无法启动批处理进程"); - string output = await process.StandardOutput.ReadToEndAsync(); - string error = await process.StandardError.ReadToEndAsync(); - await process.WaitForExitAsync(); - - _logger.LogInformation("{Operation}批处理执行完成,退出码: {ExitCode}", operation, process.ExitCode); - if (!string.IsNullOrWhiteSpace(output)) - _logger.LogDebug("批处理输出: {Output}", output); - if (!string.IsNullOrWhiteSpace(error)) - _logger.LogWarning("批处理错误: {Error}", error); - - if (process.ExitCode != 0) - { - _logger.LogWarning("{Operation}批处理返回非零退出码: {ExitCode}", operation, process.ExitCode); - } + await _processRunner.RunAsync( + psi, + operationName: $"{operation}批处理", + timeout: TimeSpan.FromMinutes(2)); } catch (Exception ex) { diff --git a/CODE_REVIEW_REPORT.md b/CODE_REVIEW_REPORT.md new file mode 100644 index 0000000..2115343 --- /dev/null +++ b/CODE_REVIEW_REPORT.md @@ -0,0 +1,63 @@ +# 代码库改进审查(SystemTools) + +本轮审查基于静态阅读,重点关注了稳定性、可维护性、性能与安全边界。 + +## P0 / 高优先级 + +1. **摄像头帧对象生命周期风险(潜在内存泄漏)** + - 位置:`Services/CameraCaptureService.cs` + - 现状:`FrameCaptured` 事件通过 `frame.Clone()` 向外分发 `Mat`,但当前代码没有统一约束订阅方释放该对象;若订阅方未 `Dispose`,会持续占用非托管内存。 + - 建议: + - 将事件参数改为托管可序列化对象(如 `byte[]`/`Bitmap`),或 + - 定义明确的资源所有权协议,并在文档中强制订阅方释放,或 + - 由服务层做帧池化管理,避免无界分配。 + +2. **阻塞式采集循环影响停机一致性** + - 位置:`Services/CameraCaptureService.cs` + - 现状:采集循环使用 `Thread.Sleep(33)`,`Stop()` 只 `Wait(500)`;在设备异常/卡住时,存在任务未完全退出的窗口。 + - 建议:改为 `await Task.Delay(33, token)` 的可取消等待,并在 `Stop()` 中更严格处理超时和异常(记录日志、避免静默失败)。 + +3. **模型解压后目录名硬编码(国际化/可移植性脆弱)** + - 位置:`SettingsPage/SystemToolsSettingsViewModel.cs` + - 现状:整理目录时写死了 `"新建文件夹"` 作为来源目录名,依赖压缩包内部结构和中文目录名称。 + - 建议:解压后按“预期文件名模式”查找目标(例如检测 `.dat` 文件),避免依赖固定父目录名。 + +## P1 / 中优先级 + +4. **重复创建 `HttpClient`,可复用性不足** + - 位置:`SettingsPage/SystemToolsSettingsViewModel.cs` + - 现状:两个下载方法内均 `new HttpClient()`。 + - 建议:使用 `IHttpClientFactory` 或共享静态 `HttpClient`(带合理超时),便于连接复用与统一策略(重试、代理、证书)。 + +5. **`async void` 事件处理器过多,异常可观测性弱** + - 位置:`Triggers/UsbDeviceTrigger.cs`、`SettingsPage/SystemToolsSettingsPage.axaml.cs` 等 + - 现状:存在多个 `async void`;在非 UI 框架托管场景中,异常可能直接升级为未处理异常或仅丢日志。 + - 建议: + - 对必须 `async void` 的 UI 事件统一包装异常处理与日志; + - 业务层尽量改为返回 `Task` 的异步链路。 + +6. **外部进程调用分散,错误处理风格不一致** + - 位置:`Actions/*Action.cs` 多处 + - 现状:大量 `Process.Start` 直接调用,部分检查退出码、部分不检查,异常文案与日志粒度不统一。 + - 建议:抽象统一的 `IProcessRunner`(超时、退出码策略、标准输出/错误、结构化日志),减少重复并提升可测试性。 + +## P2 / 低优先级 + +7. **吞异常场景可增加最小日志** + - 位置:`Services/CameraCaptureService.cs` + - 现状:`catch { }` 直接吞掉异常(虽然注释说明了原因)。 + - 建议:至少记录 debug 级别日志,便于排查偶发硬件问题。 + +8. **重复文件操作逻辑可下沉复用** + - 位置:`Actions/CopyAction.cs`、`Actions/MoveAction.cs`、`Actions/DeleteAction.cs` + - 现状:路径校验、异常转换、日志模板在多文件重复。 + - 建议:提取共享工具层(路径规范化、重试策略、统一错误码),降低维护成本。 + +--- + +## 建议的下一步落地顺序 + +1. 先做 `CameraCaptureService` 的取消与资源管理重构(P0)。 +2. 修复模型解压目录硬编码(P0)。 +3. 引入统一进程执行器并迁移高频动作(P1)。 +4. 最后做 `HttpClient` 复用和文件操作抽象(P1/P2)。 diff --git a/Controls/FaceRecognitionAuthorizer.axaml.cs b/Controls/FaceRecognitionAuthorizer.axaml.cs index 5da4ea3..846174f 100644 --- a/Controls/FaceRecognitionAuthorizer.axaml.cs +++ b/Controls/FaceRecognitionAuthorizer.axaml.cs @@ -56,7 +56,7 @@ protected override async void OnLoaded(RoutedEventArgs e) await Dispatcher.UIThread.InvokeAsync(() => { Settings.OperationFinished = true; - }); + }); return; } @@ -87,7 +87,7 @@ private void StartCamera() private void OnFrameCaptured(object? sender, Mat frame) { var oldFrame = _currentFrame; - _currentFrame = frame; + _currentFrame = frame.Clone(); oldFrame?.Dispose(); if (_isDrawing) return; @@ -169,13 +169,17 @@ private async void OnCaptureClick(object? sender, RoutedEventArgs e) using var snapshot = _currentFrame.Clone(); var encoding = await Task.Run(() => { - try - { - byte[] rgbBytes = MatToRgbBytes(snapshot); - return _faceService.ExtractFaceEncoding(rgbBytes, snapshot.Width, snapshot.Height); - } - catch { return null; } - }); + try + { + byte[] rgbBytes = MatToRgbBytes(snapshot); + return _faceService.ExtractFaceEncoding(rgbBytes, snapshot.Width, snapshot.Height); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"提取人脸特征失败: {ex}"); + return null; + } + }); if (encoding != null) { @@ -234,6 +238,7 @@ private async Task DoVerifyAsync(Mat mat, CancellationToken cancellationToken) } catch (OperationCanceledException) { + System.Diagnostics.Debug.WriteLine("人脸验证已取消"); } catch (Exception ex) { @@ -293,4 +298,4 @@ public void Dispose() _verifySemaphore?.Dispose(); GC.SuppressFinalize(this); } -} \ No newline at end of file +} diff --git a/Plugin.cs b/Plugin.cs index adb6b79..65f9b8e 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -54,6 +54,7 @@ public override void Initialize(HostBuilderContext context, IServiceCollection s GlobalConstants.MainConfig = new MainConfigHandler(PluginConfigFolder); services.AddLogging(); + services.AddSingleton(); // ========== 注册可选人脸识别 ========== if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) diff --git a/Services/CameraCaptureService.cs b/Services/CameraCaptureService.cs index cda1ed6..b51e861 100644 --- a/Services/CameraCaptureService.cs +++ b/Services/CameraCaptureService.cs @@ -1,5 +1,6 @@ using OpenCvSharp; using System; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -31,24 +32,50 @@ public bool Start(int cameraIndex = 0, int width = 640, int height = 480) return true; } - private void CaptureLoop() + private async Task CaptureLoop() { using var frame = new Mat(); + var token = _cts?.Token ?? CancellationToken.None; - while (_cts?.IsCancellationRequested == false) + while (!token.IsCancellationRequested) { - if (_capture?.Read(frame) == true && !frame.Empty()) + if (_capture?.Read(frame) == true && !frame.Empty() && FrameCaptured != null) { - FrameCaptured?.Invoke(this, frame.Clone()); + try + { + // 由服务层管理原始帧生命周期;订阅方如需跨线程/跨作用域使用应自行 Clone。 + using var snapshot = frame.Clone(); + FrameCaptured.Invoke(this, snapshot); + } + catch (Exception ex) + { + Debug.WriteLine($"[SystemTools] FrameCaptured 处理异常: {ex}"); + } + } + + try + { + await Task.Delay(33, token); + } + catch (OperationCanceledException) + { + break; } - Thread.Sleep(33); } } public void Stop() { _cts?.Cancel(); - _captureTask?.Wait(500); + + try + { + _captureTask?.Wait(1000); + } + catch (Exception ex) + { + Debug.WriteLine($"[SystemTools] 等待摄像头采集任务结束失败: {ex}"); + } try { @@ -61,10 +88,17 @@ public void Stop() _capture.Dispose(); } } - catch { /* 防止硬件已被拔出等异常导致报错 */ } + catch (Exception ex) + { + // 防止硬件已被拔出等异常导致报错 + Debug.WriteLine($"[SystemTools] 停止摄像头时出现异常: {ex}"); + } finally { _capture = null; + _captureTask = null; + _cts?.Dispose(); + _cts = null; } } @@ -74,4 +108,4 @@ public void Dispose() Stop(); _cts?.Dispose(); } -} \ No newline at end of file +} diff --git a/Services/IProcessRunner.cs b/Services/IProcessRunner.cs new file mode 100644 index 0000000..aaa8903 --- /dev/null +++ b/Services/IProcessRunner.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace SystemTools.Services; + +public interface IProcessRunner +{ + Task RunAsync( + ProcessStartInfo startInfo, + string operationName, + IReadOnlyCollection? successExitCodes = null, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default); +} + +public sealed record ProcessRunResult(int ExitCode, string StandardOutput, string StandardError); diff --git a/Services/ProcessRunner.cs b/Services/ProcessRunner.cs new file mode 100644 index 0000000..6f4a33a --- /dev/null +++ b/Services/ProcessRunner.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace SystemTools.Services; + +public class ProcessRunner(ILogger logger) : IProcessRunner +{ + private readonly ILogger _logger = logger; + + public async Task RunAsync( + ProcessStartInfo startInfo, + string operationName, + IReadOnlyCollection? successExitCodes = null, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default) + { + successExitCodes ??= new[] { 0 }; + timeout ??= TimeSpan.FromSeconds(30); + + _logger.LogInformation("[{Operation}] 启动进程: {FileName} {Arguments}", + operationName, startInfo.FileName, startInfo.Arguments); + + using var process = Process.Start(startInfo) + ?? throw new InvalidOperationException($"[{operationName}] 无法启动进程: {startInfo.FileName}"); + + var stdoutTask = startInfo.RedirectStandardOutput + ? process.StandardOutput.ReadToEndAsync() + : Task.FromResult(string.Empty); + var stderrTask = startInfo.RedirectStandardError + ? process.StandardError.ReadToEndAsync() + : Task.FromResult(string.Empty); + + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCts.CancelAfter(timeout.Value); + + try + { + await process.WaitForExitAsync(timeoutCts.Token); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + TryKill(process, operationName); + throw new TimeoutException($"[{operationName}] 进程超时(>{timeout.Value.TotalSeconds:F0}s): {startInfo.FileName}"); + } + + var stdout = await stdoutTask; + var stderr = await stderrTask; + var result = new ProcessRunResult(process.ExitCode, stdout, stderr); + + _logger.LogInformation("[{Operation}] 进程结束,退出码: {ExitCode}", operationName, result.ExitCode); + + if (!string.IsNullOrWhiteSpace(result.StandardOutput)) + _logger.LogDebug("[{Operation}] 标准输出: {Output}", operationName, result.StandardOutput); + if (!string.IsNullOrWhiteSpace(result.StandardError)) + _logger.LogWarning("[{Operation}] 标准错误: {Error}", operationName, result.StandardError); + + if (!successExitCodes.Contains(result.ExitCode)) + { + throw new InvalidOperationException( + $"[{operationName}] 进程执行失败,退出码: {result.ExitCode},期望: {string.Join(",", successExitCodes)}"); + } + + return result; + } + + private void TryKill(Process process, string operationName) + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + _logger.LogWarning("[{Operation}] 进程超时后已强制结束。", operationName); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "[{Operation}] 进程超时后结束失败。", operationName); + } + } +} From 48553906f59ef226c7dabe1c95495c7a98bed187 Mon Sep 17 00:00:00 2001 From: Wang Haoyu Date: Mon, 2 Mar 2026 17:35:20 +0800 Subject: [PATCH 2/2] fix: bind camera capture loop to explicit cancellation token --- Services/CameraCaptureService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Services/CameraCaptureService.cs b/Services/CameraCaptureService.cs index b51e861..c6b2256 100644 --- a/Services/CameraCaptureService.cs +++ b/Services/CameraCaptureService.cs @@ -27,15 +27,15 @@ public bool Start(int cameraIndex = 0, int width = 640, int height = 480) _capture.FrameHeight = height; _cts = new CancellationTokenSource(); - _captureTask = Task.Run(CaptureLoop, _cts.Token); + var token = _cts.Token; + _captureTask = Task.Run(() => CaptureLoop(token), token); return true; } - private async Task CaptureLoop() + private async Task CaptureLoop(CancellationToken token) { using var frame = new Mat(); - var token = _cts?.Token ?? CancellationToken.None; while (!token.IsCancellationRequested) {