Files
Ayay/SHH.ProcessLaunchers/ProcessManager.cs

254 lines
9.9 KiB
C#
Raw Normal View History

2026-01-03 08:44:38 +08:00
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
}
}