KB0122:使用 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); } };