我有一个遗留的VB6应用程序,其中我们引用了.NET DLL设置以向COM公开。在这个.NET DLL中的一个方法中,我们需要在等待事件时阻塞线程,一旦我们收到该事件,我们就可以从该方法返回。目前我正在创建一个Task
对象来执行我们收到事件后所做的工作,然后我用这个任务订阅了所述事件,最后我WaitOne
上的AutoResetEvent
我用来控制线程。我还将任务ContinueWith
设置为Set
AutoResetEvent
。
当我使用控制台应用程序(用.NET编写)测试此设置时,我没有一个问题。但是,当我通过VB6调用相同的方法时,有时它可以正常工作,有时则不会,并且会无限期地挂起,等待该事件触发。
我一直在使用WinDbg分开它,我注意到所有挂起的尝试,都会抛出访问冲突。我正在尝试找出当前这种访问冲突的来源,但是在.NET代码中我们调用事件委托(如果不是null)是正确的。
以下是来自WinDbg的示例:
>>>实例化对象
>>>开始事件线程
>>>订阅OnCreateCtiConf
>>>等待任务结果。
>>>排队OnCreateCTIConf:没有消息
(1e44.1c4c):访问冲突 - 代码c0000005(第一次机会)
在任何异常处理之前报告第一次机会异常。
可以预期并处理此异常。
eax = 0a61e9d0 ebx = 0a65bccc ecx = 00000000 edx = 0a7057a4 esi = 0a61e9d0 edi = 0caef804
eip = 091249a4 esp = 0caef7e0 ebp = 0caef810 iopl = 0 nv up ei pl nz na po nc
cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00010202
091249a4 3909 cmp dword ptr [ecx],ecx ds:0023:00000000 = ????????
以下是我们调用的方法有时挂起:
public bool AgentLogin(string extension, string agentId, string password)
{
_extension = extension;
_agentId = agentId;
_eventQueue = new Queue<AvayaEvent>();
try
{
_phone = new CTISoftphone($"ext={extension},logFile={Settings.LogLocation}");
}
catch (Exception ex)
{
addEvent(new AvayaEvent(CTISoftphone.CTIEvent.OnError, $"{_wrapperLogPrefix} Unable to create CTISoftPhone. The error was: {ex.Message}"));
return false;
}
_phone.PhoneEvent += new CTISoftphone.PhoneEventHandler((evnt, msg) =>
{
Trace.WriteLine($">>> Enqueued {evnt} : {(string.IsNullOrEmpty(msg) ? "No message" : msg)}");
addEvent(new AvayaEvent(evnt, msg));
});
Trace.WriteLine(">>> Starting events thread");
t_avayaEvents.Start();
var are = new AutoResetEvent(false);
var task = new Task(() =>
{
try
{
Trace.WriteLine(">>> Calling LoginAgent");
_phone.LoginAgent(agentId, password);
}
catch (Exception)
{
}
});
task.ContinueWith((t) => are.Set());
var task_handler = new OnCreateCtiConfHandler(task.Start);
Trace.WriteLine(">>> Subscribing to OnCreateCtiConf");
OnCreateCtiConf += task_handler;
Trace.WriteLine(">>> Waiting for task result.");
are.WaitOne();
Trace.WriteLine(">>> Unsubscribing from OnCreateCtiConf");
OnCreateCtiConf -= task_handler;
return true;
}
我们最初认为在我们订阅公开的PhoneEvent
之前错过了事件,因此事件队列循环线程。基本上我们将AvayaEvent
添加到Queue<T>
,并且单独的线程处理这些事件。如果有一个委托,它会触发并忘记,否则,该事件将被重新排队。作为参考,我们在实现事件队列之前仍然遇到了这个问题。
对于COM互操作,我们做了一些事情:
已为Phone
对象及其事件(成员和事件的单独接口)创建了接口
ComVisible(true)
已设置在我们需要向COM公开的任何内容
DispId
已在每个接口的所有成员上设置
我们的主要对象Phone
的装饰方式如下:
[ComSourceInterfaces(typeof(IClickToDialEvents))]
[ComDefaultInterface(typeof(IClickToDial))]
[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
public class Phone : IClickToDial
{
[...]
}
引用的接口IClickToDial
和IClickToDialEvents
的装饰方式如下:
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IClickToDial
{
[...]
}
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IClickToDialEvents
{
[...]
}
事件是Phone
类的成员,下面是它们的样本示例:
[ComVisible(false)]
public delegate void OnCreateCtiConfHandler();
public event OnCreateCtiConfHandler OnCreateCtiConf;
进程事件循环如下所示:
private void processEventsInQueue()
{
while (true)
{
if (_eventQueue?.Count > 0)
{
var evnt = getNextEvent();
Trace.WriteLine($">>> Dequeued {evnt.Type}");
switch (evnt.Type)
{
case CTISoftphone.CTIEvent.OnCreateCtiConf:
if (OnCreateCtiConf != null)
OnCreateCtiConf();
else
addEvent(evnt);
break;
[...]
}
}
}
}
最后,addEvent
和getNextEvent
方法如下所示:
private AvayaEvent getNextEvent()
{
lock (this)
return _eventQueue.Dequeue();
}
private void addEvent(AvayaEvent evnt)
{
lock (this)
{
try
{
_eventQueue.Enqueue(evnt);
}
catch (NullReferenceException)
{
//swallow the nullrefexception since we null out _eventQueue when we dont want to queue anything
}
}
}
最后,当我们用COM注册这个DLL时,我们使用以下RegAsm命令:.\RegAsm.exe 'C:\Program Files\Avaya\ClickToDial\<OurDll>.dll' /codebase /tlb
然后我们在VB6应用程序中引用生成的tlb文件。
就像我说的,有时这很好用,我们从AgentLogin方法返回没有问题。 Othertimes,我们无限期挂起,我总是看到这些挂起的访问冲突。我说分裂大约是50/50。
这是我参与的第一个COM互操作项目,所以我可能错过了一些明显的东西,但这让我发疯了!如果需要更多信息/说明,请告诉我。
提前感谢您的帮助!