using System; using System.Collections.Generic; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Media; using TeamAAS; namespace TeamAAS.Theme { /// /// 应用级主题资源守护:防御第三方控件(海康 VM 系列,经 WindowsFormsHost/ElementHost 进入视觉树) /// 对 Application.Resources 的越界写入。 /// /// 背景故障(2026-09-04): /// 打开"VM 工具块"编辑视图后切换明暗主题,齿轮 Popup 布局时抛 /// InvalidOperationException:"#FF797979"不是属性"Color"的有效值,且被全局异常处理器 /// Handled=true 后每帧重爆、日志刷屏(10MB/5min)。 /// /// 根因(日志 + 反编译确认): /// 1. 某第三方控件把应用级主题同名键(XxxColor/XxxBrush 形态)写成了字符串 "#FF797979" /// (Color.ToString() 的产物;所有 VM DLL 静态字节里均无此字面量,纯运行时生成)。 /// 写入资源字典不做类型检查,因此污染当时不报错。 /// 2. HandyControl 的画刷是 <SolidColorBrush Color="{DynamicResource XxxColor}"/> 结构; /// 画刷 Color 表达式在重新求值时把污染的字符串求进有效值(表达式值延迟类型检查)。 /// 3. Border.ArrangeOverride 读取画刷 Color 时类型检查失败才抛异常。 /// 4. VMControls.Winform.Release 的控件只是 ElementHost 壳,内部仍是 VMControls.WPF 的 /// WPF 控件(反编译确认),所以"改用 WinForms 版避免污染"无效。 /// /// 防御体系(切主题入口消毒,单道防线): /// - ThemeManager 切主题入口消毒:切换/基准捕获前 Purge 第三方注入字典与污染条目,保证求值链纯净; /// - 结构损坏 / 坏画刷 / 基准缺失时 ForceReplaceTheme 整替官方字典兜底。 /// 注:常驻 Watchdog 轮询与未处理异常急救已移除——ThemeOverlay 纯值覆盖层对污染天然免疫。 /// internal static class VmResourceGuard { /// 颜色字面量:#RRGGBB 或 #AARRGGBB(污染值的典型形态) private static readonly Regex ColorLiteralRegex = new Regex(@"^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$", RegexOptions.Compiled); /// 官方主题字典识别标记:pack Source 中含此串才允许触碰/保留 private const string OfficialThemeMarker = "HandyControl"; /// /// 仅消毒:移除应用级资源中"字符串形式颜色"的污染条目。 /// 供切主题入口(整替前)调用——污染清干净后,画刷求值才安全。 /// public static void Sanitize(string trigger) { ScanAll(trigger); } /// /// 检查主题字典中是否存在坏画刷(Color 有效值已被字符串污染、读取即爆)。 /// 供切主题判断:即使字典结构标准,画刷坏了也必须强制整替。 /// public static bool HasBrokenBrush() { var app = Application.Current; if (app == null) return false; bool broken = false; try { ScanDict(app.Resources, "HealthCheck", "HealthCheck", ref broken); } catch (Exception ex) { AppLogger.Error("[资源守护:HealthCheck] 扫描失败", ex, nameof(VmResourceGuard)); } return broken; } private static void ScanAll(string trigger) { var app = Application.Current; if (app == null) return; try { // 第一步:移除第三方控件注入应用合并字典的非 HC 字典(含坏 BAML/字符串颜色键, // 是画刷求值链被击穿的污染源) PurgeForeignMergedDictionaries(trigger); bool brushBroken = false; int removed = ScanDict(app.Resources, "Application.Resources", trigger, ref brushBroken); if (removed > 0) { AppLogger.Warning( $"[资源守护:{trigger}] 已清除 {removed} 个污染条目(明细见上方日志)", nameof(VmResourceGuard)); } } catch (Exception ex) { AppLogger.Error($"[资源守护:{trigger}] 扫描失败", ex, nameof(VmResourceGuard)); } } /// /// 急救/结构修复:强制整替 HandyControl 明暗双字典(调色板 + 画刷/样式), /// 用全新画刷实例替换所有被污染写坏的旧画刷。 /// 正常切主题走覆盖层重建(见 ThemeManager),本方法仅在 /// 「字典结构损坏」「基准未捕获」「检测到坏画刷」时由守护/异常钩子调用。 /// /// 目标明暗;null 时沿用 ThemeOverlay 记录的当前明暗(守护轮询路径)。 public static void ForceReplaceTheme(bool? dark = null) { var merged = Application.Current.Resources.MergedDictionaries; bool isDark = dark.HasValue ? dark.Value : ThemeOverlay.CurrentDark; // 先摘除覆盖层:保证整替与基准捕获读到的是官方字典真值(覆盖层查找优先级最高) ThemeOverlay.RemoveOverlay(); // 消毒:移除第三方注入字典/污染条目(Watchdog/异常钩子可能不经 Sanitize 直达此处), // 保证基准捕获走的是纯净求值链 Sanitize("ForceReplaceTheme"); var skinUri = new Uri(isDark ? "pack://application:,,,/HandyControl;component/Themes/SkinDark.xaml" : "pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"); var themeUri = new Uri("pack://application:,,,/HandyControl;component/Themes/Theme.xaml"); // 1) 调色板槽位 int skinIdx = FindSlot(merged, d => d.Source != null && (d.Source.OriginalString.Contains("SkinDefault.xaml") || d.Source.OriginalString.Contains("SkinDark.xaml"))); if (skinIdx < 0) skinIdx = 0; merged[skinIdx] = new ResourceDictionary { Source = skinUri }; // 2) 画刷+样式槽位:Source 含 Theme.xaml,或旧方案残留的 Theme 实例(Source=null) int themeIdx = FindSlot(merged, d => d is HandyControl.Themes.Theme || (d.Source != null && d.Source.OriginalString.Contains("Theme.xaml"))); if (themeIdx < 0) themeIdx = merged.Count > 1 ? 1 : merged.Count; var themeDict = new ResourceDictionary { Source = themeUri }; if (themeIdx >= merged.Count) merged.Add(themeDict); else merged[themeIdx] = themeDict; // 重新捕获该明暗的官方基准(此时求值链干净)并重建覆盖层(纯值画刷,免疫污染) ThemeOverlay.CaptureBase(isDark); ThemeOverlay.Apply(isDark, ThemeOverlay.CurrentAccent); } /// /// 移除应用合并字典中"非白名单"的字典(第三方控件框架级注入的字典: /// 实测含坏 BAML(GradientStop.Color 解析即爆)与字符串形式颜色键,它们挂在 /// Application 求值链上会让所有 DynamicResource 画刷被击穿)。 /// 白名单字典保留;第三方控件自身的资源需求由其元素级资源机制解决。 /// private static void PurgeForeignMergedDictionaries(string trigger) { var merged = Application.Current.Resources.MergedDictionaries; // 先收集后移除,避免枚举中改集合 List foreign = null; for (int i = 0; i < merged.Count; i++) { var dict = merged[i]; if (IsOfficialThemeDict(dict)) continue; if (ThemeOverlay.IsOverlay(dict)) continue; // 主题覆盖层(我们自己的纯值字典)放行 string describe = Describe(dict); if (string.IsNullOrEmpty(describe)) describe = "(Source=null)"; AppLogger.Warning( $"[资源守护:{trigger}] 移除第三方注入的应用级字典 md[{i}] {describe}", nameof(VmResourceGuard)); if (foreign == null) foreign = new List(); foreign.Add(dict); } if (foreign != null) { foreach (var dict in foreign) { try { merged.Remove(dict); } catch { /* 无法移除时忽略(下一轮再试) */ } } } } /// 是否官方主题字典(Source 含 HandyControl 标记;Source=null 不算)。 private static bool IsOfficialThemeDict(ResourceDictionary dict) { try { var src = dict.Source; return src != null && src.OriginalString.Contains(OfficialThemeMarker); } catch { return false; } } /// /// 递归扫描字典树:移除污染条目(先收集后移除,避免枚举中改集合), /// 顺带用 try 读画刷 Color 检测坏画刷。返回本次移除的条目数。 /// 只递归白名单字典——第三方字典含坏 BAML(展开即爆),触碰只会制造 /// 第一次机会异常并中断扫描,其中的污染由 PurgeForeignMergedDictionaries 整体移除。 /// private static int ScanDict(ResourceDictionary dict, string path, string trigger, ref bool brushBroken) { int removed = 0; // 1) 先扫子合并字典(仅白名单字典;第三方字典不触碰) var merged = dict.MergedDictionaries; for (int i = 0; i < merged.Count; i++) { var child = merged[i]; if (!IsOfficialThemeDict(child)) continue; string childPath = path + "/md[" + i + "]" + Describe(child); ScanDict(child, childPath, trigger, ref brushBroken); } // 2) 扫描本字典条目 List dirtyKeys = null; foreach (var key in dict.Keys) { var keyName = key as string; if (keyName == null) continue; object value; try { value = dict[key]; } catch { continue; } var s = value as string; if (s != null) { if (IsPolluted(keyName, s)) { AppLogger.Warning( $"[资源守护:{trigger}] 发现污染 {path}[\"{keyName}\"] = \"{s}\"(string,应为 Color/Brush)", nameof(VmResourceGuard)); if (dirtyKeys == null) dirtyKeys = new List(); dirtyKeys.Add(key); } continue; } // 正常画刷:读一次 Color(effective value 已缓存,代价可忽略); // 坏画刷:Color 求值命中字符串 → 抛 InvalidOperationException var brush = value as SolidColorBrush; if (brush != null && !CheckBrush(brush)) { brushBroken = true; AppLogger.Warning( $"[资源守护:{trigger}] 坏画刷 {path}[\"{keyName}\"](Color 读取抛异常,等待整替恢复)", nameof(VmResourceGuard)); } } if (dirtyKeys != null) { foreach (var k in dirtyKeys) { try { dict.Remove(k); removed++; } catch { /* 只读/冻结字典无法移除,忽略 */ } } } return removed; } /// /// 污染判定(必须精确,避免误伤业务键): /// a) 值本身就是颜色字面量(#RRGGBB / #AARRGGBB)——正常主题资源不会以 string 存颜色; /// b) 主题键形态(XxxBrush/XxxColor 结尾)却存了 string 值——HC 正常值是 Color/Brush 类型。 /// 业务键如 CenterTitleText(值 "TeamAAS")两条都不满足,不会误伤。 /// private static bool IsPolluted(string keyName, string value) { if (ColorLiteralRegex.IsMatch(value)) return true; if ((keyName.EndsWith("Brush", StringComparison.Ordinal) || keyName.EndsWith("Color", StringComparison.Ordinal)) && value.Length <= 64) { return true; } return false; } /// 画刷健康检查:能读出 Color 即健康。 private static bool CheckBrush(SolidColorBrush brush) { try { var c = brush.Color; return true; } catch (InvalidOperationException) { return false; } } private static int FindSlot(IList merged, Func match) { for (int i = 0; i < merged.Count; i++) { try { if (match(merged[i])) return i; } catch { /* Source 读取异常时跳过该槽位 */ } } return -1; } private static string Describe(ResourceDictionary dict) { try { var src = dict.Source; return src != null ? "(" + src.OriginalString + ")" : string.Empty; } catch { return string.Empty; } } } }