UIAutomation踩坑


最近有這樣一個需要,在一個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(16);
        }

        // 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 - 1; i >= 0; 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 == 0) { // 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)“我感覺也挺耐人尋味的


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM