211 lines
7.7 KiB
C#
211 lines
7.7 KiB
C#
using System.Diagnostics;
|
|
using FlaUI.Core;
|
|
using FlaUI.Core.Definitions;
|
|
using FlaUI.UIA3;
|
|
using WxAgent.Core;
|
|
|
|
namespace WxAgent.Windows;
|
|
|
|
public sealed record ProcessDiagnostic(
|
|
int Id,
|
|
long WorkingSetBytes,
|
|
int SessionId,
|
|
string? Version,
|
|
bool CanReadMemory,
|
|
string? AccessError,
|
|
int HandleCount,
|
|
int ThreadCount);
|
|
|
|
public sealed record DoctorReport(
|
|
string OperatingSystem,
|
|
bool UserInteractive,
|
|
int CurrentSessionId,
|
|
IReadOnlyList<ProcessDiagnostic> Processes,
|
|
bool WindowFound,
|
|
IReadOnlyDictionary<string, bool> RequiredControls,
|
|
int DataRootCount,
|
|
IReadOnlyList<WxAgentErrorCode> Errors,
|
|
bool InputDesktopAvailable = false);
|
|
|
|
public static class WechatDoctor
|
|
{
|
|
public static DoctorReport Run(CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
var errors = new List<WxAgentErrorCode>();
|
|
var inputDesktopAvailable = WechatDesktop.IsInputAvailable();
|
|
if (!inputDesktopAvailable) errors.Add(WxAgentErrorCode.SessionLocked);
|
|
using var currentProcess = Process.GetCurrentProcess();
|
|
var rawProcesses = Process.GetProcessesByName("Weixin");
|
|
ProcessDiagnostic[] processes;
|
|
try
|
|
{
|
|
var diagnostics = new List<ProcessDiagnostic>();
|
|
foreach (var process in rawProcesses)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
try { diagnostics.Add(CreateProcessDiagnostic(process, cancellationToken)); }
|
|
catch (InvalidOperationException) { /* Process exited during sampling. */ }
|
|
catch (System.ComponentModel.Win32Exception) { /* Process is no longer accessible. */ }
|
|
}
|
|
processes = diagnostics.OrderByDescending(process => process.WorkingSetBytes).ToArray();
|
|
}
|
|
finally
|
|
{
|
|
foreach (var process in rawProcesses) process.Dispose();
|
|
}
|
|
|
|
if (processes.Length == 0)
|
|
{
|
|
errors.Add(WxAgentErrorCode.WechatNotRunning);
|
|
}
|
|
else if (processes.All(process => !process.CanReadMemory))
|
|
{
|
|
errors.Add(WxAgentErrorCode.ProcessAccessDenied);
|
|
}
|
|
|
|
var controls = WechatLocators.RequiredAutomationIds.ToDictionary(id => id, _ => false, StringComparer.Ordinal);
|
|
var windowFound = false;
|
|
try
|
|
{
|
|
using var automation = new UIA3Automation();
|
|
var window = FindWechatWindow(automation);
|
|
windowFound = window is not null;
|
|
if (window is not null)
|
|
{
|
|
foreach (var id in controls.Keys.ToArray())
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
controls[id] = window.FindFirstDescendant(condition => condition.ByAutomationId(id)) is not null;
|
|
}
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
throw;
|
|
}
|
|
catch
|
|
{
|
|
windowFound = false;
|
|
}
|
|
|
|
if (!windowFound && processes.Length > 0)
|
|
{
|
|
errors.Add(processes.All(process => process.SessionId != currentProcess.SessionId)
|
|
? WxAgentErrorCode.SessionLocked
|
|
: WxAgentErrorCode.WindowNotFound);
|
|
}
|
|
else if (windowFound && WechatLocators.ClassifyFoundWindow(controls) is { } uiError)
|
|
{
|
|
errors.Add(uiError);
|
|
}
|
|
|
|
var dataRootCount = WechatDatabaseDiscovery.FindAccountRoots(cancellationToken: cancellationToken).Count;
|
|
if (dataRootCount == 0)
|
|
{
|
|
errors.Add(WxAgentErrorCode.DataRootNotFound);
|
|
}
|
|
|
|
return new DoctorReport(
|
|
Environment.OSVersion.VersionString,
|
|
Environment.UserInteractive,
|
|
currentProcess.SessionId,
|
|
processes,
|
|
windowFound,
|
|
controls,
|
|
dataRootCount,
|
|
errors.Distinct().ToArray(),
|
|
inputDesktopAvailable);
|
|
}
|
|
|
|
private static readonly AsyncLocal<int?> TargetProcess = new();
|
|
private static readonly AsyncLocal<long?> TargetWindow = new();
|
|
|
|
internal static int? TargetProcessId => TargetProcess.Value;
|
|
|
|
internal static IDisposable UseWindow(int processId, long windowHandle) => UseTarget(processId, windowHandle);
|
|
|
|
private static IDisposable UseTarget(int? processId, long? windowHandle)
|
|
{
|
|
var previousProcess = TargetProcess.Value;
|
|
var previousWindow = TargetWindow.Value;
|
|
TargetProcess.Value = processId;
|
|
TargetWindow.Value = windowHandle;
|
|
return new TargetScope(previousProcess, previousWindow);
|
|
}
|
|
|
|
private sealed class TargetScope(int? previousProcess, long? previousWindow) : IDisposable
|
|
{
|
|
public void Dispose()
|
|
{
|
|
TargetProcess.Value = previousProcess;
|
|
TargetWindow.Value = previousWindow;
|
|
}
|
|
}
|
|
|
|
internal static FlaUI.Core.AutomationElements.AutomationElement? FindWechatWindow(AutomationBase automation)
|
|
{
|
|
FlaUI.Core.AutomationElements.AutomationElement? fallback = null;
|
|
var processId = TargetProcess.Value;
|
|
var windowHandle = TargetWindow.Value;
|
|
var mainWindows = WechatNativeWindow.Enumerate().Where(window =>
|
|
(processId is null || window.ProcessId == processId) &&
|
|
(windowHandle is null || window.Handle == windowHandle) &&
|
|
window.Visible && !window.Minimized && WechatLocators.IsNativeMainWindow(window.ClassName, window.Title));
|
|
foreach (var native in mainWindows)
|
|
{
|
|
try
|
|
{
|
|
var candidate = automation.FromHandle((nint)native.Handle);
|
|
if (candidate.FindFirstDescendant(condition => condition.ByAutomationId(WechatLocators.MainView)) is not null)
|
|
return candidate;
|
|
fallback ??= candidate;
|
|
}
|
|
catch
|
|
{
|
|
// Ignore transient or closing subwindows.
|
|
}
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
internal static FlaUI.Core.AutomationElements.AutomationElement? FindNamedWechatWindow(
|
|
AutomationBase automation, string title, CancellationToken cancellationToken)
|
|
{
|
|
var matches = new List<FlaUI.Core.AutomationElements.AutomationElement>();
|
|
foreach (var native in WechatNativeWindow.Enumerate().Where(window => window.Visible))
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
try
|
|
{
|
|
var candidate = automation.FromHandle((nint)native.Handle);
|
|
if (string.Equals(candidate.Name, title, StringComparison.Ordinal)) matches.Add(candidate);
|
|
}
|
|
catch { /* A window may disappear during enumeration. */ }
|
|
}
|
|
if (matches.Count > 1)
|
|
throw new WxAgentException(WxAgentErrorCode.InvalidOperationState, "Multiple WeChat windows have the requested title; refusing an ambiguous target.");
|
|
return matches.SingleOrDefault();
|
|
}
|
|
|
|
private static ProcessDiagnostic CreateProcessDiagnostic(Process process, CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
string? version = null;
|
|
try
|
|
{
|
|
version = process.MainModule?.FileVersionInfo.FileVersion;
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
var access = ProcessMemoryScanner.Probe(process.Id);
|
|
var handleCount = 0;
|
|
var threadCount = 0;
|
|
try { handleCount = process.HandleCount; } catch { }
|
|
try { threadCount = process.Threads.Count; } catch { }
|
|
return new ProcessDiagnostic(process.Id, process.WorkingSet64, process.SessionId, version, access.Success, access.Error, handleCount, threadCount);
|
|
}
|
|
}
|