Prevent window enumeration deadlock and add unit tests.
For some time now, calls to EnumerateCapturableWindows could lead to a deadlock if an application's main thread is waiting on the thread that is running EnumerateCapturableWindows. This is because calls to GetWindowText and GetWindowTextLength send a message to the window if the window is owned by the current process. Since the main thread is waiting on us, it will never reply to this message and we will hang. This happens occasionally in Chromium when tearing down the NativeDesktopMediaList object, e.g. when a user clicks "cancel" on the capture target picker. We can avoid this deadlock by checking if the window we are querying is owned by the current process, and if it is then we must ensure it is responding to messages before we call a GetWindowText* API. This change also adds a unit test for this scenario. We create a window and force it to be unresponsive by creating a deadlock, and then call GetWindowList and (with the new changes) we should not hang. Without the new changes to GetWindowListHandler, this test would hang. Change-Id: I2523cd735f96fd7ea60708c30cd22e5b525803f0 Bug: chromium:1152841 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/195365 Commit-Queue: Austin Orion <auorion@microsoft.com> Reviewed-by: Jamie Walch <jamiewalch@chromium.org> Cr-Commit-Position: refs/heads/master@{#32734}
This commit is contained in:
180
modules/desktop_capture/win/window_capture_utils_unittest.cc
Normal file
180
modules/desktop_capture/win/window_capture_utils_unittest.cc
Normal file
@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
#include "modules/desktop_capture/win/window_capture_utils.h"
|
||||
|
||||
#include <winuser.h>
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "modules/desktop_capture/desktop_capturer.h"
|
||||
#include "rtc_base/thread.h"
|
||||
#include "test/gtest.h"
|
||||
|
||||
namespace webrtc {
|
||||
namespace {
|
||||
|
||||
const char kWindowThreadName[] = "window_capture_utils_test_thread";
|
||||
const WCHAR kWindowClass[] = L"WindowCaptureUtilsTestClass";
|
||||
const WCHAR kWindowTitle[] = L"Window Capture Utils Test";
|
||||
const int kWindowWidth = 300;
|
||||
const int kWindowHeight = 200;
|
||||
|
||||
struct WindowInfo {
|
||||
HWND hwnd;
|
||||
HINSTANCE window_instance;
|
||||
ATOM window_class;
|
||||
};
|
||||
|
||||
WindowInfo CreateTestWindow(const WCHAR* window_title) {
|
||||
WindowInfo info;
|
||||
::GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(&::DefWindowProc),
|
||||
&info.window_instance);
|
||||
|
||||
WNDCLASSEXW wcex;
|
||||
memset(&wcex, 0, sizeof(wcex));
|
||||
wcex.cbSize = sizeof(wcex);
|
||||
wcex.style = CS_HREDRAW | CS_VREDRAW;
|
||||
wcex.hInstance = info.window_instance;
|
||||
wcex.lpfnWndProc = &::DefWindowProc;
|
||||
wcex.lpszClassName = kWindowClass;
|
||||
info.window_class = ::RegisterClassExW(&wcex);
|
||||
|
||||
info.hwnd = ::CreateWindowW(kWindowClass, window_title, WS_OVERLAPPEDWINDOW,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT, kWindowWidth,
|
||||
kWindowHeight, /*parent_window=*/nullptr,
|
||||
/*menu_bar=*/nullptr, info.window_instance,
|
||||
/*additional_params=*/nullptr);
|
||||
|
||||
::ShowWindow(info.hwnd, SW_SHOWNORMAL);
|
||||
::UpdateWindow(info.hwnd);
|
||||
return info;
|
||||
}
|
||||
|
||||
void DestroyTestWindow(WindowInfo info) {
|
||||
::DestroyWindow(info.hwnd);
|
||||
::UnregisterClass(MAKEINTATOM(info.window_class), info.window_instance);
|
||||
}
|
||||
|
||||
std::unique_ptr<rtc::Thread> SetUpUnresponsiveWindow(std::mutex& mtx,
|
||||
WindowInfo& info) {
|
||||
std::unique_ptr<rtc::Thread> window_thread;
|
||||
window_thread = rtc::Thread::Create();
|
||||
window_thread->SetName(kWindowThreadName, nullptr);
|
||||
window_thread->Start();
|
||||
|
||||
window_thread->Invoke<void>(
|
||||
RTC_FROM_HERE, [&info]() { info = CreateTestWindow(kWindowTitle); });
|
||||
|
||||
// Intentionally create a deadlock to cause the window to become unresponsive.
|
||||
mtx.lock();
|
||||
window_thread->PostTask(RTC_FROM_HERE, [&mtx]() {
|
||||
mtx.lock();
|
||||
mtx.unlock();
|
||||
});
|
||||
|
||||
return window_thread;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(WindowCaptureUtilsTest, GetWindowList) {
|
||||
WindowInfo info = CreateTestWindow(kWindowTitle);
|
||||
DesktopCapturer::SourceList window_list;
|
||||
ASSERT_TRUE(GetWindowList(GetWindowListFlags::kNone, &window_list));
|
||||
EXPECT_GT(window_list.size(), 0ULL);
|
||||
EXPECT_NE(std::find_if(window_list.begin(), window_list.end(),
|
||||
[&info](DesktopCapturer::Source window) {
|
||||
return reinterpret_cast<HWND>(window.id) ==
|
||||
info.hwnd;
|
||||
}),
|
||||
window_list.end());
|
||||
DestroyTestWindow(info);
|
||||
}
|
||||
|
||||
TEST(WindowCaptureUtilsTest, IncludeUnresponsiveWindows) {
|
||||
std::mutex mtx;
|
||||
WindowInfo info;
|
||||
std::unique_ptr<rtc::Thread> window_thread =
|
||||
SetUpUnresponsiveWindow(mtx, info);
|
||||
|
||||
EXPECT_FALSE(IsWindowResponding(info.hwnd));
|
||||
|
||||
DesktopCapturer::SourceList window_list;
|
||||
ASSERT_TRUE(GetWindowList(GetWindowListFlags::kNone, &window_list));
|
||||
EXPECT_GT(window_list.size(), 0ULL);
|
||||
EXPECT_NE(std::find_if(window_list.begin(), window_list.end(),
|
||||
[&info](DesktopCapturer::Source window) {
|
||||
return reinterpret_cast<HWND>(window.id) ==
|
||||
info.hwnd;
|
||||
}),
|
||||
window_list.end());
|
||||
|
||||
mtx.unlock();
|
||||
window_thread->Invoke<void>(RTC_FROM_HERE,
|
||||
[&info]() { DestroyTestWindow(info); });
|
||||
window_thread->Stop();
|
||||
}
|
||||
|
||||
TEST(WindowCaptureUtilsTest, IgnoreUnresponsiveWindows) {
|
||||
std::mutex mtx;
|
||||
WindowInfo info;
|
||||
std::unique_ptr<rtc::Thread> window_thread =
|
||||
SetUpUnresponsiveWindow(mtx, info);
|
||||
|
||||
EXPECT_FALSE(IsWindowResponding(info.hwnd));
|
||||
|
||||
DesktopCapturer::SourceList window_list;
|
||||
ASSERT_TRUE(
|
||||
GetWindowList(GetWindowListFlags::kIgnoreUnresponsive, &window_list));
|
||||
EXPECT_EQ(std::find_if(window_list.begin(), window_list.end(),
|
||||
[&info](DesktopCapturer::Source window) {
|
||||
return reinterpret_cast<HWND>(window.id) ==
|
||||
info.hwnd;
|
||||
}),
|
||||
window_list.end());
|
||||
|
||||
mtx.unlock();
|
||||
window_thread->Invoke<void>(RTC_FROM_HERE,
|
||||
[&info]() { DestroyTestWindow(info); });
|
||||
window_thread->Stop();
|
||||
}
|
||||
|
||||
TEST(WindowCaptureUtilsTest, IncludeUntitledWindows) {
|
||||
WindowInfo info = CreateTestWindow(L"");
|
||||
DesktopCapturer::SourceList window_list;
|
||||
ASSERT_TRUE(GetWindowList(GetWindowListFlags::kNone, &window_list));
|
||||
EXPECT_GT(window_list.size(), 0ULL);
|
||||
EXPECT_NE(std::find_if(window_list.begin(), window_list.end(),
|
||||
[&info](DesktopCapturer::Source window) {
|
||||
return reinterpret_cast<HWND>(window.id) ==
|
||||
info.hwnd;
|
||||
}),
|
||||
window_list.end());
|
||||
DestroyTestWindow(info);
|
||||
}
|
||||
|
||||
TEST(WindowCaptureUtilsTest, IgnoreUntitledWindows) {
|
||||
WindowInfo info = CreateTestWindow(L"");
|
||||
DesktopCapturer::SourceList window_list;
|
||||
ASSERT_TRUE(GetWindowList(GetWindowListFlags::kIgnoreUntitled, &window_list));
|
||||
EXPECT_EQ(std::find_if(window_list.begin(), window_list.end(),
|
||||
[&info](DesktopCapturer::Source window) {
|
||||
return reinterpret_cast<HWND>(window.id) ==
|
||||
info.hwnd;
|
||||
}),
|
||||
window_list.end());
|
||||
DestroyTestWindow(info);
|
||||
}
|
||||
|
||||
} // namespace webrtc
|
||||
Reference in New Issue
Block a user