/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/** * The Windows-only code below exists to solve a general problem with deadlocks * that we experience when sending synchronous IPC messages to processes that * contain native windows (i.e. HWNDs). Windows (the OS) sends synchronous * messages between parent and child HWNDs in multiple circumstances (e.g. * WM_PARENTNOTIFY, WM_NCACTIVATE, etc.), even when those HWNDs are controlled * by different threads or different processes. Thus we can very easily end up * in a deadlock by a call stack like the following: * * Process A: * - CreateWindow(...) creates a "parent" HWND. * - SendCreateChildWidget(HWND) is a sync IPC message that sends the "parent" * HWND over to Process B. Process A blocks until a response is received * from Process B. * * Process B: * - RecvCreateWidget(HWND) gets the "parent" HWND from Process A. * - CreateWindow(..., HWND) creates a "child" HWND with the parent from * process A. * - Windows (the OS) generates a WM_PARENTNOTIFY message that is sent * synchronously to Process A. Process B blocks until a response is * received from Process A. Process A, however, is blocked and cannot * process the message. Both processes are deadlocked. * * The example above has a few different workarounds (e.g. setting the * WS_EX_NOPARENTNOTIFY style on the child window) but the general problem is * persists. Once two HWNDs are parented we must not block their owning * threads when manipulating either HWND. * * Windows requires any application that hosts native HWNDs to always process * messages or risk deadlock. Given our architecture the only way to meet * Windows' requirement and allow for synchronous IPC messages is to pump a * miniature message loop during a sync IPC call. We avoid processing any * queued messages during the loop (with one exception, see below), but * "nonqueued" messages (see * http://msdn.microsoft.com/en-us/library/ms644927(VS.85).aspx under the * section "Nonqueued messages") cannot be avoided. Those messages are trapped * in a special window procedure where we can either ignore the message or * process it in some fashion. * * Queued and "non-queued" messages will be processed during Interrupt calls if * modal UI related api calls block an Interrupt in-call in the child. To * prevent windows from freezing, and to allow concurrent processing of critical * events (such as painting), we spin a native event dispatch loop while * these in-calls are blocked.
*/
#ifdefined(ACCESSIBILITY) // pulled from accessibility's win utils externconstwchar_t* kPropNameTabContent; #endif
// widget related message id constants we need to defer, see nsAppShell. extern UINT sAppShellGeckoMsgId;
DWORD gUIThreadId = 0;
HWND gCOMWindow = 0; // Once initialized, gWinEventHook is never unhooked. We save the handle so // that we can check whether or not the hook is initialized.
HWINEVENTHOOK gWinEventHook = nullptr; constwchar_t kCOMWindowClassName[] = L"OleMainThreadWndClass";
// WM_GETOBJECT id pulled from uia headers #define MOZOBJID_UIAROOT -25
HWND FindCOMWindow() {
MOZ_ASSERT(gUIThreadId);
HWND last = 0; while (
(last = FindWindowExW(HWND_MESSAGE, last, kCOMWindowClassName, NULL))) { if (GetWindowThreadProcessId(last, NULL) == gUIThreadId) { return last;
}
}
return (HWND)0;
}
void CALLBACK WinEventHook(HWINEVENTHOOK aWinEventHook, DWORD aEvent,
HWND aHwnd, LONG aIdObject, LONG aIdChild,
DWORD aEventThread, DWORD aMsEventTime) {
MOZ_ASSERT(aWinEventHook == gWinEventHook);
MOZ_ASSERT(gUIThreadId == aEventThread); switch (aEvent) { case EVENT_OBJECT_CREATE: { if (aIdObject != OBJID_WINDOW || aIdChild != CHILDID_SELF) { // Not an event we're interested in return;
} wchar_t classBuf[256] = {0}; int result = ::GetClassNameW(aHwnd, classBuf, std::size(classBuf)); if (result != (std::size(kCOMWindowClassName) - 1) ||
wcsncmp(kCOMWindowClassName, classBuf, result)) { // Not a class we're interested in return;
}
MOZ_ASSERT(FindCOMWindow() == aHwnd);
gCOMWindow = aHwnd; break;
} case EVENT_OBJECT_DESTROY: { if (aHwnd == gCOMWindow && aIdObject == OBJID_WINDOW) {
MOZ_ASSERT(aIdChild == CHILDID_SELF);
gCOMWindow = 0;
} break;
} default: { return;
}
}
}
LRESULT CALLBACK DeferredMessageHook(int nCode, WPARAM wParam, LPARAM lParam) { // XXX This function is called for *both* the WH_CALLWNDPROC hook and the // WH_GETMESSAGE hook, but they have different parameters. We don't // use any of them except nCode which has the same meaning.
// Only run deferred messages if all of these conditions are met: // 1. The |nCode| indicates that this hook should do something. // 2. We have deferred messages to run. // 3. We're not being called from the PeekMessage within the WaitFor*Notify // function (indicated with MessageChannel::IsPumpingMessages). We really // only want to run after returning to the main event loop. if (nCode >= 0 && gDeferredMessages && !MessageChannel::IsPumpingMessages()) {
NS_ASSERTION(gDeferredGetMsgHook && gDeferredCallWndProcHook, "These hooks must be set if we're being called!");
NS_ASSERTION(gDeferredMessages->Length(), "No deferred messages?!");
// Unset hooks first, in case we reenter below.
UnhookWindowsHookEx(gDeferredGetMsgHook);
UnhookWindowsHookEx(gDeferredCallWndProcHook);
gDeferredGetMsgHook = 0;
gDeferredCallWndProcHook = 0;
// Unset the global and make sure we delete it when we're done here. auto messages = WrapUnique(gDeferredMessages);
gDeferredMessages = nullptr;
// Run all the deferred messages in order.
uint32_t count = messages->Length(); for (uint32_t index = 0; index < count; index++) {
messages->ElementAt(index)->Run();
}
}
// Always call the next hook. return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
void ScheduleDeferredMessageRun() { if (gDeferredMessages && !(gDeferredGetMsgHook && gDeferredCallWndProcHook)) {
NS_ASSERTION(gDeferredMessages->Length(), "No deferred messages?!");
// Most messages ask for 0 to be returned if the message is processed.
LRESULT res = 0;
switch (uMsg) { // Messages that can be deferred as-is. These must not contain pointers in // their wParam or lParam arguments! case WM_ACTIVATE: case WM_ACTIVATEAPP: case WM_CANCELMODE: case WM_CAPTURECHANGED: case WM_CHILDACTIVATE: case WM_DESTROY: case WM_ENABLE: case WM_IME_NOTIFY: case WM_IME_SETCONTEXT: case WM_KILLFOCUS: case WM_MOUSEWHEEL: case WM_NCDESTROY: case WM_PARENTNOTIFY: case WM_SETFOCUS: case WM_SYSCOMMAND: case WM_DISPLAYCHANGE: case WM_SHOWWINDOW: // Intentional fall-through. case WM_XP_THEMECHANGED: {
deferred = MakeUnique<DeferredSendMessage>(hwnd, uMsg, wParam, lParam); break;
}
case WM_DEVICECHANGE: case WM_POWERBROADCAST: case WM_NCACTIVATE: // Intentional fall-through. case WM_SETCURSOR: { // Friggin unconventional return value...
res = TRUE;
deferred = MakeUnique<DeferredSendMessage>(hwnd, uMsg, wParam, lParam); break;
}
case WM_MOUSEACTIVATE: {
res = MA_NOACTIVATE;
deferred = MakeUnique<DeferredSendMessage>(hwnd, uMsg, wParam, lParam); break;
}
// These messages need to use the RedrawWindow function to generate the // right kind of message. We can't simply fake them as the MSDN docs say // explicitly that paint messages should not be sent by an application. case WM_ERASEBKGND: {
UINT flags = RDW_INVALIDATE | RDW_ERASE | RDW_NOINTERNALPAINT |
RDW_NOFRAME | RDW_NOCHILDREN | RDW_ERASENOW;
deferred = MakeUnique<DeferredRedrawMessage>(hwnd, flags); break;
}
// This message will generate a WM_PAINT message if there are invalid // areas. case WM_PAINT: {
deferred = MakeUnique<DeferredUpdateMessage>(hwnd); break;
}
// This message holds a string in its lParam that we must copy. case WM_SETTINGCHANGE: {
deferred =
MakeUnique<DeferredSettingChangeMessage>(hwnd, uMsg, wParam, lParam); break;
}
// These messages are faked via a call to SetWindowPos. case WM_WINDOWPOSCHANGED: {
deferred = MakeUnique<DeferredWindowPosMessage>(hwnd, lParam); break;
} case WM_NCCALCSIZE: {
deferred =
MakeUnique<DeferredWindowPosMessage>(hwnd, lParam, true, wParam); break;
}
case WM_COPYDATA: {
deferred =
MakeUnique<DeferredCopyDataMessage>(hwnd, uMsg, wParam, lParam);
res = TRUE; break;
}
case WM_STYLECHANGED: {
deferred = MakeUnique<DeferredStyleChangeMessage>(hwnd, wParam, lParam); break;
}
// Messages that are safe to pass to DefWindowProc go here. case WM_ENTERIDLE: case WM_GETICON: case WM_NCPAINT: // (never trap nc paint events) case WM_GETMINMAXINFO: case WM_GETTEXT: case WM_NCHITTEST: case WM_STYLECHANGING: // Intentional fall-through. case WM_WINDOWPOSCHANGING: case WM_GETTEXTLENGTH: { return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
// Just return, prevents DefWindowProc from messaging the window // syncronously with other events, which may be deferred. Prevents // random shutdown of aero composition on the window. case WM_SYNCPAINT: return 0;
// This message causes QuickTime to make re-entrant calls. // Simply discarding it doesn't seem to hurt anything. case WM_APP - 1: return 0;
// We only support a query for our IAccessible or UIA pointers. // This should be safe, and needs to be sync. #ifdefined(ACCESSIBILITY) case WM_GETOBJECT: { LONG objId = static_cast<LONG>(lParam); if (objId == OBJID_CLIENT || objId == MOZOBJID_UIAROOT) {
WNDPROC oldWndProc = (WNDPROC)GetProp(hwnd, kOldWndProcProp); if (oldWndProc) { return CallWindowProcW(oldWndProc, hwnd, uMsg, wParam, lParam);
}
} return DefWindowProc(hwnd, uMsg, wParam, lParam);
} #endif// ACCESSIBILITY
default: { // Unknown messages only are logged in debug builds and sent to // DefWindowProc. if (uMsg && uMsg == sAppShellGeckoMsgId) { // Widget's registered native event callback
deferred = MakeUnique<DeferredSendMessage>(hwnd, uMsg, wParam, lParam);
}
}
}
// No deferred message was created and we land here, this is an // unhandled message. if (!deferred) {
DumpNeuteredMessage(hwnd, uMsg); return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
// Create the deferred message array if it doesn't exist already. if (!gDeferredMessages) {
gDeferredMessages = new DeferredMessageArray(20);
}
// Save for later. The array takes ownership of |deferred|.
gDeferredMessages->AppendElement(std::move(deferred)); return res;
}
} // namespace
LRESULT CALLBACK NeuteredWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam,
LPARAM lParam) {
WNDPROC oldWndProc = (WNDPROC)GetProp(hwnd, kOldWndProcProp); if (!oldWndProc) { // We should really never ever get here.
NS_ERROR("No old wndproc!"); return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
// See if we care about this message. We may either ignore it, send it to // DefWindowProc, or defer it for later. return ProcessOrDeferMessage(hwnd, uMsg, wParam, lParam);
}
namespace {
staticbool WindowIsDeferredWindow(HWND hWnd) { if (!IsWindow(hWnd)) {
NS_WARNING("Window has died!"); returnfalse;
}
char16_t buffer[256] = {0}; int length = GetClassNameW(hWnd, (wchar_t*)buffer, sizeof(buffer) - 1); if (length <= 0) {
NS_WARNING("Failed to get class name!"); returnfalse;
}
#ifdefined(ACCESSIBILITY) // Tab content creates a window that responds to accessible WM_GETOBJECT // calls. This window can safely be ignored. if (::GetPropW(hWnd, kPropNameTabContent)) { returnfalse;
} #endif
// Common mozilla windows we must defer messages to.
nsDependentString className(buffer, length); if (StringBeginsWith(className, u"Mozilla"_ns) ||
StringBeginsWith(className, u"Gecko"_ns) ||
className.EqualsLiteral("nsToolkitClass") ||
className.EqualsLiteral("nsAppShell:EventWindowClass")) { returntrue;
}
returnfalse;
}
bool NeuterWindowProcedure(HWND hWnd) { if (!WindowIsDeferredWindow(hWnd)) { // Some other kind of window, skip. returnfalse;
}
NS_ASSERTION(!GetProp(hWnd, kOldWndProcProp), "This should always be null!");
// It's possible to get nullptr out of SetWindowLongPtr, and the only way to // know if that's a valid old value is to use GetLastError. Clear the error // here so we can tell.
SetLastError(ERROR_SUCCESS);
LONG_PTR currentWndProc =
SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)NeuteredWindowProc); if (!currentWndProc) { if (ERROR_SUCCESS == GetLastError()) { // No error, so we set something and must therefore reset it.
SetWindowLongPtr(hWnd, GWLP_WNDPROC, currentWndProc);
} returnfalse;
}
NS_ASSERTION(currentWndProc != (LONG_PTR)NeuteredWindowProc, "This shouldn't be possible!");
void RestoreWindowProcedure(HWND hWnd) {
NS_ASSERTION(WindowIsDeferredWindow(hWnd), "Not a deferred window, this shouldn't be in our list!");
LONG_PTR oldWndProc = (LONG_PTR)GetProp(hWnd, kOldWndProcProp); if (oldWndProc) {
NS_ASSERTION(oldWndProc != (LONG_PTR)NeuteredWindowProc, "This shouldn't be possible!");
DebugOnly<LONG_PTR> currentWndProc =
SetWindowLongPtr(hWnd, GWLP_WNDPROC, oldWndProc);
NS_ASSERTION(currentWndProc == (LONG_PTR)NeuteredWindowProc, "This should never be switched out from under us!");
}
RemovePropW(hWnd, kOldWndProcProp);
}
LRESULT CALLBACK CallWindowProcedureHook(int nCode, WPARAM wParam,
LPARAM lParam) { if (nCode >= 0) {
NS_ASSERTION(gNeuteredWindows, "This should never be null!");
if (!gNeuteredWindows->Contains(hWnd) &&
!SuppressedNeuteringRegion::IsNeuteringSuppressed() &&
NeuterWindowProcedure(hWnd)) { // XXX(Bug 1631371) Check if this should use a fallible operation as it // pretended earlier.
gNeuteredWindows->AppendElement(hWnd);
}
} return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
inlinevoid AssertWindowIsNotNeutered(HWND hWnd) { #ifdef DEBUG // Make sure our neutered window hook isn't still in place.
LONG_PTR wndproc = GetWindowLongPtr(hWnd, GWLP_WNDPROC);
NS_ASSERTION(wndproc != (LONG_PTR)NeuteredWindowProc, "Window is neutered!"); #endif
}
void UnhookNeuteredWindows() { if (!gNeuteredWindows) return;
uint32_t count = gNeuteredWindows->Length(); for (uint32_t index = 0; index < count; index++) {
RestoreWindowProcedure(gNeuteredWindows->ElementAt(index));
}
gNeuteredWindows->Clear();
}
// This timeout stuff assumes a sane value of mTimeoutMs (less than the overflow // value for GetTickCount(), which is something like 50 days). It uses the // cheapest (and least accurate) method supported by Windows 2000.
void InitTimeoutData(TimeoutData* aData, int32_t aTimeoutMs) {
aData->startTicks = GetTickCount(); if (!aData->startTicks) { // How unlikely is this!
aData->startTicks++;
}
aData->targetTicks = aData->startTicks + aTimeoutMs;
}
bool TimeoutHasExpired(const TimeoutData& aData) { if (!aData.startTicks) { returnfalse;
}
DWORD now = GetTickCount();
if (aData.targetTicks < aData.startTicks) { // Overflow return now < aData.startTicks && now >= aData.targetTicks;
} return now >= aData.targetTicks;
}
} // namespace
namespace mozilla { namespace ipc { namespace windows {
void InitUIThread() { if (!XRE_UseNativeEventProcessing()) { return;
} // If we aren't setup before a call to NotifyWorkerThread, we'll hang // on startup. if (!gUIThreadId) {
gUIThreadId = GetCurrentThreadId();
}
MOZ_ASSERT(gUIThreadId);
MOZ_ASSERT(gUIThreadId == GetCurrentThreadId(), "Called InitUIThread multiple times on different threads!");
// We need to execute this after setting the hook in case the OLE window // already existed.
gCOMWindow = FindCOMWindow();
}
}
} // namespace windows
} // namespace ipc
} // namespace mozilla
MessageChannel::SyncStackFrame::SyncStackFrame(MessageChannel* channel)
: mSpinNestedEvents(false),
mListenerNotified(false),
mChannel(channel),
mPrev(mChannel->mTopFrame),
mStaticPrev(sStaticTopFrame) { // Only track stack frames when Windows message deferral behavior // is request for the channel. if (!(mChannel->GetChannelFlags() & REQUIRE_DEFERRED_MESSAGE_PROTECTION)) { return;
}
// nsAppShell's notification that gecko events are being processed. // If we are here and there is an Interrupt Incall active, we are spinning // a nested gecko event loop. In which case the remote process needs // to know about it. void/* static */
MessageChannel::NotifyGeckoEventDispatch() { // sStaticTopFrame is only valid for Interrupt channels if (!sStaticTopFrame || sStaticTopFrame->mListenerNotified) return;
// invoked by the module that receives the spin event loop // message. void MessageChannel::ProcessNativeEventsInInterruptCall() {
NS_ASSERTION(GetCurrentThreadId() == gUIThreadId, "Shouldn't be on a non-main thread in here!"); if (!mTopFrame) {
NS_ERROR("Spin logic error: no Interrupt frame"); return;
}
mTopFrame->mSpinNestedEvents = true;
}
static HHOOK gWindowHook;
staticinlinevoid StartNeutering() { if (!gUIThreadId) {
mozilla::ipc::windows::InitUIThread();
}
MOZ_ASSERT(gUIThreadId);
MOZ_ASSERT(!gWindowHook);
NS_ASSERTION(!MessageChannel::IsPumpingMessages(), "Shouldn't be pumping already!");
MessageChannel::SetIsPumpingMessages(true);
gWindowHook = ::SetWindowsHookEx(WH_CALLWNDPROC, CallWindowProcedureHook,
nullptr, gUIThreadId);
NS_ASSERTION(gWindowHook, "Failed to set hook!");
}
staticvoid StopNeutering() {
MOZ_ASSERT(MessageChannel::IsPumpingMessages());
::UnhookWindowsHookEx(gWindowHook);
gWindowHook = NULL;
::UnhookNeuteredWindows(); // Before returning we need to set a hook to run any deferred messages that // we received during the IPC call. The hook will unset itself as soon as // someone else calls GetMessage, PeekMessage, or runs code that generates // a "nonqueued" message.
::ScheduleDeferredMessageRun();
MessageChannel::SetIsPumpingMessages(false);
}
NeuteredWindowRegion::~NeuteredWindowRegion() { if (gWindowHook && mNeuteredByThis) {
StopNeutering();
}
}
void NeuteredWindowRegion::PumpOnce() { if (!gWindowHook) { // This should be a no-op if nothing has been neutered. return;
}
MSG msg = {0}; // Pump any COM messages so that we don't hang due to STA marshaling. if (gCOMWindow && ::PeekMessageW(&msg, gCOMWindow, 0, 0, PM_REMOVE)) {
::TranslateMessage(&msg);
::DispatchMessageW(&msg);
} // Expunge any nonqueued messages on the current thread.
::PeekMessageW(&msg, nullptr, 0, 0, PM_NOREMOVE);
}
if (!gUIThreadId) {
mozilla::ipc::windows::InitUIThread();
}
// Use a blocking wait if this channel does not require // Windows message deferral behavior. if (!(mFlags & REQUIRE_DEFERRED_MESSAGE_PROTECTION)) {
TimeDuration timeout = (kNoTimeout == mTimeoutMs)
? TimeDuration::Forever()
: TimeDuration::FromMilliseconds(mTimeoutMs);
// If the timeout didn't expire, we know we received an event. The // converse is not true. return WaitResponse(status == CVStatus::Timeout);
}
NS_ASSERTION(
mFlags & REQUIRE_DEFERRED_MESSAGE_PROTECTION, "Shouldn't be here for channels that don't use message deferral!");
NS_ASSERTION(mTopFrame, "No top frame!");
if (mTimeoutMs != kNoTimeout) {
InitTimeoutData(&timeoutData, mTimeoutMs);
// We only do this to ensure that we won't get stuck in // MsgWaitForMultipleObjects below.
timerId = SetTimer(nullptr, 0, mTimeoutMs, nullptr);
NS_ASSERTION(timerId, "SetTimer failed!");
}
NeuteredWindowRegion neuteredRgn(true);
{ while (1) {
MSG msg = {0}; // Don't get wrapped up in here if the child connection dies.
{
MonitorAutoLock lock(*mMonitor); if (!Connected()) { break;
}
}
// Wait until we have a message in the queue. MSDN docs are a bit unclear // but it seems that windows from two different threads (and it should be // noted that a thread in another process counts as a "different thread") // will implicitly have their message queues attached if they are parented // to one another. This wait call, then, will return for a message // delivered to *either* thread.
DWORD result =
MsgWaitForMultipleObjects(1, &mEvent, FALSE, INFINITE, QS_ALLINPUT); if (result == WAIT_OBJECT_0) { // Our NotifyWorkerThread event was signaled BOOL success = ResetEvent(mEvent); if (!success) {
gfxDevCrash(mozilla::gfx::LogReason::MessageChannelInvalidHandle)
<< "WindowsMessageChannel::WaitForSyncNotify failed to reset " "event. GetLastError: "
<< GetLastError();
} break;
} elseif (result != (WAIT_OBJECT_0 + 1)) {
NS_ERROR("Wait failed!"); break;
}
if (TimeoutHasExpired(timeoutData)) { // A timeout was specified and we've passed it. Break out.
timedout = true; break;
}
// The only way to know on which thread the message was delivered is to // use some logic on the return values of GetQueueStatus and PeekMessage. // PeekMessage will return false if there are no "queued" messages, but it // will run all "nonqueued" messages before returning. So if PeekMessage // returns false and there are no "nonqueued" messages that were run then // we know that the message we woke for was intended for a window on // another thread. bool haveSentMessagesPending =
(HIWORD(GetQueueStatus(QS_SENDMESSAGE)) & QS_SENDMESSAGE) != 0;
// Either of the PeekMessage calls below will actually process all // "nonqueued" messages that are pending before returning. If we have // "nonqueued" messages pending then we should have switched out all the // window procedures above. In that case this PeekMessage call won't // actually cause any mozilla code (or plugin code) to run.
// We have to manually pump all COM messages *after* looking at the queue // queue status but before yielding our thread below. if (gCOMWindow) { if (PeekMessageW(&msg, gCOMWindow, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
::DispatchMessageW(&msg);
}
}
// If the following PeekMessage call fails to return a message for us (and // returns false) and we didn't run any "nonqueued" messages then we must // have woken up for a message designated for a window in another thread. // If we loop immediately then we could enter a tight loop, so we'll give // up our time slice here to let the child process its message. if (!PeekMessageW(&msg, nullptr, 0, 0, PM_NOREMOVE) &&
!haveSentMessagesPending) { // Message was for child, we should wait a bit.
SwitchToThread();
}
}
}
if (timerId) {
KillTimer(nullptr, timerId);
timerId = 0;
}
if (mIsSyncWaitingOnNonMainThread) {
mMonitor->Notify(); return;
}
MOZ_RELEASE_ASSERT(mEvent, "No signal event to set, this is really bad!"); if (!SetEvent(mEvent)) {
NS_WARNING("Failed to set NotifyWorkerThread event!");
gfxDevCrash(mozilla::gfx::LogReason::MessageChannelInvalidHandle)
<< "WindowsMessageChannel failed to SetEvent. GetLastError: "
<< GetLastError();
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.