feat: 统一项目命名并补充路径失效报告
将内部项目目录、命名空间、配置目录、自启注册表值和设计/开发文档统一为 PersonalToolbox。 扩展路径校验服务,输出失效工具、字段、原因和路径,并在启动日志、设置页路径检查与导入配置流程中展示明细报告。 验证:dotnet build PersonalToolbox.sln
This commit is contained in:
251
src/PersonalToolbox/Services/ToolLaunchService.cs
Normal file
251
src/PersonalToolbox/Services/ToolLaunchService.cs
Normal file
@@ -0,0 +1,251 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using PersonalToolbox.Models;
|
||||
|
||||
namespace PersonalToolbox.Services;
|
||||
|
||||
public sealed class ToolLaunchService
|
||||
{
|
||||
public async Task<LaunchResult> LaunchAsync(
|
||||
ToolItem tool,
|
||||
IReadOnlyCollection<ToolItem> allTools,
|
||||
Action<LogMessage> log,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (tool.IsDeleted)
|
||||
{
|
||||
var result = LaunchResult.Fail(tool, LaunchResultKind.Skipped, "工具已删除。");
|
||||
log(CreateLog(LogLevel.Warning, $"跳过已删除工具:{tool.Name}"));
|
||||
return result;
|
||||
}
|
||||
|
||||
if (tool.Type == ToolType.Combination)
|
||||
{
|
||||
return await LaunchCombinationAsync(tool, allTools, log, cancellationToken);
|
||||
}
|
||||
|
||||
return LaunchSingleTool(tool, log);
|
||||
}
|
||||
|
||||
private async Task<LaunchResult> LaunchCombinationAsync(
|
||||
ToolItem combination,
|
||||
IReadOnlyCollection<ToolItem> allTools,
|
||||
Action<LogMessage> log,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var validation = ValidateCombination(combination, allTools);
|
||||
if (!validation.Success)
|
||||
{
|
||||
log(CreateLog(LogLevel.Error, $"组合校验失败:{combination.Name},{validation.ErrorMessage}"));
|
||||
return validation;
|
||||
}
|
||||
|
||||
log(CreateLog(LogLevel.Info, $"启动组合:{combination.Name}"));
|
||||
var successCount = 0;
|
||||
var failedCount = 0;
|
||||
var members = combination.Combination?.Members
|
||||
.Where(member => member.Enabled)
|
||||
.OrderBy(member => member.SortOrder)
|
||||
.ToList() ?? [];
|
||||
|
||||
foreach (var member in members)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var memberTool = allTools.FirstOrDefault(tool => tool.Id == member.ToolId && !tool.IsDeleted);
|
||||
if (memberTool is null)
|
||||
{
|
||||
failedCount++;
|
||||
log(CreateLog(LogLevel.Error, $"组合成员不存在:{member.ToolId}"));
|
||||
if (combination.Combination?.FailurePolicy == FailurePolicy.Stop)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = await LaunchAsync(memberTool, allTools, log, cancellationToken);
|
||||
if (result.Success)
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
failedCount++;
|
||||
if (combination.Combination?.FailurePolicy == FailurePolicy.Stop)
|
||||
{
|
||||
log(CreateLog(LogLevel.Warning, $"组合已停止:{combination.Name}"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (member.IntervalAfterMs > 0)
|
||||
{
|
||||
await Task.Delay(member.IntervalAfterMs, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
log(CreateLog(LogLevel.Info, $"组合执行完成:{combination.Name},成功 {successCount} 项,失败 {failedCount} 项"));
|
||||
return failedCount == 0
|
||||
? LaunchResult.Ok(combination)
|
||||
: LaunchResult.Fail(combination, LaunchResultKind.UnknownError, $"组合执行完成,但有 {failedCount} 项失败。");
|
||||
}
|
||||
|
||||
public LaunchResult ValidateCombination(ToolItem combination, IReadOnlyCollection<ToolItem> allTools)
|
||||
{
|
||||
if (combination.Type != ToolType.Combination)
|
||||
{
|
||||
return LaunchResult.Ok(combination);
|
||||
}
|
||||
|
||||
var stack = new Stack<string>();
|
||||
var finalToolIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var toolMap = allTools.Where(tool => !tool.IsDeleted).ToDictionary(tool => tool.Id, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
LaunchResult? failure = null;
|
||||
|
||||
void Visit(ToolItem current)
|
||||
{
|
||||
if (failure is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.Type != ToolType.Combination)
|
||||
{
|
||||
if (!finalToolIds.Add(current.Id))
|
||||
{
|
||||
failure = LaunchResult.Fail(combination, LaunchResultKind.DuplicateTool, $"展开后存在重复工具:{current.Name}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (stack.Contains(current.Id, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var path = string.Join(" -> ", stack.Reverse().Concat([current.Id]).Select(id => toolMap.TryGetValue(id, out var tool) ? tool.Name : id));
|
||||
failure = LaunchResult.Fail(combination, LaunchResultKind.CircularReference, $"存在循环引用:{path}");
|
||||
return;
|
||||
}
|
||||
|
||||
stack.Push(current.Id);
|
||||
var members = current.Combination?.Members
|
||||
.Where(member => member.Enabled)
|
||||
.OrderBy(member => member.SortOrder)
|
||||
?? Enumerable.Empty<CombinationMember>();
|
||||
foreach (var member in members)
|
||||
{
|
||||
if (!toolMap.TryGetValue(member.ToolId, out var memberTool))
|
||||
{
|
||||
failure = LaunchResult.Fail(combination, LaunchResultKind.MissingReference, $"成员引用不存在:{member.ToolId}");
|
||||
break;
|
||||
}
|
||||
|
||||
Visit(memberTool);
|
||||
}
|
||||
|
||||
stack.Pop();
|
||||
}
|
||||
|
||||
Visit(combination);
|
||||
return failure ?? LaunchResult.Ok(combination);
|
||||
}
|
||||
|
||||
private LaunchResult LaunchSingleTool(ToolItem tool, Action<LogMessage> log)
|
||||
{
|
||||
try
|
||||
{
|
||||
var startInfo = CreateStartInfo(tool, out var validationFailure);
|
||||
if (validationFailure is not null)
|
||||
{
|
||||
log(CreateLog(LogLevel.Error, $"启动失败:{tool.Name},{validationFailure.ErrorMessage}"));
|
||||
return validationFailure;
|
||||
}
|
||||
|
||||
log(CreateLog(LogLevel.Info, $"开始启动工具:{tool.Name}"));
|
||||
Process.Start(startInfo!);
|
||||
log(CreateLog(LogLevel.Success, $"启动成功:{tool.Name}"));
|
||||
return LaunchResult.Ok(tool);
|
||||
}
|
||||
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
|
||||
{
|
||||
log(CreateLog(LogLevel.Warning, $"用户取消管理员权限:{tool.Name}"));
|
||||
return LaunchResult.Fail(tool, LaunchResultKind.UserCancelledElevation, "用户取消管理员权限。");
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
log(CreateLog(LogLevel.Error, $"访问被拒绝:{tool.Name},{ex.Message}"));
|
||||
return LaunchResult.Fail(tool, LaunchResultKind.AccessDenied, ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log(CreateLog(LogLevel.Error, $"启动失败:{tool.Name},{ex.Message}"));
|
||||
return LaunchResult.Fail(tool, LaunchResultKind.ProcessStartFailed, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static ProcessStartInfo? CreateStartInfo(ToolItem tool, out LaunchResult? validationFailure)
|
||||
{
|
||||
validationFailure = null;
|
||||
|
||||
if (tool.Type == ToolType.Url)
|
||||
{
|
||||
if (!PathValidationService.IsValidUrl(tool.Url, out var normalizedUrl))
|
||||
{
|
||||
validationFailure = LaunchResult.Fail(tool, LaunchResultKind.InvalidUrl, "网址格式无效。");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ProcessStartInfo
|
||||
{
|
||||
FileName = normalizedUrl,
|
||||
UseShellExecute = true
|
||||
};
|
||||
}
|
||||
|
||||
var target = tool.LaunchTarget;
|
||||
if (string.IsNullOrWhiteSpace(target))
|
||||
{
|
||||
validationFailure = LaunchResult.Fail(tool, LaunchResultKind.PathMissing, "启动目标为空。");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tool.Type == ToolType.Local && !File.Exists(target) && !Directory.Exists(target))
|
||||
{
|
||||
validationFailure = LaunchResult.Fail(tool, LaunchResultKind.PathMissing, "路径不存在。");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tool.WorkingDirectory) && !Directory.Exists(tool.WorkingDirectory))
|
||||
{
|
||||
validationFailure = LaunchResult.Fail(tool, LaunchResultKind.PathMissing, "工作目录不存在。");
|
||||
return null;
|
||||
}
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = target,
|
||||
Arguments = tool.Arguments ?? "",
|
||||
WorkingDirectory = tool.WorkingDirectory ?? "",
|
||||
UseShellExecute = true
|
||||
};
|
||||
|
||||
if (tool.RunAsAdmin)
|
||||
{
|
||||
startInfo.Verb = "runas";
|
||||
}
|
||||
|
||||
return startInfo;
|
||||
}
|
||||
|
||||
private static LogMessage CreateLog(LogLevel level, string message)
|
||||
{
|
||||
return new LogMessage
|
||||
{
|
||||
Level = level,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user