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..c6b2256 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;
@@ -26,29 +27,55 @@ 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 void CaptureLoop()
+ private async Task CaptureLoop(CancellationToken token)
{
using var frame = new Mat();
- 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);
+ }
+ }
+}