Files
personal-toolbox/src/PersonalToolbox/Services/ToolLaunchService.cs
home-PC 26a22eef1c feat: 统一项目命名并补充路径失效报告
将内部项目目录、命名空间、配置目录、自启注册表值和设计/开发文档统一为 PersonalToolbox。

扩展路径校验服务,输出失效工具、字段、原因和路径,并在启动日志、设置页路径检查与导入配置流程中展示明细报告。

验证:dotnet build PersonalToolbox.sln
2026-05-27 14:20:19 +08:00

252 lines
8.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
};
}
}