fix: restore existing WeChat instance and add M6 validation tools
This commit is contained in:
@@ -130,11 +130,19 @@ public static class VisibleMessageParser
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class BoundedMessageDeduper(int capacity = 2048)
|
||||
public sealed class BoundedMessageDeduper
|
||||
{
|
||||
private readonly int _capacity;
|
||||
private readonly Queue<string> _order = new();
|
||||
private readonly HashSet<string> _seen = new(StringComparer.Ordinal);
|
||||
|
||||
public BoundedMessageDeduper(int capacity = 2048)
|
||||
{
|
||||
if (capacity <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be positive.");
|
||||
_capacity = capacity;
|
||||
}
|
||||
|
||||
public bool TryAdd(string fingerprint)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(fingerprint);
|
||||
@@ -144,7 +152,7 @@ public sealed class BoundedMessageDeduper(int capacity = 2048)
|
||||
}
|
||||
|
||||
_order.Enqueue(fingerprint);
|
||||
if (_order.Count > capacity)
|
||||
if (_order.Count > _capacity)
|
||||
{
|
||||
_seen.Remove(_order.Dequeue());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace WxAgent.Core;
|
||||
|
||||
public sealed class UiTraversalGuard(int maxNodes = 10_000, int maxDepth = 40)
|
||||
{
|
||||
private readonly HashSet<string> _visited = new(StringComparer.Ordinal);
|
||||
public int NodeCount { get; private set; }
|
||||
public bool Truncated { get; private set; }
|
||||
public bool CycleDetected { get; private set; }
|
||||
|
||||
public bool TryVisit(string runtimeId, int depth)
|
||||
{
|
||||
if (NodeCount >= maxNodes || depth > maxDepth)
|
||||
{
|
||||
Truncated = true;
|
||||
return false;
|
||||
}
|
||||
if (runtimeId.Length > 0 && !_visited.Add(runtimeId))
|
||||
{
|
||||
CycleDetected = true;
|
||||
return false;
|
||||
}
|
||||
NodeCount++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record UiSnapshotComparison(IReadOnlyList<string> AddedAutomationIds,
|
||||
IReadOnlyList<string> RemovedAutomationIds, IReadOnlyList<string> MissingRequiredControls)
|
||||
{
|
||||
public static UiSnapshotComparison Compare(UiNodeSnapshot baseline, UiNodeSnapshot current)
|
||||
{
|
||||
static IEnumerable<string> ReadIds(UiNodeSnapshot root) =>
|
||||
new[] { root.AutomationId }.Concat(root.Children.SelectMany(ReadIds));
|
||||
static HashSet<string> Ids(UiNodeSnapshot root) => ReadIds(root)
|
||||
.Where(id => id.Length > 0 && !id.StartsWith("[id:", StringComparison.Ordinal))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var before = Ids(baseline);
|
||||
var after = Ids(current);
|
||||
return new UiSnapshotComparison(
|
||||
after.Except(before).Order(StringComparer.Ordinal).ToArray(),
|
||||
before.Except(after).Order(StringComparer.Ordinal).ToArray(),
|
||||
WechatLocators.RequiredAutomationIds.Where(id => !after.Contains(id)).ToArray());
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,16 @@ namespace WxAgent.Core;
|
||||
public static class WechatLocators
|
||||
{
|
||||
public const string MainView = "MainView";
|
||||
public const string MainTabBar = "MainView.main_tabbar";
|
||||
|
||||
public static bool IsWechatTrayName(string name) =>
|
||||
name is "微信" or "WeChat" or "Weixin"
|
||||
|| name.StartsWith("微信\n", StringComparison.Ordinal)
|
||||
|| name.StartsWith("微信\r", StringComparison.Ordinal);
|
||||
|
||||
public static bool IsNativeMainWindow(string className, string title) =>
|
||||
title == "微信" && className.StartsWith("Qt", StringComparison.Ordinal)
|
||||
&& className.EndsWith("QWindowIcon", StringComparison.Ordinal);
|
||||
public const string SessionList = "session_list";
|
||||
public const string ChatPage = "chat_message_page";
|
||||
public const string MessageList = "chat_message_list";
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WxAgent.Core;
|
||||
|
||||
public sealed record WechatStabilitySample(
|
||||
DateTimeOffset Timestamp,
|
||||
int ProcessCount,
|
||||
long WorkingSetBytes,
|
||||
int HandleCount,
|
||||
int ThreadCount,
|
||||
bool WindowFound,
|
||||
IReadOnlyList<WxAgentErrorCode> Errors,
|
||||
long MessageEvents,
|
||||
long ReconnectEvents,
|
||||
long AgentWorkingSetBytes = 0,
|
||||
int AgentHandleCount = 0,
|
||||
int AgentThreadCount = 0,
|
||||
DateTimeOffset? LastSnapshotAt = null);
|
||||
|
||||
public sealed record WechatStabilityReport(
|
||||
DateTimeOffset StartedAt,
|
||||
DateTimeOffset FinishedAt,
|
||||
IReadOnlyList<WechatStabilitySample> Samples,
|
||||
long MessageEvents,
|
||||
long ReconnectEvents,
|
||||
long WorkingSetGrowthBytes,
|
||||
int HandleGrowth,
|
||||
int ThreadGrowth,
|
||||
bool Healthy,
|
||||
IReadOnlyList<string> Findings,
|
||||
double RequestedSeconds = 0,
|
||||
double ObservedSeconds = 0,
|
||||
bool CompletedDuration = false);
|
||||
|
||||
public static class WechatStabilityAnalyzer
|
||||
{
|
||||
public static WechatStabilityReport Analyze(
|
||||
DateTimeOffset startedAt,
|
||||
DateTimeOffset finishedAt,
|
||||
IReadOnlyList<WechatStabilitySample> samples,
|
||||
long messageEvents,
|
||||
long reconnectEvents)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(samples);
|
||||
if (samples.Count == 0)
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "At least one stability sample is required.");
|
||||
|
||||
var first = samples[0];
|
||||
var last = samples[^1];
|
||||
var memoryGrowth = last.WorkingSetBytes - first.WorkingSetBytes;
|
||||
var handleGrowth = last.HandleCount - first.HandleCount;
|
||||
var threadGrowth = last.ThreadCount - first.ThreadCount;
|
||||
var findings = new List<string>();
|
||||
var recoveredWindowLoss = last.WindowFound && reconnectEvents > 0;
|
||||
if (samples.Any(sample => !sample.WindowFound) && !recoveredWindowLoss) findings.Add("wechat-window-unavailable");
|
||||
foreach (var error in last.Errors) findings.Add("final-error:" + error);
|
||||
if (last.AgentWorkingSetBytes - first.AgentWorkingSetBytes > 128L * 1024 * 1024) findings.Add("agent-working-set-growth-over-128mb");
|
||||
if (last.AgentHandleCount - first.AgentHandleCount > 100) findings.Add("agent-handle-growth-over-100");
|
||||
if (last.AgentThreadCount - first.AgentThreadCount > 20) findings.Add("agent-thread-growth-over-20");
|
||||
if (memoryGrowth > 256L * 1024 * 1024) findings.Add("working-set-growth-over-256mb");
|
||||
if (handleGrowth > 500) findings.Add("handle-growth-over-500");
|
||||
if (threadGrowth > 100) findings.Add("thread-growth-over-100");
|
||||
if (samples.Any(sample => sample.LastSnapshotAt is { } lastSnapshot
|
||||
&& sample.Timestamp - lastSnapshot > TimeSpan.FromMinutes(5))) findings.Add("listener-snapshot-gap-over-5min");
|
||||
|
||||
return new WechatStabilityReport(
|
||||
startedAt,
|
||||
finishedAt,
|
||||
samples,
|
||||
messageEvents,
|
||||
reconnectEvents,
|
||||
memoryGrowth,
|
||||
handleGrowth,
|
||||
threadGrowth,
|
||||
findings.Count == 0,
|
||||
findings);
|
||||
}
|
||||
|
||||
public static async Task WriteJsonAsync(WechatStabilityReport report, string path, CancellationToken cancellationToken)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
|
||||
await File.WriteAllTextAsync(fullPath, JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true }), cancellationToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user