[BUG分享]搜狗浏览器跨线程使用IE接口的问题

【Bug现象】:

使用搜狗浏览器release1.0.1时,如果新建多个标签页浏览页面时,偶尔会出现工具栏的前进、后退、刷新等按钮为彩色可点击状态,但是点击之后页面却没有发生相应的动作。

【Bug背景】:

搜狗浏览器提供多标签方式浏览页面,每个标签即对应一个工作线程。每个工作线程都维护一个自己的IWebBrowser2接口。当用户点击工具栏的前进、后退、刷新等按钮时,工具栏发送相应的消息给UI线程,UI线程再将消息分发给对应的工作线程,工作线程在响应消息时会调用IE接口IWebBrowser2相应的函数,如GoForward,Goback,ReFresh等,最终在容器CAxControl (即对应页面区)显示结果。如果用户点击页面中的某个链接为target=”_blank”时(即在新窗口打开),CWebBrowserEventsManager会探测到IE的回调事件DISPID_NEWWINDOW2。此时浏览器会创建一个新的标签页同时也会创建一个新的工作线程,在此过程中UI线程会共享使用新线程的IWebBrowser2接口。

【知识补充】:

多线程使用COM接口时,不要在线程之间传递原始接口指针
这里写图片描述

如果一个线程要与另一个线程共享一个接口指针,它应首先封送该接口指针。如果有必要,封送接口指针可使 COM 创建一个新的代理(以及一个新的信道对象,将代理和存根结对),以允许从另一个单元向外调用。不通过封送而将原始接口指针(内存中的一个 32 位地址)传递给另一个线程,会绕过 COM 的并发机制,并且如果发送和接收的线程位于不同的单元中,将出现各种不良行为。(在 Windows 2000 中,由于两个对象可以共享一个单元,但又位于不同的上下文中,因此如果线程位于同一个单元中,可能会使您陷入困境。)典型的症状包括调用失败和返回 RPC_E_WRONG_THREAD_ERROR。

 Windows NT 4.0 和更高版本可以使用一对名为 **CoMarshalInterThreadInterfaceInStream** 和 **CoGetInterfaceAndReleaseStream** 的 API 函数,在线程之间轻松地封送接口指针。假定您应用程序中的一个线程(线程 A)创建了一个 COM 对象,继而接收了一个 IFoo 接口指针,并且同一进程中的另一个线程(线程 B)想调用这个对象。在准备将接口指针传递给线程 B 时,线程 A 应该封送该接口指针,如下所示:

CoMarshalInterThreadInterfaceInStream (IID_IFoo, pFoo, &pStream); //列集

在 CoMarshalInterThreadInterfaceInStream 返回后,线程 B 就可以安全地取消封送该接口指针:

IFoo* pFoo;
CoGetInterfaceAndReleaseStream (pStream, IID_IFoo, (void**) &pFoo); //散集

在这些示例中,pFoo 是一个 IFoo 接口指针,pStream 是一个 IStream 接口指针。COM 在调用 CoMarshalInterThreadInterfaceInStream 时初始化 IStream 接口指针,然后在 CoGetInterfaceAndReleaseStream 内部使用和释放该接口指针。实际上,您通常要使用一个事件或其他同步化基元来协调这两个线程的行为。例如,让线程 B 知道接口指针已准备好,可以取消封送。

请注意,以这种方式封送接口指针不会出现任何问题,因为 COM 有足够的智能,在不需要进行封送时不会去封送(或重新封送)指针。如果在线程之间传递接口指针时这样做,使用 COM 就轻松多了。

【Bug原因】:

release1.0.1版本UI线程在共享使用工作线程的IWebBrowser2接口时,没有经过列集和散集的方式传递时,而是直接传递原始指针,造成上述BUG现象。
例如浏览器在处理IE回调事件DISPID_NEWWINDOW2时会执行以下代码:

HRESULT CWebBrowserEventsManager::OnNewWindow2(REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr)
{
   if (pDispParams->cArgs < 2)
   {
      return E_FAIL ;
   }
   IWebBrowser2* pWebBrowser2 ;
   CComPtr spMainUI ;
   CComPtr spTraveler ;
   HRESULT hr = Util::ModuleManager::CreateInstance(CLSID_Sogou_Main_UI, __uuidof(IMainUI), (void**)&spMainUI);
   hr = Util::ModuleManager::CreateInstance(CLSID_Sogou_CTraveler, __uuidof(ITraveler), (void**)&spTraveler);
   if (!spMainUI || !spTraveler)
   {
      return S_FALSE ;
   }
   DWORD dwID = 0 ;
   CString cstrUrl = L"" ;
   if (m_pCustomSite)
   {
      CComBSTR bstrUrl ;
      m_pCustomSite->GetNavigateUrl(bstrUrl) ;
      cstrUrl = bstrUrl ;
   }
   spMainUI->CreateNewTab(cstrUrl, (DWORD)&dwID) ;
   spTraveler->GetWebBrowserByID(dwID, (IUnknown**)&pWebBrowser2) ; //此处直接使用了原始指针
   if (!pWebBrowser2)
   {
      return S_FALSE ;
   }
   IDispatch** ppDisp = pDispParams->rgvarg[1].ppdispVal;
   hr = pWebBrowser2->get_Application(ppDisp);
   return S_OK ;
}

除此之外,所有会产生新标签页的用户的操作都会涉及到多线程使用IE接口的问题,如点击主页按钮等。

【解决办法】:

加入列集和散集操作,并加入多线程同步处理函数。
1. 在主线程加入多线程同步处理代码:

…
for (;;)
{
   if (::MsgWaitForMultipleObjectsEx(1, &hEvent, INFINITE, QS_ALLINPUT, MWMO_ALERTABLE) != WAIT_OBJECT_0)
   {
     MSG msg;
     BOOL bRet = ::PeekMessage(&msg, m_hWnd, 0, 0, PM_REMOVE);
     if (bRet)
     {
        if (msg.message == WM_QUIT)
        break;
        ::TranslateMessage(&msg);
        ::DispatchMessage(&msg);
     }
   }
}
…
  1. 加入列集处理函数:
IStream* CAxControl::GetMarshalAxControl()
{
   try
   {
     //特别注意不要在单线程的情况下掉这个函数
     IStream* pStream = NULL ;
     CComPtr spWebBrowser2 ;
     HRESULT hr = QueryAxControl(IID_IWebBrowser2, (void**)&spWebBrowser2) ;
     if (FAILED(hr) || !spWebBrowser2)
     {
        return NULL ;
     }
     hr = CoMarshalInterThreadInterfaceInStream(IID_IWebBrowser2, spWebBrowser2, &pStream) ;
     if (FAILED(hr) || !pStream)
     {
        return NULL ;
     }
     return pStream ;
   }
   catch (...)
   {
     ATLASSERT(FALSE) ;
     return NULL ;
   }
}
  1. 在涉及到跨线程调用IE接口的地方加入GetMarshalAxControl()函数,例如浏览器在处理IE回调事件DISPID_NEWWINDOW2时:
HRESULT CWebBrowserEventsManager::OnNewWindow2(REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr)
{
   if (pDispParams->cArgs < 2)
   {
      return E_FAIL ;
   }
   IWebBrowser2* pWebBrowser2 = NULL ;
   CComPtr spMainUI ;
   CComPtr spTraveler ;
   HRESULT hr = Util::ModuleManager::CreateInstance(CLSID_Sogou_Main_UI, __uuidof(IMainUI), (void**)&spMainUI);
   hr = Util::ModuleManager::CreateInstance(CLSID_Sogou_CTraveler, __uuidof(ITraveler), (void**)&spTraveler);
   if (!spMainUI || !spTraveler)
   {
      return S_FALSE ; 
   }
   DWORD dwID = 0 ;
   CString cstrUrl = L"" ;
   if (m_pCustomSite)
   {
      CComBSTR bstrUrl ;
      m_pCustomSite->GetNavigateUrl(bstrUrl) ;
      cstrUrl = bstrUrl ;
   }
   if (m_bForceBackOpen)
   {
      spMainUI->CreateNewTab(cstrUrl, (DWORD)&dwID, FALSE, m_dwChildFrameID) ;
      m_bForceBackOpen = FALSE ;
   }
   else
   {
      spMainUI->CreateNewTab(cstrUrl,(DWORD)&dwID,GetAsyncKeyState(VK_CONTROL) < 0 ? FALSE : TRUE, m_dwChildFrameID) ;
   }
   IStream* pStream = NULL ;
   CAxControl* p = (CAxControl*)::GetWindowLongPtr(::FindWindowEx((HWND)dwID, NULL, NULL, NULL), GWLP_USERDATA) ;
   ::SendMessageTimeout(p->m_hWnd, WM_USER_GET_WEBBROWSER2_CROSS_THREAD, 0, 0, SMTO_NORMAL, 2000, (PDWORD_PTR)&pStream) ;//发送消息给当前标签页,使其调用列集函数封送m_spWebBrowser2指针
   CoGetInterfaceAndReleaseStream(pStream, IID_IWebBrowser2, (void**)&pWebBrowser2) ;//散集操作得到m_spWebBrowser2指针
   if (!pWebBrowser2)
   {
     return S_FALSE ;
   }
   pWebBrowser2->AddRef() ;
   IDispatch** ppDisp = pDispParams->rgvarg[1].ppdispVal;
   hr = pWebBrowser2->get_Application(ppDisp);
   return S_OK ;
}

【Bug总结】:

  1. 在设计浏览功能用例时,考虑到了对当前标签页的操作可能会影响到其他标签页,用例虽然覆盖到了,但是实际发现这个BUG是在随机测试中,因为此BUG的触发条件是不定的。因此,对于可能涉及到多线程的用例,尽量多执行几次用例或者使用自动化测试工具进行测试。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值