Files
Ayay/SHH.ProcessLaunchers/ProcessManager.cs

254 lines
9.9 KiB
C#
Raw Permalink 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;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SHH.ProcessLaunchers
{
/// <summary>
/// 进程管理器 (核心实现类)
/// <para>核心职责:作为对外统一入口 (Facade),维护所有受管进程的容器。</para>
/// <para>主要功能:负责路由外部指令(启动/停止)到具体的进程实例,并处理事件分发。</para>
/// </summary>
public class ProcessManager : IProcessManager, IDisposable
{
#region --- 1. (Fields & Events) ---
/// <summary>
/// 线程安全的进程容器
/// <para>Key: ProcessConfig.Id (唯一标识)</para>
/// <para>Value: ManagedProcess (受管实例)</para>
/// </summary>
private readonly ConcurrentDictionary<string, ManagedProcess> _processes
= new ConcurrentDictionary<string, ManagedProcess>();
/// <summary>
/// 日志服务接口 (依赖注入)
/// </summary>
private readonly ILauncherLogger _logger;
// ---------------------------------------------------------
// 对外暴露的事件定义
// ---------------------------------------------------------
/// <summary>
/// 对外事件:当接收到任意子进程的标准输出/错误流时触发
/// </summary>
public event EventHandler<ProcessOutputEventArgs> OnOutputReceived;
/// <summary>
/// 对外事件:当任意子进程的状态发生变更时触发
/// </summary>
public event EventHandler<ProcessStateEventArgs> OnStateChanged;
#endregion
#region --- 2. (Constructor & Dispose) ---
/// <summary>
/// 初始化进程管理器实例
/// </summary>
/// <param name="logger">日志实现类 (若外部未传入,则内部自动使用 NullLogger 以防止空引用异常)</param>
public ProcessManager(ILauncherLogger logger = null)
{
// 规范化:使用空合并运算符确保 _logger 永不为 null
_logger = logger ?? new NullLogger();
}
/// <summary>
/// 销毁资源,停止所有进程并清理事件订阅
/// </summary>
public void Dispose()
{
// 1. 停止所有子进程 (触发 Kill 操作,清理进程树)
StopAll();
// 2. 清空内部容器引用
_processes.Clear();
// 3. 移除所有外部事件订阅,防止 UI 端因未解绑而导致的内存泄露
OnOutputReceived = null;
OnStateChanged = null;
}
#endregion
#region --- 3. API (Public Methods) ---
/// <summary>
/// 注册一个新的进程配置到管理器中
/// </summary>
/// <param name="config">进程配置对象 (包含 Exe路径、参数、熔断策略等)</param>
/// <exception cref="ArgumentException">当 Id 为空时抛出</exception>
/// <exception cref="InvalidOperationException">当 Id 已存在时抛出</exception>
public void Register(ProcessConfig config)
{
// 1. 基础参数校验:确保 Id 存在
if (string.IsNullOrWhiteSpace(config.Id))
throw new ArgumentException("进程配置无效:必须包含唯一的 Id");
// 2. 防重复注册校验:确保字典中没有相同的 Key
if (_processes.ContainsKey(config.Id))
throw new InvalidOperationException($"进程 Id '{config.Id}' 已存在,禁止重复注册。");
// 3. 实例化受管进程对象 (传入 this 指针是为了后续回调 DispatchXXX 方法)
var process = new ManagedProcess(config, this, _logger);
// 4. 加入线程安全字典
if (_processes.TryAdd(config.Id, process))
{
_logger.LogLifecycle(config.Id, LogAction.Output, LogTrigger.System,
$"进程配置已注册: {config.DisplayName}");
}
}
/// <summary>
/// 启动指定 ID 的进程
/// </summary>
/// <param name="id">进程的唯一标识符 (ProcessConfig.Id)</param>
public void Start(string id)
{
// 尝试获取指定 ID 的进程实例
if (_processes.TryGetValue(id, out var p))
{
// 调用内部实例的启动逻辑,操作归因标记为"User" (用户手动)
p.ExecuteStart(LogTrigger.User, "用户手动启动指令");
}
else
{
// 如果找不到,记录错误日志
_logger.LogLifecycle(id, LogAction.Error, LogTrigger.User, "启动失败:未找到指定 ID 的进程配置");
}
}
/// <summary>
/// [异步] 有序批量启动所有进程
/// <para>按照 StartupOrder 从小到大排序启动,并支持启动间隙延时 (PostStartupDelayMs)。</para>
/// </summary>
/// <returns>异步任务</returns>
public async Task StartAllAsync()
{
_logger.LogLifecycle("ALL", LogAction.Start, LogTrigger.User, "执行有序批量启动");
// 1. 数据准备:从字典取出所有进程,并按配置进行排序
// 排序规则StartupOrder (小->大) -> Id (字母序) 以保证启动顺序的确定性
var sortedList = _processes.Values
.OrderBy(p => p.Config.StartupOrder) // 按用户指定的权重排
.ThenBy(p => p.Config.Id) // 权重一样时按 ID 排
.ToList();
// 2. 顺序执行启动循环
foreach (var p in sortedList)
{
// 同步调用启动指令(注意:这里不等待进程完全 Ready只负责拉起进程
p.ExecuteStart(LogTrigger.User, "有序批量启动");
// 3. 处理启动间隙延迟 (错峰启动)
// 作用:防止多个重型进程同时启动导致 CPU/IO 瞬间拥堵
int delay = p.Config.PostStartupDelayMs;
if (delay > 0)
{
// 异步等待指定毫秒数,释放线程控制权
await Task.Delay(delay);
}
}
_logger.LogLifecycle("ALL", LogAction.Start, LogTrigger.User, "有序批量启动完成");
}
/// <summary>
/// 停止指定 ID 的进程
/// </summary>
/// <param name="id">进程的唯一标识符</param>
public void Stop(string id)
{
if (_processes.TryGetValue(id, out var p))
{
p.ExecuteStop(LogTrigger.User, "用户手动停止指令");
}
}
/// <summary>
/// 批量停止所有进程 (并发执行)
/// </summary>
public void StopAll()
{
_logger.LogLifecycle("ALL", LogAction.Stop, LogTrigger.User, "执行批量停止");
// 遍历所有进程,使用 Task.Run 并发执行停止,提高效率,无需等待
foreach (var p in _processes.Values)
{
Task.Run(() => p.ExecuteStop(LogTrigger.User, "批量停止"));
}
}
/// <summary>
/// 重置/复位指定进程的资源报警状态
/// <para>当用户在 UI 上点击"已处置"后调用此方法,解除报警锁定。</para>
/// </summary>
/// <param name="id">进程的唯一标识符</param>
public void ResetGuard(string id)
{
if (_processes.TryGetValue(id, out var p))
{
// 调用内部复位逻辑,清除报警锁定状态
p.ResetGuards();
_logger.LogLifecycle(id, LogAction.ResourceCheck, LogTrigger.User, "用户手动复位资源报警锁");
}
}
/// <summary>
/// 获取当前所有进程的实时状态快照
/// <para>用于 UI 列表的数据绑定或定时刷新。</para>
/// </summary>
/// <returns>进程信息快照列表</returns>
public List<ProcessInfoSnapshot> GetSnapshot()
{
// 将字典中的所有受管对象转为 DTO 快照列表
return _processes.Values.Select(p => p.GetSnapshot()).ToList();
}
#endregion
#region --- 4. (Internal Dispatchers) ---
// 说明C# 的 event 只能在定义类内部 Invoke。
// 为了让内部类 ManagedProcess 也能触发 Manager 的对外事件,我们提供了这几个 internal 方法。
// 这些方法充当了内部类与外部事件之间的桥梁。
/// <summary>
/// 分发状态变更事件 (供 ManagedProcess 内部调用)
/// </summary>
/// <param name="processId">进程 ID</param>
/// <param name="newState">新的状态</param>
internal void DispatchStateChange(string processId, ProcessStatus newState)
{
// 线程安全地触发事件
OnStateChanged?.Invoke(this, new ProcessStateEventArgs
{
ProcessId = processId,
State = newState
});
}
/// <summary>
/// 分发日志输出事件 (供 ManagedProcess 内部调用)
/// </summary>
/// <param name="processId">进程 ID</param>
/// <param name="content">日志内容</param>
/// <param name="isError">是否为错误流</param>
internal void DispatchOutput(string processId, string content, bool isError)
{
// 线程安全地触发事件
OnOutputReceived?.Invoke(this, new ProcessOutputEventArgs
{
ProcessId = processId,
Content = content,
IsError = isError
});
}
#endregion
}
}