| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- using System;
- using System.Collections.Generic;
- using System.Text.RegularExpressions;
- using System.Windows;
- using System.Windows.Media;
- using TeamAAS;
- namespace TeamAAS.Theme
- {
- /// <summary>
- /// 应用级主题资源守护:防御第三方控件(海康 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 纯值覆盖层对污染天然免疫。
- /// </summary>
- internal static class VmResourceGuard
- {
- /// <summary>颜色字面量:#RRGGBB 或 #AARRGGBB(污染值的典型形态)</summary>
- private static readonly Regex ColorLiteralRegex =
- new Regex(@"^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$", RegexOptions.Compiled);
- /// <summary>官方主题字典识别标记:pack Source 中含此串才允许触碰/保留</summary>
- private const string OfficialThemeMarker = "HandyControl";
- /// <summary>
- /// 仅消毒:移除应用级资源中"字符串形式颜色"的污染条目。
- /// 供切主题入口(整替前)调用——污染清干净后,画刷求值才安全。
- /// </summary>
- public static void Sanitize(string trigger)
- {
- ScanAll(trigger);
- }
- /// <summary>
- /// 检查主题字典中是否存在坏画刷(Color 有效值已被字符串污染、读取即爆)。
- /// 供切主题判断:即使字典结构标准,画刷坏了也必须强制整替。
- /// </summary>
- 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));
- }
- }
- /// <summary>
- /// 急救/结构修复:强制整替 HandyControl 明暗双字典(调色板 + 画刷/样式),
- /// 用全新画刷实例替换所有被污染写坏的旧画刷。
- /// 正常切主题走覆盖层重建(见 ThemeManager),本方法仅在
- /// 「字典结构损坏」「基准未捕获」「检测到坏画刷」时由守护/异常钩子调用。
- /// </summary>
- /// <param name="dark">目标明暗;null 时沿用 ThemeOverlay 记录的当前明暗(守护轮询路径)。</param>
- 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);
- }
- /// <summary>
- /// 移除应用合并字典中"非白名单"的字典(第三方控件框架级注入的字典:
- /// 实测含坏 BAML(GradientStop.Color 解析即爆)与字符串形式颜色键,它们挂在
- /// Application 求值链上会让所有 DynamicResource 画刷被击穿)。
- /// 白名单字典保留;第三方控件自身的资源需求由其元素级资源机制解决。
- /// </summary>
- private static void PurgeForeignMergedDictionaries(string trigger)
- {
- var merged = Application.Current.Resources.MergedDictionaries;
- // 先收集后移除,避免枚举中改集合
- List<ResourceDictionary> 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<ResourceDictionary>();
- foreign.Add(dict);
- }
- if (foreign != null)
- {
- foreach (var dict in foreign)
- {
- try { merged.Remove(dict); }
- catch { /* 无法移除时忽略(下一轮再试) */ }
- }
- }
- }
- /// <summary>是否官方主题字典(Source 含 HandyControl 标记;Source=null 不算)。</summary>
- private static bool IsOfficialThemeDict(ResourceDictionary dict)
- {
- try
- {
- var src = dict.Source;
- return src != null && src.OriginalString.Contains(OfficialThemeMarker);
- }
- catch
- {
- return false;
- }
- }
- /// <summary>
- /// 递归扫描字典树:移除污染条目(先收集后移除,避免枚举中改集合),
- /// 顺带用 try 读画刷 Color 检测坏画刷。返回本次移除的条目数。
- /// 只递归白名单字典——第三方字典含坏 BAML(展开即爆),触碰只会制造
- /// 第一次机会异常并中断扫描,其中的污染由 PurgeForeignMergedDictionaries 整体移除。
- /// </summary>
- 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<object> 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<object>();
- 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;
- }
- /// <summary>
- /// 污染判定(必须精确,避免误伤业务键):
- /// a) 值本身就是颜色字面量(#RRGGBB / #AARRGGBB)——正常主题资源不会以 string 存颜色;
- /// b) 主题键形态(XxxBrush/XxxColor 结尾)却存了 string 值——HC 正常值是 Color/Brush 类型。
- /// 业务键如 CenterTitleText(值 "TeamAAS")两条都不满足,不会误伤。
- /// </summary>
- 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;
- }
- /// <summary>画刷健康检查:能读出 Color 即健康。</summary>
- private static bool CheckBrush(SolidColorBrush brush)
- {
- try
- {
- var c = brush.Color;
- return true;
- }
- catch (InvalidOperationException)
- {
- return false;
- }
- }
- private static int FindSlot(IList<ResourceDictionary> merged, Func<ResourceDictionary, bool> 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;
- }
- }
- }
- }
|