SetWinEventHook回调内的AccessibleObjectFromEvent调用导致死锁

时间:2019-01-04 00:28:48

标签: c# .net winforms winapi deadlock

回调和AccessibleObjectFromEvent调用似乎均按预期方式工作,并且我没有使用任何锁定机制,但是如果回调中存在AccessibleObjectFromEvent,则winforms窗体有时会锁定。

我注意到它冻结时在right-clicking on its taskbar icon unfreezes it处具有这个奇怪的属性。

使用调试器暂停只会将您带到Application.Run(new Form1()),而.Net端似乎没有任何阻止-调试器的窗口更新甚至可以触发一些Active Accessibility事件,然后让您在回调中逐步处理这些事件-有效-在表格保持冻结状态的所有过程中!

我注意到AccessibleObjectFromEvent通过发送WM_GETOBJECT message来工作,但是.Net端永远不会停留在AccessibleObjectFromEvent调用上,而从SetWinEventHook回调内部调用AccessibleObjectFromEvent是AFAIK做主动可访问性的正常方法。

当冻结时,我还没有注意到与Active Accessibility事件的任何关联,但是我确实没有足够的信息来排除这种情况。我还尝试过编译x86(而不是Any),并且没有区别。

我将其简化为最小形式:

using Accessibility;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace WindowsFormsApp1 {
    static class Program {

        // PInvoke declarations, see http://www.pinvoke.net/default.aspx/user32.setwineventhook
        [DllImport("user32.dll")]
        static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess,  uint idThread, WinEventFlag dwFlags);
        public delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
        [DllImport("oleacc.dll")]
        public static extern uint AccessibleObjectFromEvent(IntPtr hwnd, uint dwObjectID, uint dwChildID, out IAccessible ppacc, [MarshalAs(UnmanagedType.Struct)] out object pvarChild);

        [Flags]
        public enum WinEventFlag : uint {
            /// <summary>Events are ASYNC</summary>
            Outofcontext   = 0x0000,
            /// <summary>Don't call back for events on installer's thread</summary>
            Skipownthread  = 0x0001,
            /// <summary>Don't call back for events on installer's process</summary>
            Skipownprocess = 0x0002,
            /// <summary>Events are SYNC, this causes your dll to be injected into every process</summary>
            Incontext      = 0x0004
        }

        static IntPtr hookHandle = IntPtr.Zero;
        static GCHandle garbageCollectorHandle;

        static void AccessibilityEventHandler(IntPtr hWinEventHook, uint eventType, IntPtr hWnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) {
            try {
                object objChild = null;
                IAccessible accWindow = null;
                AccessibleObjectFromEvent(hWnd, (uint)idObject, (uint)idChild, out accWindow, out objChild);
                Debug.WriteLine("Hook was invoked");
            } catch (Exception ex) {
                Debug.WriteLine("Exception " + ex);
            }
        }

        [STAThread]
        static void Main() {
            WinEventDelegate callback = new WinEventDelegate(AccessibilityEventHandler);
            // Use the member garbageCollectorHandle to keep the delegate object in memory. Might not be needed, and can't properly pin it because it's not a primitive type.
            garbageCollectorHandle = GCHandle.Alloc(callback);

            SetWinEventHook(
                0x7546, // eventMin (0x7546 = PropertyChanged_IsOffscreen)
                0x7546, // eventMax
                IntPtr.Zero,
                callback,
                0,
                0,
                WinEventFlag.Outofcontext
            );

            // Two hooks are not necessary to cause a freeze, but with two it can happen much faster - sometimes within minutes
            SetWinEventHook(
                0x0001, // eventMin (0x0001 = System_Sound)
                0x0001, // eventMax
                IntPtr.Zero,
                callback,
                0,
                0,
                WinEventFlag.Outofcontext
            );

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

使用这种最小的应用程序,很难注意到消息泵冻结,但是一旦锁定,您将无法拖动窗口或将其置于前台。该错误是间歇性的,通常在5分钟内发生,有时需要花费我很长时间才能放弃,如果您将其放置一整夜,并且该表单在早上仍然可以响应,则可能取决于计算机/操作系统(在Win 10.0.17174上进行过尝试)和10.0.17763,.net 4.5.2和4.6.1)。

我确定99%的用户需要冻结对AccessibleObjectFromEvent的调用才能冻结应用程序,但是间歇性冻结无法完全确定。

可重入

遵循Jimi的建议,我运行了带有标志的最小应用程序

WinEventFlag.Outofcontext | WinEventFlag.Skipownthread | WinEventFlag.Skipownprocess

花了一段时间才冻结,但仍冻结了。 Debug.WriteLine()调用表明它仍在正常地响应Active Accessibility事件-即通过该回调没有发生递归繁忙循环(至少现在我正在查找),但是表单已冻结。它在任务管理器中使用0%的CPU。

冻结现在略有不同,因为任务管理器没有将其列为“无响应”,并且我可以通过左键单击任务栏图标将其置于前台-通常,任务栏甚至无法将其带来到前台。但是,表单仍然无法移动或调整大小,并且无法通过单击将其置于前台。

我还在AccessibleObjectFromEvent调用之前添加了一些Debug.WriteLine,并跟踪了重入深度,我可以看到它有时是可重入的,但通常在展开前只有一个深度,而在展开之前不超过13完全放松。这似乎是由于消息队列中已经有许多事件引起的,而不是由钩子处理程序递归地导致必须随后处理的事件引起的。该UI当前处于冻结状态,并且该钩子处理程序的深度为0(即当前未重入)。

0 个答案:

没有答案