知识库 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::RetryRejectedCalldwRejectType 参数是 SERVERCALL_RETRYLATER 时会传回非 -1 的值。随后,在执行 COM 调用的每个 STA 线程中,必须使用 CoRegisterMessageFilter 向 COM 注册此实现的实例。

例如,这是 C++ ATL RAII 实现:

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);
  }
};

分享