fix(desktop-wake): avoid in-process UIA FindAll crash (1.2.4)
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 27s

Keep DuiLib subclass wake only with GETOBJECT reentrancy guard; let ensure probe warm out-of-process.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-02 09:19:42 +08:00
parent 71091e38a9
commit 9451bef753
7 changed files with 65 additions and 155 deletions

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jiangchang-platform-kit"
version = "1.2.3"
version = "1.2.4"
description = "匠厂平台共享组件:Skill 实体 SDK + 桌面应用自动化 SDK + 桌面 UIA 暖树"
readme = "README.md"
requires-python = ">=3.10"

View File

@@ -1,6 +1,7 @@
# jiangchang_desktop_wake 验收(阶段 A:ensure 主路径)
# jiangchang_desktop_wake 验收(ensure 主路径,1.2.4+)
正式对外路径是 **`ensure`**(内部:`activate` → `desktop-wake --mode n2-wake` → `probe`)。
`n2-wake` **不再**在企微进程内跑 UIAutomation FindAll(防 `uiautomationcore.dll` 崩溃);判暖只看进程外 probe。
`c2` / `inject` 仅诊断用。
## 0. 编译(企微先完全退出,含托盘)
@@ -56,8 +57,9 @@ python -m jiangchang_desktop_wake probe --profile wecom
| `probe_after.findall_count` | **≥ 80**(常见 300+) |
| markers | 含 `con_op`、`searchedit` |
| 企微 UI | 可点,无「未响应」 |
| provider | `done=1` `subclass=1` `dui=1` `lr_client≠0` |
| provider | `done=1` `subclass=1` `dui=1` `no_inproc_uia=1`;`lr_client≠0` 更佳 |
| subclass | `method=SetWindowLongPtr`,`ok=1`,`userdata≠0` |
| 企微进程 | **不崩**、无「错误报告」对话框 |
### 失败时

View File

@@ -3,7 +3,7 @@
桌面 UIA Provider **暖树**(与 `jiangchang_skill_core` / `jiangchang_desktop_sdk` 平级)。
企微主路径:注入 `wecom_wake.dll`,对主窗 `SetWindowLongPtr` 后走 DuiLib
`GetAccessible` / `OnGetObject`(已验收冷树 findall 2→300+)。
`GetAccessible` / `OnGetObject`;**不在进程内 FindAll**(1.2.4+,避免 uiautomationcore 崩溃)。
可整目录删除:`src/jiangchang_desktop_wake` + `tests/desktop_wake`。

View File

@@ -6,23 +6,20 @@
* WakeTarget — 旧子类化(危险,仅 n2-hook)
* JcCallWndProc — 旧钩子(危险,仅 n2-hook)
*
* C2+ 主路径:
* 1) STA 工作线程 + 进程内 UIA(保持客户端)
* 2) SetWindowSubclass:WM_GETOBJECT 时走 DuiLib 已导出的
* GetAccessible / OnGetObject(不挂钩、不用 CreateStdAccessibleObject)
* 1.2.4 主路径(防 uiautomationcore 崩):
* - SetWindowLongPtr 子类化,WM_GETOBJECT → DuiLib GetAccessible/OnGetObject
* - 重入守卫:处理 GETOBJECT 时不再二次进 DuiLib
* - 不做进程内 UIAutomation FindAll / AccessibleObjectFromWindow(判暖交给进程外 probe)
*/
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <commctrl.h>
#include <oleacc.h>
#include <UIAutomationClient.h>
#include <stdio.h>
#include <string.h>
#pragma comment(lib, "oleacc.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "oleaut32.lib")
#pragma comment(lib, "uuid.lib")
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "comctl32.lib")
@@ -35,9 +32,6 @@
#ifndef CHILDID_SELF
#define CHILDID_SELF 0
#endif
#ifndef ANR_ON
#define ANR_ON 0x00000001
#endif
#ifndef UiaRootObjectId
#define UiaRootObjectId ((LONG)0xFFFFFFE7)
#endif
@@ -49,23 +43,21 @@
static HMODULE g_self = NULL;
static HWND g_hwnd = NULL;
static WNDPROC g_oldProc = NULL;
static WNDPROC g_duiOldProc = NULL;
static volatile LONG g_getobjectHits = 0;
static volatile LONG g_hookHits = 0;
static volatile LONG g_duiHits = 0;
static volatile LONG g_reenterSkip = 0;
static volatile LONG g_subclassInstalled = 0;
static HANDLE g_staThread = NULL;
static HANDLE g_staReady = NULL;
static HWND g_msgHwnd = NULL;
static volatile LONG g_staStarted = 0;
static IUIAutomation* g_auto = NULL;
static IUIAutomationElement* g_root = NULL;
static IUIAutomationElementArray* g_arr = NULL;
static IAccessible* g_acc = NULL;
/* per-thread reentrancy guard for WM_GETOBJECT */
static __declspec(thread) int t_inGetObject = 0;
static volatile LONG g_subclassInstalled = 0;
/* DuiLib thiscall helpers (x86) */
typedef IAccessible*(__thiscall* FnGetAccessible)(void* self);
typedef LRESULT(__thiscall* FnOnGetObject)(
void* self, UINT uMsg, WPARAM wParam, LPARAM lParam, bool* bHandled);
@@ -138,25 +130,6 @@ static void NudgeWinEvents(HWND hwnd)
PostMessageW(hwnd, WM_NULL, 0, 0);
}
static void TryAccUtilityState(HWND hwnd)
{
HMODULE oleacc = GetModuleHandleW(L"oleacc.dll");
if (!oleacc) oleacc = LoadLibraryW(L"oleacc.dll");
if (!oleacc) return;
typedef HRESULT(WINAPI* PFN)(HWND, DWORD, DWORD);
PFN fn = (PFN)GetProcAddress(oleacc, "AccSetRunningUtilityState");
if (!fn) return;
fn(hwnd, ANR_ON, ANR_ON);
}
static void ReleaseKeepAlive()
{
if (g_arr) { g_arr->Release(); g_arr = NULL; }
if (g_root) { g_root->Release(); g_root = NULL; }
if (g_auto) { g_auto->Release(); g_auto = NULL; }
if (g_acc) { g_acc->Release(); g_acc = NULL; }
}
static bool ResolveDuiLib()
{
if (g_fnGetAccessible && g_fnOnGetObject) return true;
@@ -171,7 +144,6 @@ static bool ResolveDuiLib()
return g_fnGetAccessible != NULL || g_fnOnGetObject != NULL;
}
/* Force DuiLib MSAA for WM_GETOBJECT — no CreateStdAccessibleObject (avoids UI hang). */
static LRESULT TryDuiLibGetObject(HWND hwnd, WPARAM wParam, LPARAM lParam)
{
if (!ResolveDuiLib())
@@ -194,7 +166,6 @@ static LRESULT TryDuiLibGetObject(HWND hwnd, WPARAM wParam, LPARAM lParam)
IAccessible* acc = g_fnGetAccessible(self);
if (acc) {
LRESULT lr = LresultFromObject(IID_IAccessible, wParam, (LPUNKNOWN)acc);
/* LresultFromObject AddRef's; release our ref from GetAccessible */
acc->Release();
return lr;
}
@@ -202,7 +173,33 @@ static LRESULT TryDuiLibGetObject(HWND hwnd, WPARAM wParam, LPARAM lParam)
return 0;
}
static WNDPROC g_duiOldProc = NULL;
static LRESULT HandleGetObject(
HWND hwnd, WPARAM wParam, LPARAM lParam, WNDPROC oldProc, bool useDefSubclass)
{
InterlockedIncrement(&g_getobjectHits);
if (t_inGetObject) {
InterlockedIncrement(&g_reenterSkip);
if (useDefSubclass)
return DefSubclassProc(hwnd, WM_GETOBJECT, wParam, lParam);
if (oldProc)
return CallWindowProcW(oldProc, hwnd, WM_GETOBJECT, wParam, lParam);
return DefWindowProcW(hwnd, WM_GETOBJECT, wParam, lParam);
}
t_inGetObject = 1;
LRESULT r = 0;
if (useDefSubclass)
r = DefSubclassProc(hwnd, WM_GETOBJECT, wParam, lParam);
else if (oldProc)
r = CallWindowProcW(oldProc, hwnd, WM_GETOBJECT, wParam, lParam);
if (r == 0)
r = TryDuiLibGetObject(hwnd, wParam, lParam);
t_inGetObject = 0;
return r;
}
static LRESULT CALLBACK DuiSubclassProc(
HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
@@ -210,34 +207,15 @@ static LRESULT CALLBACK DuiSubclassProc(
{
(void)uIdSubclass;
(void)dwRefData;
if (uMsg == WM_GETOBJECT) {
InterlockedIncrement(&g_getobjectHits);
LRESULT r = DefSubclassProc(hWnd, uMsg, wParam, lParam);
if (r != 0)
return r;
r = TryDuiLibGetObject(hWnd, wParam, lParam);
if (r != 0)
return r;
return 0;
}
if (uMsg == WM_GETOBJECT)
return HandleGetObject(hWnd, wParam, lParam, NULL, true);
return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}
/* Fallback when comctl SetWindowSubclass unavailable in host */
static LRESULT CALLBACK DuiWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (msg == WM_GETOBJECT) {
InterlockedIncrement(&g_getobjectHits);
LRESULT r = 0;
if (g_duiOldProc)
r = CallWindowProcW(g_duiOldProc, hwnd, msg, wParam, lParam);
if (r != 0)
return r;
r = TryDuiLibGetObject(hwnd, wParam, lParam);
if (r != 0)
return r;
return 0;
}
if (msg == WM_GETOBJECT)
return HandleGetObject(hwnd, wParam, lParam, g_duiOldProc, false);
if (g_duiOldProc)
return CallWindowProcW(g_duiOldProc, hwnd, msg, wParam, lParam);
return DefWindowProcW(hwnd, msg, wParam, lParam);
@@ -251,21 +229,7 @@ static bool InstallDuiSubclass(HWND hwnd)
ResolveDuiLib();
void* userdata = (void*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
LONG_PTR accPtr = 0;
LONG_PTR ongetLr = 0;
if (userdata && g_fnGetAccessible) {
IAccessible* acc = g_fnGetAccessible(userdata);
accPtr = (LONG_PTR)acc;
if (acc) {
long kids = -1;
acc->get_accChildCount(&kids);
ongetLr = kids;
/* keep one ref for diagnostics; Release extra if we AddRef'd — GetAccessible owns */
acc->Release();
}
}
/* 企微进程里 SetWindowSubclass 常失败;主用 SetWindowLongPtr(只转发 DuiLib GetAccessible,不挂钩) */
SetLastError(0);
g_duiOldProc = (WNDPROC)SetWindowLongPtrW(
hwnd, GWLP_WNDPROC, (LONG_PTR)DuiWndProc);
@@ -288,10 +252,9 @@ static bool InstallDuiSubclass(HWND hwnd)
char line[512];
sprintf_s(line,
"hwnd=%p ok=%d gle=%lu method=%s userdata=%p "
"getacc_ret=%p getacc_kids=%ld duilib=%p getacc=%p onget=%p\r\n",
"duilib=%p getacc=%p onget=%p\r\n",
hwnd, (int)ok, gle, method, userdata,
(void*)accPtr, (long)ongetLr, g_duilib,
g_fnGetAccessible, g_fnOnGetObject);
g_duilib, g_fnGetAccessible, g_fnOnGetObject);
WriteMarker("jiangchang_desktop_wake_subclass.txt", line);
if (!ok) {
InterlockedExchange(&g_subclassInstalled, 0);
@@ -300,78 +263,35 @@ static bool InstallDuiSubclass(HWND hwnd)
return true;
}
static void DoInProcessUiaWake(HWND hwnd)
/* 只装子类化 + 异步 nudge;可选一次短超时 GETOBJECT 看 lr(无进程内 UIA) */
static void DoSafeWake(HWND hwnd)
{
char line[1024];
LONG findall = -1;
LONG msaaKids = -1;
HRESULT hrAuto = E_FAIL;
HRESULT hrEl = E_FAIL;
HRESULT hrFind = E_FAIL;
HRESULT hrMsaa = E_FAIL;
char line[512];
LRESULT lrClient = 0;
LRESULT lrUia = 0;
int subclass_ok = 0;
int dui_ok = 0;
TryAccUtilityState(hwnd);
ResolveDuiLib();
dui_ok = (g_fnGetAccessible || g_fnOnGetObject) ? 1 : 0;
subclass_ok = InstallDuiSubclass(hwnd) ? 1 : 0;
NudgeWinEvents(hwnd);
/* Sync probe WM_GETOBJECT after subclass — goes through our handler */
lrClient = SendMessageW(hwnd, WM_GETOBJECT, 0, (LPARAM)OBJID_CLIENT);
lrUia = SendMessageW(hwnd, WM_GETOBJECT, 0, (LPARAM)UiaRootObjectId);
ReleaseKeepAlive();
hrAuto = CoCreateInstance(
CLSID_CUIAutomation, NULL, CLSCTX_INPROC_SERVER,
IID_IUIAutomation, (void**)&g_auto);
if (SUCCEEDED(hrAuto) && g_auto) {
hrEl = g_auto->ElementFromHandle(hwnd, &g_root);
if (SUCCEEDED(hrEl) && g_root) {
IUIAutomationCondition* cond = NULL;
if (SUCCEEDED(g_auto->CreateTrueCondition(&cond)) && cond) {
hrFind = g_root->FindAll(TreeScope_Subtree, cond, &g_arr);
cond->Release();
if (SUCCEEDED(hrFind) && g_arr) {
int len = 0;
if (SUCCEEDED(g_arr->get_Length(&len)))
findall = len;
}
}
}
}
{
IAccessible* acc = NULL;
hrMsaa = AccessibleObjectFromWindow(
hwnd, OBJID_CLIENT, IID_IAccessible, (void**)&acc);
if (SUCCEEDED(hrMsaa) && acc) {
g_acc = acc;
long kids = 0;
if (SUCCEEDED(acc->get_accChildCount(&kids)))
msaaKids = kids;
}
DWORD_PTR smResult = 0;
if (SendMessageTimeoutW(
hwnd, WM_GETOBJECT, 0, (LPARAM)OBJID_CLIENT,
SMTO_ABORTIFHUNG, 2000, &smResult)) {
lrClient = (LRESULT)smResult;
}
NudgeWinEvents(hwnd);
sprintf_s(line,
"hwnd=%p sta_tid=%lu findall=%ld msaa_kids=%ld "
"lr_client=%ld lr_uia=%ld subclass=%d dui=%d "
"dui_hits=%ld getobject_hits=%ld "
"hr_auto=0x%08lx hr_el=0x%08lx hr_find=0x%08lx hr_msaa=0x%08lx "
"keep_auto=%d keep_root=%d keep_arr=%d keep_acc=%d done=1\r\n",
hwnd, GetCurrentThreadId(), (long)findall, (long)msaaKids,
(long)lrClient, (long)lrUia, subclass_ok, dui_ok,
(long)g_duiHits, (long)g_getobjectHits,
(unsigned long)hrAuto, (unsigned long)hrEl,
(unsigned long)hrFind, (unsigned long)hrMsaa,
g_auto ? 1 : 0, g_root ? 1 : 0, g_arr ? 1 : 0, g_acc ? 1 : 0);
"hwnd=%p sta_tid=%lu lr_client=%ld subclass=%d dui=%d "
"dui_hits=%ld getobject_hits=%ld reenter_skip=%ld "
"no_inproc_uia=1 done=1\r\n",
hwnd, GetCurrentThreadId(), (long)lrClient, subclass_ok, dui_ok,
(long)g_duiHits, (long)g_getobjectHits, (long)g_reenterSkip);
WriteMarker("jiangchang_desktop_wake_provider.txt", line);
}
@@ -380,7 +300,7 @@ static LRESULT CALLBACK MsgWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lP
if (msg == WM_JC_WAKE) {
HWND target = (HWND)wParam;
if (target && IsWindow(target))
DoInProcessUiaWake(target);
DoSafeWake(target);
else
WriteMarker("jiangchang_desktop_wake_provider.txt", "err=bad_hwnd done=1\r\n");
return 0;
@@ -394,9 +314,6 @@ static LRESULT CALLBACK MsgWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lP
static DWORD WINAPI StaThreadProc(LPVOID)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
bool needUninit = SUCCEEDED(hr) || hr == S_FALSE;
WNDCLASSW wc;
ZeroMemory(&wc, sizeof(wc));
wc.lpfnWndProc = MsgWndProc;
@@ -413,7 +330,6 @@ static DWORD WINAPI StaThreadProc(LPVOID)
if (!g_msgHwnd) {
WriteMarker("jiangchang_desktop_wake_provider.txt", "err=no_msg_hwnd done=1\r\n");
if (needUninit) CoUninitialize();
return 1;
}
@@ -423,12 +339,10 @@ static DWORD WINAPI StaThreadProc(LPVOID)
DispatchMessageW(&msg);
}
ReleaseKeepAlive();
if (g_msgHwnd) {
DestroyWindow(g_msgHwnd);
g_msgHwnd = NULL;
}
if (needUninit) CoUninitialize();
return 0;
}
@@ -475,7 +389,7 @@ extern "C" __declspec(dllexport) DWORD WINAPI WakeProvider(LPVOID param)
DWORD_PTR result = 0;
LRESULT ok = SendMessageTimeoutW(
g_msgHwnd, WM_JC_WAKE, (WPARAM)hwnd, 0,
SMTO_ABORTIFHUNG, 15000, &result);
SMTO_ABORTIFHUNG, 8000, &result);
if (ok == 0) {
char line[256];
@@ -519,13 +433,7 @@ static LRESULT TryStdAccessible(HWND hwnd, WPARAM wParam, LONG objId)
static LRESULT CALLBACK SubclassProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (msg == WM_GETOBJECT) {
InterlockedIncrement(&g_getobjectHits);
LRESULT r = 0;
if (g_oldProc)
r = CallWindowProcW(g_oldProc, hwnd, msg, wParam, lParam);
if (r != 0)
return r;
r = TryDuiLibGetObject(hwnd, wParam, lParam);
LRESULT r = HandleGetObject(hwnd, wParam, lParam, g_oldProc, false);
if (r != 0)
return r;
LONG objId = (LONG)lParam;

View File

@@ -43,7 +43,7 @@ $bat = Join-Path $env:TEMP "jiangchang_build_wecom_wake.bat"
@"
@echo off
call "$vcvars32" || exit /b 1
cl /nologo /LD /O2 /EHsc /DWIN32 /D_WINDOWS /D_USRDLL /utf-8 "$srcCpp" /Fe"$dllTarget" /Fo"$objPath" /link /DEF:"$srcDef" user32.lib ole32.lib oleacc.lib oleaut32.lib uuid.lib comctl32.lib
cl /nologo /LD /O2 /EHsc /DWIN32 /D_WINDOWS /D_USRDLL /utf-8 "$srcCpp" /Fe"$dllTarget" /Fo"$objPath" /link /DEF:"$srcDef" user32.lib ole32.lib oleacc.lib comctl32.lib
exit /b %ERRORLEVEL%
"@ | Set-Content -Path $bat -Encoding ASCII