使用 think-cell,有些 Excel COM 自动化调用会失败
问题
安装 think-cell 后,有些 Excel COM 自动化调用会失败。HRESULT 错误代码是 0x8001010A RPC_E_SERVERCALL_RETRYLATER
。在 LotusScript 中,错误是 Automation object error
。
COM 服务器可以使用 IMessageFilter
机制筛选传入的调用。更具体地说,若 IMessageFilter::HandleInComingCall
实现传回 SERVERCALL_RETRYLATER
,则客户端调用将随 RPC_E_SERVERCALL_RETRYLATER
传回。
不论 think-cell 如何,Excel 都使用此机制。think-cell 的存在会提高 RPC_E_SERVERCALL_RETRYLATER
的频率,这使问题更加普遍。
已知在这些环境中会出现错误的一项产品是 IBM Lotus Notes 的脚本引擎,即 LotusScript。
解决方案
作为对 RPC_E_SERVERCALL_RETRYLATER
的响应,调用方必须短暂等待,然后重复调用。Visual Basic for Applications 内置此处理功能,因此若支持 COM 的其他 Windows 脚本环境(例如 LotusScript)不执行该操作,可以认为这是 Bug。
若在脚本环境外,或者若脚本环境不执行该操作,调用方必须自行处理 RPC_E_SERVERCALL_RETRYLATER
。通过 COM 可非常轻松地执行该操作,因为它在收到该命令后,能代表客户端自动重复调用。实际的调用代码可以保持不变。
若要启用此行为,客户端必须实现自己的 IMessageFilter
,采用的方法 IMessageFilter::RetryRejectedCall
在 dwRejectType
参数是 SERVERCALL_RETRYLATER
时会传回非 -1 的值。随后,在执行 COM 调用的每个 STA 线程中,必须使用 CoRegisterMessageFilter
向 COM 注册此实现的实例。
class CHandleRetryLaterBase :
public CComObjectRootEx<CComSingleThreadModel>,
public IMessageFilter
{
protected:
CComPtr<IMessageFilter> m_imessagefilterOld;
BEGIN_COM_MAP(CHandleRetryLaterBase)
COM_INTERFACE_ENTRY(IMessageFilter)
END_COM_MAP()
public:
// IMessageFilter implementation
DWORD STDMETHODCALLTYPE HandleInComingCall(
/* [in] */ DWORD dwCallType,
/* [in] */ HTASK htaskCaller,
/* [in] */ DWORD dwTickCount,
/* [in] */ LPINTERFACEINFO lpInterfaceInfo
) {
if( m_imessagefilterOld ) {
// pass on to old handler, no change in behavior
return m_imessagefilterOld->HandleInComingCall(
dwCallType,
htaskCaller,
dwTickCount,
lpInterfaceInfo
);
} else {
// default behavior
return SERVERCALL_ISHANDLED;
}
}
DWORD STDMETHODCALLTYPE RetryRejectedCall(
/* [in] */ HTASK htaskCallee,
/* [in] */ DWORD dwTickCount,
/* [in] */ DWORD dwRejectType
) {
if( SERVERCALL_RETRYLATER==dwRejectType ) {
// in a script, wait indefinitely for robustness under load
return 100;
} else if( m_imessagefilterOld ) {
// pass on to old handler, no change in behavior
return m_imessagefilterOld->RetryRejectedCall(
htaskCallee,
dwTickCount,
dwRejectType );
} else {
// default behavior
return (DWORD)-1;
}
}
DWORD STDMETHODCALLTYPE MessagePending(
/* [in] */ HTASK htaskCallee,
/* [in] */ DWORD dwTickCount,
/* [in] */ DWORD dwPendingType
) {
if( m_imessagefilterOld ) {
// pass on to old handler, no change in behavior
return m_imessagefilterOld->MessagePending(
htaskCallee,
dwTickCount,
dwPendingType );
} else {
// default behavior
return PENDINGMSG_WAITDEFPROCESS;
}
}
};
// Instantiate this class in your scope to enable robust IMessageFilter.
class CHandleRetryLater :
public CHandleRetryLaterBase
{
public:
CHandleRetryLater() {
// set new handler and save old one
_ASSERT( m_dwRef==0 );
HRESULT hr=CoRegisterMessageFilter( this, &m_imessagefilterOld );
// m_imessagefilterOld may be nullptr if there is no
// filter previously installed
_ASSERT( SUCCEEDED(hr) );
}
~CHandleRetryLater() {
// reset old handler
{
CComPtr<IMessageFilter> iMessageFilter;
HRESULT hr=CoRegisterMessageFilter(
m_imessagefilterOld,
&iMessageFilter );
_ASSERT( SUCCEEDED(hr) );
// make sure noone replaced our IMessageFilter
_ASSERT( iMessageFilter==static_cast<IMessageFilter*>(this) );
} // iMessageFilter is last release
_ASSERT( m_dwRef==0 );
}
STDMETHOD_(ULONG, AddRef)() throw() {
#ifdef _DEBUG
return InternalAddRef();
#else
return 0;
#endif
}
STDMETHOD_(ULONG, Release)() throw() {
#ifdef _DEBUG
return InternalRelease();
#else
return 0;
#endif
}
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) throw() {
return _InternalQueryInterface(iid, ppvObject);
}
};