using System.ComponentModel; using System.Diagnostics; using System.IO; using PersonalToolbox.Models; namespace PersonalToolbox.Services; public sealed class ToolLaunchService { public async Task LaunchAsync( ToolItem tool, IReadOnlyCollection allTools, Action 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 LaunchCombinationAsync( ToolItem combination, IReadOnlyCollection allTools, Action 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 allTools) { if (combination.Type != ToolType.Combination) { return LaunchResult.Ok(combination); } var stack = new Stack(); var finalToolIds = new HashSet(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(); 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 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 }; } }