254 lines
9.9 KiB
C#
254 lines
9.9 KiB
C#
|
|
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
|
|||
|
|
}
|
|||
|
|
}
|