将内部项目目录、命名空间、配置目录、自启注册表值和设计/开发文档统一为 PersonalToolbox。 扩展路径校验服务,输出失效工具、字段、原因和路径,并在启动日志、设置页路径检查与导入配置流程中展示明细报告。 验证:dotnet build PersonalToolbox.sln
252 lines
8.6 KiB
C#
252 lines
8.6 KiB
C#
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
|
||
};
|
||
}
|
||
}
|