All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 32s
Soft-wake search popup only; n2-wake on float crashed WeCom via wecom_wake.dll. Co-authored-by: Cursor <cursoragent@cursor.com>
747 lines
31 KiB
C#
747 lines
31 KiB
C#
// desktop-wake.exe — N1 UIA 客户端 + N2 注入/钩子编排(须 x86 以匹配 WXWork)
|
||
// 用法: desktop-wake.exe <profile> <hwnd> [--mode auto|n1|n2]
|
||
// 退出码: 0暖成功 1参数 2窗无效 3UIA异常 4仍稀疏 5注入失败
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.IO;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Windows.Automation;
|
||
|
||
namespace JiangchangDesktopWake
|
||
{
|
||
internal static class Program
|
||
{
|
||
private const int WM_GETOBJECT = 0x003D;
|
||
private const int WM_NULL = 0x0000;
|
||
private const uint OBJID_CLIENT = 0xFFFFFFFC;
|
||
private const uint OBJID_WINDOW = 0x00000000;
|
||
private const int SW_RESTORE = 9;
|
||
private const int SW_SHOW = 5;
|
||
private const int SW_MAXIMIZE = 3;
|
||
private const int WH_CALLWNDPROC = 4;
|
||
private const uint PROCESS_CREATE_THREAD = 0x0002;
|
||
private const uint PROCESS_QUERY_INFORMATION = 0x0400;
|
||
private const uint PROCESS_VM_OPERATION = 0x0008;
|
||
private const uint PROCESS_VM_WRITE = 0x0020;
|
||
private const uint PROCESS_VM_READ = 0x0010;
|
||
private const uint MEM_COMMIT = 0x1000;
|
||
private const uint MEM_RESERVE = 0x2000;
|
||
private const uint PAGE_READWRITE = 0x04;
|
||
|
||
[DllImport("user32.dll")]
|
||
private static extern bool IsWindow(IntPtr hWnd);
|
||
|
||
[DllImport("user32.dll")]
|
||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||
|
||
[DllImport("user32.dll")]
|
||
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||
|
||
[DllImport("user32.dll")]
|
||
private static extern bool IsIconic(IntPtr hWnd);
|
||
|
||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
|
||
|
||
[DllImport("user32.dll")]
|
||
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
||
|
||
[DllImport("user32.dll")]
|
||
private static extern bool PostMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
||
|
||
[DllImport("user32.dll")]
|
||
private static extern bool IsHungAppWindow(IntPtr hWnd);
|
||
|
||
[DllImport("user32.dll", SetLastError = true)]
|
||
private static extern IntPtr SendMessageTimeout(
|
||
IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam, uint fuFlags, uint uTimeout, out IntPtr lpdwResult);
|
||
|
||
[DllImport("user32.dll")]
|
||
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||
|
||
[DllImport("oleacc.dll")]
|
||
private static extern int AccessibleObjectFromWindow(
|
||
IntPtr hwnd, uint dwId, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out object ppvObject);
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true)]
|
||
private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true)]
|
||
private static extern bool CloseHandle(IntPtr hObject);
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true)]
|
||
private static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize, uint flAllocationType, uint flProtect);
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true)]
|
||
private static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, UIntPtr nSize, out UIntPtr lpNumberOfBytesWritten);
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true)]
|
||
private static extern IntPtr GetModuleHandle(string lpModuleName);
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
|
||
private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true)]
|
||
private static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, UIntPtr dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true)]
|
||
private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true)]
|
||
private static extern bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode);
|
||
|
||
[DllImport("user32.dll", SetLastError = true)]
|
||
private static extern bool SystemParametersInfo(uint uiAction, uint uiParam, IntPtr pvParam, uint fWinIni);
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true)]
|
||
private static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize, uint dwFreeType);
|
||
|
||
[DllImport("user32.dll", SetLastError = true)]
|
||
private static extern IntPtr SetWindowsHookEx(int idHook, IntPtr lpfn, IntPtr hMod, uint dwThreadId);
|
||
|
||
[DllImport("user32.dll", SetLastError = true)]
|
||
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||
private static extern IntPtr LoadLibrary(string lpFileName);
|
||
|
||
private static readonly Guid IID_IAccessible = new Guid("618736E0-3C3D-11CF-810C-00AA00389B71");
|
||
|
||
private static int Main(string[] args)
|
||
{
|
||
if (args.Length < 2)
|
||
{
|
||
Console.Error.WriteLine("usage: desktop-wake.exe <profile> <hwnd> [--mode auto|n1|n2|n2-wake|n2-hook]");
|
||
Console.Error.WriteLine(" n1=UIA; n2=inject only; n2-wake=inject+WakeProvider(C2); n2-hook=UNSAFE");
|
||
return 1;
|
||
}
|
||
|
||
string profile = (args[0] ?? "").Trim().ToLowerInvariant();
|
||
string mode = "auto";
|
||
for (int i = 2; i < args.Length; i++)
|
||
{
|
||
if (args[i] == "--mode" && i + 1 < args.Length)
|
||
{
|
||
mode = (args[i + 1] ?? "auto").Trim().ToLowerInvariant();
|
||
i++;
|
||
}
|
||
}
|
||
|
||
IntPtr hwnd;
|
||
try { hwnd = ParseHwnd(args[1]); }
|
||
catch
|
||
{
|
||
Console.Error.WriteLine("invalid hwnd: " + args[1]);
|
||
return 1;
|
||
}
|
||
|
||
if (!IsWindow(hwnd))
|
||
{
|
||
WriteJson(false, false, false, profile, hwnd, 0, 0, new string[0], "window_invalid", mode, null);
|
||
return 2;
|
||
}
|
||
|
||
int minCount = profile == "wecom" ? 80 : 30;
|
||
string[] markers = profile == "wecom"
|
||
? new[] { "con_op", "searchedit" }
|
||
: new string[0];
|
||
|
||
string method = "n1_uia_client";
|
||
string detailExtra = "";
|
||
bool n2InjectOk = false;
|
||
|
||
try
|
||
{
|
||
Activate(hwnd);
|
||
// n1/auto:进程外 UIA
|
||
// n2:仅注入探针
|
||
// n2-wake:注入 + 远程 WakeProvider(C2,无钩子)
|
||
// n2-hook:UNSAFE
|
||
bool doN2 = (mode == "n2" || mode == "n2-wake" || mode == "n2-hook") && profile == "wecom";
|
||
bool doN2Hook = (mode == "n2-hook") && profile == "wecom";
|
||
bool doWakeProvider = (mode == "n2-wake") && profile == "wecom";
|
||
// n2-wake:不在 exe 内做 UIA 深扫(易跟着目标进程一起挂);暖树由 Python probe 判定
|
||
bool doN1 = (mode == "n1" || mode == "auto" || mode == "n2-hook");
|
||
// n2-wake 不再 SPI_SETSCREENREADER:会触发 UiaRoot 查询,测试机曾崩 uiautomationcore
|
||
if (!doWakeProvider)
|
||
EnableScreenReaderFlag();
|
||
|
||
// 1.2.9:SearchResultWindow2 禁止 n2 注入(wecom_wake.dll 子类化/DuiLib 会崩企微)
|
||
if (doN2 && IsSearchResultPopup(hwnd))
|
||
{
|
||
SendGetObject(hwnd);
|
||
TryMsaa(hwnd);
|
||
bool responsivePopup = CheckUiResponsive(hwnd);
|
||
method = "popup_soft_only";
|
||
WriteJson(
|
||
responsivePopup, false, responsivePopup,
|
||
profile, hwnd, 0, 0, new string[0],
|
||
"search_popup_no_inject|soft_getobject_msaa",
|
||
mode, method);
|
||
return responsivePopup ? 0 : 6;
|
||
}
|
||
|
||
if (doN2)
|
||
{
|
||
string n2Detail;
|
||
IntPtr remoteMod;
|
||
n2InjectOk = TryN2(hwnd, doN2Hook, doWakeProvider, out n2Detail, out remoteMod);
|
||
detailExtra = n2Detail;
|
||
method = n2InjectOk ? (doWakeProvider ? "n2_wake" : "n2_inject") : "n2_inject_failed";
|
||
bool responsive = CheckUiResponsive(hwnd);
|
||
detailExtra = detailExtra + ";ui_responsive=" + responsive;
|
||
|
||
if (mode == "n2" || mode == "n2-wake")
|
||
{
|
||
// ok=注入(+provider)成功且 UI 仍响应;是否暖树由调用方 probe
|
||
WriteJson(
|
||
n2InjectOk && responsive, n2InjectOk, responsive,
|
||
profile, hwnd, 0, 0, new string[0],
|
||
(n2InjectOk ? (doWakeProvider ? "wake_provider_ok" : "inject_ok") : "inject_failed")
|
||
+ "|" + detailExtra,
|
||
mode, method);
|
||
if (!n2InjectOk) return 5;
|
||
if (!responsive) return 6;
|
||
return 0;
|
||
}
|
||
|
||
if (!n2InjectOk)
|
||
{
|
||
WriteJson(false, false, responsive, profile, hwnd, 0, 0, new string[0], n2Detail, mode, method);
|
||
return 5;
|
||
}
|
||
if (!responsive)
|
||
{
|
||
WriteJson(false, true, false, profile, hwnd, 0, 0, new string[0],
|
||
"ui_hung_after_inject|" + detailExtra, mode, method);
|
||
return 6;
|
||
}
|
||
Thread.Sleep(400);
|
||
}
|
||
|
||
if (doN1)
|
||
{
|
||
SendGetObject(hwnd);
|
||
TryMsaa(hwnd);
|
||
var root = AutomationElement.FromHandle(hwnd);
|
||
if (root == null)
|
||
{
|
||
WriteJson(false, n2InjectOk, CheckUiResponsive(hwnd), profile, hwnd, 0, 0, new string[0],
|
||
"uia_from_handle_null|" + detailExtra, mode, method);
|
||
return 3;
|
||
}
|
||
try { root.SetFocus(); } catch { }
|
||
|
||
int walk1 = WalkCount(root, 64);
|
||
int findAll = FindAllCount(root);
|
||
Thread.Sleep(200);
|
||
SendGetObject(hwnd);
|
||
int walk2 = WalkCount(root, 64);
|
||
int walk = Math.Max(walk1, Math.Max(walk2, findAll));
|
||
var foundMarkers = FindMarkers(root, markers);
|
||
bool warm = walk >= minCount
|
||
&& (markers.Length == 0 || foundMarkers.Count >= markers.Length);
|
||
bool responsive = CheckUiResponsive(hwnd);
|
||
|
||
string detail = warm ? "warm" : "still_sparse";
|
||
if (!string.IsNullOrEmpty(detailExtra))
|
||
detail = detail + "|" + detailExtra;
|
||
if (mode == "n2-wake")
|
||
method = warm ? "n2_wake" : "n2_wake_still_sparse";
|
||
else if (mode == "n2-hook")
|
||
method = n2InjectOk ? (warm ? "n2_hook" : "n2_hook_still_sparse") : method;
|
||
|
||
WriteJson(warm, n2InjectOk, responsive, profile, hwnd, walk, findAll,
|
||
foundMarkers.ToArray(), detail, mode, method);
|
||
return warm ? 0 : 4;
|
||
}
|
||
|
||
WriteJson(false, false, false, profile, hwnd, 0, 0, new string[0], "no_mode", mode, method);
|
||
return 1;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
WriteJson(false, n2InjectOk, false, profile, hwnd, 0, 0, new string[0],
|
||
"exception:" + ex.GetType().Name + ":" + ex.Message + "|" + detailExtra, mode, method);
|
||
return 3;
|
||
}
|
||
}
|
||
|
||
private static bool CheckUiResponsive(IntPtr hwnd)
|
||
{
|
||
try
|
||
{
|
||
if (IsHungAppWindow(hwnd)) return false;
|
||
IntPtr result;
|
||
IntPtr ok = SendMessageTimeout(hwnd, WM_NULL, IntPtr.Zero, IntPtr.Zero, 0x0002, 2000, out result);
|
||
return ok != IntPtr.Zero;
|
||
}
|
||
catch { return false; }
|
||
}
|
||
|
||
private static void EnableScreenReaderFlag()
|
||
{
|
||
try
|
||
{
|
||
// SPI_SETSCREENREADER=0x46
|
||
SystemParametersInfo(0x0046, 1, IntPtr.Zero, 0x0003);
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private static bool TryN2(IntPtr hwnd, bool enableHook, bool callWakeProvider, out string detail, out IntPtr remoteModule)
|
||
{
|
||
remoteModule = IntPtr.Zero;
|
||
string dll = FindPayloadDll();
|
||
if (dll == null)
|
||
{
|
||
detail = "n2_dll_missing";
|
||
return false;
|
||
}
|
||
|
||
uint pid;
|
||
uint tid = GetWindowThreadProcessId(hwnd, out pid);
|
||
if (pid == 0 || tid == 0)
|
||
{
|
||
detail = "n2_pid_tid_failed";
|
||
return false;
|
||
}
|
||
|
||
string targetFile = Path.Combine(Path.GetTempPath(), "jiangchang_desktop_wake_target.txt");
|
||
File.WriteAllText(
|
||
targetFile,
|
||
"hwnd=0x" + hwnd.ToInt64().ToString("x") + "\r\npid=" + pid + "\r\n",
|
||
Encoding.ASCII);
|
||
|
||
string loadedFile = Path.Combine(Path.GetTempPath(), "jiangchang_desktop_wake_loaded.txt");
|
||
string subclassFile = Path.Combine(Path.GetTempPath(), "jiangchang_desktop_wake_subclass.txt");
|
||
string hookFile = Path.Combine(Path.GetTempPath(), "jiangchang_desktop_wake_hook.txt");
|
||
string providerFile = Path.Combine(Path.GetTempPath(), "jiangchang_desktop_wake_provider.txt");
|
||
try { if (File.Exists(loadedFile)) File.Delete(loadedFile); } catch { }
|
||
try { if (File.Exists(subclassFile)) File.Delete(subclassFile); } catch { }
|
||
try { if (File.Exists(hookFile)) File.Delete(hookFile); } catch { }
|
||
try { if (File.Exists(providerFile)) File.Delete(providerFile); } catch { }
|
||
|
||
string injErr;
|
||
bool injected = InjectLoadLibrary(pid, dll, out injErr, out remoteModule);
|
||
|
||
string hookErr = enableHook ? "" : "skipped_unsafe_hook";
|
||
bool hooked = false;
|
||
if (enableHook)
|
||
{
|
||
hooked = InstallCallWndHook(dll, tid, hwnd, out hookErr);
|
||
PostMessage(hwnd, WM_NULL, IntPtr.Zero, IntPtr.Zero);
|
||
Thread.Sleep(200);
|
||
}
|
||
|
||
string wakeErr = "skipped";
|
||
bool wakeCalled = false;
|
||
if (callWakeProvider && injected && remoteModule != IntPtr.Zero)
|
||
{
|
||
wakeCalled = CallRemoteWakeProvider(pid, dll, remoteModule, hwnd, out wakeErr);
|
||
// STA UIA 可能写完 marker 略晚;等 done=1
|
||
for (int i = 0; i < 40; i++)
|
||
{
|
||
if (File.Exists(providerFile))
|
||
{
|
||
try
|
||
{
|
||
string pt = File.ReadAllText(providerFile);
|
||
if (pt.IndexOf("done=1", StringComparison.Ordinal) >= 0)
|
||
{
|
||
wakeCalled = true;
|
||
break;
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
Thread.Sleep(250);
|
||
}
|
||
if (!wakeCalled && File.Exists(providerFile))
|
||
wakeCalled = true;
|
||
}
|
||
|
||
bool loadedMarker = false;
|
||
if (File.Exists(loadedFile))
|
||
{
|
||
try
|
||
{
|
||
string text = File.ReadAllText(loadedFile);
|
||
loadedMarker = text.IndexOf("pid=" + pid, StringComparison.Ordinal) >= 0
|
||
|| text.IndexOf("match=1", StringComparison.Ordinal) >= 0;
|
||
}
|
||
catch { loadedMarker = File.Exists(loadedFile); }
|
||
}
|
||
bool subclassMarker = File.Exists(subclassFile);
|
||
bool hookMarker = File.Exists(hookFile);
|
||
bool providerMarker = File.Exists(providerFile);
|
||
string providerText = "";
|
||
if (providerMarker)
|
||
{
|
||
try { providerText = File.ReadAllText(providerFile).Replace("\r", " ").Replace("\n", " ").Trim(); }
|
||
catch { }
|
||
}
|
||
|
||
detail = string.Format(
|
||
"pid={0};tid={1};inject={2};hook={3};wake={4};loaded_marker={5};provider_marker={6};provider={7};subclass_marker={8};hook_marker={9};injErr={10};hookErr={11};wakeErr={12};dll={13};remoteMod=0x{14}",
|
||
pid, tid, injected, hooked, wakeCalled, loadedMarker, providerMarker, providerText,
|
||
subclassMarker, hookMarker, injErr, hookErr, wakeErr, Path.GetFileName(dll),
|
||
remoteModule.ToInt64().ToString("x"));
|
||
|
||
// DLL 已在目标进程时再次 LoadLibrary 不会重跑 DllMain,故 remoteMod!=0 也算注入成功
|
||
bool injectOk = injected && (loadedMarker || remoteModule != IntPtr.Zero);
|
||
// WakeProvider 为 best-effort:注入成功即可,暖树由外层 probe 判定
|
||
return injectOk || (hooked && (hookMarker || subclassMarker));
|
||
}
|
||
|
||
private static string FindPayloadDll()
|
||
{
|
||
try
|
||
{
|
||
string exe = Process.GetCurrentProcess().MainModule.FileName;
|
||
var dir = new DirectoryInfo(Path.GetDirectoryName(exe));
|
||
for (int i = 0; i < 6 && dir != null; i++)
|
||
{
|
||
string newest = null;
|
||
DateTime best = DateTime.MinValue;
|
||
string[] searchDirs = new string[] { dir.FullName, Path.Combine(dir.FullName, "native") };
|
||
foreach (var sd in searchDirs)
|
||
{
|
||
if (!Directory.Exists(sd)) continue;
|
||
// 优先正式名 wecom_wake.dll(发版包)
|
||
string primary = Path.Combine(sd, "wecom_wake.dll");
|
||
if (File.Exists(primary)) return Path.GetFullPath(primary);
|
||
foreach (var f in Directory.GetFiles(sd, "wecom_wake*.dll"))
|
||
{
|
||
DateTime t = File.GetLastWriteTimeUtc(f);
|
||
if (t >= best) { best = t; newest = f; }
|
||
}
|
||
}
|
||
if (newest != null) return Path.GetFullPath(newest);
|
||
dir = dir.Parent;
|
||
}
|
||
}
|
||
catch { }
|
||
return null;
|
||
}
|
||
|
||
private static bool InjectLoadLibrary(uint pid, string dllPath, out string err, out IntPtr remoteModule)
|
||
{
|
||
err = "";
|
||
remoteModule = IntPtr.Zero;
|
||
IntPtr hProcess = OpenProcess(
|
||
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
|
||
false, pid);
|
||
if (hProcess == IntPtr.Zero)
|
||
{
|
||
err = "OpenProcess:" + Marshal.GetLastWin32Error();
|
||
return false;
|
||
}
|
||
try
|
||
{
|
||
byte[] pathBytes = Encoding.Unicode.GetBytes(dllPath + "\0");
|
||
IntPtr remote = VirtualAllocEx(hProcess, IntPtr.Zero, (UIntPtr)pathBytes.Length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||
if (remote == IntPtr.Zero)
|
||
{
|
||
err = "VirtualAllocEx:" + Marshal.GetLastWin32Error();
|
||
return false;
|
||
}
|
||
UIntPtr written;
|
||
if (!WriteProcessMemory(hProcess, remote, pathBytes, (UIntPtr)pathBytes.Length, out written))
|
||
{
|
||
err = "WriteProcessMemory:" + Marshal.GetLastWin32Error();
|
||
return false;
|
||
}
|
||
IntPtr hKernel = GetModuleHandle("kernel32.dll");
|
||
IntPtr loadLib = GetProcAddress(hKernel, "LoadLibraryW");
|
||
if (loadLib == IntPtr.Zero)
|
||
{
|
||
err = "GetProcAddress LoadLibraryW failed";
|
||
return false;
|
||
}
|
||
IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, UIntPtr.Zero, loadLib, remote, 0, IntPtr.Zero);
|
||
if (hThread == IntPtr.Zero)
|
||
{
|
||
err = "CreateRemoteThread:" + Marshal.GetLastWin32Error();
|
||
return false;
|
||
}
|
||
uint wait = WaitForSingleObject(hThread, 5000);
|
||
uint exitCode = 0;
|
||
GetExitCodeThread(hThread, out exitCode);
|
||
CloseHandle(hThread);
|
||
VirtualFreeEx(hProcess, remote, UIntPtr.Zero, 0x8000);
|
||
if (wait != 0)
|
||
{
|
||
err = "LoadLibrary wait timeout/status=" + wait;
|
||
return false;
|
||
}
|
||
if (exitCode == 0)
|
||
{
|
||
err = "LoadLibrary returned NULL";
|
||
return false;
|
||
}
|
||
remoteModule = new IntPtr(unchecked((int)exitCode)); // x86 HMODULE
|
||
return true;
|
||
}
|
||
finally
|
||
{
|
||
CloseHandle(hProcess);
|
||
}
|
||
}
|
||
|
||
private static bool CallRemoteWakeProvider(
|
||
uint pid, string dllPath, IntPtr remoteModule, IntPtr hwnd, out string err)
|
||
{
|
||
err = "";
|
||
IntPtr localMod = LoadLibrary(dllPath);
|
||
if (localMod == IntPtr.Zero)
|
||
{
|
||
err = "local LoadLibrary:" + Marshal.GetLastWin32Error();
|
||
return false;
|
||
}
|
||
IntPtr localProc = GetProcAddress(localMod, "WakeProvider");
|
||
if (localProc == IntPtr.Zero)
|
||
localProc = GetProcAddress(localMod, "_WakeProvider@4");
|
||
if (localProc == IntPtr.Zero)
|
||
{
|
||
err = "WakeProvider export missing";
|
||
return false;
|
||
}
|
||
long offset = localProc.ToInt64() - localMod.ToInt64();
|
||
IntPtr remoteProc = new IntPtr(remoteModule.ToInt64() + offset);
|
||
|
||
IntPtr hProcess = OpenProcess(
|
||
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
|
||
false, pid);
|
||
if (hProcess == IntPtr.Zero)
|
||
{
|
||
err = "OpenProcess wake:" + Marshal.GetLastWin32Error();
|
||
return false;
|
||
}
|
||
try
|
||
{
|
||
IntPtr hThread = CreateRemoteThread(
|
||
hProcess, IntPtr.Zero, UIntPtr.Zero, remoteProc, hwnd, 0, IntPtr.Zero);
|
||
if (hThread == IntPtr.Zero)
|
||
{
|
||
err = "CreateRemoteThread WakeProvider:" + Marshal.GetLastWin32Error();
|
||
return false;
|
||
}
|
||
// STA + 进程内 FindAll 可能较慢
|
||
uint wait = WaitForSingleObject(hThread, 20000);
|
||
CloseHandle(hThread);
|
||
if (wait != 0)
|
||
{
|
||
err = "WakeProvider wait=" + wait;
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
finally
|
||
{
|
||
CloseHandle(hProcess);
|
||
}
|
||
}
|
||
|
||
private static bool InstallCallWndHook(string dllPath, uint tid, IntPtr hwnd, out string err)
|
||
{
|
||
err = "";
|
||
IntPtr hMod = LoadLibrary(dllPath);
|
||
if (hMod == IntPtr.Zero)
|
||
{
|
||
err = "LoadLibrary:" + Marshal.GetLastWin32Error();
|
||
return false;
|
||
}
|
||
// x86 __stdcall 可能是 _JcCallWndProc@12;优先无修饰名(.def 导出)
|
||
IntPtr proc = GetProcAddress(hMod, "JcCallWndProc");
|
||
if (proc == IntPtr.Zero)
|
||
proc = GetProcAddress(hMod, "_JcCallWndProc@12");
|
||
if (proc == IntPtr.Zero)
|
||
{
|
||
err = "GetProcAddress JcCallWndProc:" + Marshal.GetLastWin32Error();
|
||
return false;
|
||
}
|
||
IntPtr hhk = SetWindowsHookEx(WH_CALLWNDPROC, proc, hMod, tid);
|
||
if (hhk == IntPtr.Zero)
|
||
{
|
||
err = "SetWindowsHookEx:" + Marshal.GetLastWin32Error();
|
||
return false;
|
||
}
|
||
PostMessage(hwnd, WM_NULL, IntPtr.Zero, IntPtr.Zero);
|
||
Thread.Sleep(400);
|
||
// 保持钩子,避免 DLL 被立刻卸载;进程退出时清理
|
||
GC.KeepAlive(hhk);
|
||
return true;
|
||
}
|
||
|
||
private static IntPtr ParseHwnd(string s)
|
||
{
|
||
s = (s ?? "").Trim();
|
||
if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||
return new IntPtr(Convert.ToInt64(s, 16));
|
||
// 含 a-f 一定是 hex;纯数字优先按十进制(Python 可传 {hwnd})
|
||
bool hasHexLetter = false;
|
||
for (int i = 0; i < s.Length; i++)
|
||
{
|
||
char c = s[i];
|
||
if ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { hasHexLetter = true; break; }
|
||
}
|
||
if (hasHexLetter)
|
||
return new IntPtr(Convert.ToInt64(s, 16));
|
||
long asDec;
|
||
if (long.TryParse(s, out asDec))
|
||
return new IntPtr(asDec);
|
||
return new IntPtr(Convert.ToInt64(s, 16));
|
||
}
|
||
|
||
private static bool IsSearchResultPopup(IntPtr hwnd)
|
||
{
|
||
try
|
||
{
|
||
var sb = new StringBuilder(256);
|
||
if (GetClassName(hwnd, sb, sb.Capacity) <= 0) return false;
|
||
return string.Equals(sb.ToString(), "SearchResultWindow2", StringComparison.Ordinal);
|
||
}
|
||
catch { return false; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 主窗可 Maximize;SearchResultWindow2 等浮层禁止 Maximize(会撑爆/关掉浮层)。
|
||
/// </summary>
|
||
private static bool ShouldMaximize(IntPtr hwnd)
|
||
{
|
||
try
|
||
{
|
||
var sb = new StringBuilder(256);
|
||
if (GetClassName(hwnd, sb, sb.Capacity) <= 0) return true;
|
||
string cls = sb.ToString();
|
||
if (string.Equals(cls, "SearchResultWindow2", StringComparison.Ordinal))
|
||
return false;
|
||
if (cls.IndexOf("Shadow", StringComparison.OrdinalIgnoreCase) >= 0)
|
||
return false;
|
||
}
|
||
catch { }
|
||
return true;
|
||
}
|
||
|
||
private static void Activate(IntPtr hwnd)
|
||
{
|
||
try
|
||
{
|
||
if (IsIconic(hwnd)) ShowWindow(hwnd, SW_RESTORE);
|
||
else if (ShouldMaximize(hwnd)) ShowWindow(hwnd, SW_MAXIMIZE);
|
||
else ShowWindow(hwnd, SW_SHOW);
|
||
SetForegroundWindow(hwnd);
|
||
Thread.Sleep(250);
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private static void SendGetObject(IntPtr hwnd)
|
||
{
|
||
SendMessage(hwnd, WM_GETOBJECT, IntPtr.Zero, new IntPtr(unchecked((int)OBJID_CLIENT)));
|
||
SendMessage(hwnd, WM_GETOBJECT, IntPtr.Zero, new IntPtr(unchecked((int)OBJID_WINDOW)));
|
||
}
|
||
|
||
private static void TryMsaa(IntPtr hwnd)
|
||
{
|
||
try
|
||
{
|
||
Guid iid = IID_IAccessible;
|
||
object acc;
|
||
int hr = AccessibleObjectFromWindow(hwnd, OBJID_CLIENT, ref iid, out acc);
|
||
if (hr == 0 && acc != null)
|
||
Marshal.FinalReleaseComObject(acc);
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private static int WalkCount(AutomationElement root, int maxDepth)
|
||
{
|
||
int count = 0;
|
||
var walker = TreeWalker.ControlViewWalker;
|
||
Walk(walker, root, 0, maxDepth, ref count);
|
||
return count;
|
||
}
|
||
|
||
private static void Walk(TreeWalker walker, AutomationElement node, int depth, int maxDepth, ref int count)
|
||
{
|
||
if (node == null || depth > maxDepth) return;
|
||
count++;
|
||
AutomationElement child;
|
||
try { child = walker.GetFirstChild(node); }
|
||
catch { return; }
|
||
while (child != null)
|
||
{
|
||
Walk(walker, child, depth + 1, maxDepth, ref count);
|
||
try { child = walker.GetNextSibling(child); }
|
||
catch { break; }
|
||
}
|
||
}
|
||
|
||
private static int FindAllCount(AutomationElement root)
|
||
{
|
||
try
|
||
{
|
||
var all = root.FindAll(TreeScope.Subtree, Condition.TrueCondition);
|
||
return all == null ? 0 : all.Count;
|
||
}
|
||
catch { return 0; }
|
||
}
|
||
|
||
private static List<string> FindMarkers(AutomationElement root, string[] markers)
|
||
{
|
||
var found = new List<string>();
|
||
if (markers == null || markers.Length == 0) return found;
|
||
foreach (var m in markers)
|
||
{
|
||
try
|
||
{
|
||
var el = root.FindFirst(TreeScope.Subtree, new PropertyCondition(AutomationElement.NameProperty, m));
|
||
if (el != null) { found.Add(m); continue; }
|
||
el = root.FindFirst(TreeScope.Subtree, new PropertyCondition(AutomationElement.AutomationIdProperty, m));
|
||
if (el != null) found.Add(m);
|
||
}
|
||
catch { }
|
||
}
|
||
return found;
|
||
}
|
||
|
||
private static void WriteJson(
|
||
bool ok, bool injectOk, bool uiResponsive, string profile, IntPtr hwnd,
|
||
int walk, int findAll, string[] markers, string detail, string mode, string method)
|
||
{
|
||
var sb = new StringBuilder();
|
||
sb.Append("{");
|
||
sb.Append("\"ok\":").Append(ok ? "true" : "false").Append(",");
|
||
sb.Append("\"inject_ok\":").Append(injectOk ? "true" : "false").Append(",");
|
||
sb.Append("\"ui_responsive\":").Append(uiResponsive ? "true" : "false").Append(",");
|
||
sb.Append("\"profile\":\"").Append(Escape(profile)).Append("\",");
|
||
sb.Append("\"hwnd\":\"").Append(hwnd.ToInt64().ToString("x")).Append("\",");
|
||
sb.Append("\"mode\":\"").Append(Escape(mode ?? "")).Append("\",");
|
||
sb.Append("\"method\":\"").Append(Escape(method ?? "")).Append("\",");
|
||
sb.Append("\"walk_count\":").Append(walk).Append(",");
|
||
sb.Append("\"findall_count\":").Append(findAll).Append(",");
|
||
sb.Append("\"markers_found\":[");
|
||
for (int i = 0; i < markers.Length; i++)
|
||
{
|
||
if (i > 0) sb.Append(",");
|
||
sb.Append("\"").Append(Escape(markers[i])).Append("\"");
|
||
}
|
||
sb.Append("],");
|
||
sb.Append("\"detail\":\"").Append(Escape(detail)).Append("\"");
|
||
sb.Append("}");
|
||
Console.WriteLine(sb.ToString());
|
||
}
|
||
|
||
private static string Escape(string s)
|
||
{
|
||
if (string.IsNullOrEmpty(s)) return "";
|
||
return s.Replace("\\", "\\\\").Replace("\"", "\\\"");
|
||
}
|
||
}
|
||
}
|