| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/test/base/always_on_top_window_killer_win.h" |
| |
| #include <Windows.h> |
| |
| #include <ios> |
| #include <string> |
| |
| #include "base/command_line.h" |
| #include "base/files/file_path.h" |
| #include "base/logging.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/test/bind.h" |
| #include "base/win/window_enumerator.h" |
| #include "chrome/test/base/process_lineage_win.h" |
| #include "chrome/test/base/save_desktop_snapshot.h" |
| #include "ui/display/win/screen_win.h" |
| |
| namespace { |
| |
| constexpr char kDialogFoundBeforeTest[] = |
| "There is an always on top dialog on the desktop. This was most likely " |
| "caused by a previous test and may cause this test to fail. Trying to " |
| "close it;"; |
| |
| constexpr char kDialogFoundPostTest[] = |
| "There is an always on top dialog on the desktop after this test timed " |
| "out. This was most likely caused by this test and may cause future tests " |
| "to fail, trying to close it;"; |
| |
| constexpr char kWindowFoundBeforeTest[] = |
| "There is an always on top window on the desktop. This may have been " |
| "caused by a previous test and may cause this test to fail;"; |
| |
| constexpr char kWindowFoundPostTest[] = |
| "There is an always on top window on the desktop after this test timed " |
| "out. This may have been caused by this test or a previous test and may " |
| "cause flakes;"; |
| |
| } // namespace |
| |
| void KillAlwaysOnTopWindows(RunType run_type, |
| const base::CommandLine* child_command_line) { |
| const base::FilePath output_dir = |
| base::CommandLine::ForCurrentProcess()->GetSwitchValuePath( |
| kSnapshotOutputDir); |
| bool saved_snapshot = false; |
| if (run_type == RunType::AFTER_TEST_TIMEOUT && !output_dir.empty()) { |
| base::FilePath snapshot_file = SaveDesktopSnapshot(output_dir); |
| if (!snapshot_file.empty()) { |
| saved_snapshot = true; |
| |
| std::wostringstream sstream; |
| sstream << "Screen snapshot saved to file: \"" << snapshot_file.value() |
| << "\" after timeout of test"; |
| if (child_command_line) { |
| sstream << " process with command line: \"" |
| << child_command_line->GetCommandLineString() << "\"."; |
| } else { |
| sstream << "."; |
| } |
| LOG(ERROR) << sstream.str(); |
| } |
| } |
| |
| base::win::EnumerateChildWindows( |
| ::GetDesktopWindow(), base::BindLambdaForTesting([&](HWND hwnd) { |
| const bool kContinueIterating = false; |
| |
| if (!::IsWindowVisible(hwnd) || ::IsIconic(hwnd) || |
| !base::win::IsTopmostWindow(hwnd)) { |
| return kContinueIterating; |
| } |
| |
| const std::wstring class_name = base::win::GetWindowClass(hwnd); |
| if (class_name.empty()) { |
| return kContinueIterating; |
| } |
| |
| // Ignore specific windows owned by the shell. |
| if (base::win::IsShellWindow(hwnd)) { |
| return kContinueIterating; |
| } |
| |
| // All other always-on-top windows may be problematic, but in theory |
| // tests should not be creating an always on top window that outlives |
| // the test. Prepare details of the command line of the test that timed |
| // out (if provided), the process owning the window, and the location of |
| // a snapshot taken of the screen. |
| std::wstring details; |
| if (LOG_IS_ON(ERROR)) { |
| std::wostringstream sstream; |
| |
| if (!base::win::IsSystemDialog(hwnd)) { |
| sstream << " window class name: " << class_name << ";"; |
| } |
| |
| if (child_command_line) { |
| sstream << " subprocess command line: \"" |
| << child_command_line->GetCommandLineString() << "\";"; |
| } |
| |
| // Save a snapshot of the screen if one hasn't already been saved and |
| // an output directory was specified. |
| base::FilePath snapshot_file; |
| if (!saved_snapshot && !output_dir.empty()) { |
| snapshot_file = SaveDesktopSnapshot(output_dir); |
| if (!snapshot_file.empty()) { |
| saved_snapshot = true; |
| } |
| } |
| |
| DWORD process_id = 0; |
| GetWindowThreadProcessId(hwnd, &process_id); |
| ProcessLineage lineage = ProcessLineage::Create(process_id); |
| if (!lineage.IsEmpty()) { |
| sstream << " owning process lineage: " << lineage.ToString() << ";"; |
| } |
| |
| if (!snapshot_file.empty()) { |
| sstream << " screen snapshot saved to file: \"" |
| << snapshot_file.value() << "\";"; |
| } |
| |
| details = sstream.str(); |
| } |
| |
| // System dialogs may be present if a child process triggers an |
| // assert(), for example. |
| if (base::win::IsSystemDialog(hwnd)) { |
| LOG(ERROR) << (run_type == RunType::BEFORE_SHARD |
| ? kDialogFoundBeforeTest |
| : kDialogFoundPostTest) |
| << details; |
| // We don't own the dialog, so we can't destroy it. CloseWindow() |
| // results in iconifying the window. An alternative may be to focus |
| // it, then send return and wait for close. As we reboot machines |
| // running interactive ui tests at least every 12 hours we're going |
| // with the simple for now. |
| CloseWindow(hwnd); |
| } else { |
| LOG(ERROR) << (run_type == RunType::BEFORE_SHARD |
| ? kWindowFoundBeforeTest |
| : kWindowFoundPostTest) |
| << details; |
| // Try to strip the style and iconify the window. |
| if (::SetWindowLongPtr( |
| hwnd, GWL_EXSTYLE, |
| ::GetWindowLong(hwnd, GWL_EXSTYLE) & ~WS_EX_TOPMOST)) { |
| LOG(ERROR) << "Stripped WS_EX_TOPMOST."; |
| } else { |
| PLOG(ERROR) << "Failed to strip WS_EX_TOPMOST"; |
| } |
| if (::ShowWindow(hwnd, SW_FORCEMINIMIZE)) { |
| LOG(ERROR) << "Minimized window."; |
| } else { |
| PLOG(ERROR) << "Failed to minimize window"; |
| } |
| } |
| |
| return kContinueIterating; |
| })); |
| } |