在我们已有15年以上的应用程序中,我们有一种进度条。 这个进度条用于持久的C ++操作,并且还有一种情况,当我们进行COM调用时,COM调用返回需要很长时间。 一般来说,我们希望用户知道某些事情需要很长时间才能完成,并且如果他认为这需要花费太多时间,他就有机会取消。
对于COM操作,多年前我们为COM调用实现了一个定制的IMessageFilter,耗时太长。我们希望用户能够取消,但是当操作完成时,取消的提示也会自行消失。它已经工作多年了。我们的大多数客户都很保守,并且仍在运行Windows 7.最近,使用Windows 10的客户发现了一个问题,即Windows 10上的COM调用似乎永远不会完成。
我们的进度条出现,进度控制循环和循环,但操作似乎永远不会完成。在调查之后,似乎ICancelMethodCalls :: TestCancel()方法总是返回RPC_S_CALLPENDING,它永远不会返回RPC_E_CALL_COMPLETE。在Windows 7,以前版本的Windows和Windows 8.1上,它可以正常工作,但不能在Windows 10上运行。
我在Visual Studio 2013中创建了一个演示此问题的最小测试解决方案。一个项目是ATL服务器,另一个项目是MFC客户端应用程序。这里是一个示例解决方案的zip文件的链接:https://www.dropbox.com/s/1dkchplsi7d6tda/MyTimeOutServer.zip?dl=0
基本上ATL服务器有一个设置延迟的属性,以及一个等待延迟长度的方法。目的是模拟花费太长时间的COM操作。
interface IMyTimoutServer : IDispatch{
[propget, id(1), helpstring("Timeout value in milliseconds")] HRESULT TimeOut([out, retval] LONG* pVal);
[propput, id(1), helpstring("Timeout value in milliseconds")] HRESULT TimeOut([in] LONG newVal);
[id(2)] HRESULT DoTimeOut([out, retval] VARIANT_BOOL* Result);
};
下一个重要的事情是客户端应用程序中的IMessageFilter。在某些时候,有人认为从COleMessageFilter派生是好的,这是默认的MFC实现。 (我们不要争论这是否是一个好主意。)
该类的重要方法是MessagePending()和MyNotResponding()。
DWORD CMyMessageFilter::XMyMessageFilter::MessagePending(HTASK htaskCallee, DWORD dwTickCount, DWORD dwPendingType)
{
METHOD_PROLOGUE_EX(CMyMessageFilter, MyMessageFilter);
ASSERT_VALID(pThis);
MSG msg;
if (dwTickCount > pThis->m_nTimeout && !pThis->m_bUnblocking && pThis->IsSignificantMessage(&msg))
{
if (pThis->m_bEnableNotResponding)
{
pThis->m_bUnblocking = TRUE; // avoid reentrant calls
// eat all mouse and keyboard messages in our queue
while (PeekMessage(&msg, NULL, WM_MOUSEFIRST, AFX_WM_MOUSELAST, PM_REMOVE | PM_NOYIELD));
while (PeekMessage(&msg, NULL, WM_KEYFIRST, WM_KEYLAST, PM_REMOVE | PM_NOYIELD));
// show not responding dialog
pThis->m_dwTickCount = dwTickCount;
bool bCancel = pThis->MyNotResponding(htaskCallee) == RPC_E_CALL_CANCELED;
pThis->m_bUnblocking = FALSE;
return bCancel ? PENDINGMSG_CANCELCALL : PENDINGMSG_WAITNOPROCESS; // if canceled, the COM call will return RPC_E_CALL_CANCELED
}
}
// don't process re-entrant messages
if (pThis->m_bUnblocking)
return PENDINGMSG_WAITDEFPROCESS;
// allow application to process pending message
if (::PeekMessage(&msg, NULL, NULL, NULL, PM_NOREMOVE | PM_NOYIELD))
pThis->OnMessagePending(&msg); // IK: could also return a value from extended OnMessagePending() to cancel the call
// by default we return pending MSG wait
return PENDINGMSG_WAITNOPROCESS;
}
超时后,我们会显示一个状态类型窗口,该窗口在NotMyResponding()方法中更新。
int CMyMessageFilter::MyNotResponding(HTASK hTaskBusy)
{
TRY
{
CComPtr<ICancelMethodCalls> pCancel;
HRESULT hRes = ::CoGetCancelObject(0, IID_ICancelMethodCalls, (void**)&pCancel);
ATLASSERT(SUCCEEDED(hRes)); // COM should automatically create Cancel objects for pending calls [both on client and server]
if (pCancel == NULL)
{
return COleBusyDialog::retry;
}
m_pFrame->EnableWindow(FALSE);
CMyCancelDlg dlg;
dlg.Create(CMyCancelDlg::IDD);
dlg.ShowWindow(SW_SHOWNORMAL);
HWND hWndDlg = dlg.GetSafeHwnd();
do
{
MSG msg;
for (int i = 0; i < 100 && PeekMessage(&msg, 0, 0, 0, PM_REMOVE | PM_NOYIELD); ++i)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
if (dlg.StepAndCheckCancel())
{
dlg.DestroyWindow();
m_pFrame->EnableWindow(TRUE);
return RPC_E_CALL_CANCELED; // signals to MessagePending() that the COM call should be cancelled
}
Sleep(250); // this call has been pending for many seconds now... sleep for some time to avoid CPU utilization by this loop and yield if needed
hRes = pCancel->TestCancel();
} while (hRes == RPC_S_CALLPENDING);
ATLASSERT(hRes == RPC_E_CALL_COMPLETE);
dlg.DestroyWindow();
m_pFrame->EnableWindow(TRUE);
}
END_TRY
return RPC_E_CALL_COMPLETE;
}
基本上,在MyNotResponding()中,我们创建一个Cancel对象,创建一个带有取消按钮的窗口,泵消息,并查找按下取消按钮或者TestCancel()是否返回RPC_S_CALLPENDING之外的东西。
好吧,在Windows 10上,它保持在循环中,并且始终从TestCancel()返回RPC_S_CALLPENDING。
有没有人在Windows 10上看到过这样的内容?我们做错了什么,我们真的只是在Windows 7上运气好吗?
MFC的默认实现会建立一个OLEUIBusy对话框,用于提取消息。它永远不会测试取消对象。