UIAutomation踩坑
阅读原文时间:2023年07月13日阅读:1

最近有这样一个需要,在一个AppDomain中通过UIAutomation做一些操作,并在操作完成后卸载掉这个AppDomain。
然而在卸载这个AppDomain时,总会出现System.CannotUnloadAppDomainException异常,不过从异常的Message里的“HRESULT: 0x80131015”这段能看出来,应该是AppDomain中的某个线程在释放过程中发生异常了。

经过好长时间的debug,终于发现罪魁祸首居然有两个线程,分别来自UIAutomationClient.dll和UIAutomationClientsideProviders.dll

UIAutomationClientsideProviders这个里面起了一个while(true)的线程,就不多展开了,其余部分也和UIAutomationClient差不多,我主要说一下UIAutomationClient的情况
UIAutomationClient源代码在这里:
https://referencesource.microsoft.com/UIAutomationClient/UIAutomationClient.csproj.html

在通过UIAutomationClient的ClientEventManager为控件事件增加监听回调时,不论是什么类别的事件,最终都会调用AddListener这个方法:

internal static void AddListener(AutomationElement rawEl, Delegate eventCallback, EventListener l)
{
lock (_classLock)
{
// If we are adding a listener then a proxy could be created as a result of an event so make sure they are loaded
ProxyManager.LoadDefaultProxies();

    if (\_listeners == null)  
    {  
        // enough space for 16 AddXxxListeners (100 bytes)  
        \_listeners = new ArrayList();  
    }

    // Start the callback queue that gets us off the server's  
    // UI thread when events arrive cross-proc  
    CheckStartCallbackQueueing();

    //  
    // The framework handles some events on behalf of providers; do those here  
    //

    // If listening for BoundingRectangleProperty then may need to start listening on the  
    // client-side for LocationChange WinEvent (only use \*one\* BoundingRectTracker instance).  
    if (\_winEventTrackers\[(int)Tracker.BoundingRect\] == null && HasProperty(AutomationElement.BoundingRectangleProperty, l.Properties))  
    {  
        //  
        AddWinEventListener(Tracker.BoundingRect, new BoundingRectTracker());  
    }

    // Start listening for menu event in order to raise MenuOpened/Closed events.  
    if ( \_winEventTrackers \[(int)Tracker.MenuOpenedOrClosed\] == null && (l.EventId == AutomationElement.MenuOpenedEvent || l.EventId == AutomationElement.MenuClosedEvent) )  
    {  
        AddWinEventListener( Tracker.MenuOpenedOrClosed, new MenuTracker( new MenuHandler( OnMenuEvent ) ) );  
    }

    // Begin watching for hwnd open/close/show/hide so can advise of what events are being listened for.  
    // Only advise UI contexts of events being added if the event might be raised by a provider.  
    // TopLevelWindow event is raised by UI Automation framework so no need to track new UI.  
    //  
    if (\_winEventTrackers\[(int)Tracker.WindowShowOrOpen\] == null )  
    {  
        AddWinEventListener( Tracker.WindowShowOrOpen, new WindowShowOrOpenTracker( new WindowShowOrOpenHandler( OnWindowShowOrOpen ) ) );  
        AddWinEventListener( Tracker.WindowHideOrClose, new WindowHideOrCloseTracker( new WindowHideOrCloseHandler( OnWindowHideOrClose ) ) );  
    }

    // If listening for WindowInteractionStateProperty then may need to start listening on the  
    // client-side for ObjectStateChange WinEvent.  
    if (\_winEventTrackers\[(int)Tracker.WindowInteractionState\] == null && HasProperty(WindowPattern.WindowInteractionStateProperty, l.Properties))  
    {  
        AddWinEventListener(Tracker.WindowInteractionState, new WindowInteractionStateTracker());  
    }

    // If listening for WindowVisualStateProperty then may need to start listening on the  
    // client-side for ObjectLocationChange WinEvent.  
    if (\_winEventTrackers\[(int)Tracker.WindowVisualState\] == null && HasProperty(WindowPattern.WindowVisualStateProperty, l.Properties))  
    {  
        AddWinEventListener(Tracker.WindowVisualState, new WindowVisualStateTracker());  
    }

    // Wrap and store this record on the client...  
    EventListenerClientSide ec = new EventListenerClientSide(rawEl, eventCallback, l);  
    \_listeners.Add(ec);

    // Only advise UI contexts of events being added if the event might be raised by  
    // a provider.  TopLevelWindow event is raised by UI Automation framework.  
    if (ShouldAdviseProviders( l.EventId ))  
    {  
        // .. then let the server know about this listener  
        ec.EventHandle = UiaCoreApi.UiaAddEvent(rawEl.RawNode, l.EventId.Id, ec.CallbackDelegate, l.TreeScope, PropertyArrayToIntArray(l.Properties), l.CacheRequest);  
    }  
}  

}

能看出在第一次调用时,方法会进行一系列初始化操作,并开始监听事件。
其中CheckStartCallbackQueueing()方法是用来启动回调队列线程的,代码如下:

private static void CheckStartCallbackQueueing()
{
if (!_isBkgrdThreadRunning)
{
_isBkgrdThreadRunning = true;
_callbackQueue = new QueueProcessor();
_callbackQueue.StartOnThread();
}
}

internal void StartOnThread()
{
_quitting = false;

// create and start a background thread for this worker window to run on  
// (background threads will exit if the main and foreground threads exit)  
ThreadStart threadStart = new ThreadStart(WaitForWork);  
\_thread = new Thread(threadStart);  
\_thread.IsBackground = true;  
\_thread.Start();  

}

也就是说ClientEventManager启动了一个线程,通过WaitForWork()方法循环获取消息并处理对应事件回调。
其中WaitForWork()内部循环的终止条件是_quitting == false,只有一处PostQuit()方法能使其暂停

internal void PostQuit()
{
_quitting = true;
_ev.Set();
}

而PostQuit()也只有一处CheckStopCallbackQueueing()方法在调用

private static void CheckStopCallbackQueueing()
{
// anything to stop?
if (!_isBkgrdThreadRunning)
return;

// if there are listeners then can't stop  
if (\_listeners != null)  
    return;

// Are any WinEvents currently being tracked for this client?  
foreach (WinEventWrap eventWrapper in \_winEventTrackers)  
{  
    if (eventWrapper != null)  
    {  
        return;  
    }  
}

// OK to stop the queue now  
\_isBkgrdThreadRunning = false;  
\_callbackQueue.PostQuit();  
// Intentionally not setting \_callbackQueue null here; don't want to mess with it from this thread.  

}

到这里可能还看不出什么问题,继续往上找,发现有两处在调用这个方法,分别是RemoveWinEventListener()和RemoveAllListeners(),因为我的代码没用到RemoveAllListeners,所以先看看前者

private static void RemoveWinEventListener(Tracker idx, Delegate eventCallback)
{
WinEventWrap eventWrapper = _winEventTrackers[(int)idx];
if (eventWrapper == null)
return;

bool fRemovedLastListener = eventWrapper.RemoveCallback(eventCallback);  
if (fRemovedLastListener)  
{  
    \_callbackQueue.PostSyncWorkItem(new WinEventQueueItem(eventWrapper, WinEventQueueItem.StopListening));  
    \_winEventTrackers\[(int)idx\] = null;

    CheckStopCallbackQueueing();  
}  

}

和AddWinEventListener()只在AddListener()被调用一样,RemoveWinEventListener()也只在RemoveLisener()里集中调用,

internal static void RemoveListener( AutomationEvent eventId, AutomationElement el, Delegate eventCallback )
{
lock( _classLock )
{
if( _listeners != null )
{
bool boundingRectListeners = false; // if not removing BoundingRect listeners no need to do check below
bool menuListeners = false; // if not removing MenuOpenedOrClosed listeners no need to do check below
bool windowInteracationListeners = false; // if not removing WindowsIntercation listeners no need to do check below
bool windowVisualListeners = false; // if not removing WindowsVisual listeners no need to do check below

        for (int i = \_listeners.Count - ; i >= ; i--)  
        {  
            EventListenerClientSide ec = (EventListenerClientSide)\_listeners\[i\];  
            if( ec.IsListeningFor( eventId, el, eventCallback ) )  
            {  
                EventListener l = ec.EventListener;

                // Only advise UI contexts of events being removed if the event might be raised by  
                // a provider.  TopLevelWindow event is raised by UI Automation framework.  
                if ( ShouldAdviseProviders(eventId) )  
                {  
                    // Notify the server-side that this event is no longer interesting  
                    try  
                    {  
                        ec.EventHandle.Dispose(); // Calls UiaCoreApi.UiaRemoveEvent  
                    }  

// PRESHARP: Warning - Catch statements should not have empty bodies
#pragma warning disable 6502
catch (ElementNotAvailableException)
{
// the element is gone already; continue on and remove the listener
}
#pragma warning restore 6502
finally
{
ec.Dispose();
}
}

                // before removing, check if this delegate was listening for the below events  
                // and see if we can stop clientside WinEvent trackers.  
                if (HasProperty(AutomationElement.BoundingRectangleProperty, l.Properties))  
                {  
                    boundingRectListeners = true;  
                }

                if( eventId == AutomationElement.MenuOpenedEvent || eventId == AutomationElement.MenuClosedEvent )  
                {  
                    menuListeners = true;  
                }

                if (HasProperty(WindowPattern.WindowInteractionStateProperty, l.Properties))  
                {  
                    windowInteracationListeners = true;  
                }

                if (HasProperty(WindowPattern.WindowVisualStateProperty, l.Properties))  
                {  
                    windowVisualListeners = true;  
                }

                // delete this one  
                \_listeners.RemoveAt( i );  
            }  
        }

        // Check listeners bools to see if clientside listeners can be removed  
        if (boundingRectListeners)  
        {  
            RemovePropertyTracker(AutomationElement.BoundingRectangleProperty, Tracker.BoundingRect);  
        }

        if (menuListeners)  
        {  
            RemoveMenuListeners();  
        }

        if (windowInteracationListeners)  
        {  
            RemovePropertyTracker(WindowPattern.WindowInteractionStateProperty, Tracker.WindowInteractionState);  
        }

        if (windowVisualListeners)  
        {  
            RemovePropertyTracker(WindowPattern.WindowVisualStateProperty, Tracker.WindowVisualState);  
        }

        // See if we can cleanup completely  
        if (\_listeners.Count == )  
        {  
            // as long as OnWindowShowOrOpen is static can just use new here and get same object instance  
            // (if there's no WindowShowOrOpen listener, this method just returns)  
            RemoveWinEventListener(Tracker.WindowShowOrOpen, new WindowShowOrOpenHandler(OnWindowShowOrOpen));  
            RemoveWinEventListener( Tracker.WindowHideOrClose, new WindowHideOrCloseHandler( OnWindowHideOrClose ) );

            \_listeners = null;  
        }  
    }  
}  

}

从RemoveWinEventListener()和RemoveListener()的逻辑也能大致看出,此处的意图应该是在最后一个用户添加的Listener被移除时,移除初始化中对部分window事件的监听,释放所有资源。
而第一次添加Listener时,ClientEventManager会进行一系列初始化,并创建线程去处理队列信息。
设计思路也是一致的,以第一次Add为起点,最后一次Remove为终点。但问题就出在上面标出来的这一段。

进去时_listeners是一个无元素非空的数组,满足条件。
但RemoveWinEventListener()中的CheckStopCallbackQueueing()会检查_listeners是否为null,如果不为null则表示不应该结束。
然后问题就出现了,最后一次RemoveWinEventListener()时,这是逻辑上最后一次调用中的CheckStopCallbackQueueing()来停止监听的机会,但由于_listeners还不是null,被提前return了,而出来之后又被赋了null。
但这回赋了null,因为后面再也没有机会调用CheckStopCallbackQueueing(),于是线程就停不下来了……

这回我们再往前看,还有个RemoveAllListeners()也包含了但RemoveWinEventListener(),那么如果我在调用了RemoveListener()之后再调用一次RemoveAllListeners()能不能停止进程呢?还是不行
因为RemoveAllListeners()在一开始就会判断_listeners是否为null,而我们的_listeners已经在之前被赋了null了…………

internal static void RemoveAllListeners()
{
lock (_classLock)
{
if (_listeners == null)
return;

看到这我已经想不明白了,为什么?是我哪里理解有问题吗……

而且注释里的这句”(background threads will exit if the main and foreground threads exit)“我感觉也挺耐人寻味的