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 loaderTaskbar = new(); public static double loaderTaskbarProgress; // 平滑后的进度 private static TaskbarItemProgressState loaderTaskbarProgressLast = TaskbarItemProgressState.None; // 文件夹刷新类委托 private static readonly Dictionary loaderFolderDictionary = new(); public static void LoaderTaskbarAdd(LoaderCombo 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; } } /// /// 执行以文件夹检测作为输入的加载器。加载器需以文件夹路径为输入值。 /// 返回是否执行了加载器。 /// /// 用于检查文件夹修改的额外路径。该路径不会传入加载器。 /// 如果不想要文件夹路径为输入值,则传入期望数据 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; } // 各类加载器 /// /// 加载器的统一基类。 /// 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 = "等待加载器执行超时。"; /// /// 用于状态改变检测的同步锁。 /// public readonly object lockState = new(); /// /// 使用 LoaderCombo 加载时,该任务是否会阻碍后续任务的进行。 /// public bool block = true; public bool hasOnStateChangedThread; /// /// 当前加载器是否由 IsForceRestart 强制调起。 /// 这个属性自身不会干任何事,而是提供给加载器执行的函数,使得加载器调用另一个加载器时,可以继承强制重启属性。 /// public bool isForceRestarting; /// /// 加载器的名称。 /// public string name; /// /// 父加载器。 /// public LoaderBase parent; /// /// 该加载器是否显示在列表中。 /// public bool show = true; // 基础属性 /// /// 加载器的标识编号。 /// public int Uuid = ModBase.GetUuid(); public LoaderBase() { name = "未命名任务 " + Uuid + "#"; } /// /// 最上级的加载器。 /// 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; } } /// /// 简易的在 UI 线程添加触发事件的方式。主要用于在新建 Loader 时直接使用 With 绑定事件,以及进行老代码兼容。 /// public Action OnStateChanged { set { OnStateChangedUi += (loader, newState, oldState) => value(loader); } } // 状态监控 /// /// 加载器的状态。 /// 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; /// /// 若加载器出错,可提供给外部参考的异常。 /// public Exception Error { get; set; } // 进度监控 /// /// 加载器的执行进度,为 0 至 1 的小数。 /// 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; /// /// 计算总进度时的权重。它应该为预计时间(秒)。 /// 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; } // 事件 /// /// 当状态改变时,在工作线程触发代码。在添加事件后,必须将 HasOnStateChangedThread 设为 True。 /// public event OnStateChangedThreadEventHandler? OnStateChangedThread; /// /// 当状态改变时,在 UI 线程触发代码。 /// public event OnStateChangedUiEventHandler? OnStateChangedUi; /// /// 在加载器目标事件执行完成,加载器状态即将变为 Finish 时调用。可以视为扩展加载器目标事件。 /// public event PreviewFinishEventHandler? PreviewFinish; protected void RaisePreviewFinish() { PreviewFinish?.Invoke(this); } // 状态变化 public abstract void Start(object? input = null, bool isForceRestart = false); public abstract void Abort(); /// /// 无限期地等待加载器完成,直到结束或抛出异常。若加载器尚未开始,则会开始执行。 /// 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); } // 保留调用堆栈,同时不影响信息输出与单元测试 } /// /// 等待加载器完成,直到结束、抛出异常或超时。若加载器尚未开始,则会开始执行。 /// /// 等待的超时时间,以毫秒为单位。 /// 若执行超时,将会抛出的异常信息。 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 { /// /// 上次完成加载时的时间。 /// public long lastFinishedTime; /// /// 最后一次运行加载器的线程。可能为 Nothing,或线程已结束。 /// public Task? lastRunningTask; /// /// 在输入相同时使用原有结果的超时,单位为毫秒。 /// public int reloadTimeout = -1; // 状态指示 /// /// 当前执行线程是否应当中断。只应用在加载器的工作线程中判断,不可跨线程调用。 /// public bool IsAborted => IsAbortedWithThread(Task.CurrentId ?? -1); /// /// 当前执行线程是否应当中断。需要手动提供加载器线程,用于需要跨线程检查的情况。 /// 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? inputDelegate = null); } /// /// 用于异步执行并监控单一函数的加载器。 /// public class LoaderTask : LoaderTask { // 输入输出 public InputType input; protected internal Func? inputDelegate; // 执行事件 protected internal Action> loadDelegate; public OutputType output = default; private CancellationTokenSource? cancelToken; // 线程设定 protected internal ThreadPriority threadPriority; public LoaderTask(string name, Action> loadDelegate, Func? inputDelegate = null, ThreadPriority priority = ThreadPriority.Normal) { this.name = name; this.loadDelegate = loadDelegate; this.inputDelegate = inputDelegate; } // 获取输入 public InputType? StartGetInput(InputType? input = default, Func? inputDelegate = null) // InputDelegate 参数存在匿名调用 { inputDelegate ??= this.inputDelegate; // 按照龙猫的逻辑,此处将 input 与默认值直接进行等价比较,若相等则认为 input 未传入具体值,而调用 inputDelegate 获取 // 这种逻辑未考虑值类型恰好传入 default 值 (如 double 传了 0.0) 的情况,这是一个陷阱,可能会产生 undefined behavior if (EqualityComparer.Default.Equals(input, default) && inputDelegate is not null) ModBase.RunInUiWait(() => input = inputDelegate()); return input; } public override object? StartGetInputNoType(object? input = null, Func? 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(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(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; } } /// /// 支持多个加载器连续运作的复合加载器。 /// public class LoaderCombo : LoaderBase { public object? input; public List loaders = new(); public LoaderCombo(string name, IEnumerable 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; } } /// /// 子任务状态变更。 /// 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; } } } /// /// 触发一次更新,以启动新加载器或完成。 /// 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 ? 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(); } } /// /// 获得最底层的,应被显示给用户的加载器列表,并追加于 List。 /// public static void GetLoaderList(LoaderCombo loader, ref List 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); } } /// /// 获得最底层的,应被显示给用户的加载器列表,并追加于 List。 /// public void GetLoaderList(ref List list, bool requireShow = true) { GetLoaderList(this, ref list, requireShow); } /// /// 获得最底层的,应被显示给用户的加载器列表。 /// public List GetLoaderList(bool requireShow = true) { var list = new List(); GetLoaderList(ref list, requireShow); return list; } } /// /// 支持多个加载器连续运作的复合加载器(泛型版本)。 /// public class LoaderCombo : LoaderCombo { public new InputType typedInput; public LoaderCombo(string name, IEnumerable loaders) : base(name, loaders) { } public override void Start(object input = null, bool isForceRestart = false) { this.typedInput = Conversions.ToGenericParameter(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.Default.Equals(lastCheckTime, entry.lastCheckTime) && (folderPath ?? "") == (entry.folderPath ?? ""); } } }