CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
1031 lines
37 KiB
C#
1031 lines
37 KiB
C#
using Microsoft.VisualBasic.CompilerServices;
|
||
using PCL.Core.App;
|
||
using PCL.Core.Utils;
|
||
using System.Collections;
|
||
using System.IO;
|
||
using System.Windows.Shell;
|
||
using PCL.Network;
|
||
using PCL.Network.Loaders;
|
||
|
||
using PCL.Core.App.Localization;
|
||
namespace PCL;
|
||
|
||
public static class ModLoader
|
||
{
|
||
public enum LoaderFolderRunType
|
||
{
|
||
RunOnUpdated,
|
||
ForceRun,
|
||
UpdateOnly
|
||
}
|
||
|
||
// 任务栏进度条
|
||
public static ModBase.SafeList<LoaderBase> loaderTaskbar = new();
|
||
public static double loaderTaskbarProgress; // 平滑后的进度
|
||
private static TaskbarItemProgressState loaderTaskbarProgressLast = TaskbarItemProgressState.None;
|
||
|
||
// 文件夹刷新类委托
|
||
private static readonly Dictionary<LoaderBase, LoaderFolderDictionaryEntry> loaderFolderDictionary = new();
|
||
|
||
public static void LoaderTaskbarAdd<T>(LoaderCombo<T> loader)
|
||
{
|
||
if (ModMain.frmSpeedLeft is not null)
|
||
ModMain.frmSpeedLeft.TaskRemove(loader);
|
||
loaderTaskbar.Add(loader);
|
||
ModBase.Log($"[Taskbar] {loader.name} 已加入任务列表");
|
||
}
|
||
|
||
public static void LoaderTaskbarProgressRefresh()
|
||
{
|
||
try
|
||
{
|
||
TaskbarItemProgressState newState;
|
||
var newProgress = LoaderTaskbarProgressGet();
|
||
// 若单个任务已中止,或全部任务已完成,则刷新并移除
|
||
foreach (var Task in loaderTaskbar)
|
||
if (loaderTaskbar.All(l => l.State != ModBase.LoadState.Loading) ||
|
||
Task.State == ModBase.LoadState.Waiting || Task.State == ModBase.LoadState.Aborted)
|
||
{
|
||
ModMain.frmSpeedLeft?.TaskRefresh(Task);
|
||
loaderTaskbar.Remove(Task);
|
||
ModBase.Log($"[Taskbar] {Task.name} 已移出任务列表");
|
||
}
|
||
|
||
// 更新平滑后的进度
|
||
if (newProgress <= 0d || newProgress >= 1d || loaderTaskbarProgress > newProgress)
|
||
loaderTaskbarProgress = newProgress;
|
||
else
|
||
loaderTaskbarProgress = loaderTaskbarProgress * 0.9d + newProgress * 0.1d;
|
||
ModBase.RunInUi(() => ModMain.frmMain.BtnExtraDownload.Progress = loaderTaskbarProgress);
|
||
// 更新任务栏信息
|
||
if (!loaderTaskbar.Any() || loaderTaskbarProgress == 1d)
|
||
{
|
||
newState = TaskbarItemProgressState.None;
|
||
}
|
||
else if (loaderTaskbarProgress < 0.015d)
|
||
{
|
||
newState = TaskbarItemProgressState.Indeterminate;
|
||
}
|
||
else
|
||
{
|
||
newState = TaskbarItemProgressState.Normal;
|
||
ModMain.frmMain.TaskbarItemInfo.ProgressValue = loaderTaskbarProgress;
|
||
}
|
||
|
||
if (loaderTaskbarProgressLast != newState)
|
||
{
|
||
loaderTaskbarProgressLast = newState;
|
||
ModMain.frmMain.TaskbarItemInfo.ProgressState = newState;
|
||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ModBase.Log(
|
||
ex,
|
||
"刷新任务栏进度显示失败",
|
||
ModBase.LogLevel.Feedback,
|
||
userSummary: Lang.Text("Application.Loader.Error.OperationFailed"));
|
||
}
|
||
}
|
||
|
||
public static double LoaderTaskbarProgressGet()
|
||
{
|
||
try
|
||
{
|
||
if (!loaderTaskbar.Any())
|
||
return 1d;
|
||
|
||
return ModBase.MathClamp(
|
||
loaderTaskbar.Select(l => l.Progress).Average(),
|
||
0,
|
||
1
|
||
);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ModBase.Log(
|
||
ex,
|
||
"获取任务栏进度出错",
|
||
ModBase.LogLevel.Feedback,
|
||
userSummary: Lang.Text("Application.Loader.Error.OperationFailed"));
|
||
return 0.5d;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行以文件夹检测作为输入的加载器。加载器需以文件夹路径为输入值。
|
||
/// 返回是否执行了加载器。
|
||
/// </summary>
|
||
/// <param name="extraPath">用于检查文件夹修改的额外路径。该路径不会传入加载器。</param>
|
||
/// <param name="loaderInput">如果不想要文件夹路径为输入值,则传入期望数据</param>
|
||
public static bool LoaderFolderRun(LoaderBase loader, string folderPath, LoaderFolderRunType type, int maxDepth = 0,
|
||
string extraPath = "", bool waitForExit = false, object loaderInput = null)
|
||
{
|
||
DirectoryInfo folderInfo;
|
||
var value = new LoaderFolderDictionaryEntry { folderPath = folderPath + extraPath, lastCheckTime = default };
|
||
try
|
||
{
|
||
// 获取数据
|
||
folderInfo = new DirectoryInfo(folderPath + extraPath);
|
||
value.lastCheckTime = folderInfo.Exists ? GetActualLastWriteTimeUtc(folderInfo, maxDepth) : null;
|
||
// 如果已经检查过,则跳过
|
||
if (type == LoaderFolderRunType.RunOnUpdated && loaderFolderDictionary.ContainsKey(loader))
|
||
{
|
||
if (folderInfo.Exists)
|
||
{
|
||
if (loaderFolderDictionary[loader].lastCheckTime is not null &&
|
||
value.Equals(loaderFolderDictionary[loader]))
|
||
return false;
|
||
}
|
||
else if (loaderFolderDictionary[loader].lastCheckTime is null)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ModBase.Log(ex, "文件夹加载器启动检测出错");
|
||
}
|
||
|
||
// 写入检查数据
|
||
loaderFolderDictionary[loader] = value;
|
||
// 开始检查
|
||
if (type == LoaderFolderRunType.UpdateOnly)
|
||
return false;
|
||
if (waitForExit)
|
||
loader.WaitForExit(loaderInput ?? folderPath, isForceRestart: true);
|
||
else
|
||
loader.Start(loaderInput ?? folderPath, true);
|
||
return true;
|
||
}
|
||
|
||
private static DateTime GetActualLastWriteTimeUtc(DirectoryInfo folderInfo, int maxDepth)
|
||
{
|
||
var time = folderInfo.LastWriteTimeUtc;
|
||
if (maxDepth > 0)
|
||
foreach (var Folder in folderInfo.EnumerateDirectories())
|
||
{
|
||
var folderTime = GetActualLastWriteTimeUtc(Folder, maxDepth - 1);
|
||
if (folderTime > time)
|
||
time = folderTime;
|
||
}
|
||
|
||
return time;
|
||
}
|
||
|
||
// 各类加载器
|
||
/// <summary>
|
||
/// 加载器的统一基类。
|
||
/// </summary>
|
||
public abstract class LoaderBase : ILoadingTrigger
|
||
{
|
||
public delegate void OnStateChangedThreadEventHandler(LoaderBase loader, ModBase.LoadState newState,
|
||
ModBase.LoadState oldState);
|
||
|
||
public delegate void OnStateChangedUiEventHandler(LoaderBase loader, ModBase.LoadState newState,
|
||
ModBase.LoadState oldState);
|
||
|
||
public delegate void PreviewFinishEventHandler(LoaderBase loader);
|
||
|
||
// 等待结束
|
||
public const string waitForExitTimeoutMessage = "等待加载器执行超时。";
|
||
|
||
/// <summary>
|
||
/// 用于状态改变检测的同步锁。
|
||
/// </summary>
|
||
public readonly object lockState = new();
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// 使用 LoaderCombo 加载时,该任务是否会阻碍后续任务的进行。
|
||
/// </summary>
|
||
public bool block = true;
|
||
|
||
public bool hasOnStateChangedThread;
|
||
|
||
/// <summary>
|
||
/// 当前加载器是否由 IsForceRestart 强制调起。
|
||
/// 这个属性自身不会干任何事,而是提供给加载器执行的函数,使得加载器调用另一个加载器时,可以继承强制重启属性。
|
||
/// </summary>
|
||
public bool isForceRestarting;
|
||
|
||
/// <summary>
|
||
/// 加载器的名称。
|
||
/// </summary>
|
||
public string name;
|
||
|
||
/// <summary>
|
||
/// 父加载器。
|
||
/// </summary>
|
||
public LoaderBase parent;
|
||
|
||
/// <summary>
|
||
/// 该加载器是否显示在列表中。
|
||
/// </summary>
|
||
public bool show = true;
|
||
|
||
// 基础属性
|
||
/// <summary>
|
||
/// 加载器的标识编号。
|
||
/// </summary>
|
||
public int Uuid = ModBase.GetUuid();
|
||
|
||
public LoaderBase()
|
||
{
|
||
name = "未命名任务 " + Uuid + "#";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 最上级的加载器。
|
||
/// </summary>
|
||
public LoaderBase RealParent
|
||
{
|
||
get
|
||
{
|
||
LoaderBase realParentRet = default;
|
||
try
|
||
{
|
||
realParentRet = parent;
|
||
while (realParentRet is not null && realParentRet.parent is not null)
|
||
realParentRet = realParentRet.parent;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ModBase.Log(
|
||
ex,
|
||
"获取父加载器失败(" + name + ")",
|
||
ModBase.LogLevel.Feedback,
|
||
userSummary: Lang.Text("Application.Loader.Error.OperationFailed"));
|
||
return null;
|
||
}
|
||
|
||
return realParentRet;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 简易的在 UI 线程添加触发事件的方式。主要用于在新建 Loader 时直接使用 With 绑定事件,以及进行老代码兼容。
|
||
/// </summary>
|
||
public Action<LoaderBase> OnStateChanged
|
||
{
|
||
set { OnStateChangedUi += (loader, newState, oldState) => value(loader); }
|
||
}
|
||
|
||
// 状态监控
|
||
/// <summary>
|
||
/// 加载器的状态。
|
||
/// </summary>
|
||
public ModBase.LoadState State
|
||
{
|
||
get => field;
|
||
set
|
||
{
|
||
if (field == value)
|
||
return;
|
||
var oldState = field;
|
||
if (value == ModBase.LoadState.Finished && Config.Debug.AddRandomDelay)
|
||
Thread.Sleep(RandomUtils.NextInt(100, 2000));
|
||
field = value;
|
||
ModBase.Log("[Loader] 加载器 " + name + " 状态改变:" + ModBase.GetStringFromEnum(value));
|
||
// 实现 ILoadingTrigger 接口与 OnStateChanged 回调
|
||
ModBase.RunInUi(() =>
|
||
{
|
||
switch (value)
|
||
{
|
||
case ModBase.LoadState.Loading:
|
||
{
|
||
LoadingState = MyLoading.MyLoadingState.Run;
|
||
break;
|
||
}
|
||
case ModBase.LoadState.Failed:
|
||
{
|
||
LoadingState = MyLoading.MyLoadingState.Error;
|
||
break;
|
||
}
|
||
|
||
default:
|
||
{
|
||
LoadingState = MyLoading.MyLoadingState.Stop;
|
||
break;
|
||
}
|
||
}
|
||
|
||
OnStateChangedUi?.Invoke(this, value, oldState);
|
||
});
|
||
if (hasOnStateChangedThread)
|
||
ModBase.RunInThread(() => OnStateChangedThread?.Invoke(this, value, oldState));
|
||
}
|
||
} = ModBase.LoadState.Waiting;
|
||
|
||
/// <summary>
|
||
/// 若加载器出错,可提供给外部参考的异常。
|
||
/// </summary>
|
||
public Exception Error { get; set; }
|
||
|
||
// 进度监控
|
||
/// <summary>
|
||
/// 加载器的执行进度,为 0 至 1 的小数。
|
||
/// </summary>
|
||
public virtual double Progress
|
||
{
|
||
get
|
||
{
|
||
switch (State)
|
||
{
|
||
case ModBase.LoadState.Waiting:
|
||
{
|
||
return 0d;
|
||
}
|
||
case ModBase.LoadState.Loading:
|
||
{
|
||
return field == -1 ? 0.02d : field;
|
||
}
|
||
|
||
default:
|
||
{
|
||
return 1d;
|
||
}
|
||
}
|
||
}
|
||
set
|
||
{
|
||
if (field == value)
|
||
return;
|
||
var oldValue = field;
|
||
field = value;
|
||
ProgressChanged?.Invoke(value, oldValue);
|
||
}
|
||
} = -1;
|
||
|
||
/// <summary>
|
||
/// 计算总进度时的权重。它应该为预计时间(秒)。
|
||
/// </summary>
|
||
public double ProgressWeight { get; set; } = 1d;
|
||
|
||
public bool IsLoader { get; } = true;
|
||
|
||
public MyLoading.MyLoadingState LoadingState
|
||
{
|
||
get => field;
|
||
set
|
||
{
|
||
if (field == value)
|
||
return;
|
||
var oldState = field;
|
||
field = value;
|
||
LoadingStateChanged?.Invoke(value, oldState);
|
||
}
|
||
} = MyLoading.MyLoadingState.Stop;
|
||
|
||
public event ILoadingTrigger.LoadingStateChangedEventHandler? LoadingStateChanged;
|
||
public event ILoadingTrigger.ProgressChangedEventHandler? ProgressChanged;
|
||
|
||
public virtual void InitParent(LoaderBase parent)
|
||
{
|
||
this.parent = parent;
|
||
}
|
||
|
||
// 事件
|
||
|
||
/// <summary>
|
||
/// 当状态改变时,在工作线程触发代码。在添加事件后,必须将 HasOnStateChangedThread 设为 True。
|
||
/// </summary>
|
||
public event OnStateChangedThreadEventHandler? OnStateChangedThread;
|
||
|
||
/// <summary>
|
||
/// 当状态改变时,在 UI 线程触发代码。
|
||
/// </summary>
|
||
public event OnStateChangedUiEventHandler? OnStateChangedUi;
|
||
|
||
/// <summary>
|
||
/// 在加载器目标事件执行完成,加载器状态即将变为 Finish 时调用。可以视为扩展加载器目标事件。
|
||
/// </summary>
|
||
public event PreviewFinishEventHandler? PreviewFinish;
|
||
|
||
protected void RaisePreviewFinish()
|
||
{
|
||
PreviewFinish?.Invoke(this);
|
||
}
|
||
|
||
// 状态变化
|
||
public abstract void Start(object? input = null, bool isForceRestart = false);
|
||
public abstract void Abort();
|
||
|
||
/// <summary>
|
||
/// 无限期地等待加载器完成,直到结束或抛出异常。若加载器尚未开始,则会开始执行。
|
||
/// </summary>
|
||
public void WaitForExit(object input = null, LoaderBase loaderToSyncProgress = null,
|
||
bool isForceRestart = false)
|
||
{
|
||
Start(input, isForceRestart);
|
||
while (State == ModBase.LoadState.Loading)
|
||
{
|
||
if (loaderToSyncProgress is not null)
|
||
loaderToSyncProgress.Progress = Progress;
|
||
Thread.Sleep(10);
|
||
}
|
||
|
||
if (State == ModBase.LoadState.Finished)
|
||
{
|
||
}
|
||
else if (State == ModBase.LoadState.Aborted)
|
||
{
|
||
throw new ThreadInterruptedException("加载器执行已中断。");
|
||
}
|
||
else if (Error is null)
|
||
{
|
||
throw new Exception("未知错误!");
|
||
}
|
||
else
|
||
{
|
||
throw new Exception(Error.Message, Error);
|
||
} // 保留调用堆栈,同时不影响信息输出与单元测试
|
||
}
|
||
|
||
/// <summary>
|
||
/// 等待加载器完成,直到结束、抛出异常或超时。若加载器尚未开始,则会开始执行。
|
||
/// </summary>
|
||
/// <param name="timeout">等待的超时时间,以毫秒为单位。</param>
|
||
/// <param name="timeoutMessage">若执行超时,将会抛出的异常信息。</param>
|
||
public void WaitForExitTime(int timeout, object input = null, string timeoutMessage = waitForExitTimeoutMessage,
|
||
object loaderToSyncProgress = null, bool isForceRestart = false)
|
||
{
|
||
Start(input, isForceRestart);
|
||
while (State == ModBase.LoadState.Loading)
|
||
{
|
||
if (loaderToSyncProgress is not null)
|
||
((dynamic)loaderToSyncProgress).Progress = Progress;
|
||
Thread.Sleep(10);
|
||
timeout -= 10;
|
||
if (timeout < 0)
|
||
throw new TimeoutException(timeoutMessage);
|
||
}
|
||
|
||
if (State == ModBase.LoadState.Finished)
|
||
{
|
||
}
|
||
else if (State == ModBase.LoadState.Aborted)
|
||
{
|
||
throw new ThreadInterruptedException("加载器执行已中断。");
|
||
}
|
||
else if (Error is null)
|
||
{
|
||
throw new Exception("未知错误!");
|
||
}
|
||
else
|
||
{
|
||
throw Error;
|
||
}
|
||
}
|
||
|
||
// 相同重载
|
||
public override bool Equals(object obj)
|
||
{
|
||
var @base = obj as LoaderBase;
|
||
return @base is not null && Uuid == @base.Uuid;
|
||
}
|
||
}
|
||
|
||
// 说实话,我真的觉得 C# 应该学学 VB 的那种近乎 Java 泛型擦除的兼容性,省掉一堆麻烦
|
||
public abstract class LoaderTask : LoaderBase
|
||
{
|
||
/// <summary>
|
||
/// 上次完成加载时的时间。
|
||
/// </summary>
|
||
public long lastFinishedTime;
|
||
|
||
/// <summary>
|
||
/// 最后一次运行加载器的线程。可能为 Nothing,或线程已结束。
|
||
/// </summary>
|
||
public Task? lastRunningTask;
|
||
|
||
/// <summary>
|
||
/// 在输入相同时使用原有结果的超时,单位为毫秒。
|
||
/// </summary>
|
||
public int reloadTimeout = -1;
|
||
|
||
// 状态指示
|
||
/// <summary>
|
||
/// 当前执行线程是否应当中断。只应用在加载器的工作线程中判断,不可跨线程调用。
|
||
/// </summary>
|
||
public bool IsAborted => IsAbortedWithThread(Task.CurrentId ?? -1);
|
||
|
||
/// <summary>
|
||
/// 当前执行线程是否应当中断。需要手动提供加载器线程,用于需要跨线程检查的情况。
|
||
/// </summary>
|
||
public bool IsAbortedWithThread(int compareTaskId)
|
||
{
|
||
return lastRunningTask is null || compareTaskId != lastRunningTask.Id ||
|
||
State == ModBase.LoadState.Aborted;
|
||
}
|
||
|
||
public abstract bool ShouldStart(ref object? input, bool isForceRestart = false, bool ignoreReloadTimeout = false);
|
||
|
||
// 装箱!装箱!装箱圣地!
|
||
public abstract object? StartGetInputNoType(object? input = null, Func<object>? inputDelegate = null);
|
||
|
||
}
|
||
|
||
/// <summary>
|
||
/// 用于异步执行并监控单一函数的加载器。
|
||
/// </summary>
|
||
public class LoaderTask<InputType, OutputType> : LoaderTask
|
||
{
|
||
// 输入输出
|
||
public InputType input;
|
||
protected internal Func<InputType?>? inputDelegate;
|
||
|
||
// 执行事件
|
||
protected internal Action<LoaderTask<InputType, OutputType>> loadDelegate;
|
||
public OutputType output = default;
|
||
|
||
private CancellationTokenSource? cancelToken;
|
||
|
||
// 线程设定
|
||
protected internal ThreadPriority threadPriority;
|
||
|
||
public LoaderTask(string name, Action<LoaderTask<InputType, OutputType>> loadDelegate,
|
||
Func<InputType?>? inputDelegate = null, ThreadPriority priority = ThreadPriority.Normal)
|
||
{
|
||
this.name = name;
|
||
this.loadDelegate = loadDelegate;
|
||
this.inputDelegate = inputDelegate;
|
||
}
|
||
|
||
// 获取输入
|
||
public InputType? StartGetInput(InputType? input = default, Func<InputType?>? inputDelegate = null) // InputDelegate 参数存在匿名调用
|
||
{
|
||
inputDelegate ??= this.inputDelegate;
|
||
// 按照龙猫的逻辑,此处将 input 与默认值直接进行等价比较,若相等则认为 input 未传入具体值,而调用 inputDelegate 获取
|
||
// 这种逻辑未考虑值类型恰好传入 default 值 (如 double 传了 0.0) 的情况,这是一个陷阱,可能会产生 undefined behavior
|
||
if (EqualityComparer<InputType>.Default.Equals(input, default) && inputDelegate is not null)
|
||
ModBase.RunInUiWait(() => input = inputDelegate());
|
||
return input;
|
||
}
|
||
|
||
public override object? StartGetInputNoType(object? input = null, Func<object?>? inputDelegate = null)
|
||
{
|
||
return StartGetInput(input is null ? default : (InputType?)input, inputDelegate is null ? null : () => (InputType?)inputDelegate());
|
||
}
|
||
|
||
// 代码执行
|
||
public override bool ShouldStart(ref object? input, bool isForceRestart = false, bool ignoreReloadTimeout = false)
|
||
{
|
||
// 获取输入
|
||
try
|
||
{
|
||
input = StartGetInput(Conversions.ToGenericParameter<InputType>(input));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ModBase.Log(
|
||
ex,
|
||
"加载输入获取失败(" + name + ")",
|
||
ModBase.LogLevel.Hint,
|
||
userSummary: Lang.Text("Application.Loader.Error.OperationFailed"));
|
||
Error = ex;
|
||
lock (lockState)
|
||
{
|
||
State = ModBase.LoadState.Failed;
|
||
}
|
||
}
|
||
|
||
// 检验输入以确定情况
|
||
if (isForceRestart)
|
||
return true; // 强制要求重启
|
||
if (input is null != this.input is null || (input is not null && !input.Equals(this.input)))
|
||
return true; // 输入不同
|
||
if ((State == ModBase.LoadState.Loading || State == ModBase.LoadState.Finished) && (ignoreReloadTimeout ||
|
||
reloadTimeout == -1 || lastFinishedTime == 0L ||
|
||
TimeUtils.GetTimeTick() - lastFinishedTime < reloadTimeout)) // 正在加载或已结束
|
||
// 没有超时
|
||
return false; // 则不重试
|
||
|
||
return true;
|
||
// 需要开始
|
||
}
|
||
|
||
public override void Start(object input = null, bool isForceRestart = false)
|
||
{
|
||
// 确认是否开始加载
|
||
if (ShouldStart(ref input, isForceRestart))
|
||
{
|
||
// 输入不同或失败,开始加载
|
||
if (State == ModBase.LoadState.Loading)
|
||
TriggerThreadAbort();
|
||
this.input = Conversions.ToGenericParameter<InputType>(input);
|
||
lock (lockState)
|
||
{
|
||
State = ModBase.LoadState.Loading;
|
||
Progress = -1;
|
||
}
|
||
}
|
||
else return;
|
||
|
||
// 如果线程是因为判断到 IsAborted 而提前中止,则代表已有新线程被重启,此时不应当改为 Aborted
|
||
// 如果线程是在没有 IsAborted 时手动引发了 ThreadInterruptedException,则代表没有重启线程,这通常代表用户手动取消,应当改为 Aborted
|
||
lastRunningTask = Task.Run(() =>
|
||
{
|
||
try
|
||
{
|
||
isForceRestarting = isForceRestart;
|
||
if (ModBase.modeDebug)
|
||
ModBase.Log(
|
||
$"[Loader] 加载线程 {name} ({Task.CurrentId}) 已{(isForceRestarting ? "强制" : "")}启动");
|
||
loadDelegate(this);
|
||
if (IsAborted)
|
||
{
|
||
ModBase.Log(
|
||
$"[Loader] 加载线程 {name} ({Task.CurrentId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{(lastRunningTask is null ? -1 : lastRunningTask.Id)})",
|
||
ModBase.LogLevel.Developer);
|
||
return;
|
||
}
|
||
|
||
if (ModBase.modeDebug)
|
||
ModBase.Log($"[Loader] 加载线程 {name} ({Task.CurrentId}) 已完成");
|
||
RaisePreviewFinish();
|
||
State = ModBase.LoadState.Finished;
|
||
lastFinishedTime = TimeUtils.GetTimeTick();
|
||
}
|
||
catch (ModBase.CancelledException ex)
|
||
{
|
||
if (ModBase.modeDebug)
|
||
ModBase.Log(ex,
|
||
$"加载线程 {name} ({Task.CurrentId}) 已触发取消中断,已完成 {Math.Round(Progress * 100d)}%");
|
||
if (!IsAborted) State = ModBase.LoadState.Aborted;
|
||
}
|
||
catch (ThreadInterruptedException ex)
|
||
{
|
||
if (ModBase.modeDebug)
|
||
ModBase.Log(ex,
|
||
$"加载线程 {name} ({Task.CurrentId}) 已触发线程中断,已完成 {Math.Round(Progress * 100d)}%");
|
||
if (!IsAborted) State = ModBase.LoadState.Aborted;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
if (IsAborted) return;
|
||
ModBase.Log(ex,
|
||
$"加载线程 {name} ({Task.CurrentId}) 出错,已完成 {Math.Round(Progress * 100d)}%",
|
||
ModBase.LogLevel.Developer);
|
||
Error = ex;
|
||
State = ModBase.LoadState.Failed;
|
||
}
|
||
}, (cancelToken ??= new CancellationTokenSource()).Token); // 未中断,本次输出有效
|
||
// LastRunningTask.Start(); // 不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsAborted
|
||
}
|
||
|
||
public override void Abort()
|
||
{
|
||
if (State != ModBase.LoadState.Loading)
|
||
return;
|
||
lock (lockState)
|
||
{
|
||
State = ModBase.LoadState.Aborted;
|
||
}
|
||
|
||
TriggerThreadAbort();
|
||
}
|
||
|
||
private void TriggerThreadAbort()
|
||
{
|
||
if (lastRunningTask is null) return;
|
||
if (ModBase.modeDebug) ModBase.Log($"[Loader] 加载线程 {name} ({lastRunningTask.Id}) 已中断");
|
||
if (!lastRunningTask.IsCompleted) cancelToken?.Cancel();
|
||
lastRunningTask = null;
|
||
cancelToken = null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 支持多个加载器连续运作的复合加载器。
|
||
/// </summary>
|
||
public class LoaderCombo : LoaderBase
|
||
{
|
||
public object? input;
|
||
|
||
public List<LoaderBase> loaders = new();
|
||
|
||
public LoaderCombo(string name, IEnumerable<LoaderBase> loaders)
|
||
{
|
||
this.loaders.Clear();
|
||
foreach (var Loader in loaders)
|
||
if (Loader is not null)
|
||
{
|
||
this.loaders.Add(Loader);
|
||
Loader.OnStateChangedThread += SubTaskStateChanged;
|
||
Loader.hasOnStateChangedThread = true;
|
||
}
|
||
|
||
InitParent(null);
|
||
this.name = name;
|
||
}
|
||
|
||
public override double Progress
|
||
{
|
||
get
|
||
{
|
||
switch (State)
|
||
{
|
||
case ModBase.LoadState.Waiting:
|
||
{
|
||
return 0d;
|
||
}
|
||
case ModBase.LoadState.Loading:
|
||
{
|
||
var total = 0d;
|
||
var finished = 0d;
|
||
foreach (var Loader in loaders)
|
||
{
|
||
total += Loader.ProgressWeight;
|
||
finished += Loader.ProgressWeight * Loader.Progress;
|
||
}
|
||
|
||
if (total == 0d)
|
||
return 0d;
|
||
return finished / total;
|
||
}
|
||
|
||
default:
|
||
{
|
||
return 1d;
|
||
}
|
||
}
|
||
}
|
||
set => throw new Exception("多重加载器不支持设置进度");
|
||
}
|
||
|
||
public override void InitParent(LoaderBase parent)
|
||
{
|
||
this.parent = parent;
|
||
foreach (var Loader in loaders)
|
||
Loader.InitParent(this);
|
||
}
|
||
|
||
public override void Start(object input = null, bool isForceRestart = false)
|
||
{
|
||
isForceRestarting = isForceRestart;
|
||
lock (lockState)
|
||
{
|
||
if (State == ModBase.LoadState.Loading) return;
|
||
|
||
State = ModBase.LoadState.Loading;
|
||
}
|
||
|
||
// 启动加载
|
||
this.input = input;
|
||
if (isForceRestart)
|
||
foreach (var Loader in loaders)
|
||
Loader.State = ModBase.LoadState.Waiting;
|
||
ModBase.RunInThread(Update);
|
||
}
|
||
|
||
public override void Abort()
|
||
{
|
||
lock (lockState)
|
||
{
|
||
if (State != ModBase.LoadState.Loading && State != ModBase.LoadState.Waiting)
|
||
return;
|
||
}
|
||
|
||
foreach (var Loader in loaders) Loader.Abort();
|
||
|
||
lock (lockState)
|
||
{
|
||
if (State == ModBase.LoadState.Loading || State == ModBase.LoadState.Waiting)
|
||
State = ModBase.LoadState.Aborted;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 子任务状态变更。
|
||
/// </summary>
|
||
private void SubTaskStateChanged(LoaderBase loader, ModBase.LoadState newState, ModBase.LoadState oldState)
|
||
{
|
||
switch (newState)
|
||
{
|
||
case ModBase.LoadState.Loading:
|
||
{
|
||
break;
|
||
}
|
||
// 开始,啥都不干
|
||
case ModBase.LoadState.Waiting:
|
||
{
|
||
break;
|
||
}
|
||
// 子加载器可能由于外部输入改变而暂时变为 Waiting,之后会立即重新启动
|
||
// 所以啥都不干就行
|
||
case ModBase.LoadState.Finished:
|
||
{
|
||
// 正常结束,触发刷新
|
||
Update();
|
||
break;
|
||
}
|
||
case ModBase.LoadState.Aborted:
|
||
{
|
||
// 被中断,这个任务也中断
|
||
Abort();
|
||
break;
|
||
}
|
||
|
||
default:
|
||
{
|
||
// 完蛋,出错了
|
||
lock (lockState)
|
||
{
|
||
if (State >= ModBase.LoadState.Finished)
|
||
return;
|
||
Error = new Exception(loader.name + "失败", loader.Error);
|
||
State = loader.State;
|
||
}
|
||
|
||
foreach (var currentLoader in loaders)
|
||
{
|
||
loader = currentLoader;
|
||
loader.Abort();
|
||
}
|
||
|
||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 触发一次更新,以启动新加载器或完成。
|
||
/// </summary>
|
||
private void Update()
|
||
{
|
||
if (State == ModBase.LoadState.Finished
|
||
|| State == ModBase.LoadState.Failed
|
||
|| State == ModBase.LoadState.Aborted)
|
||
return;
|
||
|
||
var isFinished = true;
|
||
var blocked = false;
|
||
object input = this.input;
|
||
|
||
foreach (var loader in loaders)
|
||
switch (loader.State)
|
||
{
|
||
case ModBase.LoadState.Finished:
|
||
{
|
||
if (loader is LoaderTask task)
|
||
{
|
||
var genericArg = task.GetType().GenericTypeArguments.FirstOrDefault();
|
||
var shouldInput = input is not null && genericArg == input.GetType()
|
||
? input
|
||
: null;
|
||
|
||
if (task.ShouldStart(ref shouldInput, false, true))
|
||
{
|
||
ModBase.Log("[Loader] 由于输入条件变更,重启已完成的加载器 " + loader.name);
|
||
goto Restart;
|
||
}
|
||
|
||
input = ((dynamic)loader).output; // 何意味啊,没法匹配 LoaderTask<,>
|
||
}
|
||
|
||
if (loader.block && !isFinished)
|
||
blocked = true;
|
||
|
||
break;
|
||
}
|
||
|
||
case ModBase.LoadState.Loading:
|
||
{
|
||
if (loader is LoaderTask task)
|
||
{
|
||
var genericArg = loader.GetType().GenericTypeArguments.FirstOrDefault();
|
||
var shouldInput = input is not null && genericArg == input.GetType()
|
||
? input
|
||
: null;
|
||
if (task.ShouldStart(ref shouldInput, false, true))
|
||
{
|
||
ModBase.Log($"[Loader] 由于输入条件变更,重启进行中的加载器 {loader.name}",
|
||
ModBase.LogLevel.Developer);
|
||
goto Restart;
|
||
}
|
||
}
|
||
|
||
isFinished = false;
|
||
blocked = true;
|
||
break;
|
||
}
|
||
|
||
default:
|
||
|
||
Restart:
|
||
|
||
isFinished = false;
|
||
|
||
if (blocked)
|
||
continue;
|
||
|
||
if (input is not null)
|
||
{
|
||
switch (loader)
|
||
{
|
||
case LoaderTask:
|
||
case LoaderCombo:
|
||
var genericArg = loader.GetType().GenericTypeArguments.FirstOrDefault();
|
||
|
||
loader.Start(
|
||
genericArg == input.GetType() ? input : null,
|
||
isForceRestarting);
|
||
break;
|
||
case not null:
|
||
loader.Start(
|
||
input is List<DownloadFile> ? input : null,
|
||
isForceRestarting);
|
||
break;
|
||
default:
|
||
throw new Exception($"未知的加载器类型({loader?.GetType()})");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
loader.Start(isForceRestart: isForceRestarting);
|
||
}
|
||
|
||
if (loader.block)
|
||
blocked = true;
|
||
|
||
break;
|
||
}
|
||
|
||
if (isFinished)
|
||
{
|
||
RaisePreviewFinish();
|
||
State = ModBase.LoadState.Finished;
|
||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获得最底层的,应被显示给用户的加载器列表,并追加于 List。
|
||
/// </summary>
|
||
public static void GetLoaderList(LoaderCombo loader, ref List<LoaderBase> list, bool requireShow = true)
|
||
{
|
||
foreach (var SubLoader in loader.loaders)
|
||
{
|
||
if (SubLoader.show || !requireShow)
|
||
list.Add(SubLoader);
|
||
if (SubLoader is LoaderCombo combo)
|
||
GetLoaderList(combo, ref list);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获得最底层的,应被显示给用户的加载器列表,并追加于 List。
|
||
/// </summary>
|
||
public void GetLoaderList(ref List<LoaderBase> list, bool requireShow = true)
|
||
{
|
||
GetLoaderList(this, ref list, requireShow);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获得最底层的,应被显示给用户的加载器列表。
|
||
/// </summary>
|
||
public List<LoaderBase> GetLoaderList(bool requireShow = true)
|
||
{
|
||
var list = new List<LoaderBase>();
|
||
GetLoaderList(ref list, requireShow);
|
||
return list;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 支持多个加载器连续运作的复合加载器(泛型版本)。
|
||
/// </summary>
|
||
public class LoaderCombo<InputType> : LoaderCombo
|
||
{
|
||
public new InputType typedInput;
|
||
|
||
public LoaderCombo(string name, IEnumerable<LoaderBase> loaders) : base(name, loaders) { }
|
||
|
||
public override void Start(object input = null, bool isForceRestart = false)
|
||
{
|
||
this.typedInput = Conversions.ToGenericParameter<InputType>(input);
|
||
base.Start(this.typedInput, isForceRestart);
|
||
}
|
||
}
|
||
|
||
private struct LoaderFolderDictionaryEntry
|
||
{
|
||
public DateTime? lastCheckTime;
|
||
public string folderPath;
|
||
|
||
public override bool Equals(object obj)
|
||
{
|
||
if (obj is not LoaderFolderDictionaryEntry)
|
||
return false;
|
||
var entry = (LoaderFolderDictionaryEntry)obj;
|
||
return EqualityComparer<DateTime?>.Default.Equals(lastCheckTime, entry.lastCheckTime) &&
|
||
(folderPath ?? "") == (entry.folderPath ?? "");
|
||
}
|
||
}
|
||
}
|